refactor(tunnel): rename aether-proxy to aether-tunnel

This commit is contained in:
fawney19
2026-05-20 01:02:01 +08:00
parent f4d0d5904a
commit 94760dbc14
57 changed files with 939 additions and 889 deletions

View File

@@ -25,6 +25,7 @@ pub(crate) fn mount_public_support_routes(router: Router<AppState>) -> Router<Ap
.route("/api/capabilities/user-configurable", get(proxy_request))
.route("/api/capabilities/model/{*model_path}", get(proxy_request))
.route("/install/{*install_path}", get(proxy_request))
.route("/install-tunnel/{*install_path}", get(proxy_request))
.route("/install-proxy/{*install_path}", get(proxy_request))
.route("/i/{*install_path}", get(proxy_request))
.route("/", get(proxy_request))

View File

@@ -755,6 +755,7 @@ pub(super) fn classify_public_support_route(
))
} else if method == http::Method::GET
&& (has_single_segment_after_prefix(normalized_path, "/install/")
|| has_single_segment_after_prefix(normalized_path, "/install-tunnel/")
|| has_single_segment_after_prefix(normalized_path, "/install-proxy/")
|| has_single_segment_after_prefix(normalized_path, "/i/"))
{

View File

@@ -380,7 +380,7 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
};
if !existing.tunnel_mode {
return Ok(Some(bad_request_response(
"non-tunnel mode is no longer supported, please upgrade aether-proxy to use tunnel mode",
"non-tunnel mode is no longer supported, please upgrade aether-tunnel to use tunnel mode",
)));
}
let Some(node) = state.apply_proxy_node_heartbeat(&mutation).await? else {
@@ -1049,7 +1049,7 @@ async fn test_proxy_node_connectivity(
None,
None,
Some(
"non-tunnel mode is no longer supported, please upgrade aether-proxy to use tunnel mode"
"non-tunnel mode is no longer supported, please upgrade aether-tunnel to use tunnel mode"
.to_string(),
),
);
@@ -1559,7 +1559,8 @@ fn admin_proxy_node_test_node_id_from_path(path: &str) -> Option<String> {
fn normalize_proxy_upgrade_version(value: &str) -> String {
value
.trim()
.strip_prefix("proxy-v")
.strip_prefix("tunnel-v")
.or_else(|| value.trim().strip_prefix("proxy-v"))
.unwrap_or(value.trim())
.to_ascii_lowercase()
}
@@ -2155,7 +2156,7 @@ async fn create_proxy_install_management_token(
user,
token_hash: hash_proxy_install_management_token(&raw_token),
token_prefix: proxy_install_management_token_prefix(&raw_token),
name: format!("aether-proxy {node_name} {short_id}"),
name: format!("aether-tunnel {node_name} {short_id}"),
description: Some("Created by proxy node one-click installer".to_string()),
allowed_ips: None,
permissions: Some(json!(["admin:proxy_nodes:write"])),

View File

@@ -14,11 +14,11 @@ use super::{
const INSTALL_SESSION_TTL_SECS: u64 = 15 * 60;
const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
const PROXY_INSTALL_SESSION_KEY_PREFIX: &str = "proxy-install:session:";
const PROXY_INSTALL_UNIX_SCRIPT_URL: &str =
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.sh";
const PROXY_INSTALL_POWERSHELL_SCRIPT_URL: &str =
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.ps1";
const TUNNEL_INSTALL_SESSION_KEY_PREFIX: &str = "tunnel-install:session:";
const TUNNEL_INSTALL_UNIX_SCRIPT_URL: &str =
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh";
const TUNNEL_INSTALL_POWERSHELL_SCRIPT_URL: &str =
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1";
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
@@ -55,7 +55,7 @@ struct StoredInstallSession {
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredProxyInstallSession {
struct StoredTunnelInstallSession {
aether_url: String,
management_token: String,
node_name: String,
@@ -91,9 +91,10 @@ fn install_code_from_path(request_path: &str) -> Option<(String, bool)> {
(!code.is_empty()).then(|| (code.to_string(), is_powershell))
}
fn proxy_install_code_from_path(request_path: &str) -> Option<(String, bool)> {
fn tunnel_install_code_from_path(request_path: &str) -> Option<(String, bool)> {
let raw = request_path
.strip_prefix("/install-proxy/")?
.strip_prefix("/install-tunnel/")
.or_else(|| request_path.strip_prefix("/install-proxy/"))?
.trim()
.trim_matches('/');
if raw.is_empty() || raw.contains('/') {
@@ -108,8 +109,8 @@ fn install_session_runtime_key(code: &str) -> String {
format!("{INSTALL_SESSION_KEY_PREFIX}{code}")
}
fn proxy_install_session_runtime_key(code: &str) -> String {
format!("{PROXY_INSTALL_SESSION_KEY_PREFIX}{code}")
fn tunnel_install_session_runtime_key(code: &str) -> String {
format!("{TUNNEL_INSTALL_SESSION_KEY_PREFIX}{code}")
}
fn generate_install_code() -> String {
@@ -164,42 +165,42 @@ fn powershell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
fn build_proxy_unix_script(session: &StoredProxyInstallSession) -> String {
fn build_tunnel_unix_script(session: &StoredTunnelInstallSession) -> String {
format!(
r###"#!/bin/sh
set -eu
export AETHER_PROXY_AETHER_URL={aether_url}
export AETHER_PROXY_MANAGEMENT_TOKEN={management_token}
export AETHER_PROXY_NODE_NAME={node_name}
export AETHER_TUNNEL_AETHER_URL={aether_url}
export AETHER_TUNNEL_MANAGEMENT_TOKEN={management_token}
export AETHER_TUNNEL_NODE_NAME={node_name}
if command -v curl >/dev/null 2>&1; then
curl -fsSL {script_url} | sh
elif command -v wget >/dev/null 2>&1; then
wget -qO- {script_url} | sh
else
printf '%s\n' "[Aether Proxy] 需要 curl 或 wget 下载安装脚本" >&2
printf '%s\n' "[Aether Tunnel] 需要 curl 或 wget 下载安装脚本" >&2
exit 1
fi
"###,
aether_url = shell_single_quote(&session.aether_url),
management_token = shell_single_quote(&session.management_token),
node_name = shell_single_quote(&session.node_name),
script_url = shell_single_quote(PROXY_INSTALL_UNIX_SCRIPT_URL),
script_url = shell_single_quote(TUNNEL_INSTALL_UNIX_SCRIPT_URL),
)
}
fn build_proxy_powershell_script(session: &StoredProxyInstallSession) -> String {
fn build_tunnel_powershell_script(session: &StoredTunnelInstallSession) -> String {
format!(
r###"$ErrorActionPreference = 'Stop'
$env:AETHER_PROXY_AETHER_URL = {aether_url}
$env:AETHER_PROXY_MANAGEMENT_TOKEN = {management_token}
$env:AETHER_PROXY_NODE_NAME = {node_name}
$env:AETHER_TUNNEL_AETHER_URL = {aether_url}
$env:AETHER_TUNNEL_MANAGEMENT_TOKEN = {management_token}
$env:AETHER_TUNNEL_NODE_NAME = {node_name}
irm {script_url} | iex
"###,
aether_url = powershell_single_quote(&session.aether_url),
management_token = powershell_single_quote(&session.management_token),
node_name = powershell_single_quote(&session.node_name),
script_url = powershell_single_quote(PROXY_INSTALL_POWERSHELL_SCRIPT_URL),
script_url = powershell_single_quote(TUNNEL_INSTALL_POWERSHELL_SCRIPT_URL),
)
}
@@ -659,7 +660,7 @@ pub(crate) async fn build_proxy_node_install_session_response(
) -> Response<Body> {
let code = generate_install_code();
let expires_at_unix_secs = unix_secs_now().saturating_add(INSTALL_SESSION_TTL_SECS);
let session = StoredProxyInstallSession {
let session = StoredTunnelInstallSession {
aether_url: base_url_from_request(headers, request_context),
management_token,
node_name,
@@ -670,14 +671,14 @@ pub(crate) async fn build_proxy_node_install_session_response(
Err(err) => {
return build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("proxy install session serialize failed: {err:?}"),
format!("tunnel install session serialize failed: {err:?}"),
false,
)
}
};
if let Err(err) = state
.runtime_kv_setex(
&proxy_install_session_runtime_key(&code),
&tunnel_install_session_runtime_key(&code),
&serialized,
INSTALL_SESSION_TTL_SECS,
)
@@ -685,7 +686,7 @@ pub(crate) async fn build_proxy_node_install_session_response(
{
return build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("proxy install session create failed: {err:?}"),
format!("tunnel install session create failed: {err:?}"),
false,
);
}
@@ -697,8 +698,8 @@ pub(crate) async fn build_proxy_node_install_session_response(
"expires_in_seconds": INSTALL_SESSION_TTL_SECS,
"node_name": session.node_name,
"aether_url": session.aether_url,
"unix_command": format!("curl -fsSL {base_url}/install-proxy/{code} | sh"),
"powershell_command": format!("irm {base_url}/install-proxy/{code}.ps1 | iex"),
"unix_command": format!("curl -fsSL {base_url}/install-tunnel/{code} | sh"),
"powershell_command": format!("irm {base_url}/install-tunnel/{code}.ps1 | iex"),
}))
.into_response()
}
@@ -711,8 +712,10 @@ pub(super) async fn maybe_build_local_install_response(
if decision.route_family.as_deref() != Some("install") {
return None;
}
if request_context.request_path.starts_with("/install-proxy/") {
return Some(maybe_build_local_proxy_install_response(state, request_context).await);
if request_context.request_path.starts_with("/install-tunnel/")
|| request_context.request_path.starts_with("/install-proxy/")
{
return Some(maybe_build_local_tunnel_install_response(state, request_context).await);
}
let Some((code, wants_powershell)) = install_code_from_path(&request_context.request_path)
else {
@@ -789,45 +792,45 @@ pub(super) async fn maybe_build_local_install_response(
Some(response)
}
async fn maybe_build_local_proxy_install_response(
async fn maybe_build_local_tunnel_install_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
) -> Response<Body> {
let Some((code, wants_powershell)) =
proxy_install_code_from_path(&request_context.request_path)
tunnel_install_code_from_path(&request_context.request_path)
else {
return build_auth_error_response(
http::StatusCode::NOT_FOUND,
"proxy install code 不存在或已失效",
"tunnel install code 不存在或已失效",
false,
);
};
let raw = match state
.runtime_kv_getdel(&proxy_install_session_runtime_key(&code))
.runtime_kv_getdel(&tunnel_install_session_runtime_key(&code))
.await
{
Ok(Some(value)) => value,
Ok(None) => {
return build_auth_error_response(
http::StatusCode::NOT_FOUND,
"proxy install code 不存在、已过期或已使用",
"tunnel install code 不存在、已过期或已使用",
false,
)
}
Err(err) => {
return build_auth_error_response(
http::StatusCode::INTERNAL_SERVER_ERROR,
format!("proxy install session lookup failed: {err:?}"),
format!("tunnel install session lookup failed: {err:?}"),
false,
)
}
};
let session = match serde_json::from_str::<StoredProxyInstallSession>(&raw) {
let session = match serde_json::from_str::<StoredTunnelInstallSession>(&raw) {
Ok(value) => value,
Err(_) => {
return build_auth_error_response(
http::StatusCode::BAD_REQUEST,
"proxy install code 数据无效",
"tunnel install code 数据无效",
false,
)
}
@@ -835,14 +838,14 @@ async fn maybe_build_local_proxy_install_response(
if session.expires_at_unix_secs <= unix_secs_now() {
return build_auth_error_response(
http::StatusCode::NOT_FOUND,
"proxy install code 已过期",
"tunnel install code 已过期",
false,
);
}
let body = if wants_powershell {
build_proxy_powershell_script(&session)
build_tunnel_powershell_script(&session)
} else {
build_proxy_unix_script(&session)
build_tunnel_unix_script(&session)
};
let content_type = if wants_powershell {
"text/plain; charset=utf-8"
@@ -885,8 +888,8 @@ mod tests {
}
}
fn test_proxy_session() -> StoredProxyInstallSession {
StoredProxyInstallSession {
fn test_tunnel_session() -> StoredTunnelInstallSession {
StoredTunnelInstallSession {
aether_url: "https://aether.example".to_string(),
management_token: "ae-test-token".to_string(),
node_name: "jp-proxy-01".to_string(),
@@ -895,41 +898,45 @@ mod tests {
}
#[test]
fn proxy_install_path_accepts_shell_and_powershell_codes() {
fn tunnel_install_path_accepts_shell_and_powershell_codes() {
assert_eq!(
proxy_install_code_from_path("/install-proxy/abc123"),
tunnel_install_code_from_path("/install-tunnel/abc123"),
Some(("abc123".to_string(), false))
);
assert_eq!(
proxy_install_code_from_path("/install-proxy/abc123.ps1"),
tunnel_install_code_from_path("/install-tunnel/abc123.ps1"),
Some(("abc123".to_string(), true))
);
assert_eq!(proxy_install_code_from_path("/install-proxy/a/b"), None);
assert_eq!(
tunnel_install_code_from_path("/install-proxy/abc123"),
Some(("abc123".to_string(), false))
);
assert_eq!(tunnel_install_code_from_path("/install-tunnel/a/b"), None);
}
#[test]
fn proxy_unix_script_exports_session_values_and_reuses_proxy_installer() {
let script = build_proxy_unix_script(&test_proxy_session());
fn tunnel_unix_script_exports_session_values_and_reuses_tunnel_installer() {
let script = build_tunnel_unix_script(&test_tunnel_session());
assert!(script.contains("export AETHER_PROXY_AETHER_URL='https://aether.example'"));
assert!(script.contains("export AETHER_PROXY_MANAGEMENT_TOKEN='ae-test-token'"));
assert!(script.contains("export AETHER_PROXY_NODE_NAME='jp-proxy-01'"));
assert!(script.contains("export AETHER_TUNNEL_AETHER_URL='https://aether.example'"));
assert!(script.contains("export AETHER_TUNNEL_MANAGEMENT_TOKEN='ae-test-token'"));
assert!(script.contains("export AETHER_TUNNEL_NODE_NAME='jp-proxy-01'"));
assert!(script.contains(
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.sh"
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh"
));
assert!(!script.contains("aether-rust-pioneer"));
assert!(!script.contains("[[servers]]"));
}
#[test]
fn proxy_powershell_script_exports_session_values_and_reuses_proxy_installer() {
let script = build_proxy_powershell_script(&test_proxy_session());
fn tunnel_powershell_script_exports_session_values_and_reuses_tunnel_installer() {
let script = build_tunnel_powershell_script(&test_tunnel_session());
assert!(script.contains("$env:AETHER_PROXY_AETHER_URL = 'https://aether.example'"));
assert!(script.contains("$env:AETHER_PROXY_MANAGEMENT_TOKEN = 'ae-test-token'"));
assert!(script.contains("$env:AETHER_PROXY_NODE_NAME = 'jp-proxy-01'"));
assert!(script.contains("$env:AETHER_TUNNEL_AETHER_URL = 'https://aether.example'"));
assert!(script.contains("$env:AETHER_TUNNEL_MANAGEMENT_TOKEN = 'ae-test-token'"));
assert!(script.contains("$env:AETHER_TUNNEL_NODE_NAME = 'jp-proxy-01'"));
assert!(script.contains(
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.ps1"
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1"
));
assert!(!script.contains("aether-rust-pioneer"));
assert!(!script.contains("[[servers]]"));

View File

@@ -956,7 +956,8 @@ fn resolve_rollout_probe_config(
fn normalize_rollout_version(version: &str) -> String {
version
.trim()
.strip_prefix("proxy-v")
.strip_prefix("tunnel-v")
.or_else(|| version.trim().strip_prefix("proxy-v"))
.unwrap_or(version.trim())
.to_ascii_lowercase()
}

View File

@@ -253,7 +253,7 @@ async fn proxy_upgrade_rollout_advances_next_wave_after_version_health_confirmat
dns_failures_delta: Some(0),
stream_errors_delta: Some(0),
proxy_metadata: Some(json!({"version": "2.0.0"})),
proxy_version: Some("proxy-v2.0.0".to_string()),
proxy_version: Some("tunnel-v2.0.0".to_string()),
})
.await
.expect("heartbeat should succeed");

View File

@@ -89,6 +89,7 @@ fn frontend_path_bypasses_static(path: &str) -> bool {
|| path.starts_with("/_gateway/")
|| path.starts_with("/.well-known/")
|| path.starts_with("/install/")
|| path.starts_with("/install-tunnel/")
|| path.starts_with("/install-proxy/")
|| path.starts_with("/i/")
}

View File

@@ -1396,7 +1396,7 @@ fn usage_reporting_does_not_log_raw_report_context() {
#[test]
fn proxy_registration_client_does_not_log_raw_management_response_body() {
let source = read_workspace_file("apps/aether-proxy/src/registration/client.rs");
let source = read_workspace_file("apps/aether-tunnel/src/registration/client.rs");
assert!(
!source.contains("error!(body = %text"),
"registration/client.rs should not log raw management response bodies"

View File

@@ -1,6 +1,6 @@
/// Proxy-side WebSocket connection handler
///
/// Handles the lifecycle of a single aether-proxy connection:
/// Handles the lifecycle of a single aether-tunnel connection:
/// accept -> authenticate (headers) -> read loop -> cleanup
use std::sync::Arc;
use std::time::Duration;

View File

@@ -1,19 +0,0 @@
# Aether server URL
AETHER_PROXY_AETHER_URL=https://aether.example.com
# Management Token (ae_xxx, must belong to an ADMIN user)
AETHER_PROXY_MANAGEMENT_TOKEN=ae_xxxxx
# Node identification
AETHER_PROXY_NODE_NAME=jp-proxy-01
# Maximum request body buffered for 307/308 replay (supports K/M/G, 0 disables body replay buffering)
AETHER_PROXY_REDIRECT_REPLAY_BUDGET_BYTES=5M
# Logging
AETHER_PROXY_LOG_LEVEL=info
AETHER_PROXY_LOG_DESTINATION=stdout
AETHER_PROXY_LOG_DIR=/var/log/aether-proxy
AETHER_PROXY_LOG_ROTATION=daily
AETHER_PROXY_LOG_RETENTION_DAYS=7
AETHER_PROXY_LOG_MAX_FILES=30

View File

@@ -1,231 +0,0 @@
# aether-proxy
Aether Tunnel 代理节点,部署在海外 VPS 上,通过 WebSocket 隧道为 Aether 实例中转 API 流量。
Tunnel 模式下代理节点**无需对外监听端口**,仅需出站连接到 Aether 服务器。
## 安装
`aether-proxy` 会根据宿主机自动选择服务管理器:
- 常规 Linux 发行版:`systemd`
- Alpine Linux`OpenRC`
### 下载预编译二进制
<!-- DOWNLOAD_TABLE_START -->
| Platform | Download |
|----------|----------|
| Linux x86_64 (GNU) | [aether-proxy-linux-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-linux-amd64.tar.gz) |
| Linux ARM64 (GNU) | [aether-proxy-linux-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-linux-arm64.tar.gz) |
| Linux x86_64 (musl) | [aether-proxy-linux-musl-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-linux-musl-amd64.tar.gz) |
| Linux ARM64 (musl) | [aether-proxy-linux-musl-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-linux-musl-arm64.tar.gz) |
| macOS x86_64 | [aether-proxy-macos-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-macos-amd64.tar.gz) |
| macOS ARM64 | [aether-proxy-macos-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-macos-arm64.tar.gz) |
| Windows x86_64 | [aether-proxy-windows-amd64.zip](https://github.com/fawney19/Aether/releases/download/proxy-v0.3.11/aether-proxy-windows-amd64.zip) |
<!-- DOWNLOAD_TABLE_END -->
上表展示的是最新已发布版本的下载链接。从下一次 `proxy-v*` 发布开始,表格会自动补上 `Linux x86_64 (musl)` / `Linux ARM64 (musl)` 包,供 Alpine 等 musl 系统直接使用。
## 快速开始
### 一键安装 / 添加节点
一键脚本会自动从 GitHub Releases 中筛选最新的 `proxy-v*` tag并按当前系统下载对应制品Linux x86_64/ARM64GNU 或 musl、macOS x86_64/ARM64、Windows x86_64。仓库的通用 `latest` release 可能不是 proxy 版本,因此脚本不会使用 `/releases/latest`
脚本会安装/更新 `aether-proxy` 二进制,并把新的服务器配置追加到 `aether-proxy.toml``[[servers]]` 数组中;如果配置文件已存在,不会覆盖原有内容。检测到相同 `aether_url + node_name` 时会跳过追加。
macOS / Linux:
```bash
curl -fsSL https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.sh | sh
```
Windows PowerShell:
```powershell
irm https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.ps1 | iex
```
也可以用环境变量非交互式执行,适合在控制台“添加代理节点”时生成命令:
```bash
curl -fsSL https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.sh | \
AETHER_PROXY_AETHER_URL="https://aether.example.com" \
AETHER_PROXY_MANAGEMENT_TOKEN="ae_xxx" \
AETHER_PROXY_NODE_NAME="jp-proxy-01" \
sh
```
```powershell
$env:AETHER_PROXY_AETHER_URL = "https://aether.example.com"
$env:AETHER_PROXY_MANAGEMENT_TOKEN = "ae_xxx"
$env:AETHER_PROXY_NODE_NAME = "jp-proxy-01"
irm https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-proxy/install.ps1 | iex
```
可选变量:`AETHER_PROXY_RELEASE_TAG` 固定安装某个 `proxy-v*` tag`AETHER_PROXY_CONFIG` 指定配置文件路径,`AETHER_PROXY_INSTALL_DIR` 指定二进制安装目录。
```bash
# 1. 首次安装配置TUI 向导,勾选 Install Service 随系统启动服务)
sudo ./aether-proxy setup
# 2. 日常管理 (勾选 Install Service 作为系统服务的情况下)
aether-proxy status # 看状态
sudo aether-proxy logs # 看日志
sudo aether-proxy start # 启动服务
sudo aether-proxy stop # 停止服务
sudo aether-proxy restart # 重启服务
# 3. 重新配置(改完自动重启服务)
sudo aether-proxy setup
# 4. 彻底卸载
sudo aether-proxy uninstall
```
完成向导后, 配置自动保存到 `aether-proxy.toml`,如果启用了 Install Service将自动注册并启动当前系统支持的服务`systemd``OpenRC`)。
### 直接运行
如果不需要安装为系统服务,可以直接运行。缺少必填参数时会自动进入 setup 向导:
```bash
./aether-proxy
```
## 配置
配置按以下优先级加载(高优先级覆盖低优先级):
1. CLI 参数
2. 环境变量(`AETHER_PROXY_*`
3. 配置文件(`aether-proxy.toml`,或通过 `AETHER_PROXY_CONFIG` 指定路径)
### 参数一览
#### 基础配置
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--aether-url` | `AETHER_PROXY_AETHER_URL` | **必填** | Aether 服务器地址 |
| `--management-token` | `AETHER_PROXY_MANAGEMENT_TOKEN` | **必填** | 管理员 Token`ae_xxx` 格式) |
| `--node-name` | `AETHER_PROXY_NODE_NAME` | **必填** | 节点名称标识 |
| `--public-ip` | `AETHER_PROXY_PUBLIC_IP` | 自动检测 | 公网 IP |
| `--node-region` | `AETHER_PROXY_NODE_REGION` | 自动检测 | 地区标识 |
| `--heartbeat-interval` | `AETHER_PROXY_HEARTBEAT_INTERVAL` | `5` | 心跳间隔(秒) |
| `--allowed-ports` | `AETHER_PROXY_ALLOWED_PORTS` | `80,443,8080,8443` | 允许代理的目标端口 |
| `--allow-private-targets` | `AETHER_PROXY_ALLOW_PRIVATE_TARGETS` | `true` | 允许 private/reserved 目标地址,通过后仍受 `allowed_ports` 限制;设为 `false` 可恢复严格拦截 |
#### Tunnel 连接
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--tunnel-connections` | `AETHER_PROXY_TUNNEL_CONNECTIONS` | 自动(硬件估算) | 最小连接池大小;显式设置后默认固定为该值 |
| `--tunnel-connections-max` | `AETHER_PROXY_TUNNEL_CONNECTIONS_MAX` | 自动(硬件估算) | 连接池自动扩容上限;大于 `tunnel_connections` 时启用 autoscale |
| `--tunnel-max-streams` | `AETHER_PROXY_TUNNEL_MAX_STREAMS` | 自动(硬件估算) | 单连接最大并发 stream 数 |
| `--tunnel-ping-interval-ms` | `AETHER_PROXY_TUNNEL_PING_INTERVAL_MS` | `10000` | WebSocket ping 周期(毫秒) |
| `--tunnel-connect-timeout-ms` | `AETHER_PROXY_TUNNEL_CONNECT_TIMEOUT_MS` | `3000` | tunnel 建连超时(毫秒) |
| `--tunnel-stale-timeout-ms` | `AETHER_PROXY_TUNNEL_STALE_TIMEOUT_MS` | `30000` | 无入站数据断连阈值(毫秒) |
| `--tunnel-scale-check-interval-ms` | `AETHER_PROXY_TUNNEL_SCALE_CHECK_INTERVAL_MS` | `1000` | autoscale 采样周期(毫秒) |
| `--tunnel-scale-up-threshold-percent` | `AETHER_PROXY_TUNNEL_SCALE_UP_THRESHOLD_PERCENT` | `50` | 单 tunnel 占用率超过该值时扩容 |
| `--tunnel-scale-down-threshold-percent` | `AETHER_PROXY_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT` | `35` | 单 tunnel 占用率持续低于该值时允许缩容 |
| `--tunnel-scale-down-grace-secs` | `AETHER_PROXY_TUNNEL_SCALE_DOWN_GRACE_SECS` | `15` | 低负载持续时间达到该值后才回收次级 tunnel |
| `--tunnel-tcp-keepalive-secs` | `AETHER_PROXY_TUNNEL_TCP_KEEPALIVE_SECS` | `30` | TCP keepalive 初始延迟(秒) |
| `--tunnel-tcp-nodelay` | `AETHER_PROXY_TUNNEL_TCP_NODELAY` | `true` | 禁用 Nagle 算法 |
| `--tunnel-reconnect-base-ms` | `AETHER_PROXY_TUNNEL_RECONNECT_BASE_MS` | `50` | 指数退避基础延迟(毫秒) |
| `--tunnel-reconnect-max-ms` | `AETHER_PROXY_TUNNEL_RECONNECT_MAX_MS` | `250` | 指数退避上限(毫秒) |
省略 `tunnel_connections`proxy 会按设备能力自动计算一个基线值和偏单机上限的扩容上限:默认至少保留 2 条常驻 tunnel并会更早触发扩容如果显式设置了 `tunnel_connections` 但没有设置 `tunnel_connections_max`,则保持固定连接池,不自动扩缩。
#### 上游 HTTP 请求
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--upstream-connect-timeout-secs` | `AETHER_PROXY_UPSTREAM_CONNECT_TIMEOUT_SECS` | `30` | 上游建连超时(秒) |
| `--upstream-pool-max-idle-per-host` | `AETHER_PROXY_UPSTREAM_POOL_MAX_IDLE_PER_HOST` | `64` | 每 Host 最大空闲连接数 |
| `--upstream-pool-idle-timeout-secs` | `AETHER_PROXY_UPSTREAM_POOL_IDLE_TIMEOUT_SECS` | `300` | 连接池空闲超时(秒) |
| `--upstream-tcp-keepalive-secs` | `AETHER_PROXY_UPSTREAM_TCP_KEEPALIVE_SECS` | `60` | TCP keepalive0 关闭) |
| `--upstream-tcp-nodelay` | `AETHER_PROXY_UPSTREAM_TCP_NODELAY` | `true` | 启用 TCP_NODELAY |
| `--upstream-proxy-url` | `AETHER_PROXY_UPSTREAM_PROXY_URL` | 空 | 仅 provider 上游请求使用的出口代理 |
| `--redirect-replay-budget-bytes` | `AETHER_PROXY_REDIRECT_REPLAY_BUDGET_BYTES` | `5M` | 307/308 请求体重放的预读预算,支持 `K/M/G``0` 表示禁用 body replay buffering |
出口代理支持 `http://``socks5://``socks5h://`。配合 WARP sidecar 时可填写:
```toml
upstream_proxy_url = "socks5h://microwarp:1080"
```
如果需要让 Aether 管理 API 和 WebSocket tunnel 也走代理,使用 `aether_proxy_url`
#### Aether API 客户端
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--aether-request-timeout-secs` | `AETHER_PROXY_AETHER_REQUEST_TIMEOUT_SECS` | `10` | 请求总超时(秒) |
| `--aether-connect-timeout-secs` | `AETHER_PROXY_AETHER_CONNECT_TIMEOUT_SECS` | `10` | 建连超时(秒) |
| `--aether-proxy-url` | `AETHER_PROXY_AETHER_PROXY_URL` | 空 | Aether 注册、心跳和 WebSocket tunnel 回连使用的出口代理(默认不走代理) |
| `--aether-retry-max-attempts` | `AETHER_PROXY_AETHER_RETRY_MAX_ATTEMPTS` | `3` | 最大重试次数 |
#### DNS 与安全
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--allow-private-targets` | `AETHER_PROXY_ALLOW_PRIVATE_TARGETS` | `true` | 默认允许 private/reserved 目标地址;设为 `false` 可恢复拦截,且仅影响重启后的进程 |
| `--dns-cache-ttl-secs` | `AETHER_PROXY_DNS_CACHE_TTL_SECS` | `60` | DNS 缓存 TTL |
| `--dns-cache-capacity` | `AETHER_PROXY_DNS_CACHE_CAPACITY` | `1024` | DNS 缓存容量(条目数) |
#### 日志
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--log-level` | `AETHER_PROXY_LOG_LEVEL` | `info` | 日志级别 |
| `--log-destination` | `AETHER_PROXY_LOG_DESTINATION` | `both` | 输出到 `stdout`、文件或两者同时输出 |
| `--log-dir` | `AETHER_PROXY_LOG_DIR` | `logs` | 文件日志目录,`file/both` 时必填 |
| `--log-rotation` | `AETHER_PROXY_LOG_ROTATION` | `daily` | 文件日志按小时或按天轮转 |
| `--log-retention-days` | `AETHER_PROXY_LOG_RETENTION_DAYS` | `7` | 文件日志保留天数 |
| `--log-max-files` | `AETHER_PROXY_LOG_MAX_FILES` | `30` | 文件日志最多保留文件数 |
### 日志落点
- 默认 `AETHER_PROXY_LOG_DESTINATION=both`,同时输出到 stdout 和 `logs/` 文件目录
- 需要只交给容器日志驱动或宿主机服务管理器时,可改成 `stdout`setup TUI 里可用 `Save Logs to File` 开关关闭文件日志
- 文件日志固定写普通文本,并支持 `hourly/daily` 轮转;默认按天轮换、保留 7 天,最多保留 30 个文件
-`systemd``OpenRC` 安装时默认会额外打开文件日志到 `/var/log/aether-proxy`
- OpenRC 安装时,`aether-proxy logs` 实际读取 `/var/log/aether-proxy/current.log``/var/log/aether-proxy/error.log`;这些文件通常需要用 `sudo aether-proxy logs` 查看
### 隧道健康上报Heartbeat
proxy 会在心跳 `proxy_metadata` 中主动上报隧道稳定性指标,便于后端直接入库/告警:
- `proxy_metadata.tunnel_metrics`:建连尝试/成功/失败、断开次数、累计在线时长、心跳 RTT、WebSocket 收发帧与字节等。
- `proxy_metadata.recent_tunnel_errors`:最近隧道异常事件(时间戳、类别、错误摘要,环形缓冲)。
说明:仅主连接(`conn=0`)发送 heartbeat避免多条 tunnel 重复上报同一份全局指标。
### 多服务器配置
`aether-proxy.toml` 中使用 `[[servers]]` 配置 Aether 服务器。即使只有一个服务器,也必须写成一个 `[[servers]]` 条目;旧的顶层单服务器写法已不再支持。
```toml
[[servers]]
aether_url = "https://aether-1.example.com"
management_token = "ae_xxx"
node_name = "jp-proxy-01"
[[servers]]
aether_url = "https://aether-2.example.com"
management_token = "ae_yyy"
node_name = "jp-proxy-02"
```
## 发布新版本
推送 `proxy-v*` 格式的 tagGitHub Actions 会自动:
- 编译所有平台二进制并发布到 Releases
- 更新 README 中的下载链接表格
```bash
git tag proxy-v0.2.0
git push origin proxy-v0.2.0
```

View File

@@ -0,0 +1,19 @@
# Aether server URL
AETHER_TUNNEL_AETHER_URL=https://aether.example.com
# Management Token (ae_xxx, must belong to an ADMIN user)
AETHER_TUNNEL_MANAGEMENT_TOKEN=ae_xxxxx
# Node identification
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
# Logging
AETHER_TUNNEL_LOG_LEVEL=info
AETHER_TUNNEL_LOG_DESTINATION=stdout
AETHER_TUNNEL_LOG_DIR=/var/log/aether-tunnel
AETHER_TUNNEL_LOG_ROTATION=daily
AETHER_TUNNEL_LOG_RETENTION_DAYS=7
AETHER_TUNNEL_LOG_MAX_FILES=30

View File

@@ -9,8 +9,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aether-proxy"
version = "0.3.11"
name = "aether-tunnel"
version = "0.3.12"
dependencies = [
"anyhow",
"arc-swap",
@@ -1059,7 +1059,7 @@ dependencies = [
[[package]]
name = "instability"
version = "0.3.11"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d"
dependencies = [

View File

@@ -1,8 +1,8 @@
[package]
name = "aether-proxy"
name = "aether-tunnel"
version = "0.3.12"
edition = "2021"
description = "Tunnel proxy for Aether"
description = "Tunnel agent for Aether"
[dependencies]
aether-contracts.workspace = true

View File

@@ -0,0 +1,231 @@
# aether-tunnel
Aether Tunnel 代理节点,部署在海外 VPS 上,通过 WebSocket 隧道为 Aether 实例中转 API 流量。
Tunnel 模式下代理节点**无需对外监听端口**,仅需出站连接到 Aether 服务器。
## 安装
`aether-tunnel` 会根据宿主机自动选择服务管理器:
- 常规 Linux 发行版:`systemd`
- Alpine Linux`OpenRC`
### 下载预编译二进制
<!-- DOWNLOAD_TABLE_START -->
| Platform | Download |
|----------|----------|
| Linux x86_64 (GNU) | [aether-tunnel-linux-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-linux-amd64.tar.gz) |
| Linux ARM64 (GNU) | [aether-tunnel-linux-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-linux-arm64.tar.gz) |
| Linux x86_64 (musl) | [aether-tunnel-linux-musl-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-linux-musl-amd64.tar.gz) |
| Linux ARM64 (musl) | [aether-tunnel-linux-musl-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-linux-musl-arm64.tar.gz) |
| macOS x86_64 | [aether-tunnel-macos-amd64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-macos-amd64.tar.gz) |
| macOS ARM64 | [aether-tunnel-macos-arm64.tar.gz](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-macos-arm64.tar.gz) |
| Windows x86_64 | [aether-tunnel-windows-amd64.zip](https://github.com/fawney19/Aether/releases/download/tunnel-v0.3.12/aether-tunnel-windows-amd64.zip) |
<!-- DOWNLOAD_TABLE_END -->
上表展示的是最新已发布版本的下载链接。从下一次 `tunnel-v*` 发布开始,表格会自动补上 `Linux x86_64 (musl)` / `Linux ARM64 (musl)` 包,供 Alpine 等 musl 系统直接使用。
## 快速开始
### 一键安装 / 添加节点
一键脚本会自动从 GitHub Releases 中筛选最新的 `tunnel-v*` tag并按当前系统下载对应制品Linux x86_64/ARM64GNU 或 musl、macOS x86_64/ARM64、Windows x86_64。仓库的通用 `latest` release 可能不是 tunnel 版本,因此脚本不会使用 `/releases/latest`
脚本会安装/更新 `aether-tunnel` 二进制,并把新的服务器配置追加到 `aether-tunnel.toml``[[servers]]` 数组中;如果配置文件已存在,不会覆盖原有内容。检测到相同 `aether_url + node_name` 时会跳过追加。
macOS / Linux:
```bash
curl -fsSL https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh | sh
```
Windows PowerShell:
```powershell
irm https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1 | iex
```
也可以用环境变量非交互式执行,适合在控制台“添加隧道节点”时生成命令:
```bash
curl -fsSL https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh | \
AETHER_TUNNEL_AETHER_URL="https://aether.example.com" \
AETHER_TUNNEL_MANAGEMENT_TOKEN="ae_xxx" \
AETHER_TUNNEL_NODE_NAME="jp-proxy-01" \
sh
```
```powershell
$env:AETHER_TUNNEL_AETHER_URL = "https://aether.example.com"
$env:AETHER_TUNNEL_MANAGEMENT_TOKEN = "ae_xxx"
$env:AETHER_TUNNEL_NODE_NAME = "jp-proxy-01"
irm https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1 | iex
```
可选变量:`AETHER_TUNNEL_RELEASE_TAG` 固定安装某个 `tunnel-v*` tag`AETHER_TUNNEL_CONFIG` 指定配置文件路径,`AETHER_TUNNEL_INSTALL_DIR` 指定二进制安装目录。
```bash
# 1. 首次安装配置TUI 向导,勾选 Install Service 随系统启动服务)
sudo ./aether-tunnel setup
# 2. 日常管理 (勾选 Install Service 作为系统服务的情况下)
aether-tunnel status # 看状态
sudo aether-tunnel logs # 看日志
sudo aether-tunnel start # 启动服务
sudo aether-tunnel stop # 停止服务
sudo aether-tunnel restart # 重启服务
# 3. 重新配置(改完自动重启服务)
sudo aether-tunnel setup
# 4. 彻底卸载
sudo aether-tunnel uninstall
```
完成向导后, 配置自动保存到 `aether-tunnel.toml`,如果启用了 Install Service将自动注册并启动当前系统支持的服务`systemd``OpenRC`)。
### 直接运行
如果不需要安装为系统服务,可以直接运行。缺少必填参数时会自动进入 setup 向导:
```bash
./aether-tunnel
```
## 配置
配置按以下优先级加载(高优先级覆盖低优先级):
1. CLI 参数
2. 环境变量(`AETHER_TUNNEL_*`
3. 配置文件(`aether-tunnel.toml`,或通过 `AETHER_TUNNEL_CONFIG` 指定路径)
### 参数一览
#### 基础配置
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--aether-url` | `AETHER_TUNNEL_AETHER_URL` | **必填** | Aether 服务器地址 |
| `--management-token` | `AETHER_TUNNEL_MANAGEMENT_TOKEN` | **必填** | 管理员 Token`ae_xxx` 格式) |
| `--node-name` | `AETHER_TUNNEL_NODE_NAME` | **必填** | 节点名称标识 |
| `--public-ip` | `AETHER_TUNNEL_PUBLIC_IP` | 自动检测 | 公网 IP |
| `--node-region` | `AETHER_TUNNEL_NODE_REGION` | 自动检测 | 地区标识 |
| `--heartbeat-interval` | `AETHER_TUNNEL_HEARTBEAT_INTERVAL` | `5` | 心跳间隔(秒) |
| `--allowed-ports` | `AETHER_TUNNEL_ALLOWED_PORTS` | `80,443,8080,8443` | 允许代理的目标端口 |
| `--allow-private-targets` | `AETHER_TUNNEL_ALLOW_PRIVATE_TARGETS` | `true` | 允许 private/reserved 目标地址,通过后仍受 `allowed_ports` 限制;设为 `false` 可恢复严格拦截 |
#### Tunnel 连接
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--tunnel-connections` | `AETHER_TUNNEL_CONNECTIONS` | 自动(硬件估算) | 最小连接池大小;显式设置后默认固定为该值 |
| `--tunnel-connections-max` | `AETHER_TUNNEL_CONNECTIONS_MAX` | 自动(硬件估算) | 连接池自动扩容上限;大于 `tunnel_connections` 时启用 autoscale |
| `--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-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 占用率超过该值时扩容 |
| `--tunnel-scale-down-threshold-percent` | `AETHER_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT` | `35` | 单 tunnel 占用率持续低于该值时允许缩容 |
| `--tunnel-scale-down-grace-secs` | `AETHER_TUNNEL_SCALE_DOWN_GRACE_SECS` | `15` | 低负载持续时间达到该值后才回收次级 tunnel |
| `--tunnel-tcp-keepalive-secs` | `AETHER_TUNNEL_TCP_KEEPALIVE_SECS` | `30` | TCP keepalive 初始延迟(秒) |
| `--tunnel-tcp-nodelay` | `AETHER_TUNNEL_TCP_NODELAY` | `true` | 禁用 Nagle 算法 |
| `--tunnel-reconnect-base-ms` | `AETHER_TUNNEL_RECONNECT_BASE_MS` | `50` | 指数退避基础延迟(毫秒) |
| `--tunnel-reconnect-max-ms` | `AETHER_TUNNEL_RECONNECT_MAX_MS` | `250` | 指数退避上限(毫秒) |
省略 `tunnel_connections`tunnel 会按设备能力自动计算一个基线值和偏单机上限的扩容上限:默认至少保留 2 条常驻 tunnel并会更早触发扩容如果显式设置了 `tunnel_connections` 但没有设置 `tunnel_connections_max`,则保持固定连接池,不自动扩缩。
#### 上游 HTTP 请求
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--upstream-connect-timeout-secs` | `AETHER_TUNNEL_UPSTREAM_CONNECT_TIMEOUT_SECS` | `30` | 上游建连超时(秒) |
| `--upstream-pool-max-idle-per-host` | `AETHER_TUNNEL_UPSTREAM_POOL_MAX_IDLE_PER_HOST` | `64` | 每 Host 最大空闲连接数 |
| `--upstream-pool-idle-timeout-secs` | `AETHER_TUNNEL_UPSTREAM_POOL_IDLE_TIMEOUT_SECS` | `300` | 连接池空闲超时(秒) |
| `--upstream-tcp-keepalive-secs` | `AETHER_TUNNEL_UPSTREAM_TCP_KEEPALIVE_SECS` | `60` | TCP keepalive0 关闭) |
| `--upstream-tcp-nodelay` | `AETHER_TUNNEL_UPSTREAM_TCP_NODELAY` | `true` | 启用 TCP_NODELAY |
| `--upstream-proxy-url` | `AETHER_TUNNEL_UPSTREAM_PROXY_URL` | 空 | 仅 provider 上游请求使用的出口代理 |
| `--redirect-replay-budget-bytes` | `AETHER_TUNNEL_REDIRECT_REPLAY_BUDGET_BYTES` | `5M` | 307/308 请求体重放的预读预算,支持 `K/M/G``0` 表示禁用 body replay buffering |
出口代理支持 `http://``socks5://``socks5h://`。配合 WARP sidecar 时可填写:
```toml
upstream_proxy_url = "socks5h://microwarp:1080"
```
如果需要让 Aether 管理 API 和 WebSocket tunnel 也走代理,使用 `aether_outbound_proxy_url`
#### Aether API 客户端
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--aether-request-timeout-secs` | `AETHER_TUNNEL_AETHER_REQUEST_TIMEOUT_SECS` | `10` | 请求总超时(秒) |
| `--aether-connect-timeout-secs` | `AETHER_TUNNEL_AETHER_CONNECT_TIMEOUT_SECS` | `10` | 建连超时(秒) |
| `--aether-outbound-proxy-url` | `AETHER_TUNNEL_AETHER_OUTBOUND_PROXY_URL` | 空 | Aether 注册、心跳和 WebSocket tunnel 回连使用的出口代理(默认不走代理) |
| `--aether-retry-max-attempts` | `AETHER_TUNNEL_AETHER_RETRY_MAX_ATTEMPTS` | `3` | 最大重试次数 |
#### DNS 与安全
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--allow-private-targets` | `AETHER_TUNNEL_ALLOW_PRIVATE_TARGETS` | `true` | 默认允许 private/reserved 目标地址;设为 `false` 可恢复拦截,且仅影响重启后的进程 |
| `--dns-cache-ttl-secs` | `AETHER_TUNNEL_DNS_CACHE_TTL_SECS` | `60` | DNS 缓存 TTL |
| `--dns-cache-capacity` | `AETHER_TUNNEL_DNS_CACHE_CAPACITY` | `1024` | DNS 缓存容量(条目数) |
#### 日志
| 参数 | 环境变量 | 默认值 | 说明 |
|------|----------|--------|------|
| `--log-level` | `AETHER_TUNNEL_LOG_LEVEL` | `info` | 日志级别 |
| `--log-destination` | `AETHER_TUNNEL_LOG_DESTINATION` | `both` | 输出到 `stdout`、文件或两者同时输出 |
| `--log-dir` | `AETHER_TUNNEL_LOG_DIR` | `logs` | 文件日志目录,`file/both` 时必填 |
| `--log-rotation` | `AETHER_TUNNEL_LOG_ROTATION` | `daily` | 文件日志按小时或按天轮转 |
| `--log-retention-days` | `AETHER_TUNNEL_LOG_RETENTION_DAYS` | `7` | 文件日志保留天数 |
| `--log-max-files` | `AETHER_TUNNEL_LOG_MAX_FILES` | `30` | 文件日志最多保留文件数 |
### 日志落点
- 默认 `AETHER_TUNNEL_LOG_DESTINATION=both`,同时输出到 stdout 和 `logs/` 文件目录
- 需要只交给容器日志驱动或宿主机服务管理器时,可改成 `stdout`setup TUI 里可用 `Save Logs to File` 开关关闭文件日志
- 文件日志固定写普通文本,并支持 `hourly/daily` 轮转;默认按天轮换、保留 7 天,最多保留 30 个文件
-`systemd``OpenRC` 安装时默认会额外打开文件日志到 `/var/log/aether-tunnel`
- OpenRC 安装时,`aether-tunnel logs` 实际读取 `/var/log/aether-tunnel/current.log``/var/log/aether-tunnel/error.log`;这些文件通常需要用 `sudo aether-tunnel logs` 查看
### 隧道健康上报Heartbeat
tunnel 会在心跳兼容字段 `proxy_metadata` 中主动上报隧道稳定性指标,便于后端直接入库/告警:
- `proxy_metadata.tunnel_metrics`:建连尝试/成功/失败、断开次数、累计在线时长、心跳 RTT、WebSocket 收发帧与字节等。
- `proxy_metadata.recent_tunnel_errors`:最近隧道异常事件(时间戳、类别、错误摘要,环形缓冲)。
说明:仅主连接(`conn=0`)发送 heartbeat避免多条 tunnel 重复上报同一份全局指标。
### 多服务器配置
`aether-tunnel.toml` 中使用 `[[servers]]` 配置 Aether 服务器。即使只有一个服务器,也必须写成一个 `[[servers]]` 条目;旧的顶层单服务器写法已不再支持。
```toml
[[servers]]
aether_url = "https://aether-1.example.com"
management_token = "ae_xxx"
node_name = "jp-proxy-01"
[[servers]]
aether_url = "https://aether-2.example.com"
management_token = "ae_yyy"
node_name = "jp-proxy-02"
```
## 发布新版本
推送 `tunnel-v*` 格式的 tagGitHub Actions 会自动:
- 编译所有平台二进制并发布到 Releases
- 更新 README 中的下载链接表格
```bash
git tag tunnel-v0.2.0
git push origin tunnel-v0.2.0
```

View File

@@ -1,12 +1,12 @@
$ErrorActionPreference = 'Stop'
$Repo = if ($env:AETHER_PROXY_RELEASE_REPO) { $env:AETHER_PROXY_RELEASE_REPO } else { 'fawney19/Aether' }
$ReleaseTag = $env:AETHER_PROXY_RELEASE_TAG
$InstallDir = $env:AETHER_PROXY_INSTALL_DIR
$ConfigPath = $env:AETHER_PROXY_CONFIG
$Repo = if ($env:AETHER_TUNNEL_RELEASE_REPO) { $env:AETHER_TUNNEL_RELEASE_REPO } else { 'fawney19/Aether' }
$ReleaseTag = $env:AETHER_TUNNEL_RELEASE_TAG
$InstallDir = $env:AETHER_TUNNEL_INSTALL_DIR
$ConfigPath = $env:AETHER_TUNNEL_CONFIG
function Say([string]$Message) { Write-Host "[Aether Proxy] $Message" }
function Fail([string]$Message) { throw "[Aether Proxy] $Message" }
function Say([string]$Message) { Write-Host "[Aether Tunnel] $Message" }
function Fail([string]$Message) { throw "[Aether Tunnel] $Message" }
function Prompt-IfEmpty([string]$Name, [string]$Value, [string]$Prompt) {
if (-not [string]::IsNullOrWhiteSpace($Value)) { return $Value }
@@ -19,13 +19,13 @@ function ConvertTo-TomlQuotedString([string]$Value) {
return ($Value | ConvertTo-Json -Compress)
}
function Resolve-LatestProxyTag {
function Resolve-LatestTunnelTag {
if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) { return $ReleaseTag }
$Uri = "https://api.github.com/repos/$Repo/releases?per_page=100"
$Releases = Invoke-RestMethod -Uri $Uri -Headers @{ 'User-Agent' = 'aether-proxy-installer' }
$ProxyReleases = @($Releases | Where-Object { -not $_.draft -and $_.tag_name -like 'proxy-v*' } | Sort-Object published_at -Descending)
if ($ProxyReleases.Count -eq 0) { Fail "No proxy-v* release found in $Repo" }
return $ProxyReleases[0].tag_name
$Releases = Invoke-RestMethod -Uri $Uri -Headers @{ 'User-Agent' = 'aether-tunnel-installer' }
$TunnelReleases = @($Releases | Where-Object { -not $_.draft -and $_.tag_name -like 'tunnel-v*' } | Sort-Object published_at -Descending)
if ($TunnelReleases.Count -eq 0) { Fail "No tunnel-v* release found in $Repo" }
return $TunnelReleases[0].tag_name
}
function Test-IsAdministrator {
@@ -37,23 +37,23 @@ function Test-IsAdministrator {
function Initialize-Paths {
if ([string]::IsNullOrWhiteSpace($script:InstallDir)) {
if (Test-IsAdministrator) {
$script:InstallDir = Join-Path $env:ProgramFiles 'AetherProxy'
$script:InstallDir = Join-Path $env:ProgramFiles 'AetherTunnel'
} else {
$script:InstallDir = Join-Path $env:LOCALAPPDATA 'AetherProxy'
$script:InstallDir = Join-Path $env:LOCALAPPDATA 'AetherTunnel'
}
}
if ([string]::IsNullOrWhiteSpace($script:ConfigPath)) {
if (Test-IsAdministrator) {
$script:ConfigPath = Join-Path $env:ProgramData 'AetherProxy\aether-proxy.toml'
$script:ConfigPath = Join-Path $env:ProgramData 'AetherTunnel\aether-tunnel.toml'
} else {
$script:ConfigPath = Join-Path $env:APPDATA 'AetherProxy\aether-proxy.toml'
$script:ConfigPath = Join-Path $env:APPDATA 'AetherTunnel\aether-tunnel.toml'
}
}
}
function Install-AetherProxyBinary([string]$Tag, [string]$TempDir) {
function Install-AetherTunnelBinary([string]$Tag, [string]$TempDir) {
if (-not [Environment]::Is64BitOperatingSystem) { Fail 'Windows release currently supports amd64 only' }
$Asset = 'aether-proxy-windows-amd64.zip'
$Asset = 'aether-tunnel-windows-amd64.zip'
$Base = "https://github.com/$Repo/releases/download/$Tag"
$Archive = Join-Path $TempDir $Asset
$Sums = Join-Path $TempDir 'SHA256SUMS.txt'
@@ -73,11 +73,11 @@ function Install-AetherProxyBinary([string]$Tag, [string]$TempDir) {
$ExtractDir = Join-Path $TempDir 'extract'
Expand-Archive -Path $Archive -DestinationPath $ExtractDir -Force
$Binary = Join-Path $ExtractDir 'aether-proxy.exe'
if (-not (Test-Path $Binary)) { Fail 'aether-proxy.exe not found in release asset' }
$Binary = Join-Path $ExtractDir 'aether-tunnel.exe'
if (-not (Test-Path $Binary)) { Fail 'aether-tunnel.exe not found in release asset' }
New-Item -ItemType Directory -Force -Path $script:InstallDir | Out-Null
Copy-Item $Binary (Join-Path $script:InstallDir 'aether-proxy.exe') -Force
Say "Installed binary: $(Join-Path $script:InstallDir 'aether-proxy.exe')"
Copy-Item $Binary (Join-Path $script:InstallDir 'aether-tunnel.exe') -Force
Say "Installed binary: $(Join-Path $script:InstallDir 'aether-tunnel.exe')"
}
function Test-LegacySingleServerConfig([string]$Path) {
@@ -110,7 +110,7 @@ function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]
New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null
if (Test-LegacySingleServerConfig $script:ConfigPath) {
Fail "Existing config uses removed top-level aether_url/management_token. Run aether-proxy setup to migrate to [[servers]] first: $script:ConfigPath"
Fail "Existing config uses removed top-level aether_url/management_token. Run aether-tunnel setup to migrate to [[servers]] first: $script:ConfigPath"
}
$QuotedUrl = ConvertTo-TomlQuotedString $AetherUrl
@@ -128,7 +128,7 @@ function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]
$Prefix = if ((Test-Path $script:ConfigPath) -and ((Get-Item $script:ConfigPath).Length -gt 0)) { "`n" } else { '' }
$Block = @(
"$Prefix# Added by Aether Proxy one-click installer. Existing config is preserved.",
"$Prefix# Added by Aether Tunnel one-click installer. Existing config is preserved.",
'[[servers]]',
"aether_url = $QuotedUrl",
"management_token = $QuotedToken",
@@ -140,22 +140,22 @@ function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]
function Main {
Initialize-Paths
$AetherUrl = Prompt-IfEmpty 'AETHER_PROXY_AETHER_URL' $env:AETHER_PROXY_AETHER_URL 'Aether URL'
$ManagementToken = Prompt-IfEmpty 'AETHER_PROXY_MANAGEMENT_TOKEN' $env:AETHER_PROXY_MANAGEMENT_TOKEN 'Management token (ae_xxx)'
$NodeName = Prompt-IfEmpty 'AETHER_PROXY_NODE_NAME' $env:AETHER_PROXY_NODE_NAME 'Node name'
$AetherUrl = Prompt-IfEmpty 'AETHER_TUNNEL_AETHER_URL' $env:AETHER_TUNNEL_AETHER_URL 'Aether URL'
$ManagementToken = Prompt-IfEmpty 'AETHER_TUNNEL_MANAGEMENT_TOKEN' $env:AETHER_TUNNEL_MANAGEMENT_TOKEN 'Management token (ae_xxx)'
$NodeName = Prompt-IfEmpty 'AETHER_TUNNEL_NODE_NAME' $env:AETHER_TUNNEL_NODE_NAME 'Node name'
$TempDir = Join-Path ([IO.Path]::GetTempPath()) ("aether-proxy-" + [Guid]::NewGuid().ToString('N'))
$TempDir = Join-Path ([IO.Path]::GetTempPath()) ("aether-tunnel-" + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $TempDir | Out-Null
try {
$Tag = Resolve-LatestProxyTag
Install-AetherProxyBinary $Tag $TempDir
$Tag = Resolve-LatestTunnelTag
Install-AetherTunnelBinary $Tag $TempDir
Add-ServerConfig $AetherUrl $ManagementToken $NodeName
} finally {
Remove-Item -Recurse -Force $TempDir -ErrorAction SilentlyContinue
}
Say 'Complete. Start or configure the node with:'
Say " & '$(Join-Path $script:InstallDir 'aether-proxy.exe')' setup '$script:ConfigPath'"
Say " & '$(Join-Path $script:InstallDir 'aether-tunnel.exe')' setup '$script:ConfigPath'"
}
Main

View File

@@ -1,14 +1,14 @@
#!/bin/sh
set -eu
REPO="${AETHER_PROXY_RELEASE_REPO:-fawney19/Aether}"
TAG="${AETHER_PROXY_RELEASE_TAG:-}"
INSTALL_DIR="${AETHER_PROXY_INSTALL_DIR:-}"
CONFIG_PATH="${AETHER_PROXY_CONFIG:-}"
REPO="${AETHER_TUNNEL_RELEASE_REPO:-fawney19/Aether}"
TAG="${AETHER_TUNNEL_RELEASE_TAG:-}"
INSTALL_DIR="${AETHER_TUNNEL_INSTALL_DIR:-}"
CONFIG_PATH="${AETHER_TUNNEL_CONFIG:-}"
TMP_DIR=""
say() { printf '%s\n' "[Aether Proxy] $1"; }
fail() { printf '%s\n' "[Aether Proxy] $1" >&2; exit 1; }
say() { printf '%s\n' "[Aether Tunnel] $1"; }
fail() { printf '%s\n' "[Aether Tunnel] $1" >&2; exit 1; }
cleanup() {
if [ -n "$TMP_DIR" ] && [ -d "$TMP_DIR" ]; then
@@ -61,7 +61,7 @@ toml_quote() {
fi
}
resolve_latest_proxy_tag() {
resolve_latest_tunnel_tag() {
[ -n "$TAG" ] && { printf '%s\n' "$TAG"; return; }
api_url="https://api.github.com/repos/${REPO}/releases?per_page=100"
releases="$TMP_DIR/releases.json"
@@ -70,13 +70,13 @@ resolve_latest_proxy_tag() {
python3 - "$releases" <<'PY'
import json, sys
releases = json.load(open(sys.argv[1], encoding='utf-8'))
proxy = [r for r in releases if not r.get('draft') and str(r.get('tag_name', '')).startswith('proxy-v')]
proxy.sort(key=lambda r: r.get('published_at') or r.get('created_at') or '', reverse=True)
if proxy:
print(proxy[0]['tag_name'])
tunnel = [r for r in releases if not r.get('draft') and str(r.get('tag_name', '')).startswith('tunnel-v')]
tunnel.sort(key=lambda r: r.get('published_at') or r.get('created_at') or '', reverse=True)
if tunnel:
print(tunnel[0]['tag_name'])
PY
else
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"proxy-v[^"]*"' "$releases" | head -n 1 | sed 's/.*"\(proxy-v[^"]*\)".*/\1/'
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"tunnel-v[^"]*"' "$releases" | head -n 1 | sed 's/.*"\(tunnel-v[^"]*\)".*/\1/'
fi
}
@@ -98,9 +98,9 @@ detect_asset() {
esac
if [ "$platform" = "linux" ] && command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
printf 'aether-proxy-linux-musl-%s.tar.gz\n' "$cpu"
printf 'aether-tunnel-linux-musl-%s.tar.gz\n' "$cpu"
else
printf 'aether-proxy-%s-%s.tar.gz\n' "$platform" "$cpu"
printf 'aether-tunnel-%s-%s.tar.gz\n' "$platform" "$cpu"
fi
}
@@ -114,9 +114,9 @@ choose_paths() {
fi
if [ -z "$CONFIG_PATH" ]; then
if [ "$(id -u 2>/dev/null || printf 1)" = "0" ]; then
CONFIG_PATH="/etc/aether-proxy/aether-proxy.toml"
CONFIG_PATH="/etc/aether-tunnel/aether-tunnel.toml"
else
CONFIG_PATH="$HOME/.aether-proxy/aether-proxy.toml"
CONFIG_PATH="$HOME/.aether-tunnel/aether-tunnel.toml"
fi
fi
}
@@ -150,11 +150,11 @@ install_binary() {
verify_checksum "$archive" "$TMP_DIR/SHA256SUMS.txt" "$asset"
tar -xzf "$archive" -C "$TMP_DIR"
[ -f "$TMP_DIR/aether-proxy" ] || fail "制品中未找到 aether-proxy"
[ -f "$TMP_DIR/aether-tunnel" ] || fail "制品中未找到 aether-tunnel"
mkdir -p "$INSTALL_DIR"
cp "$TMP_DIR/aether-proxy" "$INSTALL_DIR/aether-proxy"
chmod +x "$INSTALL_DIR/aether-proxy"
say "已安装二进制:$INSTALL_DIR/aether-proxy"
cp "$TMP_DIR/aether-tunnel" "$INSTALL_DIR/aether-tunnel"
chmod +x "$INSTALL_DIR/aether-tunnel"
say "已安装二进制:$INSTALL_DIR/aether-tunnel"
}
has_legacy_single_server_keys() {
@@ -193,7 +193,7 @@ append_server_config() {
quoted_name=$(toml_quote "$node_name")
if has_legacy_single_server_keys; then
fail "现有配置仍使用旧的顶层 aether_url/management_token请先运行 aether-proxy setup 迁移为 [[servers]] 后重试:$CONFIG_PATH"
fail "现有配置仍使用旧的顶层 aether_url/management_token请先运行 aether-tunnel setup 迁移为 [[servers]] 后重试:$CONFIG_PATH"
fi
if server_exists "$quoted_url" "$quoted_name"; then
@@ -209,7 +209,7 @@ append_server_config() {
if [ -f "$CONFIG_PATH" ] && [ -s "$CONFIG_PATH" ]; then
printf '\n'
fi
printf '# Added by Aether Proxy one-click installer. Existing config is preserved.\n'
printf '# Added by Aether Tunnel one-click installer. Existing config is preserved.\n'
printf '[[servers]]\n'
printf 'aether_url = %s\n' "$quoted_url"
printf 'management_token = %s\n' "$quoted_token"
@@ -220,22 +220,22 @@ append_server_config() {
}
main() {
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t aether-proxy)
TMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t aether-tunnel)
need_cmd tar
choose_paths
aether_url=$(prompt_if_empty AETHER_PROXY_AETHER_URL "${AETHER_PROXY_AETHER_URL:-}" "Aether URL: ")
management_token=$(prompt_if_empty AETHER_PROXY_MANAGEMENT_TOKEN "${AETHER_PROXY_MANAGEMENT_TOKEN:-}" "Management token (ae_xxx): ")
node_name=$(prompt_if_empty AETHER_PROXY_NODE_NAME "${AETHER_PROXY_NODE_NAME:-}" "Node name: ")
aether_url=$(prompt_if_empty AETHER_TUNNEL_AETHER_URL "${AETHER_TUNNEL_AETHER_URL:-}" "Aether URL: ")
management_token=$(prompt_if_empty AETHER_TUNNEL_MANAGEMENT_TOKEN "${AETHER_TUNNEL_MANAGEMENT_TOKEN:-}" "Management token (ae_xxx): ")
node_name=$(prompt_if_empty AETHER_TUNNEL_NODE_NAME "${AETHER_TUNNEL_NODE_NAME:-}" "Node name: ")
tag=$(resolve_latest_proxy_tag)
[ -n "$tag" ] || fail "没有找到可用的 proxy-v* release"
tag=$(resolve_latest_tunnel_tag)
[ -n "$tag" ] || fail "没有找到可用的 tunnel-v* release"
asset=$(detect_asset)
install_binary "$tag" "$asset"
append_server_config "$aether_url" "$management_token" "$node_name"
say "完成。运行以下命令启动/配置服务:"
say " $INSTALL_DIR/aether-proxy setup $CONFIG_PATH"
say " $INSTALL_DIR/aether-tunnel setup $CONFIG_PATH"
}
main "$@"

View File

@@ -22,7 +22,7 @@ use crate::config::{Config, ServerEntry, TunnelPoolSizing};
use crate::net;
use crate::registration::client::AetherClient;
use crate::runtime::{self, DynamicConfig};
use crate::state::{AppState, ProxyMetrics, ServerContext, TunnelMetrics};
use crate::state::{AppState, ServerContext, TunnelMetrics, TunnelRequestMetrics};
use crate::upstream_client;
use crate::{hardware, target_filter, tunnel};
@@ -99,12 +99,12 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
version = env!("CARGO_PKG_VERSION"),
node_name = %config.node_name,
server_count = servers.len(),
"aether-proxy starting (tunnel mode)"
"aether-tunnel starting (tunnel mode)"
);
if let Some(proxy_url) = config.effective_aether_proxy_url() {
if let Some(proxy_url) = config.effective_aether_outbound_proxy_url() {
if let Ok(proxy) = crate::egress_proxy::UpstreamProxyConfig::parse(proxy_url) {
info!(
aether_proxy_url = %proxy.redacted_url(),
aether_outbound_proxy_url = %proxy.redacted_url(),
"Aether control and tunnel egress proxy configured"
);
}
@@ -258,7 +258,7 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
};
if let Some(limit) = state.config.max_in_flight_streams {
state = state
.with_stream_concurrency_gate(Arc::new(ConcurrencyGate::new("proxy_streams", limit)));
.with_stream_concurrency_gate(Arc::new(ConcurrencyGate::new("tunnel_streams", limit)));
}
if let Some(limit) = state.config.distributed_stream_limit {
let redis_url = state
@@ -275,7 +275,7 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
)
.await?;
let distributed_gate = runtime.semaphore(
"proxy_streams_distributed",
"tunnel_streams_distributed",
limit,
RuntimeSemaphoreConfig {
lease_ttl_ms: state.config.distributed_stream_lease_ttl_ms,
@@ -363,7 +363,7 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> anyhow::Resul
// Wait for all tunnel tasks
await_all_handles(&tunnel_handles).await;
info!("aether-proxy stopped");
info!("aether-tunnel stopped");
Ok(())
}
@@ -379,7 +379,7 @@ fn spawn_diagnostics_server(
.route("/stats", get(diagnostics_stats))
.with_state(diagnostics_state);
info!(bind = %bind_addr, "proxy diagnostics server listening");
info!(bind = %bind_addr, "tunnel diagnostics server listening");
Ok(tokio::spawn(async move {
let graceful_shutdown = async move {
while !*shutdown.borrow() {
@@ -392,7 +392,7 @@ fn spawn_diagnostics_server(
.with_graceful_shutdown(graceful_shutdown)
.await
{
error!(error = %error, "proxy diagnostics server exited with error");
error!(error = %error, "tunnel diagnostics server exited with error");
}
}))
}
@@ -414,7 +414,7 @@ async fn diagnostics_health(
Json(serde_json::json!({
"status": "ok",
"service": "aether-proxy",
"service": "aether-tunnel",
"version": env!("CARGO_PKG_VERSION"),
"protocol_version": aether_contracts::tunnel::CURRENT_TUNNEL_PROTOCOL_VERSION,
"server_count": servers.len(),
@@ -456,7 +456,7 @@ async fn diagnostics_stats(
Json(serde_json::json!({
"status": "ok",
"service": "aether-proxy",
"service": "aether-tunnel",
"version": env!("CARGO_PKG_VERSION"),
"protocol_version": aether_contracts::tunnel::CURRENT_TUNNEL_PROTOCOL_VERSION,
"capacities": {
@@ -485,7 +485,7 @@ fn diagnostics_server_stats(server: &ServerContext) -> serde_json::Value {
"node_id": node_id,
"node_name": dynamic.node_name.clone(),
"active_connections": server.active_connections.load(Ordering::Acquire),
"proxy_metrics": server.metrics.snapshot(),
"request_metrics": server.metrics.snapshot(),
"tunnel_metrics": server.tunnel_metrics.snapshot(),
"recent_tunnel_errors": server.tunnel_metrics.recent_errors(16),
})
@@ -686,7 +686,7 @@ fn build_server_context(
aether_client: client,
dynamic: Arc::new(ArcSwap::from_pointee(dynamic)),
active_connections: Arc::new(AtomicU64::new(0)),
metrics: Arc::new(ProxyMetrics::new()),
metrics: Arc::new(TunnelRequestMetrics::new()),
tunnel_metrics: Arc::new(TunnelMetrics::new()),
})
}
@@ -938,9 +938,9 @@ fn init_tracing(config: &Config) {
&config.log_level,
config
.service_runtime_config()
.expect("proxy service runtime config should be valid"),
.expect("tunnel service runtime config should be valid"),
)
.expect("proxy tracing should initialize");
.expect("tunnel tracing should initialize");
runtime::set_log_reloader(reloader);
}
@@ -962,10 +962,10 @@ mod tests {
use serde_json::json;
use crate::config::{
ProxyLogDestinationArg, ProxyLogRotationArg, DEFAULT_REDIRECT_REPLAY_BUDGET_BYTES,
TunnelLogDestinationArg, TunnelLogRotationArg, DEFAULT_REDIRECT_REPLAY_BUDGET_BYTES,
};
use crate::hardware::HardwareInfo;
use crate::state::AppState as ProxyAppState;
use crate::state::AppState as TunnelAppState;
use crate::target_filter::DnsCache;
use super::*;
@@ -1064,7 +1064,7 @@ mod tests {
.await
.expect("health response should parse");
assert_eq!(health["status"], "ok");
assert_eq!(health["service"], "aether-proxy");
assert_eq!(health["service"], "aether-tunnel");
assert_eq!(health["server_count"], 1);
let metrics = client
@@ -1077,8 +1077,8 @@ mod tests {
.text()
.await
.expect("metrics response should read");
assert!(metrics.contains("service_up{service=\"aether-proxy\"} 1"));
assert!(metrics.contains("proxy_active_connections{server=\"server\"} 0"));
assert!(metrics.contains("service_up{service=\"aether-tunnel\"} 1"));
assert!(metrics.contains("tunnel_active_connections{server=\"server\"} 0"));
let stats: serde_json::Value = client
.get(format!("{base_url}/stats"))
@@ -1264,12 +1264,12 @@ mod tests {
}
}
fn sample_state(config: Config) -> Arc<ProxyAppState> {
fn sample_state(config: Config) -> Arc<TunnelAppState> {
let config = Arc::new(config);
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(ProxyAppState {
Arc::new(TunnelAppState {
config,
dns_cache,
upstream_client_pool,
@@ -1281,7 +1281,7 @@ mod tests {
}
fn sample_registered_server(
state: &Arc<ProxyAppState>,
state: &Arc<TunnelAppState>,
label: &str,
node_id: &str,
) -> Arc<ServerContext> {
@@ -1310,7 +1310,7 @@ mod tests {
aether_url: aether_url.to_string(),
management_token: "token".to_string(),
public_ip: None,
node_name: "proxy-test".to_string(),
node_name: "tunnel-test".to_string(),
node_region: None,
heartbeat_interval: 1,
allowed_ports: vec![80, 443],
@@ -1322,7 +1322,7 @@ mod tests {
aether_tcp_keepalive_secs: 60,
aether_tcp_nodelay: true,
aether_http2: true,
aether_proxy_url: None,
aether_outbound_proxy_url: None,
aether_retry_max_attempts: 1,
aether_retry_base_delay_ms: 50,
aether_retry_max_delay_ms: 100,
@@ -1346,9 +1346,9 @@ mod tests {
redirect_replay_budget_bytes: DEFAULT_REDIRECT_REPLAY_BUDGET_BYTES,
emit_proxy_timing_header: true,
log_level: "info".to_string(),
log_destination: ProxyLogDestinationArg::Stdout,
log_destination: TunnelLogDestinationArg::Stdout,
log_dir: None,
log_rotation: ProxyLogRotationArg::Daily,
log_rotation: TunnelLogRotationArg::Daily,
log_retention_days: 7,
log_max_files: 30,
tunnel_reconnect_base_ms: 50,

View File

@@ -71,9 +71,9 @@ const AUTO_TUNNEL_CONNECTIONS_BASE_CAP: u64 = 4;
const AUTO_TUNNEL_CONNECTIONS_PER_CPU_CAP: u64 = 4;
const AUTO_TUNNEL_CONNECTIONS_MAX_CAP: u64 = 32;
const TUNNEL_PING_INTERVAL_MS_ENV: &str = "AETHER_PROXY_TUNNEL_PING_INTERVAL_MS";
const TUNNEL_CONNECT_TIMEOUT_MS_ENV: &str = "AETHER_PROXY_TUNNEL_CONNECT_TIMEOUT_MS";
const TUNNEL_STALE_TIMEOUT_MS_ENV: &str = "AETHER_PROXY_TUNNEL_STALE_TIMEOUT_MS";
const TUNNEL_PING_INTERVAL_MS_ENV: &str = "AETHER_TUNNEL_PING_INTERVAL_MS";
const TUNNEL_CONNECT_TIMEOUT_MS_ENV: &str = "AETHER_TUNNEL_CONNECT_TIMEOUT_MS";
const TUNNEL_STALE_TIMEOUT_MS_ENV: &str = "AETHER_TUNNEL_STALE_TIMEOUT_MS";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TunnelPoolSizing {
@@ -215,39 +215,39 @@ pub fn format_byte_size_human(bytes: usize) -> String {
#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProxyLogDestinationArg {
pub enum TunnelLogDestinationArg {
Stdout,
File,
Both,
}
impl From<ProxyLogDestinationArg> for LogDestination {
fn from(value: ProxyLogDestinationArg) -> Self {
impl From<TunnelLogDestinationArg> for LogDestination {
fn from(value: TunnelLogDestinationArg) -> Self {
match value {
ProxyLogDestinationArg::Stdout => LogDestination::Stdout,
ProxyLogDestinationArg::File => LogDestination::File,
ProxyLogDestinationArg::Both => LogDestination::Both,
TunnelLogDestinationArg::Stdout => LogDestination::Stdout,
TunnelLogDestinationArg::File => LogDestination::File,
TunnelLogDestinationArg::Both => LogDestination::Both,
}
}
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProxyLogRotationArg {
pub enum TunnelLogRotationArg {
Hourly,
Daily,
}
impl From<ProxyLogRotationArg> for LogRotation {
fn from(value: ProxyLogRotationArg) -> Self {
impl From<TunnelLogRotationArg> for LogRotation {
fn from(value: TunnelLogRotationArg) -> Self {
match value {
ProxyLogRotationArg::Hourly => LogRotation::Hourly,
ProxyLogRotationArg::Daily => LogRotation::Daily,
TunnelLogRotationArg::Hourly => LogRotation::Hourly,
TunnelLogRotationArg::Daily => LogRotation::Daily,
}
}
}
/// Aether tunnel proxy.
/// Aether tunnel agent.
///
/// Deployed on overseas VPS to relay API traffic for Aether instances
/// behind the GFW. Connects to Aether via WebSocket tunnel, registers
@@ -256,29 +256,29 @@ impl From<ProxyLogRotationArg> for LogRotation {
#[command(version, about)]
pub struct Config {
/// Aether server URL (e.g. https://aether.example.com)
#[arg(long, env = "AETHER_PROXY_AETHER_URL")]
#[arg(long, env = "AETHER_TUNNEL_AETHER_URL")]
pub aether_url: String,
/// Management Token for Aether admin API (ae_xxx)
#[arg(long, env = "AETHER_PROXY_MANAGEMENT_TOKEN")]
#[arg(long, env = "AETHER_TUNNEL_MANAGEMENT_TOKEN")]
pub management_token: String,
/// Public IP address of this node (auto-detected if omitted)
#[arg(long, env = "AETHER_PROXY_PUBLIC_IP")]
#[arg(long, env = "AETHER_TUNNEL_PUBLIC_IP")]
pub public_ip: Option<String>,
/// Human-readable node name
#[arg(long, env = "AETHER_PROXY_NODE_NAME")]
#[arg(long, env = "AETHER_TUNNEL_NODE_NAME")]
pub node_name: String,
/// Region label (e.g. ap-northeast-1)
#[arg(long, env = "AETHER_PROXY_NODE_REGION")]
#[arg(long, env = "AETHER_TUNNEL_NODE_REGION")]
pub node_region: Option<String>,
/// Heartbeat interval in seconds
#[arg(
long,
env = "AETHER_PROXY_HEARTBEAT_INTERVAL",
env = "AETHER_TUNNEL_HEARTBEAT_INTERVAL",
default_value_t = DEFAULT_HEARTBEAT_INTERVAL_SECS
)]
pub heartbeat_interval: u64,
@@ -286,7 +286,7 @@ pub struct Config {
/// Allowed destination ports (default: 80,443,8080,8443)
#[arg(
long,
env = "AETHER_PROXY_ALLOWED_PORTS",
env = "AETHER_TUNNEL_ALLOWED_PORTS",
value_delimiter = ',',
default_values_t = vec![80, 443, 8080, 8443]
)]
@@ -295,7 +295,7 @@ pub struct Config {
/// Allow private/reserved upstream IP targets. Enabled by default.
#[arg(
long,
env = "AETHER_PROXY_ALLOW_PRIVATE_TARGETS",
env = "AETHER_TUNNEL_ALLOW_PRIVATE_TARGETS",
default_value_t = true
)]
pub allow_private_targets: bool,
@@ -303,7 +303,7 @@ pub struct Config {
/// Aether API request timeout in seconds
#[arg(
long,
env = "AETHER_PROXY_AETHER_REQUEST_TIMEOUT",
env = "AETHER_TUNNEL_AETHER_REQUEST_TIMEOUT",
default_value_t = 10
)]
pub aether_request_timeout_secs: u64,
@@ -311,7 +311,7 @@ pub struct Config {
/// Aether API connect timeout in seconds
#[arg(
long,
env = "AETHER_PROXY_AETHER_CONNECT_TIMEOUT",
env = "AETHER_TUNNEL_AETHER_CONNECT_TIMEOUT",
default_value_t = 10
)]
pub aether_connect_timeout_secs: u64,
@@ -319,7 +319,7 @@ pub struct Config {
/// Aether API max idle connections per host
#[arg(
long,
env = "AETHER_PROXY_AETHER_POOL_MAX_IDLE_PER_HOST",
env = "AETHER_TUNNEL_AETHER_POOL_MAX_IDLE_PER_HOST",
default_value_t = 8
)]
pub aether_pool_max_idle_per_host: usize,
@@ -327,32 +327,32 @@ pub struct Config {
/// Aether API idle timeout in seconds
#[arg(
long,
env = "AETHER_PROXY_AETHER_POOL_IDLE_TIMEOUT",
env = "AETHER_TUNNEL_AETHER_POOL_IDLE_TIMEOUT",
default_value_t = 90
)]
pub aether_pool_idle_timeout_secs: u64,
/// Aether API TCP keepalive in seconds (0 disables)
#[arg(long, env = "AETHER_PROXY_AETHER_TCP_KEEPALIVE", default_value_t = 60)]
#[arg(long, env = "AETHER_TUNNEL_AETHER_TCP_KEEPALIVE", default_value_t = 60)]
pub aether_tcp_keepalive_secs: u64,
/// Aether API TCP_NODELAY
#[arg(long, env = "AETHER_PROXY_AETHER_TCP_NODELAY", default_value_t = true)]
#[arg(long, env = "AETHER_TUNNEL_AETHER_TCP_NODELAY", default_value_t = true)]
pub aether_tcp_nodelay: bool,
/// Enable HTTP/2 when talking to Aether API
#[arg(long, env = "AETHER_PROXY_AETHER_HTTP2", default_value_t = true)]
#[arg(long, env = "AETHER_TUNNEL_AETHER_HTTP2", default_value_t = true)]
pub aether_http2: bool,
/// Optional egress proxy used for Aether API registration and WebSocket tunnel reconnects.
/// Supported schemes: http, socks5, socks5h.
#[arg(long, env = "AETHER_PROXY_AETHER_PROXY_URL")]
pub aether_proxy_url: Option<String>,
#[arg(long, env = "AETHER_TUNNEL_AETHER_OUTBOUND_PROXY_URL")]
pub aether_outbound_proxy_url: Option<String>,
/// Aether API retry attempts (including initial)
#[arg(
long,
env = "AETHER_PROXY_AETHER_RETRY_MAX_ATTEMPTS",
env = "AETHER_TUNNEL_AETHER_RETRY_MAX_ATTEMPTS",
default_value_t = 3
)]
pub aether_retry_max_attempts: u32,
@@ -360,7 +360,7 @@ pub struct Config {
/// Aether API retry base delay in milliseconds
#[arg(
long,
env = "AETHER_PROXY_AETHER_RETRY_BASE_DELAY_MS",
env = "AETHER_TUNNEL_AETHER_RETRY_BASE_DELAY_MS",
default_value_t = 200
)]
pub aether_retry_base_delay_ms: u64,
@@ -368,40 +368,40 @@ pub struct Config {
/// Aether API retry max delay in milliseconds
#[arg(
long,
env = "AETHER_PROXY_AETHER_RETRY_MAX_DELAY_MS",
env = "AETHER_TUNNEL_AETHER_RETRY_MAX_DELAY_MS",
default_value_t = 2000
)]
pub aether_retry_max_delay_ms: u64,
/// Optional local diagnostics listener for /health, /metrics, and /stats.
/// Bind only to loopback addresses, for example 127.0.0.1:9311.
#[arg(long, env = "AETHER_PROXY_DIAGNOSTICS_BIND")]
#[arg(long, env = "AETHER_TUNNEL_DIAGNOSTICS_BIND")]
pub diagnostics_bind: Option<SocketAddr>,
/// Maximum concurrent TCP connections (defaults to hardware estimate)
#[arg(long, env = "AETHER_PROXY_MAX_CONCURRENT_CONNECTIONS")]
#[arg(long, env = "AETHER_TUNNEL_MAX_CONCURRENT_CONNECTIONS")]
pub max_concurrent_connections: Option<u64>,
/// Maximum in-flight tunneled streams accepted by this proxy instance.
#[arg(long, env = "AETHER_PROXY_MAX_IN_FLIGHT_STREAMS")]
/// Maximum in-flight tunneled streams accepted by this tunnel instance.
#[arg(long, env = "AETHER_TUNNEL_MAX_IN_FLIGHT_STREAMS")]
pub max_in_flight_streams: Option<usize>,
/// Maximum in-flight tunneled streams admitted across all proxy instances.
#[arg(long, env = "AETHER_PROXY_DISTRIBUTED_STREAM_LIMIT")]
/// Maximum in-flight tunneled streams admitted across all tunnel instances.
#[arg(long, env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_LIMIT")]
pub distributed_stream_limit: Option<usize>,
/// Redis URL used for cross-instance stream admission.
#[arg(long, env = "AETHER_PROXY_DISTRIBUTED_STREAM_REDIS_URL")]
#[arg(long, env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_REDIS_URL")]
pub distributed_stream_redis_url: Option<String>,
/// Optional key prefix for cross-instance stream admission state.
#[arg(long, env = "AETHER_PROXY_DISTRIBUTED_STREAM_REDIS_KEY_PREFIX")]
#[arg(long, env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_REDIS_KEY_PREFIX")]
pub distributed_stream_redis_key_prefix: Option<String>,
/// Lease TTL in milliseconds for distributed stream admission permits.
#[arg(
long,
env = "AETHER_PROXY_DISTRIBUTED_STREAM_LEASE_TTL_MS",
env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_LEASE_TTL_MS",
default_value_t = 30_000
)]
pub distributed_stream_lease_ttl_ms: u64,
@@ -409,7 +409,7 @@ pub struct Config {
/// Renew interval in milliseconds for distributed stream admission permits.
#[arg(
long,
env = "AETHER_PROXY_DISTRIBUTED_STREAM_RENEW_INTERVAL_MS",
env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_RENEW_INTERVAL_MS",
default_value_t = 10_000
)]
pub distributed_stream_renew_interval_ms: u64,
@@ -417,23 +417,23 @@ pub struct Config {
/// Command timeout in milliseconds for distributed stream admission Redis calls.
#[arg(
long,
env = "AETHER_PROXY_DISTRIBUTED_STREAM_COMMAND_TIMEOUT_MS",
env = "AETHER_TUNNEL_DISTRIBUTED_STREAM_COMMAND_TIMEOUT_MS",
default_value_t = 1_000
)]
pub distributed_stream_command_timeout_ms: u64,
/// DNS cache TTL in seconds
#[arg(long, env = "AETHER_PROXY_DNS_CACHE_TTL", default_value_t = 60)]
#[arg(long, env = "AETHER_TUNNEL_DNS_CACHE_TTL", default_value_t = 60)]
pub dns_cache_ttl_secs: u64,
/// DNS cache capacity (entries)
#[arg(long, env = "AETHER_PROXY_DNS_CACHE_CAPACITY", default_value_t = 1024)]
#[arg(long, env = "AETHER_TUNNEL_DNS_CACHE_CAPACITY", default_value_t = 1024)]
pub dns_cache_capacity: usize,
/// Upstream HTTP client connect timeout in seconds
#[arg(
long,
env = "AETHER_PROXY_UPSTREAM_CONNECT_TIMEOUT",
env = "AETHER_TUNNEL_UPSTREAM_CONNECT_TIMEOUT",
default_value_t = 30
)]
pub upstream_connect_timeout_secs: u64,
@@ -441,7 +441,7 @@ pub struct Config {
/// Upstream HTTP client max idle connections per host
#[arg(
long,
env = "AETHER_PROXY_UPSTREAM_POOL_MAX_IDLE_PER_HOST",
env = "AETHER_TUNNEL_UPSTREAM_POOL_MAX_IDLE_PER_HOST",
default_value_t = 64
)]
pub upstream_pool_max_idle_per_host: usize,
@@ -449,7 +449,7 @@ pub struct Config {
/// Upstream HTTP client idle timeout in seconds
#[arg(
long,
env = "AETHER_PROXY_UPSTREAM_POOL_IDLE_TIMEOUT",
env = "AETHER_TUNNEL_UPSTREAM_POOL_IDLE_TIMEOUT",
default_value_t = 300
)]
pub upstream_pool_idle_timeout_secs: u64,
@@ -457,7 +457,7 @@ pub struct Config {
/// Upstream TCP keepalive in seconds (0 disables)
#[arg(
long,
env = "AETHER_PROXY_UPSTREAM_TCP_KEEPALIVE",
env = "AETHER_TUNNEL_UPSTREAM_TCP_KEEPALIVE",
default_value_t = 60
)]
pub upstream_tcp_keepalive_secs: u64,
@@ -465,21 +465,21 @@ pub struct Config {
/// Upstream TCP_NODELAY
#[arg(
long,
env = "AETHER_PROXY_UPSTREAM_TCP_NODELAY",
env = "AETHER_TUNNEL_UPSTREAM_TCP_NODELAY",
default_value_t = true
)]
pub upstream_tcp_nodelay: bool,
/// Optional egress proxy used only for provider upstream requests.
/// Supported schemes: http, socks5, socks5h.
#[arg(long, env = "AETHER_PROXY_UPSTREAM_PROXY_URL")]
#[arg(long, env = "AETHER_TUNNEL_UPSTREAM_PROXY_URL")]
pub upstream_proxy_url: Option<String>,
/// Maximum request body bytes buffered to support 307/308 redirect replay.
/// Accepts values like 5M / 512K / 1G. Set to 0 to disable request-body replay buffering.
#[arg(
long,
env = "AETHER_PROXY_REDIRECT_REPLAY_BUDGET_BYTES",
env = "AETHER_TUNNEL_REDIRECT_REPLAY_BUDGET_BYTES",
value_parser = parse_byte_size,
default_value = DEFAULT_REDIRECT_REPLAY_BUDGET_HUMAN
)]
@@ -488,41 +488,41 @@ pub struct Config {
/// Emit detailed x-proxy-timing headers on tunneled upstream responses.
#[arg(
long,
env = "AETHER_PROXY_EMIT_PROXY_TIMING_HEADER",
env = "AETHER_TUNNEL_EMIT_PROXY_TIMING_HEADER",
default_value_t = true
)]
pub emit_proxy_timing_header: bool,
/// Log level (trace, debug, info, warn, error)
#[arg(long, env = "AETHER_PROXY_LOG_LEVEL", default_value = "info")]
#[arg(long, env = "AETHER_TUNNEL_LOG_LEVEL", default_value = "info")]
pub log_level: String,
/// Log destination (stdout, file, both)
#[arg(
long,
env = "AETHER_PROXY_LOG_DESTINATION",
env = "AETHER_TUNNEL_LOG_DESTINATION",
value_enum,
default_value = "both"
)]
pub log_destination: ProxyLogDestinationArg,
pub log_destination: TunnelLogDestinationArg,
/// Log directory when file logging is enabled
#[arg(long, env = "AETHER_PROXY_LOG_DIR", default_value = DEFAULT_LOG_DIR)]
#[arg(long, env = "AETHER_TUNNEL_LOG_DIR", default_value = DEFAULT_LOG_DIR)]
pub log_dir: Option<String>,
/// Log rotation schedule for file logging
#[arg(
long,
env = "AETHER_PROXY_LOG_ROTATION",
env = "AETHER_TUNNEL_LOG_ROTATION",
value_enum,
default_value = "daily"
)]
pub log_rotation: ProxyLogRotationArg,
pub log_rotation: TunnelLogRotationArg,
/// Log file retention days for file logging
#[arg(
long,
env = "AETHER_PROXY_LOG_RETENTION_DAYS",
env = "AETHER_TUNNEL_LOG_RETENTION_DAYS",
default_value_t = DEFAULT_LOG_RETENTION_DAYS
)]
pub log_retention_days: u64,
@@ -530,7 +530,7 @@ pub struct Config {
/// Maximum number of retained rolled log files
#[arg(
long,
env = "AETHER_PROXY_LOG_MAX_FILES",
env = "AETHER_TUNNEL_LOG_MAX_FILES",
default_value_t = DEFAULT_LOG_MAX_FILES
)]
pub log_max_files: usize,
@@ -538,7 +538,7 @@ pub struct Config {
/// Tunnel reconnect base delay in milliseconds (used by exponential backoff)
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_RECONNECT_BASE_MS",
env = "AETHER_TUNNEL_RECONNECT_BASE_MS",
default_value_t = DEFAULT_TUNNEL_RECONNECT_BASE_MS
)]
pub tunnel_reconnect_base_ms: u64,
@@ -546,7 +546,7 @@ pub struct Config {
/// Tunnel reconnect max delay in milliseconds (cap for exponential backoff)
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_RECONNECT_MAX_MS",
env = "AETHER_TUNNEL_RECONNECT_MAX_MS",
default_value_t = DEFAULT_TUNNEL_RECONNECT_MAX_MS
)]
pub tunnel_reconnect_max_ms: u64,
@@ -560,7 +560,7 @@ pub struct Config {
pub tunnel_ping_interval_ms: u64,
/// Maximum concurrent streams over tunnel (auto-detected from hardware if omitted)
#[arg(long, env = "AETHER_PROXY_TUNNEL_MAX_STREAMS")]
#[arg(long, env = "AETHER_TUNNEL_MAX_STREAMS")]
pub tunnel_max_streams: Option<u32>,
/// WebSocket tunnel TCP connect timeout in milliseconds
@@ -572,11 +572,11 @@ pub struct Config {
pub tunnel_connect_timeout_ms: u64,
/// WebSocket tunnel TCP keepalive in seconds (0 disables)
#[arg(long, env = "AETHER_PROXY_TUNNEL_TCP_KEEPALIVE", default_value_t = 30)]
#[arg(long, env = "AETHER_TUNNEL_TCP_KEEPALIVE", default_value_t = 30)]
pub tunnel_tcp_keepalive_secs: u64,
/// WebSocket tunnel TCP_NODELAY
#[arg(long, env = "AETHER_PROXY_TUNNEL_TCP_NODELAY", default_value_t = true)]
#[arg(long, env = "AETHER_TUNNEL_TCP_NODELAY", default_value_t = true)]
pub tunnel_tcp_nodelay: bool,
/// Tunnel connection staleness timeout in milliseconds
@@ -589,18 +589,18 @@ pub struct Config {
/// Minimum number of parallel WebSocket tunnel connections per server.
/// If omitted, a device-aware redundant value is auto-detected at startup.
#[arg(long, env = "AETHER_PROXY_TUNNEL_CONNECTIONS")]
#[arg(long, env = "AETHER_TUNNEL_CONNECTIONS")]
pub tunnel_connections: Option<u32>,
/// Maximum number of WebSocket tunnel connections per server.
/// When larger than `tunnel_connections`, the proxy may autoscale up to this limit.
#[arg(long, env = "AETHER_PROXY_TUNNEL_CONNECTIONS_MAX")]
/// When larger than `tunnel_connections`, the tunnel may autoscale up to this limit.
#[arg(long, env = "AETHER_TUNNEL_CONNECTIONS_MAX")]
pub tunnel_connections_max: Option<u32>,
/// Autoscale evaluation interval for the tunnel pool.
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_SCALE_CHECK_INTERVAL_MS",
env = "AETHER_TUNNEL_SCALE_CHECK_INTERVAL_MS",
default_value_t = DEFAULT_TUNNEL_SCALE_CHECK_INTERVAL_MS
)]
pub tunnel_scale_check_interval_ms: u64,
@@ -608,7 +608,7 @@ pub struct Config {
/// Per-tunnel occupancy percentage that triggers scale-up.
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_SCALE_UP_THRESHOLD_PERCENT",
env = "AETHER_TUNNEL_SCALE_UP_THRESHOLD_PERCENT",
default_value_t = DEFAULT_TUNNEL_SCALE_UP_THRESHOLD_PERCENT
)]
pub tunnel_scale_up_threshold_percent: u32,
@@ -616,7 +616,7 @@ pub struct Config {
/// Per-tunnel occupancy percentage that allows scale-down after the grace window.
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT",
env = "AETHER_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT",
default_value_t = DEFAULT_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT
)]
pub tunnel_scale_down_threshold_percent: u32,
@@ -624,7 +624,7 @@ pub struct Config {
/// Low-load grace window before a secondary tunnel is drained.
#[arg(
long,
env = "AETHER_PROXY_TUNNEL_SCALE_DOWN_GRACE_SECS",
env = "AETHER_TUNNEL_SCALE_DOWN_GRACE_SECS",
default_value_t = DEFAULT_TUNNEL_SCALE_DOWN_GRACE_SECS
)]
pub tunnel_scale_down_grace_secs: u64,
@@ -708,9 +708,9 @@ impl Config {
if self.upstream_connect_timeout_secs == 0 {
anyhow::bail!("upstream_connect_timeout_secs must be > 0");
}
if let Some(proxy_url) = normalized_proxy_url(&self.aether_proxy_url) {
if let Some(proxy_url) = normalized_proxy_url(&self.aether_outbound_proxy_url) {
crate::egress_proxy::UpstreamProxyConfig::parse(proxy_url)
.map_err(|err| anyhow::anyhow!("aether_proxy_url invalid: {err}"))?;
.map_err(|err| anyhow::anyhow!("aether_outbound_proxy_url invalid: {err}"))?;
}
if let Some(proxy_url) = normalized_proxy_url(&self.upstream_proxy_url) {
crate::egress_proxy::UpstreamProxyConfig::parse(proxy_url)
@@ -743,14 +743,14 @@ impl Config {
}
if matches!(
self.log_destination,
ProxyLogDestinationArg::File | ProxyLogDestinationArg::Both
TunnelLogDestinationArg::File | TunnelLogDestinationArg::Both
) && self
.log_dir
.as_deref()
.map(str::trim)
.is_none_or(|value| value.is_empty())
{
anyhow::bail!("log_dir must be set when AETHER_PROXY_LOG_DESTINATION is file or both");
anyhow::bail!("log_dir must be set when AETHER_TUNNEL_LOG_DESTINATION is file or both");
}
Ok(())
}
@@ -767,8 +767,8 @@ impl Config {
Ok(Duration::from_millis(self.tunnel_stale_timeout_ms))
}
pub fn effective_aether_proxy_url(&self) -> Option<&str> {
normalized_proxy_url(&self.aether_proxy_url)
pub fn effective_aether_outbound_proxy_url(&self) -> Option<&str> {
normalized_proxy_url(&self.aether_outbound_proxy_url)
}
pub fn resolve_tunnel_pool_sizing(
@@ -825,14 +825,14 @@ impl Config {
}
pub fn service_runtime_config(&self) -> anyhow::Result<ServiceRuntimeConfig> {
let mut config = ServiceRuntimeConfig::new("aether-proxy", "aether_proxy=info")
let mut config = ServiceRuntimeConfig::new("aether-tunnel", "aether_tunnel=info")
.with_log_format(aether_runtime::LogFormat::Pretty)
.with_log_destination(self.log_destination.into())
.with_node_role("proxy")
.with_instance_id(self.node_name.trim().to_string());
if matches!(
self.log_destination,
ProxyLogDestinationArg::File | ProxyLogDestinationArg::Both
TunnelLogDestinationArg::File | TunnelLogDestinationArg::Both
) {
let log_dir = self
.log_dir
@@ -896,8 +896,12 @@ pub struct ConfigFile {
pub aether_tcp_nodelay: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aether_http2: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aether_proxy_url: Option<String>,
#[serde(
alias = "aether_proxy_url",
alias = "aether_tunnel_url",
skip_serializing_if = "Option::is_none"
)]
pub aether_outbound_proxy_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aether_retry_max_attempts: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -935,11 +939,11 @@ pub struct ConfigFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub log_level: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_destination: Option<ProxyLogDestinationArg>,
pub log_destination: Option<TunnelLogDestinationArg>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_rotation: Option<ProxyLogRotationArg>,
pub log_rotation: Option<TunnelLogRotationArg>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_retention_days: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1026,157 +1030,157 @@ impl ConfigFile {
.as_deref()
.or(first_server.and_then(|s| s.node_name.as_deref()));
set!("AETHER_PROXY_AETHER_URL", aether_url);
set!("AETHER_PROXY_MANAGEMENT_TOKEN", management_token);
set!("AETHER_PROXY_PUBLIC_IP", self.public_ip);
set!("AETHER_PROXY_NODE_NAME", node_name);
set!("AETHER_PROXY_NODE_REGION", self.node_region);
set!("AETHER_PROXY_HEARTBEAT_INTERVAL", self.heartbeat_interval);
set!("AETHER_TUNNEL_AETHER_URL", aether_url);
set!("AETHER_TUNNEL_MANAGEMENT_TOKEN", management_token);
set!("AETHER_TUNNEL_PUBLIC_IP", self.public_ip);
set!("AETHER_TUNNEL_NODE_NAME", node_name);
set!("AETHER_TUNNEL_NODE_REGION", self.node_region);
set!("AETHER_TUNNEL_HEARTBEAT_INTERVAL", self.heartbeat_interval);
set!(
"AETHER_PROXY_ALLOW_PRIVATE_TARGETS",
"AETHER_TUNNEL_ALLOW_PRIVATE_TARGETS",
self.allow_private_targets
);
set!(
"AETHER_PROXY_AETHER_REQUEST_TIMEOUT",
"AETHER_TUNNEL_AETHER_REQUEST_TIMEOUT",
self.aether_request_timeout_secs
);
set!(
"AETHER_PROXY_AETHER_CONNECT_TIMEOUT",
"AETHER_TUNNEL_AETHER_CONNECT_TIMEOUT",
self.aether_connect_timeout_secs
);
set!(
"AETHER_PROXY_AETHER_POOL_MAX_IDLE_PER_HOST",
"AETHER_TUNNEL_AETHER_POOL_MAX_IDLE_PER_HOST",
self.aether_pool_max_idle_per_host
);
set!(
"AETHER_PROXY_AETHER_POOL_IDLE_TIMEOUT",
"AETHER_TUNNEL_AETHER_POOL_IDLE_TIMEOUT",
self.aether_pool_idle_timeout_secs
);
set!(
"AETHER_PROXY_AETHER_TCP_KEEPALIVE",
"AETHER_TUNNEL_AETHER_TCP_KEEPALIVE",
self.aether_tcp_keepalive_secs
);
set!("AETHER_PROXY_AETHER_TCP_NODELAY", self.aether_tcp_nodelay);
set!("AETHER_PROXY_AETHER_HTTP2", self.aether_http2);
set!("AETHER_PROXY_AETHER_PROXY_URL", self.aether_proxy_url);
set!("AETHER_TUNNEL_AETHER_TCP_NODELAY", self.aether_tcp_nodelay);
set!("AETHER_TUNNEL_AETHER_HTTP2", self.aether_http2);
set!(
"AETHER_PROXY_AETHER_RETRY_MAX_ATTEMPTS",
"AETHER_TUNNEL_AETHER_OUTBOUND_PROXY_URL",
self.aether_outbound_proxy_url
);
set!(
"AETHER_TUNNEL_AETHER_RETRY_MAX_ATTEMPTS",
self.aether_retry_max_attempts
);
set!(
"AETHER_PROXY_AETHER_RETRY_BASE_DELAY_MS",
"AETHER_TUNNEL_AETHER_RETRY_BASE_DELAY_MS",
self.aether_retry_base_delay_ms
);
set!(
"AETHER_PROXY_AETHER_RETRY_MAX_DELAY_MS",
"AETHER_TUNNEL_AETHER_RETRY_MAX_DELAY_MS",
self.aether_retry_max_delay_ms
);
set!("AETHER_PROXY_DIAGNOSTICS_BIND", self.diagnostics_bind);
set!("AETHER_TUNNEL_DIAGNOSTICS_BIND", self.diagnostics_bind);
set!(
"AETHER_PROXY_MAX_CONCURRENT_CONNECTIONS",
"AETHER_TUNNEL_MAX_CONCURRENT_CONNECTIONS",
self.max_concurrent_connections
);
set!("AETHER_PROXY_DNS_CACHE_TTL", self.dns_cache_ttl_secs);
set!("AETHER_PROXY_DNS_CACHE_CAPACITY", self.dns_cache_capacity);
set!("AETHER_TUNNEL_DNS_CACHE_TTL", self.dns_cache_ttl_secs);
set!("AETHER_TUNNEL_DNS_CACHE_CAPACITY", self.dns_cache_capacity);
set!(
"AETHER_PROXY_UPSTREAM_CONNECT_TIMEOUT",
"AETHER_TUNNEL_UPSTREAM_CONNECT_TIMEOUT",
self.upstream_connect_timeout_secs
);
set!(
"AETHER_PROXY_UPSTREAM_POOL_MAX_IDLE_PER_HOST",
"AETHER_TUNNEL_UPSTREAM_POOL_MAX_IDLE_PER_HOST",
self.upstream_pool_max_idle_per_host
);
set!(
"AETHER_PROXY_UPSTREAM_POOL_IDLE_TIMEOUT",
"AETHER_TUNNEL_UPSTREAM_POOL_IDLE_TIMEOUT",
self.upstream_pool_idle_timeout_secs
);
set!(
"AETHER_PROXY_UPSTREAM_TCP_KEEPALIVE",
"AETHER_TUNNEL_UPSTREAM_TCP_KEEPALIVE",
self.upstream_tcp_keepalive_secs
);
set!(
"AETHER_PROXY_UPSTREAM_TCP_NODELAY",
"AETHER_TUNNEL_UPSTREAM_TCP_NODELAY",
self.upstream_tcp_nodelay
);
set!("AETHER_PROXY_UPSTREAM_PROXY_URL", self.upstream_proxy_url);
set!("AETHER_TUNNEL_UPSTREAM_PROXY_URL", self.upstream_proxy_url);
set!(
"AETHER_PROXY_REDIRECT_REPLAY_BUDGET_BYTES",
"AETHER_TUNNEL_REDIRECT_REPLAY_BUDGET_BYTES",
self.redirect_replay_budget_bytes
);
set!(
"AETHER_PROXY_EMIT_PROXY_TIMING_HEADER",
"AETHER_TUNNEL_EMIT_PROXY_TIMING_HEADER",
self.emit_proxy_timing_header
);
set!("AETHER_PROXY_LOG_LEVEL", self.log_level);
set!("AETHER_TUNNEL_LOG_LEVEL", self.log_level);
set!(
"AETHER_PROXY_LOG_DESTINATION",
"AETHER_TUNNEL_LOG_DESTINATION",
self.log_destination.map(|v| match v {
ProxyLogDestinationArg::Stdout => "stdout",
ProxyLogDestinationArg::File => "file",
ProxyLogDestinationArg::Both => "both",
TunnelLogDestinationArg::Stdout => "stdout",
TunnelLogDestinationArg::File => "file",
TunnelLogDestinationArg::Both => "both",
})
);
set!("AETHER_PROXY_LOG_DIR", self.log_dir.as_deref());
set!("AETHER_TUNNEL_LOG_DIR", self.log_dir.as_deref());
set!(
"AETHER_PROXY_LOG_ROTATION",
"AETHER_TUNNEL_LOG_ROTATION",
self.log_rotation.map(|v| match v {
ProxyLogRotationArg::Hourly => "hourly",
ProxyLogRotationArg::Daily => "daily",
TunnelLogRotationArg::Hourly => "hourly",
TunnelLogRotationArg::Daily => "daily",
})
);
set!("AETHER_PROXY_LOG_RETENTION_DAYS", self.log_retention_days);
set!("AETHER_PROXY_LOG_MAX_FILES", self.log_max_files);
set!("AETHER_TUNNEL_LOG_RETENTION_DAYS", self.log_retention_days);
set!("AETHER_TUNNEL_LOG_MAX_FILES", self.log_max_files);
set!(
"AETHER_PROXY_TUNNEL_RECONNECT_BASE_MS",
"AETHER_TUNNEL_RECONNECT_BASE_MS",
self.tunnel_reconnect_base_ms
);
set!(
"AETHER_PROXY_TUNNEL_RECONNECT_MAX_MS",
"AETHER_TUNNEL_RECONNECT_MAX_MS",
self.tunnel_reconnect_max_ms
);
set!(TUNNEL_PING_INTERVAL_MS_ENV, self.tunnel_ping_interval_ms);
set!("AETHER_PROXY_TUNNEL_MAX_STREAMS", self.tunnel_max_streams);
set!("AETHER_TUNNEL_MAX_STREAMS", self.tunnel_max_streams);
set!(
TUNNEL_CONNECT_TIMEOUT_MS_ENV,
self.tunnel_connect_timeout_ms
);
set!(
"AETHER_PROXY_TUNNEL_TCP_KEEPALIVE",
"AETHER_TUNNEL_TCP_KEEPALIVE",
self.tunnel_tcp_keepalive_secs
);
set!("AETHER_PROXY_TUNNEL_TCP_NODELAY", self.tunnel_tcp_nodelay);
set!("AETHER_TUNNEL_TCP_NODELAY", self.tunnel_tcp_nodelay);
set!(TUNNEL_STALE_TIMEOUT_MS_ENV, self.tunnel_stale_timeout_ms);
set!("AETHER_PROXY_TUNNEL_CONNECTIONS", self.tunnel_connections);
set!("AETHER_TUNNEL_CONNECTIONS", self.tunnel_connections);
set!("AETHER_TUNNEL_CONNECTIONS_MAX", self.tunnel_connections_max);
set!(
"AETHER_PROXY_TUNNEL_CONNECTIONS_MAX",
self.tunnel_connections_max
);
set!(
"AETHER_PROXY_TUNNEL_SCALE_CHECK_INTERVAL_MS",
"AETHER_TUNNEL_SCALE_CHECK_INTERVAL_MS",
self.tunnel_scale_check_interval_ms
);
set!(
"AETHER_PROXY_TUNNEL_SCALE_UP_THRESHOLD_PERCENT",
"AETHER_TUNNEL_SCALE_UP_THRESHOLD_PERCENT",
self.tunnel_scale_up_threshold_percent
);
set!(
"AETHER_PROXY_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT",
"AETHER_TUNNEL_SCALE_DOWN_THRESHOLD_PERCENT",
self.tunnel_scale_down_threshold_percent
);
set!(
"AETHER_PROXY_TUNNEL_SCALE_DOWN_GRACE_SECS",
"AETHER_TUNNEL_SCALE_DOWN_GRACE_SECS",
self.tunnel_scale_down_grace_secs
);
// allowed_ports needs special handling (comma-separated)
if let Some(ref ports) = self.allowed_ports {
if force || std::env::var("AETHER_PROXY_ALLOWED_PORTS").is_err() {
if force || std::env::var("AETHER_TUNNEL_ALLOWED_PORTS").is_err() {
let s: String = ports
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",");
std::env::set_var("AETHER_PROXY_ALLOWED_PORTS", s);
std::env::set_var("AETHER_TUNNEL_ALLOWED_PORTS", s);
}
}
}
@@ -1345,64 +1349,75 @@ mod tests {
}
#[test]
fn config_file_deserializes_aether_proxy_url() {
let cfg: ConfigFile = toml::from_str("aether_proxy_url = \"socks5h://127.0.0.1:1080\"")
.expect("proxy URL toml");
fn config_file_deserializes_aether_outbound_proxy_url() {
let cfg: ConfigFile =
toml::from_str("aether_outbound_proxy_url = \"socks5h://127.0.0.1:1080\"")
.expect("proxy URL toml");
assert_eq!(
cfg.aether_proxy_url.as_deref(),
cfg.aether_outbound_proxy_url.as_deref(),
Some("socks5h://127.0.0.1:1080")
);
}
#[test]
fn aether_proxy_url_requires_explicit_opt_in() {
fn config_file_deserializes_legacy_aether_proxy_url_alias() {
let cfg: ConfigFile = toml::from_str("aether_proxy_url = \"socks5h://127.0.0.1:1080\"")
.expect("legacy proxy URL toml");
assert_eq!(
cfg.aether_outbound_proxy_url.as_deref(),
Some("socks5h://127.0.0.1:1080")
);
}
#[test]
fn aether_outbound_proxy_url_requires_explicit_opt_in() {
let default_direct = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--upstream-proxy-url",
"socks5h://127.0.0.1:1080",
]);
assert_eq!(default_direct.effective_aether_proxy_url(), None);
assert_eq!(default_direct.effective_aether_outbound_proxy_url(), None);
let explicit = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--upstream-proxy-url",
"socks5h://127.0.0.1:1080",
"--aether-proxy-url",
"--aether-outbound-proxy-url",
"http://127.0.0.1:8080",
]);
assert_eq!(
explicit.effective_aether_proxy_url(),
explicit.effective_aether_outbound_proxy_url(),
Some("http://127.0.0.1:8080")
);
}
#[test]
fn proxy_logs_default_to_rotating_file_and_stdout() {
fn tunnel_logs_default_to_rotating_file_and_stdout() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
]);
assert_eq!(config.log_destination, ProxyLogDestinationArg::Both);
assert_eq!(config.log_destination, TunnelLogDestinationArg::Both);
assert_eq!(config.log_dir.as_deref(), Some(DEFAULT_LOG_DIR));
assert_eq!(config.log_rotation, ProxyLogRotationArg::Daily);
assert_eq!(config.log_rotation, TunnelLogRotationArg::Daily);
assert_eq!(config.log_retention_days, DEFAULT_LOG_RETENTION_DAYS);
let runtime = config
@@ -1426,7 +1441,7 @@ mod tests {
aether_url = "https://aether.example.com"
upstream_proxy_url = "socks5://127.0.0.1:1080"
management_token = "ae_test"
node_name = "proxy-test"
node_name = "tunnel-test"
"#,
)
.expect("server-scoped proxy URL should be promoted");
@@ -1449,7 +1464,7 @@ upstream_proxy_url = "socks5://127.0.0.1:1080"
aether_url = "https://aether.example.com"
upstream_proxy_url = "socks5://127.0.0.1:1081"
management_token = "ae_test"
node_name = "proxy-test"
node_name = "tunnel-test"
"#,
)
.expect_err("conflicting proxy URLs should be rejected");
@@ -1505,13 +1520,13 @@ node_name = "proxy-test"
#[test]
fn cli_defaults_private_targets_to_enabled() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
]);
assert!(config.allow_private_targets);
}
@@ -1519,13 +1534,13 @@ node_name = "proxy-test"
#[test]
fn tunnel_fast_recovery_defaults_use_millisecond_values() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
]);
assert_eq!(
config
@@ -1558,13 +1573,13 @@ node_name = "proxy-test"
#[test]
fn tunnel_millisecond_flags_take_effect_when_explicitly_set() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-ping-interval-ms",
"100",
"--tunnel-connect-timeout-ms",
@@ -1595,13 +1610,13 @@ node_name = "proxy-test"
#[test]
fn auto_tunnel_pool_sizing_uses_hardware_capacity() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-max-streams",
"1024",
]);
@@ -1623,13 +1638,13 @@ node_name = "proxy-test"
#[test]
fn auto_tunnel_pool_sizing_prefers_redundant_floor_when_hardware_allows() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-max-streams",
"1024",
]);
@@ -1651,13 +1666,13 @@ node_name = "proxy-test"
#[test]
fn auto_tunnel_pool_sizing_keeps_single_core_nodes_redundant() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-max-streams",
"200",
]);
@@ -1679,13 +1694,13 @@ node_name = "proxy-test"
#[test]
fn auto_tunnel_pool_sizing_respects_stream_admission_limit() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-max-streams",
"45",
"--max-in-flight-streams",
@@ -1709,13 +1724,13 @@ node_name = "proxy-test"
#[test]
fn explicit_tunnel_connections_keep_fixed_pool_without_max_override() {
let config = Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--tunnel-max-streams",
"512",
"--tunnel-connections",

View File

@@ -20,12 +20,14 @@ use clap::{CommandFactory, FromArgMatches, Parser};
use config::Config;
/// Default config file name.
const DEFAULT_CONFIG: &str = "aether-proxy.toml";
const DEFAULT_CONFIG: &str = "aether-tunnel.toml";
const OUTBOUND_PROXY_ENV: &str = "AETHER_TUNNEL_AETHER_OUTBOUND_PROXY_URL";
const LEGACY_OUTBOUND_PROXY_ENV: &str = concat!("AETHER_TUNNEL_AETHER_", "PROXY_URL");
/// Build the full clap command: Config args + discoverable subcommands.
///
/// `subcommand_negates_reqs` lets subcommands bypass the required Config
/// flags so that e.g. `aether-proxy setup` doesn't demand `--aether-url`.
/// flags so that e.g. `aether-tunnel setup` doesn't demand `--aether-url`.
fn build_command() -> clap::Command {
Config::command()
.subcommand(
@@ -57,9 +59,11 @@ async fn main() -> anyhow::Result<()> {
.install_default()
.map_err(|_| anyhow::anyhow!("Failed to install rustls CryptoProvider"))?;
promote_legacy_env_overrides();
// Load config file as env-var defaults (before clap parsing)
let config_file_path =
std::env::var("AETHER_PROXY_CONFIG").unwrap_or_else(|_| DEFAULT_CONFIG.to_string());
std::env::var("AETHER_TUNNEL_CONFIG").unwrap_or_else(|_| DEFAULT_CONFIG.to_string());
let config_path = std::path::Path::new(&config_file_path);
if config_path.exists() {
match config::ConfigFile::load(config_path) {
@@ -96,9 +100,9 @@ async fn main() -> anyhow::Result<()> {
}
Some(_) => unreachable!(),
None => {
// No subcommand run the proxy with parsed config.
// No subcommand: run the tunnel with parsed config.
let config = Config::from_arg_matches(&matches)?;
run_proxy(config).await
run_tunnel(config).await
}
},
Err(e) => {
@@ -112,6 +116,14 @@ async fn main() -> anyhow::Result<()> {
}
}
fn promote_legacy_env_overrides() {
if std::env::var_os(OUTBOUND_PROXY_ENV).is_none() {
if let Some(value) = std::env::var_os(LEGACY_OUTBOUND_PROXY_ENV) {
std::env::set_var(OUTBOUND_PROXY_ENV, value);
}
}
}
/// Decide what to do after the setup wizard completes.
async fn handle_setup_result(outcome: setup::SetupOutcome) -> anyhow::Result<()> {
match outcome {
@@ -124,10 +136,10 @@ async fn handle_setup_result(outcome: setup::SetupOutcome) -> anyhow::Result<()>
Err(e) => anyhow::bail!("failed to reload config after setup: {}", e),
}
// Parse from env-only (argv may still contain "setup" etc.)
let config = Config::try_parse_from(["aether-proxy"])
let config = Config::try_parse_from(["aether-tunnel"])
.map_err(|e| anyhow::anyhow!("config invalid after setup: {}", e))?;
eprintln!(" Starting proxy...\n");
run_proxy(config).await
eprintln!(" Starting tunnel...\n");
run_tunnel(config).await
}
setup::SetupOutcome::Cancelled => {
eprintln!(" Setup cancelled.");
@@ -136,10 +148,10 @@ async fn handle_setup_result(outcome: setup::SetupOutcome) -> anyhow::Result<()>
}
}
/// Start the proxy server, checking for managed-service conflicts first.
async fn run_proxy(config: Config) -> anyhow::Result<()> {
/// Start the tunnel agent, checking for managed-service conflicts first.
async fn run_tunnel(config: Config) -> anyhow::Result<()> {
// Warn if a managed service is already running (would cause conflicts).
if std::env::var_os("AETHER_PROXY_SERVICE_MANAGER").is_none()
if std::env::var_os("AETHER_TUNNEL_SERVICE_MANAGER").is_none()
&& std::env::var_os("INVOCATION_ID").is_none()
&& setup::service::is_service_active()
{
@@ -147,15 +159,15 @@ async fn run_proxy(config: Config) -> anyhow::Result<()> {
"Warning: {} service is already running.",
setup::service::preferred_manager_name()
);
eprintln!("Use `./aether-proxy stop` to stop it first, or manage via subcommands:");
eprintln!(" ./aether-proxy status / logs / restart / stop");
eprintln!("Use `./aether-tunnel stop` to stop it first, or manage via subcommands:");
eprintln!(" ./aether-tunnel status / logs / restart / stop");
std::process::exit(1);
}
// Resolve server list: if a config file exists, it must use [[servers]].
// Otherwise fall back to CLI/env single-server mode.
let config_path =
std::env::var("AETHER_PROXY_CONFIG").unwrap_or_else(|_| DEFAULT_CONFIG.to_string());
std::env::var("AETHER_TUNNEL_CONFIG").unwrap_or_else(|_| DEFAULT_CONFIG.to_string());
let servers = if std::path::Path::new(&config_path).exists() {
let file_cfg = config::ConfigFile::load(std::path::Path::new(&config_path))?;
if file_cfg.servers.is_empty() {

View File

@@ -15,7 +15,7 @@ pub async fn detect_public_ip() -> anyhow::Result<String> {
let client = build_http_client(&HttpClientConfig {
request_timeout_ms: Some(5_000),
user_agent: Some("aether-proxy/net".to_string()),
user_agent: Some("aether-tunnel/net".to_string()),
..HttpClientConfig::default()
})?;
@@ -52,7 +52,7 @@ pub async fn detect_region(ip: &str) -> Option<String> {
let client = build_http_client(&HttpClientConfig {
request_timeout_ms: Some(5_000),
user_agent: Some("aether-proxy/net".to_string()),
user_agent: Some("aether-tunnel/net".to_string()),
..HttpClientConfig::default()
})
.ok()?;

View File

@@ -44,7 +44,7 @@ struct UnregisterRequest {
node_id: String,
}
/// Aether API client for proxy node lifecycle management.
/// Aether API client for tunnel node lifecycle management.
pub struct AetherClient {
http: Client,
base_url: String,
@@ -66,8 +66,10 @@ impl AetherClient {
},
tcp_nodelay: config.aether_tcp_nodelay,
http2_adaptive_window: config.aether_http2,
user_agent: Some(format!("aether-proxy/{}", env!("CARGO_PKG_VERSION"))),
proxy_url: config.effective_aether_proxy_url().map(str::to_string),
user_agent: Some(format!("aether-tunnel/{}", env!("CARGO_PKG_VERSION"))),
proxy_url: config
.effective_aether_outbound_proxy_url()
.map(str::to_string),
..HttpClientConfig::default()
})
.expect("failed to create HTTP client");

View File

@@ -1,4 +1,4 @@
//! Service installation and management for `aether-proxy`.
//! Service installation and management for `aether-tunnel`.
//!
//! Supports the host-native service manager we currently target:
//! `systemd` on most Linux distributions and `OpenRC` on Alpine.
@@ -8,15 +8,15 @@ use std::io::ErrorKind;
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
const SERVICE_NAME: &str = "aether-proxy";
const SERVICE_NAME: &str = "aether-tunnel";
const SYSTEMD_UNIT_PATH: &str = "/etc/systemd/system/aether-proxy.service";
const SYSTEMD_UNIT_PATH: &str = "/etc/systemd/system/aether-tunnel.service";
const OPENRC_INIT_PATH: &str = "/etc/init.d/aether-proxy";
const OPENRC_PID_PATH: &str = "/run/aether-proxy.pid";
const OPENRC_LOG_DIR: &str = "/var/log/aether-proxy";
const OPENRC_STDOUT_LOG: &str = "/var/log/aether-proxy/current.log";
const OPENRC_STDERR_LOG: &str = "/var/log/aether-proxy/error.log";
const OPENRC_INIT_PATH: &str = "/etc/init.d/aether-tunnel";
const OPENRC_PID_PATH: &str = "/run/aether-tunnel.pid";
const OPENRC_LOG_DIR: &str = "/var/log/aether-tunnel";
const OPENRC_STDOUT_LOG: &str = "/var/log/aether-tunnel/current.log";
const OPENRC_STDERR_LOG: &str = "/var/log/aether-tunnel/error.log";
const OPENRC_RUN_BINS: &[&str] = &["/sbin/openrc-run", "/usr/sbin/openrc-run", "openrc-run"];
const OPENRC_SERVICE_BINS: &[&str] = &["/sbin/rc-service", "/usr/sbin/rc-service", "rc-service"];
@@ -69,7 +69,7 @@ pub fn unavailable_hint() -> String {
match detect_service_manager() {
Some(manager) if !is_root() => {
format!(
"requires root with {}, use: sudo aether-proxy setup",
"requires root with {}, use: sudo aether-tunnel setup",
manager.display_name()
)
}
@@ -86,7 +86,7 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
.ok_or_else(|| anyhow::anyhow!("no supported service manager detected (systemd/OpenRC)"))?;
if !is_root() {
anyhow::bail!("root required, use: sudo ./aether-proxy setup");
anyhow::bail!("root required, use: sudo ./aether-tunnel setup");
}
match manager {
@@ -221,13 +221,13 @@ fn ensure_openrc_logs_readable() -> anyhow::Result<()> {
Ok(_) => {}
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
anyhow::bail!(
"OpenRC logs are stored under {} and usually require root access. Try `sudo ./aether-proxy logs`.",
"OpenRC logs are stored under {} and usually require root access. Try `sudo ./aether-tunnel logs`.",
OPENRC_LOG_DIR
);
}
Err(err) if err.kind() == ErrorKind::NotFound => {
anyhow::bail!(
"OpenRC log file not found at {}. Start the service first or check `./aether-proxy status`.",
"OpenRC log file not found at {}. Start the service first or check `./aether-tunnel status`.",
path
);
}
@@ -252,14 +252,14 @@ fn active_service_manager() -> Option<ServiceManager> {
fn ensure_service_installed() -> anyhow::Result<ServiceManager> {
installed_manager().ok_or_else(|| {
anyhow::anyhow!("service not installed, run `sudo ./aether-proxy setup` first")
anyhow::anyhow!("service not installed, run `sudo ./aether-tunnel setup` first")
})
}
fn ensure_root_and_service() -> anyhow::Result<ServiceManager> {
let manager = ensure_service_installed()?;
if !is_root() {
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
anyhow::bail!("root required, use: sudo ./aether-tunnel <command>");
}
Ok(manager)
}
@@ -295,22 +295,22 @@ fn install_systemd_service(config_path: &Path) -> anyhow::Result<()> {
let unit_content = format!(
"[Unit]\n\
Description=Aether Proxy\n\
Description=Aether Tunnel\n\
After=network.target\n\
\n\
[Service]\n\
Type=simple\n\
WorkingDirectory={working_dir}\n\
Environment=AETHER_PROXY_CONFIG={config_str}\n\
Environment=AETHER_PROXY_SERVICE_MANAGER=systemd\n\
Environment=AETHER_PROXY_LOG_DESTINATION=both\n\
Environment=AETHER_PROXY_LOG_DIR=/var/log/aether-proxy\n\
Environment=AETHER_TUNNEL_CONFIG={config_str}\n\
Environment=AETHER_TUNNEL_SERVICE_MANAGER=systemd\n\
Environment=AETHER_TUNNEL_LOG_DESTINATION=both\n\
Environment=AETHER_TUNNEL_LOG_DIR=/var/log/aether-tunnel\n\
ExecStart={exe_str}\n\
Restart=on-failure\n\
RestartSec=5\n\
LimitNOFILE=65535\n\
UMask=0077\n\
LogsDirectory=aether-proxy\n\
LogsDirectory=aether-tunnel\n\
LogsDirectoryMode=0750\n\
\n\
[Install]\n\
@@ -326,7 +326,7 @@ fn install_systemd_service(config_path: &Path) -> anyhow::Result<()> {
if manager_is_active(ServiceManager::Systemd) {
eprintln!(" Service started successfully!");
} else {
eprintln!(" Service state is not active yet. Check `sudo ./aether-proxy logs`.");
eprintln!(" Service state is not active yet. Check `sudo ./aether-tunnel logs`.");
}
print_post_install_commands();
@@ -426,7 +426,7 @@ stop() {{
"#,
openrc_run_bin(),
shell_quote(SERVICE_NAME),
shell_quote("Aether Proxy"),
shell_quote("Aether Tunnel"),
shell_quote(exe_str),
shell_quote(working_dir),
shell_quote(OPENRC_PID_PATH),
@@ -434,10 +434,10 @@ stop() {{
shell_quote(OPENRC_STDOUT_LOG),
shell_quote(OPENRC_STDERR_LOG),
shell_quote(supervise_daemon_bin()),
shell_quote(&format!("AETHER_PROXY_CONFIG={config_str}")),
shell_quote("AETHER_PROXY_SERVICE_MANAGER=openrc"),
shell_quote("AETHER_PROXY_LOG_DESTINATION=both"),
shell_quote(&format!("AETHER_PROXY_LOG_DIR={OPENRC_LOG_DIR}")),
shell_quote(&format!("AETHER_TUNNEL_CONFIG={config_str}")),
shell_quote("AETHER_TUNNEL_SERVICE_MANAGER=openrc"),
shell_quote("AETHER_TUNNEL_LOG_DESTINATION=both"),
shell_quote(&format!("AETHER_TUNNEL_LOG_DIR={OPENRC_LOG_DIR}")),
);
std::fs::write(OPENRC_INIT_PATH, &init_content)?;
set_mode(OPENRC_INIT_PATH, 0o755)?;
@@ -450,7 +450,7 @@ stop() {{
if manager_is_active(ServiceManager::OpenRc) {
eprintln!(" Service started successfully!");
} else {
eprintln!(" Service state is not active yet. Check `sudo ./aether-proxy logs`.");
eprintln!(" Service state is not active yet. Check `sudo ./aether-tunnel logs`.");
}
print_post_install_commands();
@@ -552,11 +552,11 @@ fn manager_is_active(manager: ServiceManager) -> bool {
fn print_post_install_commands() {
eprintln!();
eprintln!(" Commands:");
eprintln!(" ./aether-proxy status # service status");
eprintln!(" sudo ./aether-proxy logs # tail logs");
eprintln!(" sudo ./aether-proxy restart # restart");
eprintln!(" sudo ./aether-proxy stop # stop");
eprintln!(" sudo ./aether-proxy uninstall # remove service");
eprintln!(" ./aether-tunnel status # service status");
eprintln!(" sudo ./aether-tunnel logs # tail logs");
eprintln!(" sudo ./aether-tunnel restart # restart");
eprintln!(" sudo ./aether-tunnel stop # stop");
eprintln!(" sudo ./aether-tunnel uninstall # remove service");
eprintln!();
}

View File

@@ -1,6 +1,6 @@
//! Interactive TUI for configuring aether-proxy.
//! Interactive TUI for configuring aether-tunnel.
//!
//! Launched via `aether-proxy setup [path]`. Presents a full-screen form
//! Launched via `aether-tunnel setup [path]`. Presents a full-screen form
//! backed by ratatui where the user can navigate fields, edit values, and
//! save to a TOML config file. Supports multi-server configuration via
//! a tabbed interface.
@@ -21,8 +21,8 @@ use ratatui::Frame;
use ratatui::Terminal;
use crate::config::{
format_byte_size_human, parse_byte_size, ConfigFile, ProxyLogDestinationArg,
ProxyLogRotationArg, ServerEntry, DEFAULT_HEARTBEAT_INTERVAL_SECS, DEFAULT_LOG_MAX_FILES,
format_byte_size_human, parse_byte_size, ConfigFile, ServerEntry, TunnelLogDestinationArg,
TunnelLogRotationArg, DEFAULT_HEARTBEAT_INTERVAL_SECS, DEFAULT_LOG_MAX_FILES,
DEFAULT_LOG_RETENTION_DAYS, DEFAULT_REDIRECT_REPLAY_BUDGET_HUMAN,
};
use crate::egress_proxy::UpstreamProxyConfig;
@@ -31,7 +31,7 @@ use crate::egress_proxy::UpstreamProxyConfig;
pub enum SetupOutcome {
/// Config saved and the selected host service was installed.
ServiceInstalled,
/// Config saved; no service -- caller should start the proxy directly.
/// Config saved; no service -- caller should start the tunnel directly.
ReadyToRun(PathBuf),
/// User quit without saving.
Cancelled,
@@ -269,7 +269,7 @@ impl App {
"save_logs_to_file" => cfg.log_destination.map(|value| {
matches!(
value,
ProxyLogDestinationArg::File | ProxyLogDestinationArg::Both
TunnelLogDestinationArg::File | TunnelLogDestinationArg::Both
)
.to_string()
}),
@@ -371,7 +371,7 @@ impl App {
fn default_file_log_dir(&self) -> String {
if self.toggle_enabled("install_service") {
return "/var/log/aether-proxy".to_string();
return "/var/log/aether-tunnel".to_string();
}
let base = self
@@ -401,12 +401,12 @@ impl App {
redirect_replay_budget_bytes: self.parse_optional_redirect_replay_budget()?,
upstream_proxy_url: self.parse_optional_upstream_proxy_url()?,
log_destination: Some(if save_logs_to_file {
ProxyLogDestinationArg::Both
TunnelLogDestinationArg::Both
} else {
ProxyLogDestinationArg::Stdout
TunnelLogDestinationArg::Stdout
}),
log_dir: save_logs_to_file.then(|| self.default_file_log_dir()),
log_rotation: save_logs_to_file.then_some(ProxyLogRotationArg::Daily),
log_rotation: save_logs_to_file.then_some(TunnelLogRotationArg::Daily),
log_retention_days: save_logs_to_file.then_some(DEFAULT_LOG_RETENTION_DAYS),
log_max_files: save_logs_to_file.then_some(DEFAULT_LOG_MAX_FILES),
..ConfigFile::default()
@@ -733,9 +733,9 @@ fn ui(f: &mut Frame, app: &mut App) {
let area = f.area();
let title = if app.modified {
" Aether Proxy Setup [*] "
" Aether Tunnel Setup [*] "
} else {
" Aether Proxy Setup "
" Aether Tunnel Setup "
};
let outer = Block::default()
@@ -1014,7 +1014,7 @@ pub fn run(config_path: PathBuf) -> anyhow::Result<SetupOutcome> {
Ok(()) => return Ok(SetupOutcome::ServiceInstalled),
Err(e) => {
eprintln!(" Service install failed: {}", e);
eprintln!(" Starting proxy directly instead.\n");
eprintln!(" Starting tunnel directly instead.\n");
}
}
} else if super::service::is_installed() {
@@ -1070,7 +1070,7 @@ mod tests {
}
fn sample_app() -> App {
let mut app = App::new(PathBuf::from("aether-proxy.toml"));
let mut app = App::new(PathBuf::from("aether-tunnel.toml"));
set_server_field(&mut app, "aether_url", "https://aether.example.com");
set_server_field(&mut app, "management_token", "ae_test");
set_server_field(&mut app, "node_name", "jp-proxy-01");
@@ -1082,7 +1082,7 @@ mod tests {
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should work")
.as_nanos();
std::env::temp_dir().join(format!("aether-proxy-{name}-{nanos}.toml"))
std::env::temp_dir().join(format!("aether-tunnel-{name}-{nanos}.toml"))
}
#[test]
@@ -1119,9 +1119,9 @@ mod tests {
set_global_field(&mut app, "save_logs_to_file", "true");
let cfg = app.to_config().expect("config should serialize");
assert_eq!(cfg.log_destination, Some(ProxyLogDestinationArg::Both));
assert_eq!(cfg.log_destination, Some(TunnelLogDestinationArg::Both));
assert_eq!(cfg.log_dir.as_deref(), Some("logs"));
assert_eq!(cfg.log_rotation, Some(ProxyLogRotationArg::Daily));
assert_eq!(cfg.log_rotation, Some(TunnelLogRotationArg::Daily));
assert_eq!(cfg.log_retention_days, Some(DEFAULT_LOG_RETENTION_DAYS));
assert_eq!(cfg.log_max_files, Some(DEFAULT_LOG_MAX_FILES));
}
@@ -1133,7 +1133,7 @@ mod tests {
set_global_field(&mut app, "save_logs_to_file", "true");
let cfg = app.to_config().expect("config should serialize");
assert_eq!(cfg.log_dir.as_deref(), Some("/var/log/aether-proxy"));
assert_eq!(cfg.log_dir.as_deref(), Some("/var/log/aether-tunnel"));
}
#[test]

View File

@@ -1,4 +1,4 @@
//! Self-upgrade support for `aether-proxy`.
//! Self-upgrade support for `aether-tunnel`.
//!
//! Downloads a release from GitHub, verifies the SHA256 checksum, replaces the
//! running binary atomically, and restarts the active managed service when
@@ -69,7 +69,7 @@ fn build_github_client() -> anyhow::Result<reqwest::Client> {
reqwest::Client::builder().default_headers(headers),
&HttpClientConfig {
request_timeout_ms: Some(300_000),
user_agent: Some(format!("aether-proxy/{}", CURRENT_VERSION)),
user_agent: Some(format!("aether-tunnel/{}", CURRENT_VERSION)),
..HttpClientConfig::default()
},
)
@@ -84,11 +84,11 @@ async fn fetch_release(
) -> anyhow::Result<GithubRelease> {
match version {
Some(ver) => {
// Accept both "proxy-v0.2.0" and bare "0.2.0"
let tag = if ver.starts_with("proxy-v") {
// Accept both "tunnel-v0.2.0" and the legacy "proxy-v0.2.0".
let tag = if ver.starts_with("tunnel-v") || ver.starts_with("proxy-v") {
ver.to_string()
} else {
format!("proxy-v{}", ver)
format!("tunnel-v{}", ver)
};
let url = format!(
"{}/repos/{}/releases/tags/{}",
@@ -103,7 +103,7 @@ async fn fetch_release(
Ok(resp.json().await?)
}
None => {
// List releases and find the latest proxy-v* tag
// List releases and find the latest tunnel-v* tag
let url = format!(
"{}/repos/{}/releases?per_page=20",
GITHUB_API_BASE, GITHUB_REPO
@@ -117,8 +117,8 @@ async fn fetch_release(
let releases: Vec<GithubRelease> = resp.json().await?;
releases
.into_iter()
.find(|r| r.tag_name.starts_with("proxy-v"))
.ok_or_else(|| anyhow::anyhow!("no proxy-v* release found"))
.find(|r| r.tag_name.starts_with("tunnel-v") || r.tag_name.starts_with("proxy-v"))
.ok_or_else(|| anyhow::anyhow!("no tunnel-v* release found"))
}
}
}
@@ -171,7 +171,7 @@ async fn download_and_verify(
platform: &str,
dest: &Path,
) -> anyhow::Result<()> {
let archive_name = format!("aether-proxy-{}.tar.gz", platform);
let archive_name = format!("aether-tunnel-{}.tar.gz", platform);
eprintln!(" Downloading {}...", archive_name);
let (archive_bytes, checksum_bytes) = tokio::try_join!(
@@ -220,9 +220,9 @@ fn extract_binary(archive_bytes: &[u8], dest: &Path) -> anyhow::Result<()> {
let mut archive = Archive::new(decoder);
let binary_name = if cfg!(target_os = "windows") {
"aether-proxy.exe"
"aether-tunnel.exe"
} else {
"aether-proxy"
"aether-tunnel"
};
for entry in archive.entries()? {
@@ -310,7 +310,7 @@ async fn execute_upgrade(
let exe_dir = current_exe
.parent()
.ok_or_else(|| anyhow::anyhow!("cannot determine binary directory"))?;
let temp_path = exe_dir.join(".aether-proxy.upgrade.tmp");
let temp_path = exe_dir.join(".aether-tunnel.upgrade.tmp");
if require_root {
if !super::service::is_root() {
@@ -318,14 +318,14 @@ async fn execute_upgrade(
}
} else if !super::service::is_root() {
// Check write permission to binary directory for manual upgrade mode.
let test_path = exe_dir.join(".aether-proxy.write-test");
let test_path = exe_dir.join(".aether-tunnel.write-test");
match std::fs::File::create(&test_path) {
Ok(_) => {
let _ = std::fs::remove_file(&test_path);
}
Err(_) => {
anyhow::bail!(
"no write access to {}. Use: sudo aether-proxy upgrade",
"no write access to {}. Use: sudo aether-tunnel upgrade",
exe_dir.display()
);
}
@@ -339,7 +339,10 @@ async fn execute_upgrade(
let client = build_github_client()?;
let release = fetch_release(&client, version).await?;
let target_tag = &release.tag_name;
let target_semver = target_tag.strip_prefix("proxy-v").unwrap_or(target_tag);
let target_semver = target_tag
.strip_prefix("tunnel-v")
.or_else(|| target_tag.strip_prefix("proxy-v"))
.unwrap_or(target_tag);
eprintln!(" Target version: {} ({})", target_tag, release.name);
@@ -378,12 +381,12 @@ async fn execute_upgrade(
Ok(()) => eprintln!(" Service restarted."),
Err(e) => {
eprintln!(" WARNING: failed to restart service: {}", e);
eprintln!(" Run manually: sudo aether-proxy restart");
eprintln!(" Run manually: sudo aether-tunnel restart");
}
}
} else {
eprintln!(" Managed service is active, but restart requires root.");
eprintln!(" Run: sudo aether-proxy restart");
eprintln!(" Run: sudo aether-tunnel restart");
eprintln!(" Skipping restart.");
}
} else {
@@ -409,7 +412,7 @@ async fn execute_upgrade(
Ok(())
}
/// `aether-proxy upgrade [version]` -- self-upgrade from GitHub releases.
/// `aether-tunnel upgrade [version]` -- self-upgrade from GitHub releases.
pub async fn cmd_upgrade(version: Option<String>) -> anyhow::Result<()> {
execute_upgrade(version.as_deref(), false, RestartMode::BestEffort).await
}

View File

@@ -57,7 +57,7 @@ pub struct ServerContext {
/// Per-server active connection count.
pub active_connections: Arc<AtomicU64>,
/// Per-server request/latency metrics.
pub metrics: Arc<ProxyMetrics>,
pub metrics: Arc<TunnelRequestMetrics>,
/// Per-server tunnel stability/traffic metrics.
pub tunnel_metrics: Arc<TunnelMetrics>,
}
@@ -68,8 +68,8 @@ impl ServerContext {
samples.extend(self.tunnel_metrics.to_metric_samples(&self.server_label));
samples.push(
MetricSample::new(
"proxy_active_connections",
"Current number of active tunneled streams handled by this proxy server context.",
"tunnel_active_connections",
"Current number of active tunneled streams handled by this tunnel server context.",
MetricKind::Gauge,
self.active_connections.load(Ordering::Acquire),
)
@@ -80,7 +80,7 @@ impl ServerContext {
}
/// Aggregate metrics for reporting to Aether.
pub struct ProxyMetrics {
pub struct TunnelRequestMetrics {
pub total_requests: AtomicU64,
/// Cumulative connection-establishment latency in nanoseconds
/// (DNS + TCP/TLS + TTFB, excludes response body streaming).
@@ -92,7 +92,7 @@ pub struct ProxyMetrics {
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
pub struct ProxyMetricsSnapshot {
pub struct TunnelRequestMetricsSnapshot {
pub total_requests: u64,
pub total_latency_ns: u64,
pub failed_requests: u64,
@@ -101,7 +101,7 @@ pub struct ProxyMetricsSnapshot {
pub slow_requests: u64,
}
impl ProxyMetricsSnapshot {
impl TunnelRequestMetricsSnapshot {
pub fn average_latency_ns(self) -> Option<u64> {
self.total_latency_ns.checked_div(self.total_requests)
}
@@ -127,7 +127,7 @@ impl ProxyMetricsSnapshot {
}
}
impl ProxyMetrics {
impl TunnelRequestMetrics {
pub fn new() -> Self {
Self {
total_requests: AtomicU64::new(0),
@@ -151,8 +151,8 @@ impl ProxyMetrics {
self.slow_requests.fetch_add(1, Ordering::Release);
}
pub fn snapshot(&self) -> ProxyMetricsSnapshot {
ProxyMetricsSnapshot {
pub fn snapshot(&self) -> TunnelRequestMetricsSnapshot {
TunnelRequestMetricsSnapshot {
total_requests: self.total_requests.load(Ordering::Acquire),
total_latency_ns: self.total_latency_ns.load(Ordering::Acquire),
failed_requests: self.failed_requests.load(Ordering::Acquire),
@@ -167,50 +167,50 @@ impl ProxyMetrics {
let labels = vec![MetricLabel::new("server", server_label)];
vec![
MetricSample::new(
"proxy_requests_total",
"Total number of tunneled upstream requests completed by the proxy.",
"tunnel_requests_total",
"Total number of tunneled upstream requests completed by the tunnel.",
MetricKind::Counter,
snapshot.total_requests,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_request_latency_total_ns",
"Cumulative proxy request latency in nanoseconds through upstream response headers.",
"tunnel_request_latency_total_ns",
"Cumulative tunnel request latency in nanoseconds through upstream response headers.",
MetricKind::Counter,
snapshot.total_latency_ns,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_request_latency_avg_ns",
"Average proxy request latency in nanoseconds through upstream response headers.",
"tunnel_request_latency_avg_ns",
"Average tunnel request latency in nanoseconds through upstream response headers.",
MetricKind::Gauge,
snapshot.average_latency_ns().unwrap_or(0),
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_failed_requests_total",
"tunnel_failed_requests_total",
"Total number of tunneled upstream requests that failed before response headers.",
MetricKind::Counter,
snapshot.failed_requests,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_dns_failures_total",
"tunnel_dns_failures_total",
"Total number of tunneled upstream requests rejected or failed during target validation or DNS.",
MetricKind::Counter,
snapshot.dns_failures,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_stream_errors_total",
"tunnel_stream_errors_total",
"Total number of tunneled response body stream errors.",
MetricKind::Counter,
snapshot.stream_errors,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_slow_requests_total",
"Total number of tunneled requests crossing the proxy slow-request threshold.",
"tunnel_slow_requests_total",
"Total number of tunneled requests crossing the tunnel slow-request threshold.",
MetricKind::Counter,
snapshot.slow_requests,
)
@@ -433,92 +433,92 @@ impl TunnelMetrics {
let labels = vec![MetricLabel::new("server", server_label)];
vec![
MetricSample::new(
"proxy_tunnel_connect_attempts_total",
"tunnel_connect_attempts_total",
"Total number of WebSocket tunnel connection attempts.",
MetricKind::Counter,
snapshot.connect_attempts,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_connect_successes_total",
"tunnel_connect_successes_total",
"Total number of successful WebSocket tunnel connections.",
MetricKind::Counter,
snapshot.connect_successes,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_connect_errors_total",
"tunnel_connect_errors_total",
"Total number of WebSocket tunnel connection errors.",
MetricKind::Counter,
snapshot.connect_errors,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_disconnects_total",
"tunnel_disconnects_total",
"Total number of WebSocket tunnel disconnects.",
MetricKind::Counter,
snapshot.disconnects,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_heartbeat_sent_total",
"tunnel_heartbeat_sent_total",
"Total number of tunnel heartbeats sent.",
MetricKind::Counter,
snapshot.heartbeat_sent,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_heartbeat_ack_total",
"tunnel_heartbeat_ack_total",
"Total number of tunnel heartbeat acknowledgements received.",
MetricKind::Counter,
snapshot.heartbeat_ack,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_heartbeat_rtt_last_ms",
"tunnel_heartbeat_rtt_last_ms",
"Last observed tunnel heartbeat round-trip time in milliseconds.",
MetricKind::Gauge,
snapshot.heartbeat_rtt_last_ms,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_heartbeat_rtt_avg_ms",
"tunnel_heartbeat_rtt_avg_ms",
"Average observed tunnel heartbeat round-trip time in milliseconds.",
MetricKind::Gauge,
snapshot.heartbeat_rtt_avg_ms().unwrap_or(0.0) as u64,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_ws_in_frames_total",
"Total number of WebSocket frames received by the proxy tunnel.",
"tunnel_ws_in_frames_total",
"Total number of WebSocket frames received by the tunnel.",
MetricKind::Counter,
snapshot.ws_in_frames,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_ws_in_bytes_total",
"Total number of WebSocket bytes received by the proxy tunnel.",
"tunnel_ws_in_bytes_total",
"Total number of WebSocket bytes received by the tunnel.",
MetricKind::Counter,
snapshot.ws_in_bytes,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_ws_out_frames_total",
"Total number of WebSocket frames sent by the proxy tunnel.",
"tunnel_ws_out_frames_total",
"Total number of WebSocket frames sent by the tunnel.",
MetricKind::Counter,
snapshot.ws_out_frames,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_ws_out_bytes_total",
"Total number of WebSocket bytes sent by the proxy tunnel.",
"tunnel_ws_out_bytes_total",
"Total number of WebSocket bytes sent by the tunnel.",
MetricKind::Counter,
snapshot.ws_out_bytes,
)
.with_labels(labels.clone()),
MetricSample::new(
"proxy_tunnel_error_events_total",
"Total number of classified tunnel error events recorded by the proxy.",
"tunnel_error_events_total",
"Total number of classified tunnel error events recorded by the tunnel.",
MetricKind::Counter,
snapshot.error_events_total,
)
@@ -564,14 +564,14 @@ fn classify_tunnel_error(category: &str, _message: &str) -> TunnelErrorDiagnosti
component: "tunnel_read",
summary: "No inbound tunnel frames before stale timeout",
operator_action:
"Check gateway or reverse-proxy idle timeouts, packet loss, and WebSocket ping/pong reachability. Increase AETHER_PROXY_TUNNEL_STALE_TIMEOUT_MS if the network is high-latency.",
"Check gateway or reverse-proxy idle timeouts, packet loss, and WebSocket ping/pong reachability. Increase AETHER_TUNNEL_STALE_TIMEOUT_MS if the network is high-latency.",
},
"ws_write_error" => TunnelErrorDiagnostic {
severity: "error",
component: "tunnel_write",
summary: "WebSocket write failed because the peer closed or reset the connection",
operator_action:
"Check gateway restarts, load balancer resets, NAT/firewall connection tracking, and whether the proxy is reconnecting successfully.",
"Check gateway restarts, load balancer resets, NAT/firewall connection tracking, and whether the tunnel is reconnecting successfully.",
},
"ws_ping_error" => TunnelErrorDiagnostic {
severity: "error",
@@ -592,35 +592,35 @@ fn classify_tunnel_error(category: &str, _message: &str) -> TunnelErrorDiagnosti
component: "tunnel_connect",
summary: "Tunnel connection attempt failed",
operator_action:
"Check Aether URL reachability, DNS, TLS, management token validity, and any configured AETHER_PROXY_AETHER_PROXY_URL.",
"Check Aether URL reachability, DNS, TLS, management token validity, and any configured AETHER_TUNNEL_AETHER_OUTBOUND_PROXY_URL.",
},
"frame_decode_error" => TunnelErrorDiagnostic {
severity: "error",
component: "tunnel_protocol",
summary: "Received tunnel frame could not be decoded",
operator_action:
"Check proxy and gateway version compatibility and whether traffic is being modified by an intermediary.",
"Check tunnel and gateway version compatibility and whether traffic is being modified by an intermediary.",
},
"stream_dispatch_timeout" => TunnelErrorDiagnostic {
severity: "warning",
component: "stream_dispatch",
summary: "Request body frame could not be delivered to its stream handler in time",
operator_action:
"Check proxy CPU, memory, stream concurrency saturation, and slow upstream provider requests.",
"Check tunnel CPU, memory, stream concurrency saturation, and slow upstream provider requests.",
},
"heartbeat_ack_empty" | "heartbeat_ack_parse" => TunnelErrorDiagnostic {
severity: "warning",
component: "heartbeat",
summary: "Heartbeat ACK from gateway was missing or invalid",
operator_action:
"Check gateway heartbeat handler logs and proxy/gateway version compatibility.",
"Check gateway heartbeat handler logs and tunnel/gateway version compatibility.",
},
"writer_task_panic" | "writer_task_cancelled" => TunnelErrorDiagnostic {
severity: "error",
component: "tunnel_writer",
summary: "Tunnel writer task exited unexpectedly",
operator_action:
"Check proxy logs for the preceding write or ping error and confirm the tunnel reconnect loop is active.",
"Check tunnel logs for the preceding write or ping error and confirm the tunnel reconnect loop is active.",
},
"dispatcher_error" => TunnelErrorDiagnostic {
severity: "error",
@@ -634,16 +634,16 @@ fn classify_tunnel_error(category: &str, _message: &str) -> TunnelErrorDiagnosti
component: "tunnel",
summary: "Tunnel reported an unclassified error",
operator_action:
"Inspect the raw message and compare it with proxy, gateway, and network logs at the same time.",
"Inspect the raw message and compare it with tunnel, gateway, and network logs at the same time.",
},
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ProxyAdmissionError {
#[error("proxy stream admission saturated at {limit} for gate {gate}")]
pub enum TunnelAdmissionError {
#[error("tunnel stream admission saturated at {limit} for gate {gate}")]
Saturated { gate: &'static str, limit: usize },
#[error("proxy stream admission unavailable for gate {gate}: {message}")]
#[error("tunnel stream admission unavailable for gate {gate}: {message}")]
Unavailable {
gate: &'static str,
limit: usize,
@@ -653,14 +653,14 @@ pub enum ProxyAdmissionError {
impl AppState {
pub async fn metric_samples(&self) -> Vec<MetricSample> {
let mut samples = vec![service_up_sample("aether-proxy")];
let mut samples = vec![service_up_sample("aether-tunnel")];
if let Some(snapshot) = self.stream_concurrency_snapshot() {
samples.extend(snapshot.to_metric_samples("proxy_streams"));
samples.extend(snapshot.to_metric_samples("tunnel_streams"));
}
if let Some(gate) = self.distributed_stream_gate.as_ref() {
match gate.snapshot().await {
Ok(snapshot) => {
samples.extend(snapshot.to_metric_samples("proxy_streams_distributed"));
samples.extend(snapshot.to_metric_samples("tunnel_streams_distributed"));
}
Err(_) => samples.push(
MetricSample::new(
@@ -669,7 +669,7 @@ impl AppState {
MetricKind::Gauge,
1,
)
.with_labels(vec![MetricLabel::new("gate", "proxy_streams_distributed")]),
.with_labels(vec![MetricLabel::new("gate", "tunnel_streams_distributed")]),
),
}
}
@@ -701,14 +701,14 @@ impl AppState {
pub async fn try_acquire_stream_permit(
&self,
) -> Result<Option<AdmissionPermit>, ProxyAdmissionError> {
) -> Result<Option<AdmissionPermit>, TunnelAdmissionError> {
let local = match &self.stream_gate {
Some(gate) => Some(gate.try_acquire().map_err(|err| {
match err {
ConcurrencyError::Saturated { gate, limit } => {
ProxyAdmissionError::Saturated { gate, limit }
TunnelAdmissionError::Saturated { gate, limit }
}
ConcurrencyError::Closed { gate } => ProxyAdmissionError::Unavailable {
ConcurrencyError::Closed { gate } => TunnelAdmissionError::Unavailable {
gate,
limit: self
.stream_gate
@@ -726,20 +726,20 @@ impl AppState {
Some(gate) => Some(gate.try_acquire().await.map_err(|err| {
match err {
RuntimeSemaphoreError::Saturated { gate, limit } => {
ProxyAdmissionError::Saturated { gate, limit }
TunnelAdmissionError::Saturated { gate, limit }
}
RuntimeSemaphoreError::Unavailable {
gate,
limit,
message,
} => ProxyAdmissionError::Unavailable {
} => TunnelAdmissionError::Unavailable {
gate,
limit,
message,
},
RuntimeSemaphoreError::InvalidConfiguration(message) => {
ProxyAdmissionError::Unavailable {
gate: "proxy_streams_distributed",
TunnelAdmissionError::Unavailable {
gate: "tunnel_streams_distributed",
limit: self
.distributed_stream_gate
.as_ref()

View File

@@ -305,9 +305,9 @@ async fn connect_tunnel_tcp(
port: u16,
connect_timeout: Duration,
) -> Result<TcpStream, anyhow::Error> {
if let Some(proxy_url) = state.config.effective_aether_proxy_url() {
if let Some(proxy_url) = state.config.effective_aether_outbound_proxy_url() {
let proxy = UpstreamProxyConfig::parse(proxy_url)
.map_err(|err| anyhow::anyhow!("aether proxy URL invalid: {err}"))?;
.map_err(|err| anyhow::anyhow!("Aether outbound proxy URL invalid: {err}"))?;
debug!(
proxy_url = %proxy.redacted_url(),
host = %host,
@@ -331,7 +331,7 @@ async fn connect_tunnel_tcp(
.await
.map_err(|_| {
anyhow::anyhow!(
"tunnel proxy TCP connect timeout ({}ms)",
"tunnel outbound proxy TCP connect timeout ({}ms)",
connect_timeout.as_millis()
)
})?

View File

@@ -243,7 +243,7 @@ where
try_send_stream_error(
&frame_tx,
sid,
"proxy request body dispatch stalled",
"tunnel request body dispatch stalled",
);
}
if draining && streams.is_empty() {
@@ -424,7 +424,7 @@ mod tests {
let (high_tx, mut high_rx) = bounded_queue::<Frame>(4);
let (normal_tx, _normal_rx) = bounded_queue::<Frame>(4);
let frame_tx = FrameSender::from_test_queues(high_tx, normal_tx);
try_send_stream_error(&frame_tx, 9, "proxy request body dispatch stalled");
try_send_stream_error(&frame_tx, 9, "tunnel request body dispatch stalled");
let frame = high_rx
.recv()
@@ -434,7 +434,7 @@ mod tests {
assert_eq!(frame.msg_type, MsgType::StreamError);
assert_eq!(
frame.payload,
Bytes::from_static(b"proxy request body dispatch stalled")
Bytes::from_static(b"tunnel request body dispatch stalled")
);
}
}

View File

@@ -13,7 +13,7 @@ use tracing::{debug, info, warn};
use crate::registration::client::RemoteConfig;
use crate::runtime;
use crate::state::{AppState, ProxyMetricsSnapshot, ServerContext};
use crate::state::{AppState, ServerContext, TunnelRequestMetricsSnapshot};
use super::protocol::{Frame, MsgType};
use super::writer::FrameSender;
@@ -53,15 +53,15 @@ pub fn spawn_noop() -> HeartbeatHandle {
#[derive(Debug, Clone, Copy, Default)]
struct HeartbeatSnapshot {
cumulative: ProxyMetricsSnapshot,
window: ProxyMetricsSnapshot,
cumulative: TunnelRequestMetricsSnapshot,
window: TunnelRequestMetricsSnapshot,
}
#[derive(Debug, Clone, Copy)]
struct PendingHeartbeat {
heartbeat_id: u64,
snapshot: HeartbeatSnapshot,
cumulative: ProxyMetricsSnapshot,
cumulative: TunnelRequestMetricsSnapshot,
sent_at: Option<Instant>,
}
@@ -82,7 +82,7 @@ pub fn spawn(
// We keep the last ACKed cumulative snapshot so each payload can
// report both monotonic totals and the delta since the previous ACK.
let mut pending: Option<PendingHeartbeat> = None;
let mut last_acked_snapshot = ProxyMetricsSnapshot::default();
let mut last_acked_snapshot = TunnelRequestMetricsSnapshot::default();
let mut next_heartbeat_id: u64 = 1;
let heartbeat_session_id = format!(
"{}-{}",
@@ -342,7 +342,10 @@ fn normalize_upgrade_target(raw: String) -> Option<String> {
if trimmed.is_empty() {
return None;
}
let normalized = trimmed.strip_prefix("proxy-v").unwrap_or(trimmed);
let normalized = trimmed
.strip_prefix("tunnel-v")
.or_else(|| trimmed.strip_prefix("proxy-v"))
.unwrap_or(trimmed);
if normalized == CURRENT_VERSION {
return None;
}
@@ -402,17 +405,17 @@ mod tests {
use super::{build_heartbeat_payload, handle_ack, AckDecision, HeartbeatSnapshot};
use crate::registration::client::AetherClient;
use crate::runtime::DynamicConfig;
use crate::state::{AppState, ProxyMetrics, ServerContext, TunnelMetrics};
use crate::state::{AppState, ServerContext, TunnelMetrics, TunnelRequestMetrics};
fn sample_config() -> Arc<crate::config::Config> {
Arc::new(crate::config::Config::parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
]))
}
@@ -431,7 +434,7 @@ mod tests {
)),
dynamic: Arc::new(ArcSwap::from_pointee(DynamicConfig::from_config(&config))),
active_connections: Arc::new(AtomicU64::new(0)),
metrics: Arc::new(ProxyMetrics::new()),
metrics: Arc::new(TunnelRequestMetrics::new()),
tunnel_metrics: Arc::new(TunnelMetrics::new()),
})
}

View File

@@ -242,7 +242,9 @@ mod tests {
use crate::config::Config;
use crate::registration::client::AetherClient;
use crate::runtime::DynamicConfig;
use crate::state::{AppState as ProxyAppState, ProxyMetrics, ServerContext, TunnelMetrics};
use crate::state::{
AppState as TunnelAppState, ServerContext, TunnelMetrics, TunnelRequestMetrics,
};
use crate::target_filter::DnsCache;
use crate::tunnel::protocol;
use crate::upstream_client;
@@ -292,7 +294,7 @@ mod tests {
}
#[tokio::test]
async fn proxy_reconnects_after_gateway_restart() {
async fn tunnel_reconnects_after_gateway_restart() {
ensure_rustls_provider();
let gateway_port = reserve_local_port().expect("gateway port should reserve");
@@ -304,7 +306,7 @@ mod tests {
let state = sample_state(sample_config(&gateway_base_url));
let server = sample_server(&state, "node-recovery");
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let proxy_task = tokio::spawn({
let tunnel_task = tokio::spawn({
let state = Arc::clone(&state);
let server = Arc::clone(&server);
let (_drain_tx, drain_rx) = watch::channel(false);
@@ -338,10 +340,10 @@ mod tests {
.await;
let _ = shutdown_tx.send(true);
tokio::time::timeout(Duration::from_secs(5), proxy_task)
tokio::time::timeout(Duration::from_secs(5), tunnel_task)
.await
.expect("proxy task should stop")
.expect("proxy task should join");
.expect("tunnel task should stop")
.expect("tunnel task should join");
gateway_handle.abort();
}
@@ -452,12 +454,12 @@ mod tests {
Ok(port)
}
fn sample_state(config: Config) -> Arc<ProxyAppState> {
fn sample_state(config: Config) -> Arc<TunnelAppState> {
let config = Arc::new(config);
let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128));
let upstream_client_pool =
upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache));
Arc::new(ProxyAppState {
Arc::new(TunnelAppState {
config,
dns_cache,
upstream_client_pool,
@@ -468,7 +470,7 @@ mod tests {
})
}
fn sample_server(state: &Arc<ProxyAppState>, node_id: &str) -> Arc<ServerContext> {
fn sample_server(state: &Arc<TunnelAppState>, node_id: &str) -> Arc<ServerContext> {
let config = Arc::clone(&state.config);
Arc::new(ServerContext {
server_label: "gateway-owned-tunnel".to_string(),
@@ -483,7 +485,7 @@ mod tests {
)),
dynamic: Arc::new(ArcSwap::from_pointee(DynamicConfig::from_config(&config))),
active_connections: Arc::new(AtomicU64::new(0)),
metrics: Arc::new(ProxyMetrics::new()),
metrics: Arc::new(TunnelRequestMetrics::new()),
tunnel_metrics: Arc::new(TunnelMetrics::new()),
})
}
@@ -493,7 +495,7 @@ mod tests {
aether_url: aether_url.to_string(),
management_token: "token".to_string(),
public_ip: None,
node_name: "proxy-test".to_string(),
node_name: "tunnel-test".to_string(),
node_region: None,
heartbeat_interval: 1,
allowed_ports: vec![80, 443],
@@ -505,7 +507,7 @@ mod tests {
aether_tcp_keepalive_secs: 60,
aether_tcp_nodelay: true,
aether_http2: true,
aether_proxy_url: None,
aether_outbound_proxy_url: None,
aether_retry_max_attempts: 1,
aether_retry_base_delay_ms: 50,
aether_retry_max_delay_ms: 100,
@@ -529,9 +531,9 @@ mod tests {
redirect_replay_budget_bytes: crate::config::DEFAULT_REDIRECT_REPLAY_BUDGET_BYTES,
emit_proxy_timing_header: true,
log_level: "info".to_string(),
log_destination: crate::config::ProxyLogDestinationArg::Stdout,
log_destination: crate::config::TunnelLogDestinationArg::Stdout,
log_dir: None,
log_rotation: crate::config::ProxyLogRotationArg::Daily,
log_rotation: crate::config::TunnelLogRotationArg::Daily,
log_retention_days: 7,
log_max_files: 30,
tunnel_reconnect_base_ms: 50,

View File

@@ -229,7 +229,7 @@ fn log_stream_success(ctx: StreamLogContext<'_>, status: u16, duration: Duration
request_body_bytes = ctx.request_body_size,
slow,
sampled,
"proxy request completed"
"tunnel request completed"
);
} else {
debug!(
@@ -247,7 +247,7 @@ fn log_stream_success(ctx: StreamLogContext<'_>, status: u16, duration: Duration
request_body_bytes = ctx.request_body_size,
slow,
sampled,
"proxy request completed"
"tunnel request completed"
);
}
}
@@ -268,7 +268,7 @@ fn log_stream_failure(ctx: StreamLogContext<'_>, error: &str, duration: Duration
duration_ms = duration.as_millis() as u64,
redirect_count = ctx.redirect_count,
request_body_bytes = ctx.request_body_size,
"proxy request failed"
"tunnel request failed"
);
}
None => {
@@ -280,7 +280,7 @@ fn log_stream_failure(ctx: StreamLogContext<'_>, error: &str, duration: Duration
duration_ms = duration.as_millis() as u64,
redirect_count = ctx.redirect_count,
request_body_bytes = ctx.request_body_size,
"proxy request failed"
"tunnel request failed"
);
}
}
@@ -1157,9 +1157,9 @@ pub async fn handle_stream(
Ok(permit) => permit,
Err(err) => {
let message = match err {
crate::state::ProxyAdmissionError::Saturated { .. } => "proxy overloaded",
crate::state::ProxyAdmissionError::Unavailable { .. } => {
"proxy admission unavailable"
crate::state::TunnelAdmissionError::Saturated { .. } => "tunnel overloaded",
crate::state::TunnelAdmissionError::Unavailable { .. } => {
"tunnel admission unavailable"
}
};
log_stream_failure(
@@ -1654,7 +1654,7 @@ mod tests {
use crate::config::Config;
use crate::registration::client::AetherClient;
use crate::runtime::DynamicConfig;
use crate::state::{ProxyMetrics, TunnelMetrics};
use crate::state::{TunnelMetrics, TunnelRequestMetrics};
use crate::target_filter::DnsCache;
use crate::tunnel::client::build_tls_config;
@@ -2335,7 +2335,7 @@ mod tests {
#[tokio::test]
async fn rejects_stream_when_local_admission_gate_is_saturated() {
let gate = Arc::new(ConcurrencyGate::new("proxy_streams", 1));
let gate = Arc::new(ConcurrencyGate::new("tunnel_streams", 1));
let _permit = gate.try_acquire().expect("first permit");
let state = sample_state(Some(gate), None);
let server = sample_server(&state);
@@ -2359,7 +2359,7 @@ mod tests {
.expect("overload frame");
assert_eq!(frame.stream_id, 7);
assert_eq!(frame.msg_type, MsgType::StreamError);
assert_eq!(frame.payload, Bytes::from_static(b"proxy overloaded"));
assert_eq!(frame.payload, Bytes::from_static(b"tunnel overloaded"));
assert_eq!(
state
.stream_gate
@@ -2376,7 +2376,7 @@ mod tests {
let gate = Arc::new(
RuntimeState::memory(MemoryRuntimeStateConfig::default())
.semaphore(
"proxy_streams_distributed",
"tunnel_streams_distributed",
1,
RuntimeSemaphoreConfig::default(),
)
@@ -2405,7 +2405,7 @@ mod tests {
.expect("overload frame");
assert_eq!(frame.stream_id, 9);
assert_eq!(frame.msg_type, MsgType::StreamError);
assert_eq!(frame.payload, Bytes::from_static(b"proxy overloaded"));
assert_eq!(frame.payload, Bytes::from_static(b"tunnel overloaded"));
assert_eq!(
state
.distributed_stream_gate
@@ -2500,7 +2500,7 @@ mod tests {
)),
dynamic: Arc::new(ArcSwap::from_pointee(DynamicConfig::from_config(&config))),
active_connections: Arc::new(AtomicU64::new(0)),
metrics: Arc::new(ProxyMetrics::new()),
metrics: Arc::new(TunnelRequestMetrics::new()),
tunnel_metrics: Arc::new(TunnelMetrics::new()),
})
}
@@ -2510,7 +2510,7 @@ mod tests {
aether_url: "https://aether.example.com".to_string(),
management_token: "token".to_string(),
public_ip: None,
node_name: "proxy-test".to_string(),
node_name: "tunnel-test".to_string(),
node_region: None,
heartbeat_interval: 30,
allowed_ports: vec![80, 443],
@@ -2522,7 +2522,7 @@ mod tests {
aether_tcp_keepalive_secs: 60,
aether_tcp_nodelay: true,
aether_http2: true,
aether_proxy_url: None,
aether_outbound_proxy_url: None,
aether_retry_max_attempts: 3,
aether_retry_base_delay_ms: 200,
aether_retry_max_delay_ms: 2_000,
@@ -2546,9 +2546,9 @@ mod tests {
redirect_replay_budget_bytes: crate::config::DEFAULT_REDIRECT_REPLAY_BUDGET_BYTES,
emit_proxy_timing_header: true,
log_level: "info".to_string(),
log_destination: crate::config::ProxyLogDestinationArg::Stdout,
log_destination: crate::config::TunnelLogDestinationArg::Stdout,
log_dir: None,
log_rotation: crate::config::ProxyLogRotationArg::Daily,
log_rotation: crate::config::TunnelLogRotationArg::Daily,
log_retention_days: 7,
log_max_files: 30,
tunnel_reconnect_base_ms: 500,

View File

@@ -799,7 +799,7 @@ mod tests {
let client = proxied_client(&proxy_url);
let request = hyper::Request::builder()
.method(hyper::Method::GET)
.uri("http://example.com/proxy-test")
.uri("http://example.com/tunnel-test")
.body(full_request_body(Bytes::new()))
.expect("request should build");
@@ -816,7 +816,7 @@ mod tests {
assert_eq!(status, hyper::StatusCode::OK);
assert_eq!(&body[..], b"ok");
assert!(
raw_request.starts_with("GET http://example.com/proxy-test HTTP/1.1\r\n"),
raw_request.starts_with("GET http://example.com/tunnel-test HTTP/1.1\r\n"),
"unexpected proxy request: {raw_request:?}"
);
}
@@ -855,13 +855,13 @@ mod tests {
fn proxied_client(proxy_url: &str) -> UpstreamClient {
let _ = rustls::crypto::ring::default_provider().install_default();
let config = Config::try_parse_from([
"aether-proxy",
"aether-tunnel",
"--aether-url",
"https://aether.example.com",
"--management-token",
"ae_test",
"--node-name",
"proxy-test",
"tunnel-test",
"--upstream-proxy-url",
proxy_url,
"--upstream-connect-timeout-secs",