From 7e566fd65581bfb42c20cb3c97538007f2412823 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 14:46:15 +0300
Subject: [PATCH 01/18] Update README.md
---
README.md | 42 ++++++++++++------------------------------
1 file changed, 12 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index f6d07cf..1b082b3 100644
--- a/README.md
+++ b/README.md
@@ -4,40 +4,22 @@
## Emergency
### RU
-Многие из вас столкнулись с проблемой загрузки медиа из каналов с >100k subs...
+15 ферваля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
+- с функциональными медиа, в том числе с CDN/DC=203
+- с Ad-tag - показывайте спонсорский канал и собирайте статистику через официального бота
+- с новым подходом к безопасности и асинхронности
+- с высокоточной диагностикой криптонрафии через `ME_DIAG`
-Мы уже знаем о проблеме: она связана с dc=203 - Telegram CDN и сейчас есть подтверждённое исправление...
-
-🤐 ДОСТУПНО ТОЛЬКО В РЕЛИЗЕ 2.0.0.1 и последующих
-
-Сейчас оно принимо через добавление в конфиг:
-```toml
-[dc_overrides]
-"203" = "91.105.192.100:443"
-```
-Мы работаем над поиском всех адресов для каждого "нестандартного" DC...
-
-Фикс вне конфига будет в релизе 2.0.0.2
-
-Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, reverse engineering, network forensics - мы открыты к мыслям, предложениям, pull requests
+Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге софта, "сетевых расследованиях" - мы открыты к мыслям, предложениям, pull requests
### EN
-Many of you have encountered issues loading media from channels with over 100k subscribers…
+On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
+- functional media, including CDN/DC=203
+- Ad-tag support – promote a sponsored channel and collect statistics via Telegram bot
+- new approach to security and asynchronicity
+- high-precision cryptography diagnostics via `ME_DIAG`
-We’re already aware of the problem: it’s related to `dc=203` – Telegram CDN – and we now have a confirmed fix.
-
-🤐 AVAILABLE ONLY IN RELEASE 2.0.0.1 and later
-
-Currently, you can apply it by adding the following to your config:
-```toml
-[dc_overrides]
-"203" = "91.105.192.100:443"
-```
-We’re working on identifying all addresses for every “non‑standard” DC…
-
-The fix will be included in release 2.0.0.2, no manual config needed.
-
-If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics – we’re open to ideas, suggestions, and pull requests.
+If you have expertise in asynchronous network applications, traffic analysis, software reverse engineering, or network forensics – we're open to ideas, suggestions, and pull requests.
# Features
💥 The configuration structure has changed since version 1.1.0.0. change it in your environment!
From 8de1318c9c8415f4e9ee156d221d2c78d33010e9 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:35:44 +0300
Subject: [PATCH 02/18] Update README.md
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 1b082b3..3a0131e 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@
- с новым подходом к безопасности и асинхронности
- с высокоточной диагностикой криптонрафии через `ME_DIAG`
+Для использования нужно указать `use_middle_proxy = true` в версии `telemt` 3.0.0 и последующих
+
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге софта, "сетевых расследованиях" - мы открыты к мыслям, предложениям, pull requests
### EN
@@ -19,6 +21,8 @@ On February 15, we released `telemt 3` with support for Middle-End Proxy, whic
- new approach to security and asynchronicity
- high-precision cryptography diagnostics via `ME_DIAG`
+For using you should set `use_middle_proxy = true` in version `telemt` 3.0.0 or later
+
If you have expertise in asynchronous network applications, traffic analysis, software reverse engineering, or network forensics – we're open to ideas, suggestions, and pull requests.
# Features
From eda365c21fd21c5c302286d3ee245dd68144f021 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:46:24 +0300
Subject: [PATCH 03/18] Update README.md
---
README.md | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 3a0131e..4c6329c 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,9 @@
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as connection pooling, replay protection, detailed statistics, masking from "prying" eyes
-## Emergency
-### RU
+## NEWS and EMERGENCY
+### ✈️ Telemt 3 is released!
+#### RU
15 ферваля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
- с функциональными медиа, в том числе с CDN/DC=203
- с Ad-tag - показывайте спонсорский канал и собирайте статистику через официального бота
@@ -14,7 +15,7 @@
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге софта, "сетевых расследованиях" - мы открыты к мыслям, предложениям, pull requests
-### EN
+#### EN
On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
- functional media, including CDN/DC=203
- Ad-tag support – promote a sponsored channel and collect statistics via Telegram bot
From 9b790c7bf4597911929748b31e1612b3597a1bc8 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 15:48:42 +0300
Subject: [PATCH 04/18] Update README.md
---
README.md | 56 +++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 40 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 4c6329c..37e81d2 100644
--- a/README.md
+++ b/README.md
@@ -4,27 +4,51 @@
## NEWS and EMERGENCY
### ✈️ Telemt 3 is released!
-#### RU
-15 ферваля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
-- с функциональными медиа, в том числе с CDN/DC=203
-- с Ad-tag - показывайте спонсорский канал и собирайте статистику через официального бота
-- с новым подходом к безопасности и асинхронности
-- с высокоточной диагностикой криптонрафии через `ME_DIAG`
+
+
+
-Для использования нужно указать `use_middle_proxy = true` в версии `telemt` 3.0.0 и последующих
+### 🇷🇺 RU
-Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге софта, "сетевых расследованиях" - мы открыты к мыслям, предложениям, pull requests
+15 февраля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
-#### EN
-On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
-- functional media, including CDN/DC=203
-- Ad-tag support – promote a sponsored channel and collect statistics via Telegram bot
-- new approach to security and asynchronicity
-- high-precision cryptography diagnostics via `ME_DIAG`
+- с функциональными медиа, в том числе с CDN/DC=203
+- с Ad-tag — показывайте спонсорский канал и собирайте статистику через официального бота
+- с новым подходом к безопасности и асинхронности
+- с высокоточной диагностикой криптографии через `ME_DIAG`
-For using you should set `use_middle_proxy = true` in version `telemt` 3.0.0 or later
+Для использования нужно указать:
+```toml
+use_middle_proxy = true
+```
-If you have expertise in asynchronous network applications, traffic analysis, software reverse engineering, or network forensics – we're open to ideas, suggestions, and pull requests.
+в версии `telemt` 3.0.0 и последующих.
+
+Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях — мы открыты к идеям и pull requests.
+
+
+
+
+### 🇬🇧 EN
+
+On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
+
+- functional media, including CDN/DC=203
+- Ad-tag support – promote a sponsored channel and collect statistics via Telegram bot
+- new approach to security and asynchronicity
+- high-precision cryptography diagnostics via `ME_DIAG`
+
+To use it, set:
+```toml
+use_middle_proxy = true
+```
+in version `telemt` 3.0.0 or later.
+
+If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics — we welcome ideas, suggestions, and pull requests.
+
+
+
+
# Features
💥 The configuration structure has changed since version 1.1.0.0. change it in your environment!
From 0fa5914501ffb0460085b6c70094e41c074beee9 Mon Sep 17 00:00:00 2001
From: artemws <59208085+artemws@users.noreply.github.com>
Date: Sun, 15 Feb 2026 12:07:51 +0200
Subject: [PATCH 05/18] Add Unix socket listener support
---
src/main.rs | 95 ++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 76 insertions(+), 19 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 8a974be..5240cc8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ 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;
@@ -231,25 +232,25 @@ async fn main() -> std::result::Result<(), Box> {
// proxy-secret is from: https://core.telegram.org/getProxySecret
// =============================================================
let proxy_secret_path = config.general.proxy_secret_path.as_deref();
- match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).await {
- Ok(proxy_secret) => {
- info!(
- secret_len = proxy_secret.len(),
- key_sig = format_args!(
- "0x{:08x}",
- if proxy_secret.len() >= 4 {
- u32::from_le_bytes([
- proxy_secret[0],
- proxy_secret[1],
- proxy_secret[2],
- proxy_secret[3],
- ])
- } else {
- 0
- }
- ),
- "Proxy-secret loaded"
- );
+match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).await {
+ Ok(proxy_secret) => {
+ info!(
+ secret_len = proxy_secret.len() as usize, // ← ЯВНЫЙ ТИП usize
+ key_sig = format_args!(
+ "0x{:08x}",
+ if proxy_secret.len() >= 4 {
+ u32::from_le_bytes([
+ proxy_secret[0],
+ proxy_secret[1],
+ proxy_secret[2],
+ proxy_secret[3],
+ ])
+ } else {
+ 0
+ }
+ ),
+ "Proxy-secret loaded"
+ );
// Load ME config (v4/v6) + default DC
let mut cfg_v4 = fetch_proxy_config(
@@ -558,6 +559,62 @@ async fn main() -> std::result::Result<(), Box> {
});
}
+ #[cfg(unix)]
+ if let Some(ref unix_path) = config.server.listen_unix_sock {
+ use tokio::net::UnixListener; // ← добавь импорт, если его нет выше
+
+ // Удаляем старые файлы сокета, если они есть (стандартная практика)
+ let _ = tokio::fs::remove_file(unix_path).await;
+
+ let unix_listener = UnixListener::bind(unix_path)?;
+ info!("Listening on unix:{}", unix_path);
+
+ let config = config.clone();
+ let stats = stats.clone();
+ let upstream_manager = upstream_manager.clone();
+ let replay_checker = replay_checker.clone();
+ let buffer_pool = buffer_pool.clone();
+ let rng = rng.clone();
+ let me_pool = me_pool.clone();
+ let ip_tracker = ip_tracker.clone();
+
+ tokio::spawn(async move {
+ let unix_conn_counter = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1));
+
+ loop {
+ match unix_listener.accept().await {
+ Ok((stream, _)) => {
+ let conn_id = unix_conn_counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+ let fake_peer = SocketAddr::from(([127, 0, 0, 1], (conn_id % 65535) as u16)); // безопасный порт
+
+ let config = config.clone();
+ let stats = stats.clone();
+ let upstream_manager = upstream_manager.clone();
+ let replay_checker = replay_checker.clone();
+ let buffer_pool = buffer_pool.clone();
+ let rng = rng.clone();
+ let me_pool = me_pool.clone();
+ let ip_tracker = ip_tracker.clone();
+
+ tokio::spawn(async move {
+ if let Err(e) = crate::proxy::client::handle_client_stream(
+ stream, fake_peer, config, stats,
+ upstream_manager, replay_checker, buffer_pool, rng,
+ me_pool, ip_tracker,
+ ).await {
+ debug!(error = %e, "Unix socket connection error");
+ }
+ });
+ }
+ Err(e) => {
+ error!("Unix socket accept error: {}", e);
+ tokio::time::sleep(Duration::from_millis(100)).await;
+ }
+ }
+ }
+ });
+ }
+
match signal::ctrl_c().await {
Ok(()) => info!("Shutting down..."),
Err(e) => error!("Signal error: {}", e),
From 32bc3e1387ec129940d8b25d488d00a710d259db Mon Sep 17 00:00:00 2001
From: artemws <59208085+artemws@users.noreply.github.com>
Date: Sun, 15 Feb 2026 12:08:14 +0200
Subject: [PATCH 06/18] Refactor client handshake handling for clarity
---
src/proxy/client.rs | 269 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 222 insertions(+), 47 deletions(-)
diff --git a/src/proxy/client.rs b/src/proxy/client.rs
index 041e7cb..87d6b52 100644
--- a/src/proxy/client.rs
+++ b/src/proxy/client.rs
@@ -1,6 +1,8 @@
//! Client Handler
+use std::future::Future;
use std::net::SocketAddr;
+use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
@@ -8,6 +10,17 @@ use tokio::net::TcpStream;
use tokio::time::timeout;
use tracing::{debug, warn};
+/// Post-handshake future (relay phase, runs outside handshake timeout)
+type PostHandshakeFuture = Pin> + Send>>;
+
+/// Result of the handshake phase
+enum HandshakeOutcome {
+ /// Handshake succeeded, relay work to do (outside timeout)
+ NeedsRelay(PostHandshakeFuture),
+ /// Already fully handled (bad client masking, etc.)
+ Handled,
+}
+
use crate::config::ProxyConfig;
use crate::crypto::SecureRandom;
use crate::error::{HandshakeResult, ProxyError, Result};
@@ -24,6 +37,160 @@ use crate::proxy::handshake::{HandshakeSuccess, handle_mtproto_handshake, handle
use crate::proxy::masking::handle_bad_client;
use crate::proxy::middle_relay::handle_via_middle_proxy;
+pub async fn handle_client_stream(
+ mut stream: S,
+ peer: SocketAddr,
+ config: Arc,
+ stats: Arc,
+ upstream_manager: Arc,
+ replay_checker: Arc,
+ buffer_pool: Arc,
+ rng: Arc,
+ me_pool: Option>,
+ ip_tracker: Arc,
+) -> Result<()>
+where
+ S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+{
+ stats.increment_connects_all();
+ debug!(peer = %peer, "New connection (generic stream)");
+
+ let handshake_timeout = Duration::from_secs(config.timeouts.client_handshake);
+ let stats_for_timeout = stats.clone();
+
+ // For non-TCP streams, use a synthetic local address
+ let local_addr: SocketAddr = format!("0.0.0.0:{}", config.server.port)
+ .parse()
+ .unwrap_or_else(|_| "0.0.0.0:443".parse().unwrap());
+
+ // Phase 1: handshake (with timeout)
+ let outcome = match timeout(handshake_timeout, async {
+ let mut first_bytes = [0u8; 5];
+ stream.read_exact(&mut first_bytes).await?;
+
+ let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
+ debug!(peer = %peer, is_tls = is_tls, "Handshake type detected");
+
+ if is_tls {
+ let tls_len = u16::from_be_bytes([first_bytes[3], first_bytes[4]]) as usize;
+
+ if tls_len < 512 {
+ debug!(peer = %peer, tls_len = tls_len, "TLS handshake too short");
+ stats.increment_connects_bad();
+ let (reader, writer) = tokio::io::split(stream);
+ handle_bad_client(reader, writer, &first_bytes, &config).await;
+ return Ok(HandshakeOutcome::Handled);
+ }
+
+ let mut handshake = vec![0u8; 5 + tls_len];
+ handshake[..5].copy_from_slice(&first_bytes);
+ stream.read_exact(&mut handshake[5..]).await?;
+
+ let (read_half, write_half) = tokio::io::split(stream);
+
+ let (mut tls_reader, tls_writer, _tls_user) = match handle_tls_handshake(
+ &handshake, read_half, write_half, peer,
+ &config, &replay_checker, &rng,
+ ).await {
+ HandshakeResult::Success(result) => result,
+ HandshakeResult::BadClient { reader, writer } => {
+ stats.increment_connects_bad();
+ handle_bad_client(reader, writer, &handshake, &config).await;
+ return Ok(HandshakeOutcome::Handled);
+ }
+ HandshakeResult::Error(e) => return Err(e),
+ };
+
+ debug!(peer = %peer, "Reading MTProto handshake through TLS");
+ let mtproto_data = tls_reader.read_exact(HANDSHAKE_LEN).await?;
+ let mtproto_handshake: [u8; HANDSHAKE_LEN] = mtproto_data[..].try_into()
+ .map_err(|_| ProxyError::InvalidHandshake("Short MTProto handshake".into()))?;
+
+ let (crypto_reader, crypto_writer, success) = match handle_mtproto_handshake(
+ &mtproto_handshake, tls_reader, tls_writer, peer,
+ &config, &replay_checker, true,
+ ).await {
+ HandshakeResult::Success(result) => result,
+ HandshakeResult::BadClient { reader: _, writer: _ } => {
+ stats.increment_connects_bad();
+ debug!(peer = %peer, "Valid TLS but invalid MTProto handshake");
+ return Ok(HandshakeOutcome::Handled);
+ }
+ HandshakeResult::Error(e) => return Err(e),
+ };
+
+ Ok(HandshakeOutcome::NeedsRelay(Box::pin(
+ RunningClientHandler::handle_authenticated_static(
+ crypto_reader, crypto_writer, success,
+ upstream_manager, stats, config, buffer_pool, rng, me_pool,
+ local_addr, peer, ip_tracker.clone(),
+ ),
+ )))
+ } else {
+ if !config.general.modes.classic && !config.general.modes.secure {
+ debug!(peer = %peer, "Non-TLS modes disabled");
+ stats.increment_connects_bad();
+ let (reader, writer) = tokio::io::split(stream);
+ handle_bad_client(reader, writer, &first_bytes, &config).await;
+ return Ok(HandshakeOutcome::Handled);
+ }
+
+ let mut handshake = [0u8; HANDSHAKE_LEN];
+ handshake[..5].copy_from_slice(&first_bytes);
+ stream.read_exact(&mut handshake[5..]).await?;
+
+ let (read_half, write_half) = tokio::io::split(stream);
+
+ let (crypto_reader, crypto_writer, success) = match handle_mtproto_handshake(
+ &handshake, read_half, write_half, peer,
+ &config, &replay_checker, false,
+ ).await {
+ HandshakeResult::Success(result) => result,
+ HandshakeResult::BadClient { reader, writer } => {
+ stats.increment_connects_bad();
+ handle_bad_client(reader, writer, &handshake, &config).await;
+ return Ok(HandshakeOutcome::Handled);
+ }
+ HandshakeResult::Error(e) => return Err(e),
+ };
+
+ Ok(HandshakeOutcome::NeedsRelay(Box::pin(
+ RunningClientHandler::handle_authenticated_static(
+ crypto_reader,
+ crypto_writer,
+ success,
+ upstream_manager,
+ stats,
+ config,
+ buffer_pool,
+ rng,
+ me_pool,
+ local_addr,
+ peer,
+ ip_tracker.clone(),
+ )
+ )))
+ }
+ }).await {
+ Ok(Ok(outcome)) => outcome,
+ Ok(Err(e)) => {
+ debug!(peer = %peer, error = %e, "Handshake failed");
+ return Err(e);
+ }
+ Err(_) => {
+ stats_for_timeout.increment_handshake_timeouts();
+ debug!(peer = %peer, "Handshake timeout");
+ return Err(ProxyError::TgHandshakeTimeout);
+ }
+ };
+
+ // Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
+ match outcome {
+ HandshakeOutcome::NeedsRelay(fut) => fut.await,
+ HandshakeOutcome::Handled => Ok(()),
+ }
+}
+
pub struct ClientHandler;
pub struct RunningClientHandler {
@@ -72,6 +239,7 @@ impl RunningClientHandler {
self.stats.increment_connects_all();
let peer = self.peer;
+ let ip_tracker = self.ip_tracker.clone();
debug!(peer = %peer, "New connection");
if let Err(e) = configure_client_socket(
@@ -85,31 +253,34 @@ impl RunningClientHandler {
let handshake_timeout = Duration::from_secs(self.config.timeouts.client_handshake);
let stats = self.stats.clone();
- let result = timeout(handshake_timeout, self.do_handshake()).await;
-
- match result {
- Ok(Ok(())) => {
- debug!(peer = %peer, "Connection handled successfully");
- Ok(())
- }
+ // Phase 1: handshake (with timeout)
+ let outcome = match timeout(handshake_timeout, self.do_handshake()).await {
+ Ok(Ok(outcome)) => outcome,
Ok(Err(e)) => {
debug!(peer = %peer, error = %e, "Handshake failed");
- Err(e)
+ return Err(e);
}
Err(_) => {
stats.increment_handshake_timeouts();
debug!(peer = %peer, "Handshake timeout");
- Err(ProxyError::TgHandshakeTimeout)
+ return Err(ProxyError::TgHandshakeTimeout);
}
+ };
+
+ // Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
+ match outcome {
+ HandshakeOutcome::NeedsRelay(fut) => fut.await,
+ HandshakeOutcome::Handled => Ok(()),
}
}
- async fn do_handshake(mut self) -> Result<()> {
+ async fn do_handshake(mut self) -> Result {
let mut first_bytes = [0u8; 5];
self.stream.read_exact(&mut first_bytes).await?;
let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
let peer = self.peer;
+ let ip_tracker = self.ip_tracker.clone();
debug!(peer = %peer, is_tls = is_tls, "Handshake type detected");
@@ -120,8 +291,9 @@ impl RunningClientHandler {
}
}
- async fn handle_tls_client(mut self, first_bytes: [u8; 5]) -> Result<()> {
+ async fn handle_tls_client(mut self, first_bytes: [u8; 5]) -> Result {
let peer = self.peer;
+ let ip_tracker = self.ip_tracker.clone();
let tls_len = u16::from_be_bytes([first_bytes[3], first_bytes[4]]) as usize;
@@ -132,7 +304,7 @@ impl RunningClientHandler {
self.stats.increment_connects_bad();
let (reader, writer) = self.stream.into_split();
handle_bad_client(reader, writer, &first_bytes, &self.config).await;
- return Ok(());
+ return Ok(HandshakeOutcome::Handled);
}
let mut handshake = vec![0u8; 5 + tls_len];
@@ -162,7 +334,7 @@ impl RunningClientHandler {
HandshakeResult::BadClient { reader, writer } => {
stats.increment_connects_bad();
handle_bad_client(reader, writer, &handshake, &config).await;
- return Ok(());
+ return Ok(HandshakeOutcome::Handled);
}
HandshakeResult::Error(e) => return Err(e),
};
@@ -191,37 +363,39 @@ impl RunningClientHandler {
} => {
stats.increment_connects_bad();
debug!(peer = %peer, "Valid TLS but invalid MTProto handshake");
- return Ok(());
+ return Ok(HandshakeOutcome::Handled);
}
HandshakeResult::Error(e) => return Err(e),
};
- Self::handle_authenticated_static(
- crypto_reader,
- crypto_writer,
- success,
- self.upstream_manager,
- self.stats,
- self.config,
- buffer_pool,
- self.rng,
- self.me_pool,
- local_addr,
- peer,
- self.ip_tracker,
- )
- .await
+ Ok(HandshakeOutcome::NeedsRelay(Box::pin(
+ Self::handle_authenticated_static(
+ crypto_reader,
+ crypto_writer,
+ success,
+ self.upstream_manager,
+ self.stats,
+ self.config,
+ buffer_pool,
+ self.rng,
+ self.me_pool,
+ local_addr,
+ peer,
+ self.ip_tracker,
+ ),
+ )))
}
- async fn handle_direct_client(mut self, first_bytes: [u8; 5]) -> Result<()> {
+ async fn handle_direct_client(mut self, first_bytes: [u8; 5]) -> Result {
let peer = self.peer;
+ let ip_tracker = self.ip_tracker.clone();
if !self.config.general.modes.classic && !self.config.general.modes.secure {
debug!(peer = %peer, "Non-TLS modes disabled");
self.stats.increment_connects_bad();
let (reader, writer) = self.stream.into_split();
handle_bad_client(reader, writer, &first_bytes, &self.config).await;
- return Ok(());
+ return Ok(HandshakeOutcome::Handled);
}
let mut handshake = [0u8; HANDSHAKE_LEN];
@@ -251,26 +425,27 @@ impl RunningClientHandler {
HandshakeResult::BadClient { reader, writer } => {
stats.increment_connects_bad();
handle_bad_client(reader, writer, &handshake, &config).await;
- return Ok(());
+ return Ok(HandshakeOutcome::Handled);
}
HandshakeResult::Error(e) => return Err(e),
};
- Self::handle_authenticated_static(
- crypto_reader,
- crypto_writer,
- success,
- self.upstream_manager,
- self.stats,
- self.config,
- buffer_pool,
- self.rng,
- self.me_pool,
- local_addr,
- peer,
- self.ip_tracker,
- )
- .await
+ Ok(HandshakeOutcome::NeedsRelay(Box::pin(
+ Self::handle_authenticated_static(
+ crypto_reader,
+ crypto_writer,
+ success,
+ self.upstream_manager,
+ self.stats,
+ self.config,
+ buffer_pool,
+ self.rng,
+ self.me_pool,
+ local_addr,
+ peer,
+ self.ip_tracker,
+ ),
+ )))
}
/// Main dispatch after successful handshake.
From 84f8b786e730070c46c5e98b1ce7957717b0152c Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 17:29:52 +0300
Subject: [PATCH 07/18] Update README.md
---
README.md | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 37e81d2..7a79bb1 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,9 @@
use_middle_proxy = true
```
-в версии `telemt` 3.0.0 и последующих.
+в версии `telemt` 3.0.0 и последующих, при выполнении любого набора условий:
+- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
+- вы используете NAT 1:1 + включили STUN-пробинг
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях — мы открыты к идеям и pull requests.
@@ -42,7 +44,9 @@ To use it, set:
```toml
use_middle_proxy = true
```
-in version `telemt` 3.0.0 or later.
+in version `telemt` 3.0.0 or later, if any set of conditions is met:
+- public IP for outgoing connections is set on the instance interface with `telemt`
+- you are using NAT 1:1 + enabled STUN-probe
If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics — we welcome ideas, suggestions, and pull requests.
From 533613886a3c42aa784001c442911d8ae7325ff7 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 17:34:47 +0300
Subject: [PATCH 08/18] Update README.md
---
README.md | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/README.md b/README.md
index 7a79bb1..00134d4 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,14 @@ use_middle_proxy = true
- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
- вы используете NAT 1:1 + включили STUN-пробинг
+Если нет:
+1. Установите `use_middle_proxy = false` или Middle-End Proxy будет выключен автоматически по таймауту, но это займёт больше времени при запуске
+2. Пропишите в конец конфига
+```toml
+[dc_overrides]
+"203" = "91.105.192.100:443"
+```
+
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях — мы открыты к идеям и pull requests.
@@ -48,6 +56,14 @@ in version `telemt` 3.0.0 or later, if any set of conditions is met:
- public IP for outgoing connections is set on the instance interface with `telemt`
- you are using NAT 1:1 + enabled STUN-probe
+If not:
+1. set `use_middle_proxy = false` or Middle-End Proxy will be turned off automatically by timeout, but it will take longer at startup
+2. Place to the end of your config
+```toml
+[dc_overrides]
+"203" = "91.105.192.100:443"
+```
+
If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics — we welcome ideas, suggestions, and pull requests.
From d1348e809fed59bb00e9d8f3bcea3dfde470f6e6 Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 18:09:54 +0300
Subject: [PATCH 09/18] Update README.md
---
README.md | 45 +++++++++++++++++++++++++++++----------------
1 file changed, 29 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 00134d4..a4d3132 100644
--- a/README.md
+++ b/README.md
@@ -17,18 +17,23 @@
- с новым подходом к безопасности и асинхронности
- с высокоточной диагностикой криптографии через `ME_DIAG`
-Для использования нужно указать:
+Для использования нужно:
+0. Версия `telemt` ≥3.0.0
+1. Выполнение любого из наборов условий:
+ - публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
+ - ЛИБО
+ - вы используете NAT 1:1 + включили STUN-пробинг
+2. В конфиге, в секции `[general]` указать:
```toml
use_middle_proxy = true
```
-в версии `telemt` 3.0.0 и последующих, при выполнении любого набора условий:
-- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
-- вы используете NAT 1:1 + включили STUN-пробинг
-
-Если нет:
-1. Установите `use_middle_proxy = false` или Middle-End Proxy будет выключен автоматически по таймауту, но это займёт больше времени при запуске
-2. Пропишите в конец конфига
+Если условия из пункта 1 не выполняются:
+1. Выключите ME-режим:
+ - установите `use_middle_proxy = false`
+ - ЛИБО
+ - Middle-End Proxy будет выключен автоматически по таймауту, но это займёт больше времени при запуске
+2. В конфиге, добавьте в конец:
```toml
[dc_overrides]
"203" = "91.105.192.100:443"
@@ -48,17 +53,25 @@ On February 15, we released `telemt 3` with support for Middle-End Proxy, which
- new approach to security and asynchronicity
- high-precision cryptography diagnostics via `ME_DIAG`
-To use it, set:
+To use this feature, the following requirements must be met:
+0. `telemt` version ≥ 3.0.0
+1. One of the following conditions satisfied:
+ - the instance running `telemt` has a public IP address assigned to its network interface for outbound connections
+ - OR
+ - you are using 1:1 NAT and have STUN probing enabled
+
+3. In the config file, under the `[general]` section, specify:
```toml
use_middle_proxy = true
-```
-in version `telemt` 3.0.0 or later, if any set of conditions is met:
-- public IP for outgoing connections is set on the instance interface with `telemt`
-- you are using NAT 1:1 + enabled STUN-probe
+````
-If not:
-1. set `use_middle_proxy = false` or Middle-End Proxy will be turned off automatically by timeout, but it will take longer at startup
-2. Place to the end of your config
+If the conditions from step 1 are not satisfied:
+1. Disable Middle-End mode:
+ - set `use_middle_proxy = false`
+ - OR
+ - Middle-End Proxy will be disabled automatically after a timeout, but this will increase startup time
+
+2. In the config file, add the following at the end:
```toml
[dc_overrides]
"203" = "91.105.192.100:443"
From fbe9277f86e0cffba62836ce409c3786753cfdca Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 18:12:37 +0300
Subject: [PATCH 10/18] Update README.md
---
README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index a4d3132..b1a20e2 100644
--- a/README.md
+++ b/README.md
@@ -18,12 +18,13 @@
- с высокоточной диагностикой криптографии через `ME_DIAG`
Для использования нужно:
-0. Версия `telemt` ≥3.0.0
-1. Выполнение любого из наборов условий:
+
+1. Версия `telemt` ≥3.0.0
+2. Выполнение любого из наборов условий:
- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
- ЛИБО
- вы используете NAT 1:1 + включили STUN-пробинг
-2. В конфиге, в секции `[general]` указать:
+3. В конфиге, в секции `[general]` указать:
```toml
use_middle_proxy = true
```
@@ -54,12 +55,11 @@ On February 15, we released `telemt 3` with support for Middle-End Proxy, which
- high-precision cryptography diagnostics via `ME_DIAG`
To use this feature, the following requirements must be met:
-0. `telemt` version ≥ 3.0.0
-1. One of the following conditions satisfied:
+1. `telemt` version ≥ 3.0.0
+2. One of the following conditions satisfied:
- the instance running `telemt` has a public IP address assigned to its network interface for outbound connections
- OR
- you are using 1:1 NAT and have STUN probing enabled
-
3. In the config file, under the `[general]` section, specify:
```toml
use_middle_proxy = true
From 2112ba22f1948cc3282a69154b2a5e50d9c0a8be Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 19:31:23 +0300
Subject: [PATCH 11/18] Update rust.yml
---
.github/workflows/rust.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 7945e70..e512bab 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -2,9 +2,9 @@ name: Rust
on:
push:
- branches: [ main ]
+ branches: [ * ]
pull_request:
- branches: [ main ]
+ branches: [ * ]
env:
CARGO_TERM_COLOR: always
From f9c41ab703a8d63a5d4c0b11116b6468ed409e4d Mon Sep 17 00:00:00 2001
From: Alexey <247128645+axkurcom@users.noreply.github.com>
Date: Sun, 15 Feb 2026 19:32:29 +0300
Subject: [PATCH 12/18] Update rust.yml
---
.github/workflows/rust.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index e512bab..71f9f4e 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -2,9 +2,9 @@ name: Rust
on:
push:
- branches: [ * ]
+ branches: [ "*" ]
pull_request:
- branches: [ * ]
+ branches: [ "*" ]
env:
CARGO_TERM_COLOR: always
From 4a80bc8988152ef099caaf46009651a4fcf67e2d Mon Sep 17 00:00:00 2001
From: artemws <59208085+artemws@users.noreply.github.com>
Date: Sun, 15 Feb 2026 20:51:17 +0200
Subject: [PATCH 13/18] Refactor connectivity logging for upstream results
---
src/main.rs | 132 ++++++++++++++++++++++++++--------------------------
1 file changed, 66 insertions(+), 66 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 5240cc8..c3e5f71 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -337,77 +337,77 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let ping_results = upstream_manager.ping_all_dcs(prefer_ipv6).await;
- for upstream_result in &ping_results {
- // Show which IP version is in use and which is fallback
- 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");
- }
- } else {
- 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 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!");
- }
- }
+ 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");
+ }
+ } 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!("============================================================");
+ info!(" via {}", upstream_result.upstream_name);
+ info!("============================================================");
- // Print IPv6 results first
- 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) => {
- // Align: IPv6 addresses are longer, use fewer tabs
- // [2001:b28:f23d:f001::a]:443 = ~28 chars
- 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);
- }
- }
- }
+ // 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!("============================================================");
+ info!("============================================================");
+ }
- // Print IPv4 results
- 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) => {
- // Align: IPv4 addresses are shorter, use more tabs
- // 149.154.175.50:443 = ~18 chars
- 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
- );
- }
- }
- }
+ // 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!("============================================================");
+ }
+ }
}
// Background tasks
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 14/18] 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