From 8c15941c44f1da6880e90602a523f4b7a70a4ebe Mon Sep 17 00:00:00 2001 From: fanyang Date: Thu, 13 Aug 2026 09:56:44 +0800 Subject: [PATCH] Support configurable TCP STUN servers (#2314) tcp_stun_servers explicitly controls TCP STUN servers. If tcp_stun_servers is not configured, TCP STUN falls back to configured stun_servers. If neither is configured, TCP STUN uses the built-in default TCP STUN list. Empty lists explicitly disable the corresponding STUN server list. Empty CLI/env overrides now clear existing configured STUN servers instead of appending nothing. --- easytier-core/src/config/toml.rs | 57 ++++++++++----- easytier-core/src/instance/config.rs | 103 ++++++++++++++++++++++++++- easytier/locales/app.yml | 11 +-- easytier/src/core.rs | 67 +++++++++++++++-- 4 files changed, 207 insertions(+), 31 deletions(-) diff --git a/easytier-core/src/config/toml.rs b/easytier-core/src/config/toml.rs index 1a1f0a1d..2b0c2b53 100644 --- a/easytier-core/src/config/toml.rs +++ b/easytier-core/src/config/toml.rs @@ -12,8 +12,6 @@ use anyhow::Context; use ariadne::{CharSet, Config as AriadneConfig, IndexType, Label, Report, ReportKind, Source}; use serde::{Deserialize, Serialize}; -#[cfg(feature = "config-write")] -use crate::config::{DEFAULT_UDP_STUN_SERVERS, DEFAULT_UDP_V6_STUN_SERVERS, default_stun_servers}; use crate::proto::{ acl::Acl, common::{CompressionAlgoPb, SecureModeConfig}, @@ -27,16 +25,6 @@ pub(crate) fn default_instance_name() -> String { "default".to_owned() } -#[cfg(feature = "config-write")] -fn default_udp_stun_servers() -> Vec { - default_stun_servers(DEFAULT_UDP_STUN_SERVERS) -} - -#[cfg(feature = "config-write")] -fn default_udp_v6_stun_servers() -> Vec { - default_stun_servers(DEFAULT_UDP_V6_STUN_SERVERS) -} - pub fn gen_default_flags() -> Flags { #[allow(deprecated)] Flags { @@ -266,6 +254,11 @@ pub trait ConfigLoader: Send + Sync { fn get_stun_servers(&self) -> Option>; fn set_stun_servers(&self, servers: Option>); + fn get_tcp_stun_servers(&self) -> Option> { + None + } + fn set_tcp_stun_servers(&self, _servers: Option>) {} + fn get_stun_servers_v6(&self) -> Option>; fn set_stun_servers_v6(&self, servers: Option>); @@ -489,6 +482,7 @@ struct Config { tcp_whitelist: Option>, udp_whitelist: Option>, stun_servers: Option>, + tcp_stun_servers: Option>, stun_servers_v6: Option>, credential_file: Option, @@ -981,6 +975,14 @@ impl ConfigLoader for TomlConfig { self.config.lock().unwrap().stun_servers = servers; } + fn get_tcp_stun_servers(&self) -> Option> { + self.config.lock().unwrap().tcp_stun_servers.clone() + } + + fn set_tcp_stun_servers(&self, servers: Option>) { + self.config.lock().unwrap().tcp_stun_servers = servers; + } + fn get_stun_servers_v6(&self) -> Option> { self.config.lock().unwrap().stun_servers_v6.clone() } @@ -1028,12 +1030,6 @@ impl ConfigLoader for TomlConfig { let mut config = self.config.lock().unwrap().clone(); Self::normalize_config_source(&mut config); config.flags = Some(flags_diff_from_default(&self.get_flags())); - if config.stun_servers == Some(default_udp_stun_servers()) { - config.stun_servers = None; - } - if config.stun_servers_v6 == Some(default_udp_v6_stun_servers()) { - config.stun_servers_v6 = None; - } toml::to_string_pretty(&config).unwrap() } #[cfg(not(feature = "config-write"))] @@ -1298,6 +1294,7 @@ socket_mark = 66 let config = TomlConfigLoader::default(); let stun_servers = config.get_stun_servers(); assert!(stun_servers.is_none()); + assert!(config.get_tcp_stun_servers().is_none()); // Test setting custom stun servers let custom_servers = vec!["txt:stun.easytier.cn".to_string()]; @@ -1305,6 +1302,12 @@ socket_mark = 66 let retrieved_servers = config.get_stun_servers(); assert_eq!(retrieved_servers.unwrap(), custom_servers); + + let custom_tcp_servers = vec!["tcp-stun.example.com:3478".to_string()]; + config.set_tcp_stun_servers(Some(custom_tcp_servers.clone())); + + let retrieved_tcp_servers = config.get_tcp_stun_servers(); + assert_eq!(retrieved_tcp_servers.unwrap(), custom_tcp_servers); } #[test] @@ -1315,15 +1318,33 @@ stun_servers = [ "stun.l.google.com:19302", "stun1.l.google.com:19302", "txt:stun.easytier.cn" +] +tcp_stun_servers = [ + "tcp-stun.example.com:3478" ]"#; let config = TomlConfigLoader::new_from_str(config_str).unwrap(); let stun_servers = config.get_stun_servers().unwrap(); + let tcp_stun_servers = config.get_tcp_stun_servers().unwrap(); assert_eq!(stun_servers.len(), 3); assert_eq!(stun_servers[0], "stun.l.google.com:19302"); assert_eq!(stun_servers[1], "stun1.l.google.com:19302"); assert_eq!(stun_servers[2], "txt:stun.easytier.cn"); + assert_eq!(tcp_stun_servers, ["tcp-stun.example.com:3478"]); + } + + #[test] + fn test_empty_tcp_stun_servers_toml_parsing() { + let config = TomlConfigLoader::new_from_str( + r#" +instance_name = "test" +tcp_stun_servers = [] +"#, + ) + .unwrap(); + + assert_eq!(config.get_tcp_stun_servers(), Some(Vec::new())); } #[cfg(feature = "config-write")] diff --git a/easytier-core/src/instance/config.rs b/easytier-core/src/instance/config.rs index 87622495..ab6c38b3 100644 --- a/easytier-core/src/instance/config.rs +++ b/easytier-core/src/instance/config.rs @@ -323,6 +323,7 @@ impl CoreInstanceConfig { provider_supported: host.public_ipv6_provider_supported, }, }; + let stun_servers = config.get_stun_servers(); Ok(Self { instance_name: config.get_inst_name(), @@ -335,13 +336,17 @@ impl CoreInstanceConfig { gateway: host.gateway_enabled, }, stun: StunServerConfig { - udp_servers: config - .get_stun_servers() + udp_servers: stun_servers + .clone() .unwrap_or_else(|| StunServerConfig::default().udp_servers), + tcp_servers: config + .get_tcp_stun_servers() + .or_else(|| stun_servers.clone()) + .unwrap_or_else(|| StunServerConfig::default().tcp_servers), udp_v6_servers: config .get_stun_servers_v6() + .or_else(|| stun_servers.as_ref().map(|_| Vec::new())) .unwrap_or_else(|| StunServerConfig::default().udp_v6_servers), - ..StunServerConfig::default() }, endpoint_discovery: ManualEndpointDiscoveryConfig { user_agent: format!("easytier/{}", host.easytier_version), @@ -377,6 +382,98 @@ impl CoreInstanceConfig { mod tests { use super::*; + #[test] + fn tcp_stun_servers_follow_toml_override_rules() { + let fallback = TomlConfig::new_from_str( + r#" +stun_servers = ["fallback.example.com:3478"] +"#, + ) + .unwrap(); + let normalized = CoreInstanceConfig::from_toml(&fallback).unwrap(); + assert_eq!( + normalized.connectivity.stun.tcp_servers, + ["fallback.example.com:3478"] + ); + + let overridden = TomlConfig::new_from_str( + r#" +stun_servers = ["fallback.example.com:3478"] +tcp_stun_servers = ["tcp.example.com:3478"] +"#, + ) + .unwrap(); + let normalized = CoreInstanceConfig::from_toml(&overridden).unwrap(); + assert_eq!( + normalized.connectivity.stun.tcp_servers, + ["tcp.example.com:3478"] + ); + + let disabled = TomlConfig::new_from_str( + r#" +stun_servers = ["fallback.example.com:3478"] +tcp_stun_servers = [] +"#, + ) + .unwrap(); + let normalized = CoreInstanceConfig::from_toml(&disabled).unwrap(); + assert!(normalized.connectivity.stun.tcp_servers.is_empty()); + } + + #[test] + fn custom_udp_stun_servers_disable_default_ipv6_servers() { + let config = TomlConfig::new_from_str( + r#" +stun_servers = ["custom.example.com:3478"] +"#, + ) + .unwrap(); + let normalized = CoreInstanceConfig::from_toml(&config).unwrap(); + assert!(normalized.connectivity.stun.udp_v6_servers.is_empty()); + + let config = TomlConfig::new_from_str( + r#" +stun_servers = ["custom.example.com:3478"] +stun_servers_v6 = ["custom-v6.example.com:3478"] +"#, + ) + .unwrap(); + let normalized = CoreInstanceConfig::from_toml(&config).unwrap(); + assert_eq!( + normalized.connectivity.stun.udp_v6_servers, + ["custom-v6.example.com:3478"] + ); + } + + #[cfg(feature = "config-write")] + #[test] + fn explicit_stun_servers_survive_dump_reload() { + let assert_roundtrip = |config: TomlConfig| { + let before = CoreInstanceConfig::from_toml(&config) + .unwrap() + .connectivity + .stun; + let reloaded = TomlConfig::new_from_str(&config.dump()).unwrap(); + let after = CoreInstanceConfig::from_toml(&reloaded) + .unwrap() + .connectivity + .stun; + + assert_eq!(after, before); + }; + + let defaults = StunServerConfig::default(); + let config = TomlConfig::default(); + config.set_stun_servers(Some(defaults.udp_servers.clone())); + assert_roundtrip(config); + + let config = TomlConfig::default(); + config.set_stun_servers(Some(vec!["custom.example.com:3478".to_string()])); + config.set_tcp_stun_servers(Some(defaults.tcp_servers)); + config.set_stun_servers_v6(Some(defaults.udp_v6_servers)); + assert_roundtrip(config); + } + #[test] fn shared_toml_normalizes_instance_identity_and_connectivity() { let config = TomlConfig::new_from_str( diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 1f914e2a..b9ecbdce 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -255,11 +255,14 @@ core_clap: en: "if true, allow relay quic packets from foreign network. default is false (not forward foreign network quic packets)" zh-CN: "如果为true,则作为共享节点时也可以转发其他网络的 QUIC 数据包。默认值为false(不转发)" stun_servers: - en: "Override default STUN servers; If configured but empty, STUN servers are not used" - zh-CN: "覆盖内置的默认 STUN server 列表;如果设置了但是为空,则不使用 STUN servers;如果没设置,则使用默认 STUN server 列表" + en: "Override default UDP STUN servers. TCP STUN also uses this list when tcp-stun-servers is unset. If configured but empty, UDP STUN and fallback TCP/IPv6 STUN are disabled" + zh-CN: "覆盖内置的默认 UDP STUN server 列表;未设置 tcp-stun-servers 时,TCP STUN 也使用此列表;如果设置了但是为空,则禁用 UDP STUN 以及回退的 TCP/IPv6 STUN" stun_servers_v6: - en: "Override default STUN servers, IPv6; If configured but empty, IPv6 STUN servers are not used" - zh-CN: "覆盖内置的默认 IPv6 STUN server 列表;如果设置了但是为空,则不使用 IPv6 STUN servers;如果没设置,则使用默认 IPv6 STUN server 列表" + en: "Override default IPv6 STUN servers. If unset while stun-servers is configured, IPv6 STUN is disabled. If configured but empty, IPv6 STUN servers are not used" + zh-CN: "覆盖内置的默认 IPv6 STUN server 列表;如果未设置但已设置 stun-servers,则禁用 IPv6 STUN;如果设置了但是为空,则不使用 IPv6 STUN servers" + tcp_stun_servers: + en: "Override default TCP STUN servers. If unset, TCP STUN uses stun-servers when configured, otherwise default TCP STUN servers. If configured but empty, TCP STUN is disabled" + zh-CN: "覆盖内置的默认 TCP STUN server 列表;如果未设置,TCP STUN 会优先使用 stun-servers,否则使用默认 TCP STUN server 列表;如果设置了但是为空,则禁用 TCP STUN" secure_mode: en: "if true, enable secure mode. default is false" zh-CN: "如果为true,则启用安全模式。默认值为false" diff --git a/easytier/src/core.rs b/easytier/src/core.rs index f4a325bb..7a29d77e 100644 --- a/easytier/src/core.rs +++ b/easytier/src/core.rs @@ -692,6 +692,15 @@ struct NetworkOptions { )] stun_servers_v6: Option>, + #[arg( + long, + env = "ET_TCP_STUN_SERVERS", + value_delimiter = ',', + help = t!("core_clap.tcp_stun_servers").to_string(), + num_args = 0.. + )] + tcp_stun_servers: Option>, + #[arg( long, env = "ET_SECURE_MODE", @@ -1190,15 +1199,33 @@ impl NetworkOptions { cfg.set_udp_whitelist(old_udp_whitelist); if let Some(stun_servers) = &self.stun_servers { - let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default(); - old_stun_servers.extend(stun_servers.iter().cloned()); - cfg.set_stun_servers(Some(old_stun_servers)); + if stun_servers.is_empty() { + cfg.set_stun_servers(Some(Vec::new())); + } else { + let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default(); + old_stun_servers.extend(stun_servers.iter().cloned()); + cfg.set_stun_servers(Some(old_stun_servers)); + } } if let Some(stun_servers_v6) = &self.stun_servers_v6 { - let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default(); - old_stun_servers_v6.extend(stun_servers_v6.iter().cloned()); - cfg.set_stun_servers_v6(Some(old_stun_servers_v6)); + if stun_servers_v6.is_empty() { + cfg.set_stun_servers_v6(Some(Vec::new())); + } else { + let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default(); + old_stun_servers_v6.extend(stun_servers_v6.iter().cloned()); + cfg.set_stun_servers_v6(Some(old_stun_servers_v6)); + } + } + + if let Some(tcp_stun_servers) = &self.tcp_stun_servers { + if tcp_stun_servers.is_empty() { + cfg.set_tcp_stun_servers(Some(Vec::new())); + } else { + let mut old_tcp_stun_servers = cfg.get_tcp_stun_servers().unwrap_or_default(); + old_tcp_stun_servers.extend(tcp_stun_servers.iter().cloned()); + cfg.set_tcp_stun_servers(Some(old_tcp_stun_servers)); + } } Ok(()) } @@ -1306,6 +1333,9 @@ fn parse_cli() -> Cli { if let Some(stun_servers_v6) = &mut cli.network_options.stun_servers_v6 { stun_servers_v6.retain(|s| !s.trim().is_empty()); } + if let Some(tcp_stun_servers) = &mut cli.network_options.tcp_stun_servers { + tcp_stun_servers.retain(|s| !s.trim().is_empty()); + } cli } @@ -1771,4 +1801,29 @@ enabled = true assert_eq!(identity.network_secret_digest, None); assert_eq!(cfg.get_hostname(), "override-host"); } + + #[test] + fn empty_stun_server_options_clear_existing_config() { + let cfg = TomlConfigLoader::new_from_str( + r#" +stun_servers = ["udp.example.com:3478"] +stun_servers_v6 = ["v6.example.com:3478"] +tcp_stun_servers = ["tcp.example.com:3478"] +"#, + ) + .unwrap(); + + NetworkOptions { + stun_servers: Some(Vec::new()), + stun_servers_v6: Some(Vec::new()), + tcp_stun_servers: Some(Vec::new()), + ..Default::default() + } + .merge_into(&cfg) + .unwrap(); + + assert_eq!(cfg.get_stun_servers(), Some(Vec::new())); + assert_eq!(cfg.get_stun_servers_v6(), Some(Vec::new())); + assert_eq!(cfg.get_tcp_stun_servers(), Some(Vec::new())); + } }