mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
Add tunnel security config fields
This commit is contained in:
@@ -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],
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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(),
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user