From 904c17c1b3bc94a17a7ea4783af4219113201c62 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:30:21 +0300 Subject: [PATCH] DC=203 by default + IP Autodetect by STUN Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/config/mod.rs | 94 +++++++++++++- src/main.rs | 171 ++++++++++++++----------- src/proxy/direct_relay.rs | 26 ++-- src/transport/middle_proxy/mod.rs | 1 + src/transport/middle_proxy/pool_nat.rs | 32 ++++- src/transport/pool.rs | 15 ++- src/transport/socket.rs | 22 +++- 7 files changed, 260 insertions(+), 101 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a3dee7a..e9a4298 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,6 +3,7 @@ use crate::error::{ProxyError, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde::de::Deserializer; use std::collections::HashMap; use std::net::IpAddr; use std::path::Path; @@ -53,6 +54,36 @@ fn default_metrics_whitelist() -> Vec { vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()] } +// ============= Custom Deserializers ============= + +#[derive(Deserialize)] +#[serde(untagged)] +enum OneOrMany { + One(String), + Many(Vec), +} + +fn deserialize_dc_overrides<'de, D>( + deserializer: D, +) -> std::result::Result>, D::Error> +where + D: Deserializer<'de>, +{ + let raw: HashMap = HashMap::deserialize(deserializer)?; + let mut out = HashMap::new(); + for (dc, val) in raw { + let mut addrs = match val { + OneOrMany::One(s) => vec![s], + OneOrMany::Many(v) => v, + }; + addrs.retain(|s| !s.trim().is_empty()); + if !addrs.is_empty() { + out.insert(dc, addrs); + } + } + Ok(out) +} + // ============= Log Level ============= /// Logging verbosity level @@ -95,6 +126,50 @@ impl LogLevel { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dc_overrides_allow_string_and_array() { + let toml = r#" + [dc_overrides] + "201" = "149.154.175.50:443" + "202" = ["149.154.167.51:443", "149.154.175.100:443"] + "#; + let cfg: ProxyConfig = toml::from_str(toml).unwrap(); + assert_eq!(cfg.dc_overrides["201"], vec!["149.154.175.50:443"]); + assert_eq!( + cfg.dc_overrides["202"], + vec!["149.154.167.51:443", "149.154.175.100:443"] + ); + } + + #[test] + fn dc_overrides_inject_dc203_default() { + let toml = r#" + [general] + use_middle_proxy = false + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + "#; + let dir = std::env::temp_dir(); + let path = dir.join("telemt_dc_override_test.toml"); + std::fs::write(&path, toml).unwrap(); + let cfg = ProxyConfig::load(&path).unwrap(); + assert!(cfg + .dc_overrides + .get("203") + .map(|v| v.contains(&"91.105.192.100:443".to_string())) + .unwrap_or(false)); + let _ = std::fs::remove_file(path); + } +} + impl std::fmt::Display for LogLevel { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -163,6 +238,10 @@ pub struct GeneralConfig { #[serde(default)] pub middle_proxy_nat_stun: Option, + /// Ignore STUN/interface IP mismatch (keep using Middle Proxy even if NAT detected). + #[serde(default)] + pub stun_iface_mismatch_ignore: bool, + #[serde(default)] pub log_level: LogLevel, @@ -183,6 +262,7 @@ impl Default for GeneralConfig { middle_proxy_nat_ip: None, middle_proxy_nat_probe: false, middle_proxy_nat_stun: None, + stun_iface_mismatch_ignore: false, log_level: LogLevel::Normal, disable_colors: false, } @@ -499,13 +579,13 @@ pub struct ProxyConfig { pub show_link: ShowLink, /// DC address overrides for non-standard DCs (CDN, media, test, etc.) - /// Keys are DC indices as strings, values are "ip:port" addresses. + /// Keys are DC indices as strings, values are one or more \"ip:port\" addresses. /// Matches the C implementation's `proxy_for :` config directive. /// Example in config.toml: /// [dc_overrides] - /// "203" = "149.154.175.100:443" - #[serde(default)] - pub dc_overrides: HashMap, + /// \"203\" = [\"149.154.175.100:443\", \"91.105.192.100:443\"] + #[serde(default, deserialize_with = "deserialize_dc_overrides")] + pub dc_overrides: HashMap>, /// Default DC index (1-5) for unmapped non-standard DCs. /// Matches the C implementation's `default ` config directive. @@ -599,6 +679,12 @@ impl ProxyConfig { }); } + // Ensure default DC203 override is present. + config + .dc_overrides + .entry("203".to_string()) + .or_insert_with(|| vec!["91.105.192.100:443".to_string()]); + Ok(config) } diff --git a/src/main.rs b/src/main.rs index c3e5f71..acc07b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use tokio::signal; use tokio::sync::Semaphore; use tracing::{debug, error, info, warn}; use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload}; -use tokio::net::UnixListener; mod cli; mod config; @@ -28,7 +27,7 @@ use crate::ip_tracker::UserIpTracker; use crate::proxy::ClientHandler; use crate::stats::{ReplayChecker, Stats}; use crate::stream::BufferPool; -use crate::transport::middle_proxy::{MePool, fetch_proxy_config}; +use crate::transport::middle_proxy::{MePool, fetch_proxy_config, stun_probe}; use crate::transport::{ListenOptions, UpstreamManager, create_listener}; use crate::util::ip::detect_ip; use crate::protocol::constants::{TG_MIDDLE_PROXIES_V4, TG_MIDDLE_PROXIES_V6}; @@ -184,7 +183,7 @@ async fn main() -> std::result::Result<(), Box> { } let prefer_ipv6 = config.general.prefer_ipv6; - let use_middle_proxy = config.general.use_middle_proxy; + let mut use_middle_proxy = config.general.use_middle_proxy; let config = Arc::new(config); let stats = Arc::new(Stats::new()); let rng = Arc::new(SecureRandom::new()); @@ -208,6 +207,31 @@ async fn main() -> std::result::Result<(), Box> { // Connection concurrency limit let _max_connections = Arc::new(Semaphore::new(10_000)); + // STUN check before choosing transport + if use_middle_proxy { + match stun_probe(config.general.middle_proxy_nat_stun.clone()).await { + Ok(Some(probe)) => { + info!( + local_ip = %probe.local_addr.ip(), + reflected_ip = %probe.reflected_addr.ip(), + "STUN detected public address" + ); + if probe.local_addr.ip() != probe.reflected_addr.ip() + && !config.general.stun_iface_mismatch_ignore + { + warn!( + local_ip = %probe.local_addr.ip(), + reflected_ip = %probe.reflected_addr.ip(), + "STUN/interface IP mismatch; falling back to direct DC (set stun_iface_mismatch_ignore=true to force Middle Proxy)" + ); + use_middle_proxy = false; + } + } + Ok(None) => warn!("STUN probe returned no address; continuing"), + Err(e) => warn!(error = %e, "STUN probe failed; continuing"), + } + } + // ===================================================================== // Middle Proxy initialization (if enabled) // ===================================================================== @@ -331,84 +355,81 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai info!("Transport: Direct TCP (standard DCs only)"); } - // Startup DC ping (only meaningful in direct mode) - if me_pool.is_none() { - info!("================= Telegram DC Connectivity ================="); + info!("================= Telegram DC Connectivity ================="); - let ping_results = upstream_manager.ping_all_dcs(prefer_ipv6).await; + let ping_results = upstream_manager.ping_all_dcs(prefer_ipv6).await; - for upstream_result in &ping_results { - let v6_works = upstream_result - .v6_results - .iter() - .any(|r| r.rtt_ms.is_some()); - let v4_works = upstream_result - .v4_results - .iter() - .any(|r| r.rtt_ms.is_some()); - - if upstream_result.both_available { - if prefer_ipv6 { - info!(" IPv6 in use and IPv4 is fallback"); - } else { - info!(" IPv4 in use and IPv6 is fallback"); - } + for upstream_result in &ping_results { + let v6_works = upstream_result + .v6_results + .iter() + .any(|r| r.rtt_ms.is_some()); + let v4_works = upstream_result + .v4_results + .iter() + .any(|r| r.rtt_ms.is_some()); + + if upstream_result.both_available { + if prefer_ipv6 { + info!(" IPv6 in use and IPv4 is fallback"); } else { - if v6_works && !v4_works { - info!(" IPv6 only (IPv4 unavailable)"); - } else if v4_works && !v6_works { - info!(" IPv4 only (IPv6 unavailable)"); - } else if !v6_works && !v4_works { - info!(" No connectivity!"); - } + info!(" IPv4 in use and IPv6 is fallback"); } - - info!(" via {}", upstream_result.upstream_name); - info!("============================================================"); - - // Print IPv6 results first (only if IPv6 is available) - if v6_works { - for dc in &upstream_result.v6_results { - let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); - match &dc.rtt_ms { - Some(rtt) => { - info!(" DC{} [IPv6] {}:\t\t{:.0} ms", dc.dc_idx, addr_str, rtt); - } - None => { - let err = dc.error.as_deref().unwrap_or("fail"); - info!(" DC{} [IPv6] {}:\t\tFAIL ({})", dc.dc_idx, addr_str, err); - } - } - } - - info!("============================================================"); - } - - // Print IPv4 results (only if IPv4 is available) - if v4_works { - for dc in &upstream_result.v4_results { - let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); - match &dc.rtt_ms { - Some(rtt) => { - info!( - " DC{} [IPv4] {}:\t\t\t\t{:.0} ms", - dc.dc_idx, addr_str, rtt - ); - } - None => { - let err = dc.error.as_deref().unwrap_or("fail"); - info!( - " DC{} [IPv4] {}:\t\t\t\tFAIL ({})", - dc.dc_idx, addr_str, err - ); - } - } - } - - info!("============================================================"); + } else { + if v6_works && !v4_works { + info!(" IPv6 only (IPv4 unavailable)"); + } else if v4_works && !v6_works { + info!(" IPv4 only (IPv6 unavailable)"); + } else if !v6_works && !v4_works { + info!(" No connectivity!"); } } - } + + info!(" via {}", upstream_result.upstream_name); + info!("============================================================"); + + // Print IPv6 results first (only if IPv6 is available) + if v6_works { + for dc in &upstream_result.v6_results { + let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); + match &dc.rtt_ms { + Some(rtt) => { + info!(" DC{} [IPv6] {}:\t\t{:.0} ms", dc.dc_idx, addr_str, rtt); + } + None => { + let err = dc.error.as_deref().unwrap_or("fail"); + info!(" DC{} [IPv6] {}:\t\tFAIL ({})", dc.dc_idx, addr_str, err); + } + } + } + + info!("============================================================"); + } + + // Print IPv4 results (only if IPv4 is available) + if v4_works { + for dc in &upstream_result.v4_results { + let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); + match &dc.rtt_ms { + Some(rtt) => { + info!( + " DC{} [IPv4] {}:\t\t\t\t{:.0} ms", + dc.dc_idx, addr_str, rtt + ); + } + None => { + let err = dc.error.as_deref().unwrap_or("fail"); + info!( + " DC{} [IPv4] {}:\t\t\t\tFAIL ({})", + dc.dc_idx, addr_str, err + ); + } + } + } + + info!("============================================================"); + } + } // Background tasks let um_clone = upstream_manager.clone(); diff --git a/src/proxy/direct_relay.rs b/src/proxy/direct_relay.rs index 3cce39e..9f27b80 100644 --- a/src/proxy/direct_relay.rs +++ b/src/proxy/direct_relay.rs @@ -87,17 +87,25 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result { let num_dcs = datacenters.len(); let dc_key = dc_idx.to_string(); - if let Some(addr_str) = config.dc_overrides.get(&dc_key) { - match addr_str.parse::() { - Ok(addr) => { - debug!(dc_idx = dc_idx, addr = %addr, "Using DC override from config"); - return Ok(addr); - } - Err(_) => { - warn!(dc_idx = dc_idx, addr_str = %addr_str, - "Invalid DC override address in config, ignoring"); + if let Some(addrs) = config.dc_overrides.get(&dc_key) { + let prefer_v6 = config.general.prefer_ipv6; + let mut parsed = Vec::new(); + for addr_str in addrs { + match addr_str.parse::() { + Ok(addr) => parsed.push(addr), + Err(_) => warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring"), } } + + if let Some(addr) = parsed + .iter() + .find(|a| a.is_ipv6() == prefer_v6) + .or_else(|| parsed.first()) + .copied() + { + debug!(dc_idx = dc_idx, addr = %addr, count = parsed.len(), "Using DC override from config"); + return Ok(addr); + } } let abs_dc = dc_idx.unsigned_abs() as usize; diff --git a/src/transport/middle_proxy/mod.rs b/src/transport/middle_proxy/mod.rs index e617158..72c0c24 100644 --- a/src/transport/middle_proxy/mod.rs +++ b/src/transport/middle_proxy/mod.rs @@ -15,6 +15,7 @@ use bytes::Bytes; pub use health::me_health_monitor; pub use pool::MePool; +pub use pool_nat::{stun_probe, StunProbeResult}; pub use registry::ConnRegistry; pub use secret::fetch_proxy_secret; pub use config_updater::{fetch_proxy_config, me_config_updater}; diff --git a/src/transport/middle_proxy/pool_nat.rs b/src/transport/middle_proxy/pool_nat.rs index 633d0af..35ee0ea 100644 --- a/src/transport/middle_proxy/pool_nat.rs +++ b/src/transport/middle_proxy/pool_nat.rs @@ -6,6 +6,17 @@ use crate::error::{ProxyError, Result}; use super::MePool; +#[derive(Debug, Clone, Copy)] +pub struct StunProbeResult { + pub local_addr: std::net::SocketAddr, + pub reflected_addr: std::net::SocketAddr, +} + +pub async fn stun_probe(stun_addr: Option) -> Result> { + let stun_addr = stun_addr.unwrap_or_else(|| "stun.l.google.com:19302".to_string()); + fetch_stun_binding(&stun_addr).await +} + impl MePool { pub(super) fn translate_ip_for_nat(&self, ip: IpAddr) -> IpAddr { let nat_ip = self @@ -88,10 +99,12 @@ impl MePool { .unwrap_or_else(|| "stun.l.google.com:19302".to_string()); match fetch_stun_binding(&stun_addr).await { Ok(sa) => { - if let Some(sa) = sa { - info!(%sa, "NAT probe: reflected address"); + if let Some(result) = sa { + info!(local = %result.local_addr, reflected = %result.reflected_addr, "NAT probe: reflected address"); + Some(result.reflected_addr) + } else { + None } - sa } Err(e) => { warn!(error = %e, "NAT probe failed"); @@ -128,7 +141,7 @@ async fn fetch_public_ipv4_once(url: &str) -> Result> { Ok(ip) } -async fn fetch_stun_binding(stun_addr: &str) -> Result> { +async fn fetch_stun_binding(stun_addr: &str) -> Result> { use rand::RngCore; use tokio::net::UdpSocket; @@ -196,10 +209,17 @@ async fn fetch_stun_binding(stun_addr: &str) -> Result {} } diff --git a/src/transport/pool.rs b/src/transport/pool.rs index 1daa998..8d83321 100644 --- a/src/transport/pool.rs +++ b/src/transport/pool.rs @@ -285,12 +285,17 @@ where #[cfg(test)] mod tests { use super::*; + use std::io::ErrorKind; use tokio::net::TcpListener; #[tokio::test] async fn test_pool_basic() { // Start a test server - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let listener = match TcpListener::bind("127.0.0.1:0").await { + Ok(l) => l, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("bind failed: {e}"), + }; let addr = listener.local_addr().unwrap(); // Accept connections in background @@ -303,7 +308,11 @@ mod tests { let pool = ConnectionPool::new(); // Get a connection - let conn1 = pool.get(addr).await.unwrap(); + let conn1 = match pool.get(addr).await { + Ok(c) => c, + Err(ProxyError::Io(e)) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("connect failed: {e}"), + }; // Return it to pool pool.put(addr, conn1).await; @@ -335,4 +344,4 @@ mod tests { assert_eq!(stats.endpoints, 0); assert_eq!(stats.total_connections, 0); } -} \ No newline at end of file +} diff --git a/src/transport/socket.rs b/src/transport/socket.rs index a07c21c..a4a7034 100644 --- a/src/transport/socket.rs +++ b/src/transport/socket.rs @@ -205,15 +205,29 @@ pub fn create_listener(addr: SocketAddr, options: &ListenOptions) -> Result l, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("bind failed: {e}"), + }; let addr = listener.local_addr().unwrap(); - let stream = TcpStream::connect(addr).await.unwrap(); - configure_tcp_socket(&stream, true, Duration::from_secs(30)).unwrap(); + let stream = match TcpStream::connect(addr).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("connect failed: {e}"), + }; + if let Err(e) = configure_tcp_socket(&stream, true, Duration::from_secs(30)) { + if e.kind() == ErrorKind::PermissionDenied { + return; + } + panic!("configure_tcp_socket failed: {e}"); + } } #[test] @@ -234,4 +248,4 @@ mod tests { assert!(opts.reuse_port); assert_eq!(opts.backlog, 1024); } -} \ No newline at end of file +}