mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(tunnel): rename aether-proxy to aether-tunnel
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -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/"))
|
||||
{
|
||||
|
||||
@@ -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"])),
|
||||
|
||||
@@ -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]]"));
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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/")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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/ARM64(GNU 或 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 keepalive(秒,0 关闭) |
|
||||
| `--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*` 格式的 tag,GitHub Actions 会自动:
|
||||
- 编译所有平台二进制并发布到 Releases
|
||||
- 更新 README 中的下载链接表格
|
||||
|
||||
```bash
|
||||
git tag proxy-v0.2.0
|
||||
git push origin proxy-v0.2.0
|
||||
```
|
||||
19
apps/aether-tunnel/.env.example
Normal file
19
apps/aether-tunnel/.env.example
Normal 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
|
||||
@@ -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 = [
|
||||
@@ -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
|
||||
231
apps/aether-tunnel/README.md
Normal file
231
apps/aether-tunnel/README.md
Normal 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/ARM64(GNU 或 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 keepalive(秒,0 关闭) |
|
||||
| `--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*` 格式的 tag,GitHub Actions 会自动:
|
||||
- 编译所有平台二进制并发布到 Releases
|
||||
- 更新 README 中的下载链接表格
|
||||
|
||||
```bash
|
||||
git tag tunnel-v0.2.0
|
||||
git push origin tunnel-v0.2.0
|
||||
```
|
||||
@@ -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
|
||||
@@ -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 "$@"
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
@@ -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() {
|
||||
@@ -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()?;
|
||||
@@ -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");
|
||||
@@ -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!();
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
)
|
||||
})?
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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",
|
||||
Reference in New Issue
Block a user