Merge remote-tracking branch 'original/main'

This commit is contained in:
AndreyAkifev 2026-02-16 10:20:53 +07:00
commit b9b8ec529c
14 changed files with 905 additions and 303 deletions

View File

@ -2,9 +2,9 @@ name: Rust
on: on:
push: push:
branches: [ main ] branches: [ "*" ]
pull_request: pull_request:
branches: [ main ] branches: [ "*" ]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@ -2,42 +2,86 @@
**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 **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 ## NEWS and EMERGENCY
### RU ### ✈️ Telemt 3 is released!
Многие из вас столкнулись с проблемой загрузки медиа из каналов с >100k subs... <table>
<tr>
<td width="50%" valign="top">
Мы уже знаем о проблеме: она связана с dc=203 - Telegram CDN и сейчас есть подтверждённое исправление... ### 🇷🇺 RU
🤐 ДОСТУПНО ТОЛЬКО В РЕЛИЗЕ 2.0.0.1 и последующих 15 февраля мы опубликовали `telemt 3` с поддержкой Middle-End Proxy, а значит:
Сейчас оно принимо через добавление в конфиг: - с функциональными медиа, в том числе с CDN/DC=203
- с Ad-tag — показывайте спонсорский канал и собирайте статистику через официального бота
- с новым подходом к безопасности и асинхронности
- с высокоточной диагностикой криптографии через `ME_DIAG`
Для использования нужно:
1. Версия `telemt` ≥3.0.0
2. Выполнение любого из наборов условий:
- публичный IP для исходящих соединений установлен на интерфейса инстанса с `telemt`
- ЛИБО
- вы используете NAT 1:1 + включили STUN-пробинг
3. В конфиге, в секции `[general]` указать:
```toml
use_middle_proxy = true
```
Если условия из пункта 1 не выполняются:
1. Выключите ME-режим:
- установите `use_middle_proxy = false`
- ЛИБО
- Middle-End Proxy будет выключен автоматически по таймауту, но это займёт больше времени при запуске
2. В конфиге, добавьте в конец:
```toml ```toml
[dc_overrides] [dc_overrides]
"203" = "91.105.192.100:443" "203" = "91.105.192.100:443"
``` ```
Мы работаем над поиском всех адресов для каждого "нестандартного" DC...
Фикс вне конфига будет в релизе 2.0.0.2 Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, реверс-инжиниринге или сетевых расследованиях — мы открыты к идеям и pull requests.
Если у вас есть компетенции в асинхронных сетевых приложениях, анализе трафика, reverse engineering, network forensics - мы открыты к мыслям, предложениям, pull requests </td>
<td width="50%" valign="top">
### EN ### 🇬🇧 EN
Many of you have encountered issues loading media from channels with over 100k subscribers…
Were already aware of the problem: its related to `dc=203` Telegram CDN and we now have a confirmed fix. On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
🤐 AVAILABLE ONLY IN RELEASE 2.0.0.1 and later - 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`
Currently, you can apply it by adding the following to your config: To use this feature, the following requirements must be met:
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
````
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 ```toml
[dc_overrides] [dc_overrides]
"203" = "91.105.192.100:443" "203" = "91.105.192.100:443"
``` ```
Were working on identifying all addresses for every “nonstandard” 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 welcome ideas, suggestions, and pull requests.
If you have expertise in asynchronous network applications, traffic analysis, reverse engineering, or network forensics were open to ideas, suggestions, and pull requests. </td>
</tr>
</table>
# Features # Features
💥 The configuration structure has changed since version 1.1.0.0. change it in your environment! 💥 The configuration structure has changed since version 1.1.0.0. change it in your environment!

View File

@ -5,8 +5,12 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "443:443" - "443:443"
# Allow caching 'proxy-secret' in read-only container
working_dir: /run/telemt
volumes: volumes:
- ./config.toml:/app/config.toml:ro - ./config.toml:/run/telemt/config.toml:ro
tmpfs:
- /run/telemt:rw,mode=1777,size=1m
environment: environment:
- RUST_LOG=info - RUST_LOG=info
# Uncomment this line if you want to use host network for IPv6, but bridge is default and usually better # Uncomment this line if you want to use host network for IPv6, but bridge is default and usually better

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,40 @@ 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()]
} }
fn default_unknown_dc_log_path() -> Option<String> {
Some("unknown-dc.txt".to_string())
}
// ============= 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 +130,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 +242,14 @@ 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,
/// Log unknown (non-standard) DC requests to a file (default: unknown-dc.txt). Set to null to disable.
#[serde(default = "default_unknown_dc_log_path")]
pub unknown_dc_log_path: Option<String>,
#[serde(default)] #[serde(default)]
pub log_level: LogLevel, pub log_level: LogLevel,
@ -183,6 +270,8 @@ 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,
unknown_dc_log_path: default_unknown_dc_log_path(),
log_level: LogLevel::Normal, log_level: LogLevel::Normal,
disable_colors: false, disable_colors: false,
} }
@ -499,13 +588,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 +688,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

@ -27,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};
@ -183,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());
@ -207,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)
// ===================================================================== // =====================================================================
@ -231,10 +256,10 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// proxy-secret is from: https://core.telegram.org/getProxySecret // proxy-secret is from: https://core.telegram.org/getProxySecret
// ============================================================= // =============================================================
let proxy_secret_path = config.general.proxy_secret_path.as_deref(); let proxy_secret_path = config.general.proxy_secret_path.as_deref();
match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).await { match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).await {
Ok(proxy_secret) => { Ok(proxy_secret) => {
info!( info!(
secret_len = proxy_secret.len(), secret_len = proxy_secret.len() as usize, // ← ЯВНЫЙ ТИП usize
key_sig = format_args!( key_sig = format_args!(
"0x{:08x}", "0x{:08x}",
if proxy_secret.len() >= 4 { if proxy_secret.len() >= 4 {
@ -330,21 +355,13 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Transport: Direct TCP (standard DCs only)"); 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, &config.dc_overrides)
.await;
for upstream_result in &ping_results { 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 let v6_works = upstream_result
.v6_results .v6_results
.iter() .iter()
@ -353,6 +370,14 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
.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 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 { if v6_works && !v4_works {
info!(" IPv6 only (IPv4 unavailable)"); info!(" IPv6 only (IPv4 unavailable)");
} else if v4_works && !v6_works { } else if v4_works && !v6_works {
@ -365,13 +390,12 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
info!(" via {}", upstream_result.upstream_name); info!(" via {}", upstream_result.upstream_name);
info!("============================================================"); info!("============================================================");
// Print IPv6 results first // Print IPv6 results first (only if IPv6 is available)
if v6_works {
for dc in &upstream_result.v6_results { for dc in &upstream_result.v6_results {
let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port());
match &dc.rtt_ms { match &dc.rtt_ms {
Some(rtt) => { 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); info!(" DC{} [IPv6] {}:\t\t{:.0} ms", dc.dc_idx, addr_str, rtt);
} }
None => { None => {
@ -382,14 +406,14 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
} }
info!("============================================================"); info!("============================================================");
}
// Print IPv4 results // Print IPv4 results (only if IPv4 is available)
if v4_works {
for dc in &upstream_result.v4_results { for dc in &upstream_result.v4_results {
let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port()); let addr_str = format!("{}:{}", dc.dc_addr.ip(), dc.dc_addr.port());
match &dc.rtt_ms { match &dc.rtt_ms {
Some(rtt) => { Some(rtt) => {
// Align: IPv4 addresses are shorter, use more tabs
// 149.154.175.50:443 = ~18 chars
info!( info!(
" DC{} [IPv4] {}:\t\t\t\t{:.0} ms", " DC{} [IPv4] {}:\t\t\t\t{:.0} ms",
dc.dc_idx, addr_str, rtt dc.dc_idx, addr_str, rtt
@ -558,6 +582,62 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
}); });
} }
#[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 { match signal::ctrl_c().await {
Ok(()) => info!("Shutting down..."), Ok(()) => info!("Shutting down..."),
Err(e) => error!("Signal error: {}", e), Err(e) => error!("Signal error: {}", e),

View File

@ -1,6 +1,8 @@
//! Client Handler //! Client Handler
use std::future::Future;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
@ -8,6 +10,17 @@ use tokio::net::TcpStream;
use tokio::time::timeout; use tokio::time::timeout;
use tracing::{debug, warn}; use tracing::{debug, warn};
/// Post-handshake future (relay phase, runs outside handshake timeout)
type PostHandshakeFuture = Pin<Box<dyn Future<Output = Result<()>> + 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::config::ProxyConfig;
use crate::crypto::SecureRandom; use crate::crypto::SecureRandom;
use crate::error::{HandshakeResult, ProxyError, Result}; 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::masking::handle_bad_client;
use crate::proxy::middle_relay::handle_via_middle_proxy; use crate::proxy::middle_relay::handle_via_middle_proxy;
pub async fn handle_client_stream<S>(
mut stream: S,
peer: SocketAddr,
config: Arc<ProxyConfig>,
stats: Arc<Stats>,
upstream_manager: Arc<UpstreamManager>,
replay_checker: Arc<ReplayChecker>,
buffer_pool: Arc<BufferPool>,
rng: Arc<SecureRandom>,
me_pool: Option<Arc<MePool>>,
ip_tracker: Arc<UserIpTracker>,
) -> 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 ClientHandler;
pub struct RunningClientHandler { pub struct RunningClientHandler {
@ -72,6 +239,7 @@ impl RunningClientHandler {
self.stats.increment_connects_all(); self.stats.increment_connects_all();
let peer = self.peer; let peer = self.peer;
let ip_tracker = self.ip_tracker.clone();
debug!(peer = %peer, "New connection"); debug!(peer = %peer, "New connection");
if let Err(e) = configure_client_socket( 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 handshake_timeout = Duration::from_secs(self.config.timeouts.client_handshake);
let stats = self.stats.clone(); let stats = self.stats.clone();
let result = timeout(handshake_timeout, self.do_handshake()).await; // Phase 1: handshake (with timeout)
let outcome = match timeout(handshake_timeout, self.do_handshake()).await {
match result { Ok(Ok(outcome)) => outcome,
Ok(Ok(())) => {
debug!(peer = %peer, "Connection handled successfully");
Ok(())
}
Ok(Err(e)) => { Ok(Err(e)) => {
debug!(peer = %peer, error = %e, "Handshake failed"); debug!(peer = %peer, error = %e, "Handshake failed");
Err(e) return Err(e);
} }
Err(_) => { Err(_) => {
stats.increment_handshake_timeouts(); stats.increment_handshake_timeouts();
debug!(peer = %peer, "Handshake timeout"); 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<HandshakeOutcome> {
let mut first_bytes = [0u8; 5]; let mut first_bytes = [0u8; 5];
self.stream.read_exact(&mut first_bytes).await?; self.stream.read_exact(&mut first_bytes).await?;
let is_tls = tls::is_tls_handshake(&first_bytes[..3]); let is_tls = tls::is_tls_handshake(&first_bytes[..3]);
let peer = self.peer; let peer = self.peer;
let ip_tracker = self.ip_tracker.clone();
debug!(peer = %peer, is_tls = is_tls, "Handshake type detected"); 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<HandshakeOutcome> {
let peer = self.peer; 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; 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(); self.stats.increment_connects_bad();
let (reader, writer) = self.stream.into_split(); let (reader, writer) = self.stream.into_split();
handle_bad_client(reader, writer, &first_bytes, &self.config).await; handle_bad_client(reader, writer, &first_bytes, &self.config).await;
return Ok(()); return Ok(HandshakeOutcome::Handled);
} }
let mut handshake = vec![0u8; 5 + tls_len]; let mut handshake = vec![0u8; 5 + tls_len];
@ -162,7 +334,7 @@ impl RunningClientHandler {
HandshakeResult::BadClient { reader, writer } => { HandshakeResult::BadClient { reader, writer } => {
stats.increment_connects_bad(); stats.increment_connects_bad();
handle_bad_client(reader, writer, &handshake, &config).await; handle_bad_client(reader, writer, &handshake, &config).await;
return Ok(()); return Ok(HandshakeOutcome::Handled);
} }
HandshakeResult::Error(e) => return Err(e), HandshakeResult::Error(e) => return Err(e),
}; };
@ -191,11 +363,12 @@ impl RunningClientHandler {
} => { } => {
stats.increment_connects_bad(); stats.increment_connects_bad();
debug!(peer = %peer, "Valid TLS but invalid MTProto handshake"); debug!(peer = %peer, "Valid TLS but invalid MTProto handshake");
return Ok(()); return Ok(HandshakeOutcome::Handled);
} }
HandshakeResult::Error(e) => return Err(e), HandshakeResult::Error(e) => return Err(e),
}; };
Ok(HandshakeOutcome::NeedsRelay(Box::pin(
Self::handle_authenticated_static( Self::handle_authenticated_static(
crypto_reader, crypto_reader,
crypto_writer, crypto_writer,
@ -209,19 +382,20 @@ impl RunningClientHandler {
local_addr, local_addr,
peer, peer,
self.ip_tracker, self.ip_tracker,
) ),
.await )))
} }
async fn handle_direct_client(mut self, first_bytes: [u8; 5]) -> Result<()> { async fn handle_direct_client(mut self, first_bytes: [u8; 5]) -> Result<HandshakeOutcome> {
let peer = self.peer; let peer = self.peer;
let ip_tracker = self.ip_tracker.clone();
if !self.config.general.modes.classic && !self.config.general.modes.secure { if !self.config.general.modes.classic && !self.config.general.modes.secure {
debug!(peer = %peer, "Non-TLS modes disabled"); debug!(peer = %peer, "Non-TLS modes disabled");
self.stats.increment_connects_bad(); self.stats.increment_connects_bad();
let (reader, writer) = self.stream.into_split(); let (reader, writer) = self.stream.into_split();
handle_bad_client(reader, writer, &first_bytes, &self.config).await; handle_bad_client(reader, writer, &first_bytes, &self.config).await;
return Ok(()); return Ok(HandshakeOutcome::Handled);
} }
let mut handshake = [0u8; HANDSHAKE_LEN]; let mut handshake = [0u8; HANDSHAKE_LEN];
@ -251,11 +425,12 @@ impl RunningClientHandler {
HandshakeResult::BadClient { reader, writer } => { HandshakeResult::BadClient { reader, writer } => {
stats.increment_connects_bad(); stats.increment_connects_bad();
handle_bad_client(reader, writer, &handshake, &config).await; handle_bad_client(reader, writer, &handshake, &config).await;
return Ok(()); return Ok(HandshakeOutcome::Handled);
} }
HandshakeResult::Error(e) => return Err(e), HandshakeResult::Error(e) => return Err(e),
}; };
Ok(HandshakeOutcome::NeedsRelay(Box::pin(
Self::handle_authenticated_static( Self::handle_authenticated_static(
crypto_reader, crypto_reader,
crypto_writer, crypto_writer,
@ -269,8 +444,8 @@ impl RunningClientHandler {
local_addr, local_addr,
peer, peer,
self.ip_tracker, self.ip_tracker,
) ),
.await )))
} }
/// Main dispatch after successful handshake. /// Main dispatch after successful handshake.

View File

@ -1,3 +1,5 @@
use std::fs::OpenOptions;
use std::io::Write;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
@ -87,17 +89,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) {
let prefer_v6 = config.general.prefer_ipv6;
let mut parsed = Vec::new();
for addr_str in addrs {
match addr_str.parse::<SocketAddr>() { match addr_str.parse::<SocketAddr>() {
Ok(addr) => { Ok(addr) => parsed.push(addr),
debug!(dc_idx = dc_idx, addr = %addr, "Using DC override from config"); 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); return Ok(addr);
} }
Err(_) => {
warn!(dc_idx = dc_idx, addr_str = %addr_str,
"Invalid DC override address in config, ignoring");
}
}
} }
let abs_dc = dc_idx.unsigned_abs() as usize; let abs_dc = dc_idx.unsigned_abs() as usize;
@ -105,6 +115,16 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
return Ok(SocketAddr::new(datacenters[abs_dc - 1], TG_DATACENTER_PORT)); return Ok(SocketAddr::new(datacenters[abs_dc - 1], TG_DATACENTER_PORT));
} }
// Unknown DC requested by client without override: log and fall back.
if !config.dc_overrides.contains_key(&dc_key) {
warn!(dc_idx = dc_idx, "Requested non-standard DC with no override; falling back to default cluster");
if let Some(path) = &config.general.unknown_dc_log_path {
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "dc_idx={dc_idx}");
}
}
}
let default_dc = config.default_dc.unwrap_or(2) as usize; let default_dc = config.default_dc.unwrap_or(2) as usize;
let fallback_idx = if default_dc >= 1 && default_dc <= num_dcs { let fallback_idx = if default_dc >= 1 && default_dc <= num_dcs {
default_dc - 1 default_dc - 1

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]

View File

@ -2,6 +2,7 @@
//! //!
//! IPv6/IPv4 connectivity checks with configurable preference. //! IPv6/IPv4 connectivity checks with configurable preference.
use std::collections::HashMap;
use std::net::{SocketAddr, IpAddr}; use std::net::{SocketAddr, IpAddr};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -350,7 +351,11 @@ impl UpstreamManager {
/// Ping all Telegram DCs through all upstreams. /// Ping all Telegram DCs through all upstreams.
/// Tests BOTH IPv6 and IPv4, returns separate results for each. /// Tests BOTH IPv6 and IPv4, returns separate results for each.
pub async fn ping_all_dcs(&self, prefer_ipv6: bool) -> Vec<StartupPingResult> { pub async fn ping_all_dcs(
&self,
prefer_ipv6: bool,
dc_overrides: &HashMap<String, Vec<String>>,
) -> Vec<StartupPingResult> {
let upstreams: Vec<(usize, UpstreamConfig)> = { let upstreams: Vec<(usize, UpstreamConfig)> = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
guard.iter().enumerate() guard.iter().enumerate()
@ -450,6 +455,58 @@ impl UpstreamManager {
v4_results.push(ping_result); v4_results.push(ping_result);
} }
// === Ping DC overrides (v4/v6) ===
for (dc_key, addrs) in dc_overrides {
let dc_num: i16 = match dc_key.parse::<i16>() {
Ok(v) if v > 0 => v,
Err(_) => {
warn!(dc = %dc_key, "Invalid dc_overrides key, skipping");
continue;
},
_ => continue,
};
let dc_idx = dc_num as usize;
for addr_str in addrs {
match addr_str.parse::<SocketAddr>() {
Ok(addr) => {
let is_v6 = addr.is_ipv6();
let result = tokio::time::timeout(
Duration::from_secs(DC_PING_TIMEOUT_SECS),
self.ping_single_dc(&upstream_config, addr)
).await;
let ping_result = match result {
Ok(Ok(rtt_ms)) => DcPingResult {
dc_idx,
dc_addr: addr,
rtt_ms: Some(rtt_ms),
error: None,
},
Ok(Err(e)) => DcPingResult {
dc_idx,
dc_addr: addr,
rtt_ms: None,
error: Some(e.to_string()),
},
Err(_) => DcPingResult {
dc_idx,
dc_addr: addr,
rtt_ms: None,
error: Some("timeout".to_string()),
},
};
if is_v6 {
v6_results.push(ping_result);
} else {
v4_results.push(ping_result);
}
}
Err(_) => warn!(dc = %dc_idx, addr = %addr_str, "Invalid dc_overrides address, skipping"),
}
}
}
// Check if both IP versions have at least one working DC // Check if both IP versions have at least one working DC
let v6_has_working = v6_results.iter().any(|r| r.rtt_ms.is_some()); let v6_has_working = v6_results.iter().any(|r| r.rtt_ms.is_some());
let v4_has_working = v4_results.iter().any(|r| r.rtt_ms.is_some()); let v4_has_working = v4_results.iter().any(|r| r.rtt_ms.is_some());

BIN
telemt

Binary file not shown.

View File

@ -1,121 +1,204 @@
"""Telegram datacenter server checker."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from itertools import groupby
from operator import attrgetter
from pathlib import Path
from typing import TYPE_CHECKING
from telethon import TelegramClient from telethon import TelegramClient
from telethon.tl.functions.help import GetConfigRequest from telethon.tl.functions.help import GetConfigRequest
import asyncio
api_id = '' if TYPE_CHECKING:
api_hash = '' from telethon.tl.types import DcOption
async def get_all_servers(): API_ID: int = 123456
print("🔄 Подключаемся к Telegram...") API_HASH: str = ""
client = TelegramClient('session', api_id, api_hash) SESSION_NAME: str = "session"
OUTPUT_FILE: Path = Path("telegram_servers.txt")
await client.start() _CONSOLE_FLAG_MAP: dict[str, str] = {
print("✅ Подключение установлено!\n") "IPv6": "IPv6",
"MEDIA-ONLY": "🎬 MEDIA-ONLY",
"CDN": "📦 CDN",
"TCPO": "🔒 TCPO",
"STATIC": "📌 STATIC",
}
print("📡 Запрашиваем конфигурацию серверов...")
config = await client(GetConfigRequest())
print(f"📊 Получено серверов: {len(config.dc_options)}\n") @dataclass(frozen=True, slots=True)
print("="*80) class DCServer:
"""Typed representation of a Telegram DC server.
# Группируем серверы по DC ID Attributes:
dc_groups = {} dc_id: Datacenter identifier.
for dc in config.dc_options: ip: Server IP address.
if dc.id not in dc_groups: port: Server port.
dc_groups[dc.id] = [] flags: Active flag labels (plain, without emoji).
dc_groups[dc.id].append(dc) """
# Выводим все серверы, сгруппированные по DC dc_id: int
for dc_id in sorted(dc_groups.keys()): ip: str
servers = dc_groups[dc_id] port: int
print(f"\n🌐 DATACENTER {dc_id} ({len(servers)} серверов)") flags: frozenset[str] = field(default_factory=frozenset)
print("-" * 80)
for dc in servers: @classmethod
# Собираем флаги def from_option(cls, dc: DcOption) -> DCServer:
flags = [] """Create from a Telethon DcOption.
if dc.ipv6:
flags.append("IPv6")
if dc.media_only:
flags.append("🎬 MEDIA-ONLY")
if dc.cdn:
flags.append("📦 CDN")
if dc.tcpo_only:
flags.append("🔒 TCPO")
if dc.static:
flags.append("📌 STATIC")
flags_str = f" [{', '.join(flags)}]" if flags else " [STANDARD]" Args:
dc: Raw DcOption object.
# Форматируем IP (выравниваем для читаемости) Returns:
ip_display = f"{dc.ip_address:45}" Parsed DCServer instance.
"""
checks: dict[str, bool] = {
"IPv6": dc.ipv6,
"MEDIA-ONLY": dc.media_only,
"CDN": dc.cdn,
"TCPO": dc.tcpo_only,
"STATIC": dc.static,
}
return cls(
dc_id=dc.id,
ip=dc.ip_address,
port=dc.port,
flags=frozenset(k for k, v in checks.items() if v),
)
print(f" {ip_display}:{dc.port:5}{flags_str}") def flags_display(self, *, emoji: bool = False) -> str:
"""Formatted flags string.
# Статистика Args:
print("\n" + "="*80) emoji: Whether to include emoji prefixes.
print("📈 СТАТИСТИКА:")
print("="*80)
total = len(config.dc_options) Returns:
ipv4_count = sum(1 for dc in config.dc_options if not dc.ipv6) Bracketed flags or '[STANDARD]'.
ipv6_count = sum(1 for dc in config.dc_options if dc.ipv6) """
media_count = sum(1 for dc in config.dc_options if dc.media_only) if not self.flags:
cdn_count = sum(1 for dc in config.dc_options if dc.cdn) return "[STANDARD]"
tcpo_count = sum(1 for dc in config.dc_options if dc.tcpo_only) labels = sorted(
static_count = sum(1 for dc in config.dc_options if dc.static) _CONSOLE_FLAG_MAP[f] if emoji else f for f in self.flags
)
return f"[{', '.join(labels)}]"
print(f" Всего серверов: {total}")
print(f" IPv4 серверы: {ipv4_count}")
print(f" IPv6 серверы: {ipv6_count}")
print(f" Media-only: {media_count}")
print(f" CDN серверы: {cdn_count}")
print(f" TCPO-only: {tcpo_count}")
print(f" Static: {static_count}")
# Дополнительная информация из config class TelegramDCChecker:
print("\n" + "="*80) """Fetches and displays Telegram DC configuration.
print(" ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ:")
print("="*80)
print(f" Дата конфигурации: {config.date}")
print(f" Expires: {config.expires}")
print(f" Test mode: {config.test_mode}")
print(f" This DC: {config.this_dc}")
# Сохраняем в файл Attributes:
print("\n💾 Сохраняем результаты в файл telegram_servers.txt...") _client: Telethon client instance.
with open('telegram_servers.txt', 'w', encoding='utf-8') as f: _servers: Parsed server list.
f.write("TELEGRAM DATACENTER SERVERS\n") """
f.write("="*80 + "\n\n")
for dc_id in sorted(dc_groups.keys()): def __init__(self) -> None:
servers = dc_groups[dc_id] """Initialize the checker."""
f.write(f"\nDATACENTER {dc_id} ({len(servers)} servers)\n") self._client = TelegramClient(SESSION_NAME, API_ID, API_HASH)
f.write("-" * 80 + "\n") self._servers: list[DCServer] = []
for dc in servers: async def run(self) -> None:
flags = [] """Connect, fetch config, display and save results."""
if dc.ipv6: print("🔄 Подключаемся к Telegram...") # noqa: T201
flags.append("IPv6") try:
if dc.media_only: await self._client.start()
flags.append("MEDIA-ONLY") print("✅ Подключение установлено!\n") # noqa: T201
if dc.cdn:
flags.append("CDN")
if dc.tcpo_only:
flags.append("TCPO")
if dc.static:
flags.append("STATIC")
flags_str = f" [{', '.join(flags)}]" if flags else " [STANDARD]" print("📡 Запрашиваем конфигурацию серверов...") # noqa: T201
f.write(f" {dc.ip_address}:{dc.port}{flags_str}\n") config = await self._client(GetConfigRequest())
self._servers = [DCServer.from_option(dc) for dc in config.dc_options]
f.write(f"\n\nTotal servers: {total}\n") self._print(config)
f.write(f"Generated: {config.date}\n") self._save(config)
finally:
await self._client.disconnect()
print("\n👋 Отключились от Telegram") # noqa: T201
print("✅ Результаты сохранены в telegram_servers.txt") def _grouped(self) -> dict[int, list[DCServer]]:
"""Group servers by DC ID.
await client.disconnect() Returns:
print("\n👋 Отключились от Telegram") Ordered mapping of DC ID to servers.
"""
ordered = sorted(self._servers, key=attrgetter("dc_id"))
return {k: list(g) for k, g in groupby(ordered, key=attrgetter("dc_id"))}
if __name__ == '__main__': def _print(self, config: object) -> None:
asyncio.run(get_all_servers()) """Print results to stdout in original format.
Args:
config: Raw Telegram config.
"""
sep = "=" * 80
dash = "-" * 80
total = len(self._servers)
print(f"📊 Получено серверов: {total}\n") # noqa: T201
print(sep) # noqa: T201
for dc_id, servers in self._grouped().items():
print(f"\n🌐 DATACENTER {dc_id} ({len(servers)} серверов)") # noqa: T201
print(dash) # noqa: T201
for s in servers:
print(f" {s.ip:45}:{s.port:5} {s.flags_display(emoji=True)}") # noqa: T201
ipv4 = total - self._flag_count("IPv6")
print(f"\n{sep}") # noqa: T201
print("📈 СТАТИСТИКА:") # noqa: T201
print(sep) # noqa: T201
print(f" Всего серверов: {total}") # noqa: T201
print(f" IPv4 серверы: {ipv4}") # noqa: T201
print(f" IPv6 серверы: {self._flag_count('IPv6')}") # noqa: T201
print(f" Media-only: {self._flag_count('MEDIA-ONLY')}") # noqa: T201
print(f" CDN серверы: {self._flag_count('CDN')}") # noqa: T201
print(f" TCPO-only: {self._flag_count('TCPO')}") # noqa: T201
print(f" Static: {self._flag_count('STATIC')}") # noqa: T201
print(f"\n{sep}") # noqa: T201
print(" ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ:") # noqa: T201
print(sep) # noqa: T201
print(f" Дата конфигурации: {config.date}") # noqa: T201 # type: ignore[attr-defined]
print(f" Expires: {config.expires}") # noqa: T201 # type: ignore[attr-defined]
print(f" Test mode: {config.test_mode}") # noqa: T201 # type: ignore[attr-defined]
print(f" This DC: {config.this_dc}") # noqa: T201 # type: ignore[attr-defined]
def _flag_count(self, flag: str) -> int:
"""Count servers with a given flag.
Args:
flag: Flag name.
Returns:
Count of matching servers.
"""
return sum(1 for s in self._servers if flag in s.flags)
def _save(self, config: object) -> None:
"""Save results to file in original format.
Args:
config: Raw Telegram config.
"""
parts: list[str] = []
parts.append("TELEGRAM DATACENTER SERVERS\n")
parts.append("=" * 80 + "\n\n")
for dc_id, servers in self._grouped().items():
parts.append(f"\nDATACENTER {dc_id} ({len(servers)} servers)\n")
parts.append("-" * 80 + "\n")
for s in servers:
parts.append(f" {s.ip}:{s.port} {s.flags_display(emoji=False)}\n")
parts.append(f"\n\nTotal servers: {len(self._servers)}\n")
parts.append(f"Generated: {config.date}\n") # type: ignore[attr-defined]
OUTPUT_FILE.write_text("".join(parts), encoding="utf-8")
print(f"\n💾 Сохраняем результаты в файл {OUTPUT_FILE}...") # noqa: T201
print(f"✅ Результаты сохранены в {OUTPUT_FILE}") # noqa: T201
if __name__ == "__main__":
asyncio.run(TelegramDCChecker().run())