mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Add tunnel security config fields
This commit is contained in:
@@ -987,6 +987,8 @@ mod tests {
|
||||
aether_url: gateway_base_url.clone(),
|
||||
management_token: "token".to_string(),
|
||||
node_name: Some("node-recovery".to_string()),
|
||||
tunnel_security: None,
|
||||
tunnel_encryption_key: None,
|
||||
},
|
||||
)];
|
||||
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
||||
@@ -1289,6 +1291,8 @@ mod tests {
|
||||
aether_url: state.config.aether_url.clone(),
|
||||
management_token: state.config.management_token.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(
|
||||
&state.config,
|
||||
@@ -1311,6 +1315,8 @@ mod tests {
|
||||
management_token: "token".to_string(),
|
||||
public_ip: None,
|
||||
node_name: "tunnel-test".to_string(),
|
||||
tunnel_security: crate::config::TunnelSecurity::Off,
|
||||
tunnel_encryption_key: None,
|
||||
node_region: None,
|
||||
heartbeat_interval: 1,
|
||||
allowed_ports: vec![80, 443],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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")]
|
||||
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)
|
||||
#[arg(long, env = "AETHER_TUNNEL_NODE_REGION")]
|
||||
pub node_region: Option<String>,
|
||||
@@ -670,6 +713,13 @@ impl Config {
|
||||
if self.node_name.trim().is_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 {
|
||||
if port == 0 {
|
||||
anyhow::bail!("allowed_ports: port 0 is not valid");
|
||||
@@ -896,6 +946,12 @@ pub struct ServerEntry {
|
||||
pub management_token: String,
|
||||
/// Per-server node name override. Falls back to the global `node_name`.
|
||||
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 aether_url = first_server.map(|s| s.aether_url.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
|
||||
.node_name
|
||||
.as_deref()
|
||||
@@ -1073,6 +1132,8 @@ impl ConfigFile {
|
||||
|
||||
set!("AETHER_TUNNEL_AETHER_URL", aether_url);
|
||||
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_NODE_NAME", node_name);
|
||||
set!("AETHER_TUNNEL_NODE_REGION", self.node_region);
|
||||
@@ -1395,6 +1456,31 @@ tunnel_ipv6_only = 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]
|
||||
fn config_file_deserializes_upstream_proxy_url() {
|
||||
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]
|
||||
fn cli_accepts_tunnel_ipv4_only() {
|
||||
let config = Config::parse_from([
|
||||
|
||||
@@ -182,6 +182,8 @@ async fn run_tunnel(config: Config) -> anyhow::Result<()> {
|
||||
aether_url: config.aether_url.clone(),
|
||||
management_token: config.management_token.clone(),
|
||||
node_name: None,
|
||||
tunnel_security: Some(config.tunnel_security),
|
||||
tunnel_encryption_key: config.tunnel_encryption_key.clone(),
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
@@ -496,6 +496,8 @@ mod tests {
|
||||
management_token: "token".to_string(),
|
||||
public_ip: None,
|
||||
node_name: "tunnel-test".to_string(),
|
||||
tunnel_security: crate::config::TunnelSecurity::Off,
|
||||
tunnel_encryption_key: None,
|
||||
node_region: None,
|
||||
heartbeat_interval: 1,
|
||||
allowed_ports: vec![80, 443],
|
||||
|
||||
@@ -2511,6 +2511,8 @@ mod tests {
|
||||
management_token: "token".to_string(),
|
||||
public_ip: None,
|
||||
node_name: "tunnel-test".to_string(),
|
||||
tunnel_security: crate::config::TunnelSecurity::Off,
|
||||
tunnel_encryption_key: None,
|
||||
node_region: None,
|
||||
heartbeat_interval: 30,
|
||||
allowed_ports: vec![80, 443],
|
||||
|
||||
Reference in New Issue
Block a user