diff --git a/src/config/load.rs b/src/config/load.rs index 7892e2c..e37be71 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -346,6 +346,12 @@ impl ProxyConfig { )); } + if config.timeouts.tg_connect == 0 { + return Err(ProxyError::Config( + "timeouts.tg_connect must be > 0".to_string(), + )); + } + if config.general.upstream_unhealthy_fail_threshold == 0 { return Err(ProxyError::Config( "general.upstream_unhealthy_fail_threshold must be > 0".to_string(), @@ -1907,6 +1913,26 @@ mod tests { let _ = std::fs::remove_file(path); } + #[test] + fn tg_connect_zero_is_rejected() { + let toml = r#" + [timeouts] + tg_connect = 0 + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + "#; + let dir = std::env::temp_dir(); + let path = dir.join("telemt_tg_connect_zero_test.toml"); + std::fs::write(&path, toml).unwrap(); + let err = ProxyConfig::load(&path).unwrap_err().to_string(); + assert!(err.contains("timeouts.tg_connect must be > 0")); + let _ = std::fs::remove_file(path); + } + #[test] fn rpc_proxy_req_every_out_of_range_is_rejected() { let toml = r#" diff --git a/src/maestro/mod.rs b/src/maestro/mod.rs index 5f3fd3a..57306df 100644 --- a/src/maestro/mod.rs +++ b/src/maestro/mod.rs @@ -223,6 +223,7 @@ pub async fn run() -> std::result::Result<(), Box> { config.general.upstream_connect_retry_attempts, config.general.upstream_connect_retry_backoff_ms, config.general.upstream_connect_budget_ms, + config.timeouts.tg_connect, config.general.upstream_unhealthy_fail_threshold, config.general.upstream_connect_failfast_hard_errors, stats.clone(), diff --git a/src/transport/upstream.rs b/src/transport/upstream.rs index 1120eae..5899ee1 100644 --- a/src/transport/upstream.rs +++ b/src/transport/upstream.rs @@ -34,8 +34,6 @@ const NUM_DCS: usize = 5; /// Timeout for individual DC ping attempt const DC_PING_TIMEOUT_SECS: u64 = 5; -/// Timeout for direct TG DC TCP connect readiness. -const DIRECT_CONNECT_TIMEOUT_SECS: u64 = 10; /// Interval between upstream health-check cycles. const HEALTH_CHECK_INTERVAL_SECS: u64 = 30; /// Timeout for a single health-check connect attempt. @@ -319,6 +317,8 @@ pub struct UpstreamManager { connect_retry_attempts: u32, connect_retry_backoff: Duration, connect_budget: Duration, + /// Per-attempt TCP connect timeout to Telegram DC (`[timeouts] tg_connect`, seconds). + tg_connect_timeout_secs: u64, unhealthy_fail_threshold: u32, connect_failfast_hard_errors: bool, no_upstreams_warn_epoch_ms: Arc, @@ -332,6 +332,7 @@ impl UpstreamManager { connect_retry_attempts: u32, connect_retry_backoff_ms: u64, connect_budget_ms: u64, + tg_connect_timeout_secs: u64, unhealthy_fail_threshold: u32, connect_failfast_hard_errors: bool, stats: Arc, @@ -347,6 +348,7 @@ impl UpstreamManager { connect_retry_attempts: connect_retry_attempts.max(1), connect_retry_backoff: Duration::from_millis(connect_retry_backoff_ms), connect_budget: Duration::from_millis(connect_budget_ms.max(1)), + tg_connect_timeout_secs: tg_connect_timeout_secs.max(1), unhealthy_fail_threshold: unhealthy_fail_threshold.max(1), connect_failfast_hard_errors, no_upstreams_warn_epoch_ms: Arc::new(AtomicU64::new(0)), @@ -797,8 +799,8 @@ impl UpstreamManager { break; } let remaining_budget = self.connect_budget.saturating_sub(elapsed); - let attempt_timeout = - Duration::from_secs(DIRECT_CONNECT_TIMEOUT_SECS).min(remaining_budget); + let attempt_timeout = Duration::from_secs(self.tg_connect_timeout_secs) + .min(remaining_budget); if attempt_timeout.is_zero() { last_error = Some(ProxyError::ConnectionTimeout { addr: target.to_string(), @@ -1901,6 +1903,7 @@ mod tests { 1, 100, 1000, + 10, 1, false, Arc::new(Stats::new()),