DC=203 by default + IP Autodetect by STUN

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-02-15 23:30:21 +03:00
parent 4a80bc8988
commit 904c17c1b3
7 changed files with 260 additions and 101 deletions

View File

@@ -3,6 +3,7 @@
use crate::error::{ProxyError, Result}; use crate::error::{ProxyError, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde::de::Deserializer;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::path::Path; use std::path::Path;
@@ -53,6 +54,36 @@ fn default_metrics_whitelist() -> Vec<IpAddr> {
vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()] vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()]
} }
// ============= Custom Deserializers =============
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
fn deserialize_dc_overrides<'de, D>(
deserializer: D,
) -> std::result::Result<HashMap<String, Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
let raw: HashMap<String, OneOrMany> = 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 ============= // ============= Log Level =============
/// Logging verbosity 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 { impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@@ -163,6 +238,10 @@ pub struct GeneralConfig {
#[serde(default)] #[serde(default)]
pub middle_proxy_nat_stun: Option<String>, pub middle_proxy_nat_stun: Option<String>,
/// Ignore STUN/interface IP mismatch (keep using Middle Proxy even if NAT detected).
#[serde(default)]
pub stun_iface_mismatch_ignore: bool,
#[serde(default)] #[serde(default)]
pub log_level: LogLevel, pub log_level: LogLevel,
@@ -183,6 +262,7 @@ impl Default for GeneralConfig {
middle_proxy_nat_ip: None, middle_proxy_nat_ip: None,
middle_proxy_nat_probe: false, middle_proxy_nat_probe: false,
middle_proxy_nat_stun: None, middle_proxy_nat_stun: None,
stun_iface_mismatch_ignore: false,
log_level: LogLevel::Normal, log_level: LogLevel::Normal,
disable_colors: false, disable_colors: false,
} }
@@ -499,13 +579,13 @@ pub struct ProxyConfig {
pub show_link: ShowLink, pub show_link: ShowLink,
/// DC address overrides for non-standard DCs (CDN, media, test, etc.) /// 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 <dc_id> <ip>:<port>` config directive. /// Matches the C implementation's `proxy_for <dc_id> <ip>:<port>` config directive.
/// Example in config.toml: /// Example in config.toml:
/// [dc_overrides] /// [dc_overrides]
/// "203" = "149.154.175.100:443" /// \"203\" = [\"149.154.175.100:443\", \"91.105.192.100:443\"]
#[serde(default)] #[serde(default, deserialize_with = "deserialize_dc_overrides")]
pub dc_overrides: HashMap<String, String>, pub dc_overrides: HashMap<String, Vec<String>>,
/// Default DC index (1-5) for unmapped non-standard DCs. /// Default DC index (1-5) for unmapped non-standard DCs.
/// Matches the C implementation's `default <dc_id>` config directive. /// Matches the C implementation's `default <dc_id>` 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) Ok(config)
} }

View File

@@ -8,7 +8,6 @@ use tokio::signal;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload}; use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
use tokio::net::UnixListener;
mod cli; mod cli;
mod config; mod config;
@@ -28,7 +27,7 @@ use crate::ip_tracker::UserIpTracker;
use crate::proxy::ClientHandler; use crate::proxy::ClientHandler;
use crate::stats::{ReplayChecker, Stats}; use crate::stats::{ReplayChecker, Stats};
use crate::stream::BufferPool; 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::transport::{ListenOptions, UpstreamManager, create_listener};
use crate::util::ip::detect_ip; use crate::util::ip::detect_ip;
use crate::protocol::constants::{TG_MIDDLE_PROXIES_V4, TG_MIDDLE_PROXIES_V6}; use crate::protocol::constants::{TG_MIDDLE_PROXIES_V4, TG_MIDDLE_PROXIES_V6};
@@ -184,7 +183,7 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
} }
let prefer_ipv6 = config.general.prefer_ipv6; 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 config = Arc::new(config);
let stats = Arc::new(Stats::new()); let stats = Arc::new(Stats::new());
let rng = Arc::new(SecureRandom::new()); let rng = Arc::new(SecureRandom::new());
@@ -208,6 +207,31 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Connection concurrency limit // Connection concurrency limit
let _max_connections = Arc::new(Semaphore::new(10_000)); 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) // 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)"); info!("Transport: Direct TCP (standard DCs only)");
} }
// Startup DC ping (only meaningful in direct mode) info!("================= Telegram DC Connectivity =================");
if me_pool.is_none() {
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 { for upstream_result in &ping_results {
let v6_works = upstream_result let v6_works = upstream_result
.v6_results .v6_results
.iter() .iter()
.any(|r| r.rtt_ms.is_some()); .any(|r| r.rtt_ms.is_some());
let v4_works = upstream_result let v4_works = upstream_result
.v4_results .v4_results
.iter() .iter()
.any(|r| r.rtt_ms.is_some()); .any(|r| r.rtt_ms.is_some());
if upstream_result.both_available { if upstream_result.both_available {
if prefer_ipv6 { if prefer_ipv6 {
info!(" IPv6 in use and IPv4 is fallback"); info!(" IPv6 in use and IPv4 is fallback");
} else {
info!(" IPv4 in use and IPv6 is fallback");
}
} else { } else {
if v6_works && !v4_works { info!(" IPv4 in use and IPv6 is fallback");
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!");
}
} }
} else {
info!(" via {}", upstream_result.upstream_name); if v6_works && !v4_works {
info!("============================================================"); info!(" IPv6 only (IPv4 unavailable)");
} else if v4_works && !v6_works {
// Print IPv6 results first (only if IPv6 is available) info!(" IPv4 only (IPv6 unavailable)");
if v6_works { } else if !v6_works && !v4_works {
for dc in &upstream_result.v6_results { info!(" No connectivity!");
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!("============================================================");
} }
} }
}
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 // Background tasks
let um_clone = upstream_manager.clone(); let um_clone = upstream_manager.clone();

View File

@@ -87,17 +87,25 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
let num_dcs = datacenters.len(); let num_dcs = datacenters.len();
let dc_key = dc_idx.to_string(); let dc_key = dc_idx.to_string();
if let Some(addr_str) = config.dc_overrides.get(&dc_key) { if let Some(addrs) = config.dc_overrides.get(&dc_key) {
match addr_str.parse::<SocketAddr>() { let prefer_v6 = config.general.prefer_ipv6;
Ok(addr) => { let mut parsed = Vec::new();
debug!(dc_idx = dc_idx, addr = %addr, "Using DC override from config"); for addr_str in addrs {
return Ok(addr); match addr_str.parse::<SocketAddr>() {
} Ok(addr) => parsed.push(addr),
Err(_) => { Err(_) => warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring"),
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; let abs_dc = dc_idx.unsigned_abs() as usize;

View File

@@ -15,6 +15,7 @@ use bytes::Bytes;
pub use health::me_health_monitor; pub use health::me_health_monitor;
pub use pool::MePool; pub use pool::MePool;
pub use pool_nat::{stun_probe, StunProbeResult};
pub use registry::ConnRegistry; pub use registry::ConnRegistry;
pub use secret::fetch_proxy_secret; pub use secret::fetch_proxy_secret;
pub use config_updater::{fetch_proxy_config, me_config_updater}; pub use config_updater::{fetch_proxy_config, me_config_updater};

View File

@@ -6,6 +6,17 @@ use crate::error::{ProxyError, Result};
use super::MePool; 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<String>) -> Result<Option<StunProbeResult>> {
let stun_addr = stun_addr.unwrap_or_else(|| "stun.l.google.com:19302".to_string());
fetch_stun_binding(&stun_addr).await
}
impl MePool { impl MePool {
pub(super) fn translate_ip_for_nat(&self, ip: IpAddr) -> IpAddr { pub(super) fn translate_ip_for_nat(&self, ip: IpAddr) -> IpAddr {
let nat_ip = self let nat_ip = self
@@ -88,10 +99,12 @@ impl MePool {
.unwrap_or_else(|| "stun.l.google.com:19302".to_string()); .unwrap_or_else(|| "stun.l.google.com:19302".to_string());
match fetch_stun_binding(&stun_addr).await { match fetch_stun_binding(&stun_addr).await {
Ok(sa) => { Ok(sa) => {
if let Some(sa) = sa { if let Some(result) = sa {
info!(%sa, "NAT probe: reflected address"); info!(local = %result.local_addr, reflected = %result.reflected_addr, "NAT probe: reflected address");
Some(result.reflected_addr)
} else {
None
} }
sa
} }
Err(e) => { Err(e) => {
warn!(error = %e, "NAT probe failed"); warn!(error = %e, "NAT probe failed");
@@ -128,7 +141,7 @@ async fn fetch_public_ipv4_once(url: &str) -> Result<Option<Ipv4Addr>> {
Ok(ip) Ok(ip)
} }
async fn fetch_stun_binding(stun_addr: &str) -> Result<Option<std::net::SocketAddr>> { async fn fetch_stun_binding(stun_addr: &str) -> Result<Option<StunProbeResult>> {
use rand::RngCore; use rand::RngCore;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
@@ -196,10 +209,17 @@ async fn fetch_stun_binding(stun_addr: &str) -> Result<Option<std::net::SocketAd
} else { } else {
(u16::from_be_bytes(port_bytes), ip_bytes) (u16::from_be_bytes(port_bytes), ip_bytes)
}; };
return Ok(Some(std::net::SocketAddr::new( let reflected = std::net::SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])), IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])),
port, port,
))); );
let local_addr = socket.local_addr().map_err(|e| {
ProxyError::Proxy(format!("STUN local_addr failed: {e}"))
})?;
return Ok(Some(StunProbeResult {
local_addr,
reflected_addr: reflected,
}));
} }
_ => {} _ => {}
} }

View File

@@ -285,12 +285,17 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::ErrorKind;
use tokio::net::TcpListener; use tokio::net::TcpListener;
#[tokio::test] #[tokio::test]
async fn test_pool_basic() { async fn test_pool_basic() {
// Start a test server // 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(); let addr = listener.local_addr().unwrap();
// Accept connections in background // Accept connections in background
@@ -303,7 +308,11 @@ mod tests {
let pool = ConnectionPool::new(); let pool = ConnectionPool::new();
// Get a connection // 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 // Return it to pool
pool.put(addr, conn1).await; pool.put(addr, conn1).await;

View File

@@ -205,15 +205,29 @@ pub fn create_listener(addr: SocketAddr, options: &ListenOptions) -> Result<Sock
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::io::ErrorKind;
use tokio::net::TcpListener; use tokio::net::TcpListener;
#[tokio::test] #[tokio::test]
async fn test_configure_socket() { async fn test_configure_socket() {
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(); let addr = listener.local_addr().unwrap();
let stream = TcpStream::connect(addr).await.unwrap(); let stream = match TcpStream::connect(addr).await {
configure_tcp_socket(&stream, true, Duration::from_secs(30)).unwrap(); 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] #[test]