Add tunnel security config fields

This commit is contained in:
RWDai
2026-05-21 16:07:27 +08:00
parent b84e4a96e2
commit bd4f6b9206
5 changed files with 151 additions and 0 deletions

View File

@@ -987,6 +987,8 @@ mod tests {
aether_url: gateway_base_url.clone(), aether_url: gateway_base_url.clone(),
management_token: "token".to_string(), management_token: "token".to_string(),
node_name: Some("node-recovery".to_string()), node_name: Some("node-recovery".to_string()),
tunnel_security: None,
tunnel_encryption_key: None,
}, },
)]; )];
let (shutdown_tx, shutdown_rx) = watch::channel(false); let (shutdown_tx, shutdown_rx) = watch::channel(false);
@@ -1289,6 +1291,8 @@ mod tests {
aether_url: state.config.aether_url.clone(), aether_url: state.config.aether_url.clone(),
management_token: state.config.management_token.clone(), management_token: state.config.management_token.clone(),
node_name: Some(state.config.node_name.clone()), node_name: Some(state.config.node_name.clone()),
tunnel_security: Some(state.config.tunnel_security),
tunnel_encryption_key: state.config.tunnel_encryption_key.clone(),
}; };
let client = Arc::new(AetherClient::new( let client = Arc::new(AetherClient::new(
&state.config, &state.config,
@@ -1311,6 +1315,8 @@ mod tests {
management_token: "token".to_string(), management_token: "token".to_string(),
public_ip: None, public_ip: None,
node_name: "tunnel-test".to_string(), node_name: "tunnel-test".to_string(),
tunnel_security: crate::config::TunnelSecurity::Off,
tunnel_encryption_key: None,
node_region: None, node_region: None,
heartbeat_interval: 1, heartbeat_interval: 1,
allowed_ports: vec![80, 443], allowed_ports: vec![80, 443],

View File

@@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use aether_runtime::{FileLoggingConfig, LogDestination, LogRotation, ServiceRuntimeConfig}; use aether_runtime::{FileLoggingConfig, LogDestination, LogRotation, ServiceRuntimeConfig};
@@ -247,6 +248,36 @@ impl From<TunnelLogRotationArg> for LogRotation {
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TunnelSecurity {
Off,
NonTlsRequired,
}
impl fmt::Display for TunnelSecurity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
TunnelSecurity::Off => "off",
TunnelSecurity::NonTlsRequired => "non_tls_required",
})
}
}
impl FromStr for TunnelSecurity {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim() {
"off" => Ok(Self::Off),
"non_tls_required" | "non-tls-required" => Ok(Self::NonTlsRequired),
other => Err(format!(
"invalid tunnel_security {other:?}; expected off or non_tls_required"
)),
}
}
}
/// Aether tunnel agent. /// Aether tunnel agent.
/// ///
/// Deployed on overseas VPS to relay API traffic for Aether instances /// Deployed on overseas VPS to relay API traffic for Aether instances
@@ -271,6 +302,18 @@ pub struct Config {
#[arg(long, env = "AETHER_TUNNEL_NODE_NAME")] #[arg(long, env = "AETHER_TUNNEL_NODE_NAME")]
pub node_name: String, pub node_name: String,
/// Application-layer tunnel security mode.
#[arg(
long,
env = "AETHER_TUNNEL_SECURITY",
default_value_t = TunnelSecurity::Off
)]
pub tunnel_security: TunnelSecurity,
/// Base64-encoded 32-byte PSK used when tunnel_security=non_tls_required.
#[arg(long, env = "AETHER_TUNNEL_ENCRYPTION_KEY")]
pub tunnel_encryption_key: Option<String>,
/// Region label (e.g. ap-northeast-1) /// Region label (e.g. ap-northeast-1)
#[arg(long, env = "AETHER_TUNNEL_NODE_REGION")] #[arg(long, env = "AETHER_TUNNEL_NODE_REGION")]
pub node_region: Option<String>, pub node_region: Option<String>,
@@ -670,6 +713,13 @@ impl Config {
if self.node_name.trim().is_empty() { if self.node_name.trim().is_empty() {
anyhow::bail!("node_name must not be empty"); anyhow::bail!("node_name must not be empty");
} }
if self.tunnel_security == TunnelSecurity::NonTlsRequired
&& normalized_proxy_url(&self.tunnel_encryption_key).is_none()
{
anyhow::bail!(
"tunnel_encryption_key must be set when tunnel_security=non_tls_required"
);
}
for &port in &self.allowed_ports { for &port in &self.allowed_ports {
if port == 0 { if port == 0 {
anyhow::bail!("allowed_ports: port 0 is not valid"); anyhow::bail!("allowed_ports: port 0 is not valid");
@@ -896,6 +946,12 @@ pub struct ServerEntry {
pub management_token: String, pub management_token: String,
/// Per-server node name override. Falls back to the global `node_name`. /// Per-server node name override. Falls back to the global `node_name`.
pub node_name: Option<String>, pub node_name: Option<String>,
/// Per-server tunnel security mode.
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_security: Option<TunnelSecurity>,
/// Per-server PSK for secure non-TLS tunnel handshakes.
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_encryption_key: Option<String>,
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1066,6 +1122,9 @@ impl ConfigFile {
let first_server = self.servers.first(); let first_server = self.servers.first();
let aether_url = first_server.map(|s| s.aether_url.as_str()); let aether_url = first_server.map(|s| s.aether_url.as_str());
let management_token = first_server.map(|s| s.management_token.as_str()); let management_token = first_server.map(|s| s.management_token.as_str());
let tunnel_security =
first_server.map(|s| s.tunnel_security.unwrap_or(TunnelSecurity::Off));
let tunnel_encryption_key = first_server.and_then(|s| s.tunnel_encryption_key.as_deref());
let node_name = self let node_name = self
.node_name .node_name
.as_deref() .as_deref()
@@ -1073,6 +1132,8 @@ impl ConfigFile {
set!("AETHER_TUNNEL_AETHER_URL", aether_url); set!("AETHER_TUNNEL_AETHER_URL", aether_url);
set!("AETHER_TUNNEL_MANAGEMENT_TOKEN", management_token); set!("AETHER_TUNNEL_MANAGEMENT_TOKEN", management_token);
set!("AETHER_TUNNEL_SECURITY", tunnel_security);
set!("AETHER_TUNNEL_ENCRYPTION_KEY", tunnel_encryption_key);
set!("AETHER_TUNNEL_PUBLIC_IP", self.public_ip); set!("AETHER_TUNNEL_PUBLIC_IP", self.public_ip);
set!("AETHER_TUNNEL_NODE_NAME", node_name); set!("AETHER_TUNNEL_NODE_NAME", node_name);
set!("AETHER_TUNNEL_NODE_REGION", self.node_region); set!("AETHER_TUNNEL_NODE_REGION", self.node_region);
@@ -1395,6 +1456,31 @@ tunnel_ipv6_only = false
assert_eq!(cfg.tunnel_ipv6_only, Some(false)); assert_eq!(cfg.tunnel_ipv6_only, Some(false));
} }
#[test]
fn config_file_deserializes_server_tunnel_security_fields() {
let cfg: ConfigFile = toml::from_str(
r#"
[[servers]]
aether_url = "http://aether.example.com"
management_token = "ae_test"
node_name = "jp-proxy-01"
tunnel_security = "non_tls_required"
tunnel_encryption_key = "base64-32-bytes"
"#,
)
.expect("server tunnel security TOML");
assert_eq!(cfg.servers.len(), 1);
assert_eq!(
cfg.servers[0].tunnel_security,
Some(TunnelSecurity::NonTlsRequired)
);
assert_eq!(
cfg.servers[0].tunnel_encryption_key.as_deref(),
Some("base64-32-bytes")
);
}
#[test] #[test]
fn config_file_deserializes_upstream_proxy_url() { fn config_file_deserializes_upstream_proxy_url() {
let cfg: ConfigFile = toml::from_str("upstream_proxy_url = \"http://proxy.example:8080\"") let cfg: ConfigFile = toml::from_str("upstream_proxy_url = \"http://proxy.example:8080\"")
@@ -1608,6 +1694,59 @@ node_name = "tunnel-test"
); );
} }
#[test]
fn cli_defaults_tunnel_security_to_off() {
let config = Config::parse_from([
"aether-tunnel",
"--aether-url",
"https://example.com",
"--management-token",
"ae_test",
"--node-name",
"tunnel-test",
]);
assert_eq!(config.tunnel_security, TunnelSecurity::Off);
assert!(config.tunnel_encryption_key.is_none());
}
#[test]
fn validate_requires_encryption_key_for_non_tls_security() {
let config = Config::parse_from([
"aether-tunnel",
"--aether-url",
"http://example.com",
"--management-token",
"ae_test",
"--node-name",
"tunnel-test",
"--tunnel-security",
"non_tls_required",
]);
let error = config
.validate()
.expect_err("non_tls_required should require a PSK");
assert!(error.to_string().contains("tunnel_encryption_key"));
let with_key = Config::parse_from([
"aether-tunnel",
"--aether-url",
"http://example.com",
"--management-token",
"ae_test",
"--node-name",
"tunnel-test",
"--tunnel-security",
"non_tls_required",
"--tunnel-encryption-key",
"base64-32-bytes",
]);
with_key
.validate()
.expect("non_tls_required with a PSK should validate");
}
#[test] #[test]
fn cli_accepts_tunnel_ipv4_only() { fn cli_accepts_tunnel_ipv4_only() {
let config = Config::parse_from([ let config = Config::parse_from([

View File

@@ -182,6 +182,8 @@ async fn run_tunnel(config: Config) -> anyhow::Result<()> {
aether_url: config.aether_url.clone(), aether_url: config.aether_url.clone(),
management_token: config.management_token.clone(), management_token: config.management_token.clone(),
node_name: None, node_name: None,
tunnel_security: Some(config.tunnel_security),
tunnel_encryption_key: config.tunnel_encryption_key.clone(),
}] }]
}; };

View File

@@ -496,6 +496,8 @@ mod tests {
management_token: "token".to_string(), management_token: "token".to_string(),
public_ip: None, public_ip: None,
node_name: "tunnel-test".to_string(), node_name: "tunnel-test".to_string(),
tunnel_security: crate::config::TunnelSecurity::Off,
tunnel_encryption_key: None,
node_region: None, node_region: None,
heartbeat_interval: 1, heartbeat_interval: 1,
allowed_ports: vec![80, 443], allowed_ports: vec![80, 443],

View File

@@ -2511,6 +2511,8 @@ mod tests {
management_token: "token".to_string(), management_token: "token".to_string(),
public_ip: None, public_ip: None,
node_name: "tunnel-test".to_string(), node_name: "tunnel-test".to_string(),
tunnel_security: crate::config::TunnelSecurity::Off,
tunnel_encryption_key: None,
node_region: None, node_region: None,
heartbeat_interval: 30, heartbeat_interval: 30,
allowed_ports: vec![80, 443], allowed_ports: vec![80, 443],