mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge pull request #519 from RWDai/feat/aether-tunnel-ip-family-options
feat(tunnel): add tunnel IP family controls
This commit is contained in:
13
apps/aether-proxy/install.ps1
Normal file
13
apps/aether-proxy/install.ps1
Normal file
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:AETHER_PROXY_AETHER_URL -and -not $env:AETHER_TUNNEL_AETHER_URL) {
|
||||
$env:AETHER_TUNNEL_AETHER_URL = $env:AETHER_PROXY_AETHER_URL
|
||||
}
|
||||
if ($env:AETHER_PROXY_MANAGEMENT_TOKEN -and -not $env:AETHER_TUNNEL_MANAGEMENT_TOKEN) {
|
||||
$env:AETHER_TUNNEL_MANAGEMENT_TOKEN = $env:AETHER_PROXY_MANAGEMENT_TOKEN
|
||||
}
|
||||
if ($env:AETHER_PROXY_NODE_NAME -and -not $env:AETHER_TUNNEL_NODE_NAME) {
|
||||
$env:AETHER_TUNNEL_NODE_NAME = $env:AETHER_PROXY_NODE_NAME
|
||||
}
|
||||
|
||||
irm 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1' | iex
|
||||
21
apps/aether-proxy/install.sh
Normal file
21
apps/aether-proxy/install.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ -n "${AETHER_PROXY_AETHER_URL:-}" ] && [ -z "${AETHER_TUNNEL_AETHER_URL:-}" ]; then
|
||||
export AETHER_TUNNEL_AETHER_URL="${AETHER_PROXY_AETHER_URL}"
|
||||
fi
|
||||
if [ -n "${AETHER_PROXY_MANAGEMENT_TOKEN:-}" ] && [ -z "${AETHER_TUNNEL_MANAGEMENT_TOKEN:-}" ]; then
|
||||
export AETHER_TUNNEL_MANAGEMENT_TOKEN="${AETHER_PROXY_MANAGEMENT_TOKEN}"
|
||||
fi
|
||||
if [ -n "${AETHER_PROXY_NODE_NAME:-}" ] && [ -z "${AETHER_TUNNEL_NODE_NAME:-}" ]; then
|
||||
export AETHER_TUNNEL_NODE_NAME="${AETHER_PROXY_NODE_NAME}"
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh' | sh
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh' | sh
|
||||
else
|
||||
printf '%s\n' "[Aether Tunnel] 需要 curl 或 wget 下载安装脚本" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -10,6 +10,10 @@ AETHER_TUNNEL_NODE_NAME=jp-proxy-01
|
||||
# Maximum request body buffered for 307/308 replay (supports K/M/G, 0 disables body replay buffering)
|
||||
AETHER_TUNNEL_REDIRECT_REPLAY_BUDGET_BYTES=5M
|
||||
|
||||
# Optional tunnel TCP address-family restriction (set at most one to true)
|
||||
AETHER_TUNNEL_IPV4_ONLY=false
|
||||
AETHER_TUNNEL_IPV6_ONLY=false
|
||||
|
||||
# Logging
|
||||
AETHER_TUNNEL_LOG_LEVEL=info
|
||||
AETHER_TUNNEL_LOG_DESTINATION=stdout
|
||||
|
||||
@@ -126,6 +126,8 @@ sudo aether-tunnel uninstall
|
||||
| `--tunnel-max-streams` | `AETHER_TUNNEL_MAX_STREAMS` | 自动(硬件估算) | 单连接最大并发 stream 数 |
|
||||
| `--tunnel-ping-interval-ms` | `AETHER_TUNNEL_PING_INTERVAL_MS` | `10000` | WebSocket ping 周期(毫秒) |
|
||||
| `--tunnel-connect-timeout-ms` | `AETHER_TUNNEL_CONNECT_TIMEOUT_MS` | `3000` | tunnel 建连超时(毫秒) |
|
||||
| `--tunnel-ipv4-only` | `AETHER_TUNNEL_IPV4_ONLY` | `false` | 仅使用 IPv4 地址建立直连 WebSocket tunnel;配置 `aether_outbound_proxy_url` 时仅限制代理端点解析 |
|
||||
| `--tunnel-ipv6-only` | `AETHER_TUNNEL_IPV6_ONLY` | `false` | 仅使用 IPv6 地址建立直连 WebSocket tunnel;配置 `aether_outbound_proxy_url` 时仅限制代理端点解析 |
|
||||
| `--tunnel-stale-timeout-ms` | `AETHER_TUNNEL_STALE_TIMEOUT_MS` | `30000` | 无入站数据断连阈值(毫秒) |
|
||||
| `--tunnel-scale-check-interval-ms` | `AETHER_TUNNEL_SCALE_CHECK_INTERVAL_MS` | `1000` | autoscale 采样周期(毫秒) |
|
||||
| `--tunnel-scale-up-threshold-percent` | `AETHER_TUNNEL_SCALE_UP_THRESHOLD_PERCENT` | `50` | 单 tunnel 占用率超过该值时扩容 |
|
||||
@@ -138,6 +140,8 @@ sudo aether-tunnel uninstall
|
||||
|
||||
省略 `tunnel_connections` 时,tunnel 会按设备能力自动计算一个基线值和偏单机上限的扩容上限:默认至少保留 2 条常驻 tunnel,并会更早触发扩容;如果显式设置了 `tunnel_connections` 但没有设置 `tunnel_connections_max`,则保持固定连接池,不自动扩缩。
|
||||
|
||||
`tunnel_ipv4_only` / `tunnel_ipv6_only` 只能二选一。它们只改变 WebSocket tunnel 回连的 TCP 地址选择:直连 Aether 时过滤 Aether 域名的 DNS 结果;配置 `aether_outbound_proxy_url` 时过滤代理服务器端点的 DNS 结果,Host/SNI 仍使用原始 WebSocket URL。该选项不会影响 provider 上游请求;如需限制 provider 上游流量,请在 `upstream_proxy_url` 或系统网络层处理。对于 Cloudflare 等边缘 IP 会变化的域名,优先使用该选项而不是固定 `/etc/hosts`。
|
||||
|
||||
#### 上游 HTTP 请求
|
||||
|
||||
| 参数 | 环境变量 | 默认值 | 说明 |
|
||||
|
||||
@@ -1356,6 +1356,8 @@ mod tests {
|
||||
tunnel_ping_interval_ms: 1_000,
|
||||
tunnel_max_streams: Some(8),
|
||||
tunnel_connect_timeout_ms: 2_000,
|
||||
tunnel_ipv4_only: false,
|
||||
tunnel_ipv6_only: false,
|
||||
tunnel_tcp_keepalive_secs: 30,
|
||||
tunnel_tcp_nodelay: true,
|
||||
tunnel_stale_timeout_ms: 5_000,
|
||||
|
||||
@@ -571,6 +571,30 @@ pub struct Config {
|
||||
)]
|
||||
pub tunnel_connect_timeout_ms: u64,
|
||||
|
||||
/// Force direct WebSocket tunnel TCP connects, or Aether outbound proxy endpoint connects, to IPv4 addresses only.
|
||||
#[arg(
|
||||
long,
|
||||
env = "AETHER_TUNNEL_IPV4_ONLY",
|
||||
default_value_t = false,
|
||||
action = clap::ArgAction::Set,
|
||||
default_missing_value = "true",
|
||||
num_args = 0..=1,
|
||||
require_equals = true
|
||||
)]
|
||||
pub tunnel_ipv4_only: bool,
|
||||
|
||||
/// Force direct WebSocket tunnel TCP connects, or Aether outbound proxy endpoint connects, to IPv6 addresses only.
|
||||
#[arg(
|
||||
long,
|
||||
env = "AETHER_TUNNEL_IPV6_ONLY",
|
||||
default_value_t = false,
|
||||
action = clap::ArgAction::Set,
|
||||
default_missing_value = "true",
|
||||
num_args = 0..=1,
|
||||
require_equals = true
|
||||
)]
|
||||
pub tunnel_ipv6_only: bool,
|
||||
|
||||
/// WebSocket tunnel TCP keepalive in seconds (0 disables)
|
||||
#[arg(long, env = "AETHER_TUNNEL_TCP_KEEPALIVE", default_value_t = 30)]
|
||||
pub tunnel_tcp_keepalive_secs: u64,
|
||||
@@ -655,6 +679,9 @@ impl Config {
|
||||
if tunnel_connect_timeout.is_zero() {
|
||||
anyhow::bail!("effective tunnel connect timeout must be > 0");
|
||||
}
|
||||
if self.tunnel_ipv4_only && self.tunnel_ipv6_only {
|
||||
anyhow::bail!("tunnel_ipv4_only and tunnel_ipv6_only cannot both be enabled");
|
||||
}
|
||||
let tunnel_ping_interval = self.tunnel_ping_interval()?;
|
||||
if tunnel_ping_interval.is_zero() {
|
||||
anyhow::bail!("effective tunnel ping interval must be > 0");
|
||||
@@ -763,6 +790,16 @@ impl Config {
|
||||
Ok(Duration::from_millis(self.tunnel_connect_timeout_ms))
|
||||
}
|
||||
|
||||
pub fn tunnel_ip_family(&self) -> crate::egress_proxy::IpFamily {
|
||||
if self.tunnel_ipv4_only {
|
||||
crate::egress_proxy::IpFamily::Ipv4Only
|
||||
} else if self.tunnel_ipv6_only {
|
||||
crate::egress_proxy::IpFamily::Ipv6Only
|
||||
} else {
|
||||
crate::egress_proxy::IpFamily::Any
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tunnel_stale_timeout(&self) -> anyhow::Result<Duration> {
|
||||
Ok(Duration::from_millis(self.tunnel_stale_timeout_ms))
|
||||
}
|
||||
@@ -959,6 +996,10 @@ pub struct ConfigFile {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tunnel_connect_timeout_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tunnel_ipv4_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tunnel_ipv6_only: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tunnel_tcp_keepalive_secs: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tunnel_tcp_nodelay: Option<bool>,
|
||||
@@ -1147,6 +1188,8 @@ impl ConfigFile {
|
||||
TUNNEL_CONNECT_TIMEOUT_MS_ENV,
|
||||
self.tunnel_connect_timeout_ms
|
||||
);
|
||||
set!("AETHER_TUNNEL_IPV4_ONLY", self.tunnel_ipv4_only);
|
||||
set!("AETHER_TUNNEL_IPV6_ONLY", self.tunnel_ipv6_only);
|
||||
set!(
|
||||
"AETHER_TUNNEL_TCP_KEEPALIVE",
|
||||
self.tunnel_tcp_keepalive_secs
|
||||
@@ -1338,6 +1381,20 @@ mod tests {
|
||||
assert_eq!(cfg.allow_private_targets, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_deserializes_tunnel_ip_family_flags() {
|
||||
let cfg: ConfigFile = toml::from_str(
|
||||
r#"
|
||||
tunnel_ipv4_only = true
|
||||
tunnel_ipv6_only = false
|
||||
"#,
|
||||
)
|
||||
.expect("tunnel IP-family TOML");
|
||||
|
||||
assert_eq!(cfg.tunnel_ipv4_only, Some(true));
|
||||
assert_eq!(cfg.tunnel_ipv6_only, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_deserializes_upstream_proxy_url() {
|
||||
let cfg: ConfigFile = toml::from_str("upstream_proxy_url = \"http://proxy.example:8080\"")
|
||||
@@ -1531,6 +1588,131 @@ node_name = "tunnel-test"
|
||||
assert!(config.allow_private_targets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_defaults_tunnel_ip_family_to_any() {
|
||||
let config = Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
]);
|
||||
|
||||
assert!(!config.tunnel_ipv4_only);
|
||||
assert!(!config.tunnel_ipv6_only);
|
||||
assert_eq!(
|
||||
config.tunnel_ip_family(),
|
||||
crate::egress_proxy::IpFamily::Any
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_accepts_tunnel_ipv4_only() {
|
||||
let config = Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
"--tunnel-ipv4-only",
|
||||
]);
|
||||
|
||||
assert!(config.tunnel_ipv4_only);
|
||||
assert_eq!(
|
||||
config.tunnel_ip_family(),
|
||||
crate::egress_proxy::IpFamily::Ipv4Only
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_accepts_tunnel_ipv6_only() {
|
||||
let config = Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
"--tunnel-ipv6-only",
|
||||
]);
|
||||
|
||||
assert!(config.tunnel_ipv6_only);
|
||||
assert_eq!(
|
||||
config.tunnel_ip_family(),
|
||||
crate::egress_proxy::IpFamily::Ipv6Only
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_conflicting_tunnel_ip_family_flags_before_validation() {
|
||||
let config = Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
"--tunnel-ipv4-only",
|
||||
"--tunnel-ipv6-only",
|
||||
]);
|
||||
|
||||
assert!(config.tunnel_ipv4_only);
|
||||
assert!(config.tunnel_ipv6_only);
|
||||
let error = config
|
||||
.validate()
|
||||
.expect_err("conflicting tunnel IP-family flags should fail validation");
|
||||
assert!(error.to_string().contains("tunnel_ipv4_only"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_accepts_explicit_false_tunnel_ip_family_flags() {
|
||||
let config = Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
"--tunnel-ipv4-only=false",
|
||||
"--tunnel-ipv6-only=false",
|
||||
]);
|
||||
|
||||
assert!(!config.tunnel_ipv4_only);
|
||||
assert!(!config.tunnel_ipv6_only);
|
||||
config
|
||||
.validate()
|
||||
.expect("explicit false family flags should be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_conflicting_toml_tunnel_ip_family_flags() {
|
||||
let config = Config {
|
||||
tunnel_ipv4_only: true,
|
||||
tunnel_ipv6_only: true,
|
||||
..Config::parse_from([
|
||||
"aether-tunnel",
|
||||
"--aether-url",
|
||||
"https://example.com",
|
||||
"--management-token",
|
||||
"ae_test",
|
||||
"--node-name",
|
||||
"tunnel-test",
|
||||
])
|
||||
};
|
||||
|
||||
let error = config
|
||||
.validate()
|
||||
.expect_err("conflicting TOML-injected tunnel family flags should fail validation");
|
||||
assert!(error.to_string().contains("tunnel_ipv4_only"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_fast_recovery_defaults_use_millisecond_values() {
|
||||
let config = Config::parse_from([
|
||||
|
||||
@@ -8,6 +8,31 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum IpFamily {
|
||||
Any,
|
||||
Ipv4Only,
|
||||
Ipv6Only,
|
||||
}
|
||||
|
||||
impl IpFamily {
|
||||
pub(crate) fn allows(self, addr: SocketAddr) -> bool {
|
||||
match self {
|
||||
Self::Any => true,
|
||||
Self::Ipv4Only => addr.is_ipv4(),
|
||||
Self::Ipv6Only => addr.is_ipv6(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn no_address_message(self, context: &str) -> String {
|
||||
match self {
|
||||
Self::Any => format!("{context} DNS returned no addresses"),
|
||||
Self::Ipv4Only => format!("{context} DNS returned no IPv4 addresses"),
|
||||
Self::Ipv6Only => format!("{context} DNS returned no IPv6 addresses"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum UpstreamProxyScheme {
|
||||
Http,
|
||||
@@ -126,6 +151,7 @@ pub(crate) struct ProxyConnectOptions {
|
||||
pub connect_timeout: Duration,
|
||||
pub tcp_nodelay: bool,
|
||||
pub tcp_keepalive: Option<Duration>,
|
||||
pub ip_family: IpFamily,
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_target_via_proxy(
|
||||
@@ -139,6 +165,7 @@ pub(crate) async fn connect_target_via_proxy(
|
||||
options.connect_timeout,
|
||||
options.tcp_nodelay,
|
||||
options.tcp_keepalive,
|
||||
options.ip_family,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -159,6 +186,7 @@ pub(crate) async fn connect_proxy_tcp(
|
||||
connect_timeout: Duration,
|
||||
tcp_nodelay: bool,
|
||||
tcp_keepalive: Option<Duration>,
|
||||
ip_family: IpFamily,
|
||||
) -> io::Result<TcpStream> {
|
||||
let resolved = tokio::time::timeout(
|
||||
connect_timeout,
|
||||
@@ -169,7 +197,7 @@ pub(crate) async fn connect_proxy_tcp(
|
||||
.map_err(|err| io::Error::other(format!("proxy DNS failed: {err}")))?;
|
||||
|
||||
let mut last_error = None;
|
||||
for addr in resolved {
|
||||
for addr in resolved.filter(|addr| ip_family.allows(*addr)) {
|
||||
match tokio::time::timeout(connect_timeout, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(stream)) => {
|
||||
configure_tcp_stream(&stream, tcp_nodelay, tcp_keepalive)?;
|
||||
@@ -185,7 +213,7 @@ pub(crate) async fn connect_proxy_tcp(
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or_else(|| io::Error::other("proxy DNS returned no addresses")))
|
||||
Err(last_error.unwrap_or_else(|| io::Error::other(ip_family.no_address_message("proxy"))))
|
||||
}
|
||||
|
||||
fn configure_tcp_stream(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! WebSocket tunnel client: connect, authenticate, and run the tunnel.
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -10,7 +12,9 @@ use tokio_tungstenite::tungstenite::http;
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::egress_proxy::{connect_target_via_proxy, ProxyConnectOptions, UpstreamProxyConfig};
|
||||
use crate::egress_proxy::{
|
||||
connect_target_via_proxy, IpFamily, ProxyConnectOptions, UpstreamProxyConfig,
|
||||
};
|
||||
use crate::state::{AppState, ServerContext};
|
||||
use aether_contracts::tunnel::{CURRENT_TUNNEL_PROTOCOL_VERSION, TUNNEL_PROTOCOL_VERSION_HEADER};
|
||||
|
||||
@@ -325,6 +329,7 @@ async fn connect_tunnel_tcp(
|
||||
tcp_nodelay: state.config.tunnel_tcp_nodelay,
|
||||
tcp_keepalive: (state.config.tunnel_tcp_keepalive_secs > 0)
|
||||
.then(|| Duration::from_secs(state.config.tunnel_tcp_keepalive_secs)),
|
||||
ip_family: state.config.tunnel_ip_family(),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -338,15 +343,54 @@ async fn connect_tunnel_tcp(
|
||||
.map_err(anyhow::Error::from);
|
||||
}
|
||||
|
||||
tokio::time::timeout(connect_timeout, TcpStream::connect((host, port)))
|
||||
let ip_family = state.config.tunnel_ip_family();
|
||||
tokio::time::timeout(
|
||||
connect_timeout,
|
||||
connect_direct_tunnel_tcp(host, port, ip_family),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"tunnel TCP connect timeout ({}ms)",
|
||||
connect_timeout.as_millis()
|
||||
)
|
||||
})?
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
async fn connect_direct_tunnel_tcp(
|
||||
host: &str,
|
||||
port: u16,
|
||||
ip_family: IpFamily,
|
||||
) -> io::Result<TcpStream> {
|
||||
let resolved = tokio::net::lookup_host((host, port))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"tunnel TCP connect timeout ({}ms)",
|
||||
connect_timeout.as_millis()
|
||||
)
|
||||
})?
|
||||
.map_err(anyhow::Error::from)
|
||||
.map_err(|err| io::Error::other(format!("tunnel DNS failed: {err}")))?;
|
||||
let addrs = filter_socket_addrs(resolved, ip_family);
|
||||
|
||||
if addrs.is_empty() {
|
||||
return Err(io::Error::other(ip_family.no_address_message("tunnel")));
|
||||
}
|
||||
|
||||
let mut last_error = None;
|
||||
for addr in addrs {
|
||||
match TcpStream::connect(addr).await {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.unwrap_or_else(|| io::Error::other("tunnel DNS returned no addresses")))
|
||||
}
|
||||
|
||||
fn filter_socket_addrs(
|
||||
addrs: impl IntoIterator<Item = SocketAddr>,
|
||||
ip_family: IpFamily,
|
||||
) -> Vec<SocketAddr> {
|
||||
addrs
|
||||
.into_iter()
|
||||
.filter(|addr| ip_family.allows(*addr))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Configure TCP keepalive and NODELAY on an established socket.
|
||||
@@ -392,3 +436,40 @@ fn build_tunnel_url(server: &ServerContext) -> String {
|
||||
};
|
||||
format!("{}/api/internal/proxy-tunnel", ws_base)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn mixed_addrs() -> Vec<SocketAddr> {
|
||||
vec![
|
||||
SocketAddr::from((Ipv6Addr::LOCALHOST, 443)),
|
||||
SocketAddr::from((Ipv4Addr::LOCALHOST, 443)),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_socket_addrs_keeps_all_addresses_by_default() {
|
||||
let addrs = filter_socket_addrs(mixed_addrs(), IpFamily::Any);
|
||||
|
||||
assert_eq!(addrs.len(), 2);
|
||||
assert!(addrs[0].is_ipv6());
|
||||
assert!(addrs[1].is_ipv4());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_socket_addrs_keeps_only_ipv4_addresses() {
|
||||
let addrs = filter_socket_addrs(mixed_addrs(), IpFamily::Ipv4Only);
|
||||
|
||||
assert_eq!(addrs, vec![SocketAddr::from((Ipv4Addr::LOCALHOST, 443))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_socket_addrs_keeps_only_ipv6_addresses() {
|
||||
let addrs = filter_socket_addrs(mixed_addrs(), IpFamily::Ipv6Only);
|
||||
|
||||
assert_eq!(addrs, vec![SocketAddr::from((Ipv6Addr::LOCALHOST, 443))]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,6 +541,8 @@ mod tests {
|
||||
tunnel_ping_interval_ms: 1_000,
|
||||
tunnel_max_streams: Some(8),
|
||||
tunnel_connect_timeout_ms: 2_000,
|
||||
tunnel_ipv4_only: false,
|
||||
tunnel_ipv6_only: false,
|
||||
tunnel_tcp_keepalive_secs: 30,
|
||||
tunnel_tcp_nodelay: true,
|
||||
tunnel_stale_timeout_ms: 5_000,
|
||||
|
||||
@@ -2556,6 +2556,8 @@ mod tests {
|
||||
tunnel_ping_interval_ms: 15_000,
|
||||
tunnel_max_streams: Some(8),
|
||||
tunnel_connect_timeout_ms: 15_000,
|
||||
tunnel_ipv4_only: false,
|
||||
tunnel_ipv6_only: false,
|
||||
tunnel_tcp_keepalive_secs: 30,
|
||||
tunnel_tcp_nodelay: true,
|
||||
tunnel_stale_timeout_ms: 45_000,
|
||||
|
||||
@@ -281,6 +281,7 @@ impl Service<Uri> for InstrumentedConnector {
|
||||
connect_timeout: self.connect_timeout,
|
||||
tcp_nodelay: self.tcp_nodelay,
|
||||
tcp_keepalive: self.tcp_keepalive,
|
||||
ip_family: crate::egress_proxy::IpFamily::Any,
|
||||
};
|
||||
let connect_start = std::time::Instant::now();
|
||||
return Box::pin(async move {
|
||||
@@ -347,6 +348,7 @@ async fn connect_via_proxy(
|
||||
options.connect_timeout,
|
||||
options.tcp_nodelay,
|
||||
options.tcp_keepalive,
|
||||
options.ip_family,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user