### π¬π§ EN
-On February 15, we released `telemt 3` with support for Middle-End Proxy, which means:
+On February 18, we released `telemt 3.0.3`. This version introduces:
-- 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`
+- improved Middle-End Health Check method
+- high-speed recovery of Middle-End init
+- reduced latency on the hot path
+- correct Dualstack support: proper handling of IPv6 Middle-End
+- *clean* client reconnection without session "drift" between Middle-End
+- automatic degradation to Direct-DC mode in case of large-scale (>2 ME-DC groups) Middle-End unavailability
+- automatic public IP detection behind NAT; first - Middle-End handshake is performed, otherwise automatic degradation is applied
+- known special DC=203 is now handled natively: media is delivered from the CDN via Direct-DC mode
-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
-````
+[Release is available here](https://github.com/telemt/telemt/releases/tag/3.0.3)
-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"
-```
-
-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 - we welcome ideas and pull requests!
@@ -86,7 +52,9 @@ If you have expertise in asynchronous network applications, traffic analysis, re
# Features
π₯ The configuration structure has changed since version 1.1.0.0. change it in your environment!
-β Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
+β Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](#recognizability-for-dpi-and-crawler)
+
+β Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
# GOTO
- [Features](#features)
@@ -215,6 +183,7 @@ prefer_ipv6 = false
fast_mode = true
use_middle_proxy = false
# ad_tag = "..."
+# disable_colors = false # Disable colored output in logs (useful for files/systemd)
[network]
ipv4 = true
@@ -247,7 +216,9 @@ ip = "::"
# Users to show in the startup log (tg:// links)
[general.links]
-show = ["hello"] # Users to show in the startup log (tg:// links)
+show = ["hello"] # Only show links for user "hello"
+# show = ["alice", "bob"] # Only show links for alice and bob
+# show = "*" # Show links for all users
# public_host = "proxy.example.com" # Host (IP or domain) for tg:// links
# public_port = 443 # Port for tg:// links (default: server.port)
diff --git a/config.toml b/config.toml
index 28dd3b6..45d6b75 100644
--- a/config.toml
+++ b/config.toml
@@ -5,6 +5,38 @@ prefer_ipv6 = false
fast_mode = true
use_middle_proxy = true
#ad_tag = "00000000000000000000000000000000"
+# Path to proxy-secret binary (auto-downloaded if missing).
+proxy_secret_path = "proxy-secret"
+
+# === Middle Proxy (ME) ===
+# Public IP override for ME KDF when behind NAT; leave unset to auto-detect.
+#middle_proxy_nat_ip = "203.0.113.10"
+# Enable STUN probing to discover public IP:port for ME.
+middle_proxy_nat_probe = true
+# Primary STUN server (host:port); defaults to Telegram STUN when empty.
+middle_proxy_nat_stun = "stun.l.google.com:19302"
+# Optional fallback STUN servers list.
+middle_proxy_nat_stun_servers = ["stun1.l.google.com:19302", "stun2.l.google.com:19302"]
+# Desired number of concurrent ME writers in pool.
+middle_proxy_pool_size = 16
+# Pre-initialized warm-standby ME connections kept idle.
+middle_proxy_warm_standby = 8
+# Ignore STUN/interface mismatch and keep ME enabled even if IP differs.
+stun_iface_mismatch_ignore = false
+# Keepalive padding frames - fl==4
+me_keepalive_enabled = true
+me_keepalive_interval_secs = 25 # Period between keepalives
+me_keepalive_jitter_secs = 5 # Jitter added to interval
+me_keepalive_payload_random = true # Randomize 4-byte payload (vs zeros)
+# Stagger extra ME connections on warmup to de-phase lifecycles.
+me_warmup_stagger_enabled = true
+me_warmup_step_delay_ms = 500 # Base delay between extra connects
+me_warmup_step_jitter_ms = 300 # Jitter for warmup delay
+# Reconnect policy knobs.
+me_reconnect_max_concurrent_per_dc = 1 # Parallel reconnects per DC - EXPERIMENTAL! UNSTABLE!
+me_reconnect_backoff_base_ms = 500 # Backoff start
+me_reconnect_backoff_cap_ms = 30000 # Backoff cap
+me_reconnect_fast_retry_count = 11 # Quick retries before backoff
[network]
# Enable/disable families; ipv6 = true/false/auto(None)
@@ -50,10 +82,13 @@ show = ["hello"] # Users to show in the startup log (tg:// links)
# === Timeouts (in seconds) ===
[timeouts]
-client_handshake = 15
+client_handshake = 30
tg_connect = 10
client_keepalive = 60
client_ack = 300
+# Quick ME reconnects for single-address DCs (count and per-attempt timeout, ms).
+me_one_retry = 12
+me_one_timeout_ms = 1200
# === Anti-Censorship & Masking ===
[censorship]
diff --git a/src/config/defaults.rs b/src/config/defaults.rs
index 3f8254c..19269a2 100644
--- a/src/config/defaults.rs
+++ b/src/config/defaults.rs
@@ -74,6 +74,34 @@ pub(crate) fn default_unknown_dc_log_path() -> Option {
Some("unknown-dc.txt".to_string())
}
+pub(crate) fn default_pool_size() -> usize {
+ 2
+}
+
+pub(crate) fn default_keepalive_interval() -> u64 {
+ 25
+}
+
+pub(crate) fn default_keepalive_jitter() -> u64 {
+ 5
+}
+
+pub(crate) fn default_warmup_step_delay_ms() -> u64 {
+ 500
+}
+
+pub(crate) fn default_warmup_step_jitter_ms() -> u64 {
+ 300
+}
+
+pub(crate) fn default_reconnect_backoff_base_ms() -> u64 {
+ 500
+}
+
+pub(crate) fn default_reconnect_backoff_cap_ms() -> u64 {
+ 30_000
+}
+
// Custom deserializer helpers
#[derive(Deserialize)]
diff --git a/src/config/load.rs b/src/config/load.rs
index 512b734..a2fc19b 100644
--- a/src/config/load.rs
+++ b/src/config/load.rs
@@ -11,6 +11,32 @@ use crate::error::{ProxyError, Result};
use super::defaults::*;
use super::types::*;
+fn preprocess_includes(content: &str, base_dir: &Path, depth: u8) -> Result {
+ if depth > 10 {
+ return Err(ProxyError::Config("Include depth > 10".into()));
+ }
+ let mut output = String::with_capacity(content.len());
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("include") {
+ let rest = rest.trim();
+ if let Some(rest) = rest.strip_prefix('=') {
+ let path_str = rest.trim().trim_matches('"');
+ let resolved = base_dir.join(path_str);
+ let included = std::fs::read_to_string(&resolved)
+ .map_err(|e| ProxyError::Config(e.to_string()))?;
+ let included_dir = resolved.parent().unwrap_or(base_dir);
+ output.push_str(&preprocess_includes(&included, included_dir, depth + 1)?);
+ output.push('\n');
+ continue;
+ }
+ }
+ output.push_str(line);
+ output.push('\n');
+ }
+ Ok(output)
+}
+
fn validate_network_cfg(net: &mut NetworkConfig) -> Result<()> {
if !net.ipv4 && matches!(net.ipv6, Some(false)) {
return Err(ProxyError::Config(
@@ -84,10 +110,12 @@ pub struct ProxyConfig {
impl ProxyConfig {
pub fn load>(path: P) -> Result {
let content =
- std::fs::read_to_string(path).map_err(|e| ProxyError::Config(e.to_string()))?;
+ std::fs::read_to_string(&path).map_err(|e| ProxyError::Config(e.to_string()))?;
+ let base_dir = path.as_ref().parent().unwrap_or(Path::new("."));
+ let processed = preprocess_includes(&content, base_dir, 0)?;
let mut config: ProxyConfig =
- toml::from_str(&content).map_err(|e| ProxyError::Config(e.to_string()))?;
+ toml::from_str(&processed).map_err(|e| ProxyError::Config(e.to_string()))?;
// Validate secrets.
for (user, secret) in &config.access.users {
@@ -151,8 +179,10 @@ impl ProxyConfig {
validate_network_cfg(&mut config.network)?;
- // Random fake_cert_len.
- config.censorship.fake_cert_len = rand::rng().gen_range(1024..4096);
+ // Random fake_cert_len only when default is in use.
+ if config.censorship.fake_cert_len == default_fake_cert_len() {
+ config.censorship.fake_cert_len = rand::rng().gen_range(1024..4096);
+ }
// Resolve listen_tcp: explicit value wins, otherwise auto-detect.
// If unix socket is set β TCP only when listen_addr_ipv4 or listeners are explicitly provided.
@@ -208,6 +238,8 @@ impl ProxyConfig {
upstream_type: UpstreamType::Direct { interface: None },
weight: 1,
enabled: true,
+ scopes: String::new(),
+ selected_scope: String::new(),
});
}
diff --git a/src/config/types.rs b/src/config/types.rs
index ef8fef7..9f6467a 100644
--- a/src/config/types.rs
+++ b/src/config/types.rs
@@ -143,6 +143,62 @@ pub struct GeneralConfig {
#[serde(default)]
pub middle_proxy_nat_stun: Option,
+ /// Optional list of STUN servers for NAT probing fallback.
+ #[serde(default)]
+ pub middle_proxy_nat_stun_servers: Vec,
+
+ /// Desired size of active Middle-Proxy writer pool.
+ #[serde(default = "default_pool_size")]
+ pub middle_proxy_pool_size: usize,
+
+ /// Number of warm standby ME connections kept pre-initialized.
+ #[serde(default)]
+ pub middle_proxy_warm_standby: usize,
+
+ /// Enable ME keepalive padding frames.
+ #[serde(default = "default_true")]
+ pub me_keepalive_enabled: bool,
+
+ /// Keepalive interval in seconds.
+ #[serde(default = "default_keepalive_interval")]
+ pub me_keepalive_interval_secs: u64,
+
+ /// Keepalive jitter in seconds.
+ #[serde(default = "default_keepalive_jitter")]
+ pub me_keepalive_jitter_secs: u64,
+
+ /// Keepalive payload randomized (4 bytes); otherwise zeros.
+ #[serde(default = "default_true")]
+ pub me_keepalive_payload_random: bool,
+
+ /// Enable staggered warmup of extra ME writers.
+ #[serde(default = "default_true")]
+ pub me_warmup_stagger_enabled: bool,
+
+ /// Base delay between warmup connections in ms.
+ #[serde(default = "default_warmup_step_delay_ms")]
+ pub me_warmup_step_delay_ms: u64,
+
+ /// Jitter for warmup delay in ms.
+ #[serde(default = "default_warmup_step_jitter_ms")]
+ pub me_warmup_step_jitter_ms: u64,
+
+ /// Max concurrent reconnect attempts per DC.
+ #[serde(default)]
+ pub me_reconnect_max_concurrent_per_dc: u32,
+
+ /// Base backoff in ms for reconnect.
+ #[serde(default = "default_reconnect_backoff_base_ms")]
+ pub me_reconnect_backoff_base_ms: u64,
+
+ /// Cap backoff in ms for reconnect.
+ #[serde(default = "default_reconnect_backoff_cap_ms")]
+ pub me_reconnect_backoff_cap_ms: u64,
+
+ /// Fast retry attempts before backoff.
+ #[serde(default)]
+ pub me_reconnect_fast_retry_count: u32,
+
/// Ignore STUN/interface IP mismatch (keep using Middle Proxy even if NAT detected).
#[serde(default)]
pub stun_iface_mismatch_ignore: bool,
@@ -175,6 +231,20 @@ impl Default for GeneralConfig {
middle_proxy_nat_ip: None,
middle_proxy_nat_probe: false,
middle_proxy_nat_stun: None,
+ middle_proxy_nat_stun_servers: Vec::new(),
+ middle_proxy_pool_size: default_pool_size(),
+ middle_proxy_warm_standby: 0,
+ me_keepalive_enabled: true,
+ me_keepalive_interval_secs: default_keepalive_interval(),
+ me_keepalive_jitter_secs: default_keepalive_jitter(),
+ me_keepalive_payload_random: true,
+ me_warmup_stagger_enabled: true,
+ me_warmup_step_delay_ms: default_warmup_step_delay_ms(),
+ me_warmup_step_jitter_ms: default_warmup_step_jitter_ms(),
+ me_reconnect_max_concurrent_per_dc: 1,
+ me_reconnect_backoff_base_ms: default_reconnect_backoff_base_ms(),
+ me_reconnect_backoff_cap_ms: default_reconnect_backoff_cap_ms(),
+ me_reconnect_fast_retry_count: 1,
stun_iface_mismatch_ignore: false,
unknown_dc_log_path: default_unknown_dc_log_path(),
log_level: LogLevel::Normal,
@@ -403,6 +473,10 @@ pub struct UpstreamConfig {
pub weight: u16,
#[serde(default = "default_true")]
pub enabled: bool,
+ #[serde(default)]
+ pub scopes: String,
+ #[serde(skip)]
+ pub selected_scope: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/src/crypto/random.rs b/src/crypto/random.rs
index 18862ab..99aa5f3 100644
--- a/src/crypto/random.rs
+++ b/src/crypto/random.rs
@@ -11,6 +11,9 @@ pub struct SecureRandom {
inner: Mutex,
}
+unsafe impl Send for SecureRandom {}
+unsafe impl Sync for SecureRandom {}
+
struct SecureRandomInner {
rng: StdRng,
cipher: AesCtr,
@@ -211,4 +214,4 @@ mod tests {
assert_ne!(shuffled, original);
}
-}
\ No newline at end of file
+}
diff --git a/src/main.rs b/src/main.rs
index 2660213..d542b63 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -92,6 +92,10 @@ fn parse_cli() -> (String, bool, Option) {
eprintln!(" --no-start Don't start the service after install");
std::process::exit(0);
}
+ "--version" | "-V" => {
+ println!("telemt {}", env!("CARGO_PKG_VERSION"));
+ std::process::exit(0);
+ }
s if !s.starts_with('-') => {
config_path = s.to_string();
}
@@ -106,18 +110,20 @@ fn parse_cli() -> (String, bool, Option) {
}
fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
- info!("--- Proxy Links ({}) ---", host);
+ info!(target: "telemt::links", "--- Proxy Links ({}) ---", host);
for user_name in config.general.links.show.resolve_users(&config.access.users) {
if let Some(secret) = config.access.users.get(user_name) {
- info!("User: {}", user_name);
+ info!(target: "telemt::links", "User: {}", user_name);
if config.general.modes.classic {
info!(
+ target: "telemt::links",
" Classic: tg://proxy?server={}&port={}&secret={}",
host, port, secret
);
}
if config.general.modes.secure {
info!(
+ target: "telemt::links",
" DD: tg://proxy?server={}&port={}&secret=dd{}",
host, port, secret
);
@@ -125,15 +131,16 @@ fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
if config.general.modes.tls {
let domain_hex = hex::encode(&config.censorship.tls_domain);
info!(
+ target: "telemt::links",
" EE-TLS: tg://proxy?server={}&port={}&secret=ee{}{}",
host, port, secret, domain_hex
);
}
} else {
- warn!("User '{}' in show_link not found", user_name);
+ warn!(target: "telemt::links", "User '{}' in show_link not found", user_name);
}
}
- info!("------------------------");
+ info!(target: "telemt::links", "------------------------");
}
#[tokio::main]
@@ -317,6 +324,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
config.general.middle_proxy_nat_ip,
config.general.middle_proxy_nat_probe,
config.general.middle_proxy_nat_stun.clone(),
+ config.general.middle_proxy_nat_stun_servers.clone(),
probe.detected_ipv6,
config.timeouts.me_one_retry,
config.timeouts.me_one_timeout_ms,
@@ -325,18 +333,32 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
cfg_v4.default_dc.or(cfg_v6.default_dc),
decision.clone(),
rng.clone(),
+ stats.clone(),
+ config.general.me_keepalive_enabled,
+ config.general.me_keepalive_interval_secs,
+ config.general.me_keepalive_jitter_secs,
+ config.general.me_keepalive_payload_random,
+ config.general.me_warmup_stagger_enabled,
+ config.general.me_warmup_step_delay_ms,
+ config.general.me_warmup_step_jitter_ms,
+ config.general.me_reconnect_max_concurrent_per_dc,
+ config.general.me_reconnect_backoff_base_ms,
+ config.general.me_reconnect_backoff_cap_ms,
+ config.general.me_reconnect_fast_retry_count,
);
- match pool.init(2, &rng).await {
+ let pool_size = config.general.middle_proxy_pool_size.max(1);
+ match pool.init(pool_size, &rng).await {
Ok(()) => {
info!("Middle-End pool initialized successfully");
// Phase 4: Start health monitor
let pool_clone = pool.clone();
let rng_clone = rng.clone();
+ let min_conns = pool_size;
tokio::spawn(async move {
crate::transport::middle_proxy::me_health_monitor(
- pool_clone, rng_clone, 2,
+ pool_clone, rng_clone, min_conns,
)
.await;
});
@@ -740,6 +762,8 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
// Switch to user-configured log level after startup
let runtime_filter = if has_rust_log {
EnvFilter::from_default_env()
+ } else if matches!(effective_log_level, LogLevel::Silent) {
+ EnvFilter::new("warn,telemt::links=info")
} else {
EnvFilter::new(effective_log_level.to_filter_str())
};
diff --git a/src/metrics.rs b/src/metrics.rs
index 24acf30..fa6c680 100644
--- a/src/metrics.rs
+++ b/src/metrics.rs
@@ -91,6 +91,22 @@ fn render_metrics(stats: &Stats) -> String {
let _ = writeln!(out, "# TYPE telemt_handshake_timeouts_total counter");
let _ = writeln!(out, "telemt_handshake_timeouts_total {}", stats.get_handshake_timeouts());
+ let _ = writeln!(out, "# HELP telemt_me_keepalive_sent_total ME keepalive frames sent");
+ let _ = writeln!(out, "# TYPE telemt_me_keepalive_sent_total counter");
+ let _ = writeln!(out, "telemt_me_keepalive_sent_total {}", stats.get_me_keepalive_sent());
+
+ let _ = writeln!(out, "# HELP telemt_me_keepalive_failed_total ME keepalive send failures");
+ let _ = writeln!(out, "# TYPE telemt_me_keepalive_failed_total counter");
+ let _ = writeln!(out, "telemt_me_keepalive_failed_total {}", stats.get_me_keepalive_failed());
+
+ let _ = writeln!(out, "# HELP telemt_me_reconnect_attempts_total ME reconnect attempts");
+ let _ = writeln!(out, "# TYPE telemt_me_reconnect_attempts_total counter");
+ let _ = writeln!(out, "telemt_me_reconnect_attempts_total {}", stats.get_me_reconnect_attempts());
+
+ let _ = writeln!(out, "# HELP telemt_me_reconnect_success_total ME reconnect successes");
+ let _ = writeln!(out, "# TYPE telemt_me_reconnect_success_total counter");
+ let _ = writeln!(out, "telemt_me_reconnect_success_total {}", stats.get_me_reconnect_success());
+
let _ = writeln!(out, "# HELP telemt_user_connections_total Per-user total connections");
let _ = writeln!(out, "# TYPE telemt_user_connections_total counter");
let _ = writeln!(out, "# HELP telemt_user_connections_current Per-user active connections");
diff --git a/src/proxy/direct_relay.rs b/src/proxy/direct_relay.rs
index 537a93e..7b1ac1b 100644
--- a/src/proxy/direct_relay.rs
+++ b/src/proxy/direct_relay.rs
@@ -45,7 +45,7 @@ where
);
let tg_stream = upstream_manager
- .connect(dc_addr, Some(success.dc_idx))
+ .connect(dc_addr, Some(success.dc_idx), user.strip_prefix("scope_").filter(|s| !s.is_empty()))
.await?;
debug!(peer = %success.peer, dc_addr = %dc_addr, "Connected, performing TG handshake");
diff --git a/src/stats/mod.rs b/src/stats/mod.rs
index 2cdcdf9..38318cc 100644
--- a/src/stats/mod.rs
+++ b/src/stats/mod.rs
@@ -19,6 +19,10 @@ pub struct Stats {
connects_all: AtomicU64,
connects_bad: AtomicU64,
handshake_timeouts: AtomicU64,
+ me_keepalive_sent: AtomicU64,
+ me_keepalive_failed: AtomicU64,
+ me_reconnect_attempts: AtomicU64,
+ me_reconnect_success: AtomicU64,
user_stats: DashMap,
start_time: parking_lot::RwLock