diff --git a/.gitignore b/.gitignore
index 6b5f1d5..3a45e41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,5 @@ target
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
-*.rs
-target
-Cargo.lock
-src
+
+proxy-secret
diff --git a/Cargo.toml b/Cargo.toml
index fd1d892..994e11f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "telemt"
-version = "3.0.13"
+version = "3.1.0"
edition = "2024"
[dependencies]
diff --git a/README.md b/README.md
index 8d0c41a..e2a898f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
**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 Chat in Telegram**](https://t.me/telemtrs)
+
## NEWS and EMERGENCY
### ✈️ Telemt 3 is released!
@@ -10,28 +12,18 @@
### 🇷🇺 RU
-#### Драфтинг LTS и текущие улучшения
+#### Релиз 3.0.15 — 25 февраля
-С 21 февраля мы начали подготовку LTS-версии.
+25 февраля мы выпустили версию **3.0.15**
-Мы внимательно анализируем весь доступный фидбек.
-Наша цель — сделать LTS-кандидаты максимально стабильными, тщательно отлаженными и готовыми к long-run и highload production-сценариям.
+Мы предполагаем, что она станет завершающей версией поколения 3.0 и уже сейчас мы рассматриваем её как **LTS-кандидата** для версии **3.1.0**!
----
+После нескольких дней детального анализа особенностей работы Middle-End мы спроектировали и реализовали продуманный режим **ротации ME Writer**. Данный режим позволяет поддерживать стабильно высокую производительность в long-run сценариях без возникновения ошибок, связанных с некорректной конфигурацией прокси
-#### Улучшения от 23 февраля
-
-23 февраля были внесены улучшения производительности в режимах **DC** и **Middle-End (ME)**, с акцентом на обратный канал (путь клиент → DC / ME).
-
-Дополнительно реализован ряд изменений, направленных на повышение устойчивости системы:
-
-- Смягчение сетевой нестабильности
-- Повышение устойчивости к десинхронизации криптографии
-- Снижение дрейфа сессий при неблагоприятных условиях
-- Улучшение обработки ошибок в edge-case транспортных сценариях
+Будем рады вашему фидбеку и предложениям по улучшению — особенно в части **статистики** и **UX**
Релиз:
-[3.0.12](https://github.com/telemt/telemt/releases/tag/3.0.12)
+[3.0.15](https://github.com/telemt/telemt/releases/tag/3.0.15)
---
@@ -48,28 +40,18 @@
### 🇬🇧 EN
-#### LTS Drafting and Ongoing Improvements
+#### Release 3.0.15 — February 25
-Starting February 21, we began drafting the upcoming LTS version.
+On February 25, we released version **3.0.15**
-We are carefully reviewing and analyzing all available feedback.
-The goal is to ensure that LTS candidates are максимально stable, thoroughly debugged, and ready for long-run and high-load production scenarios.
+We expect this to become the final release of the 3.0 generation and at this point, we already see it as a strong **LTS candidate** for the upcoming **3.1.0** release!
----
+After several days of deep analysis of Middle-End behavior, we designed and implemented a well-engineered **ME Writer rotation mode**. This mode enables sustained high throughput in long-run scenarios while preventing proxy misconfiguration errors
-#### February 23 Improvements
-
-On February 23, we introduced performance improvements for both **DC** and **Middle-End (ME)** modes, specifically optimizing the reverse channel (client → DC / ME data path).
-
-Additionally, we implemented a set of robustness enhancements designed to:
-
-- Mitigate network-related instability
-- Improve resilience against cryptographic desynchronization
-- Reduce session drift under adverse conditions
-- Improve error handling in edge-case transport scenarios
+We are looking forward to your feedback and improvement proposals — especially regarding **statistics** and **UX**
Release:
-[3.0.12](https://github.com/telemt/telemt/releases/tag/3.0.12)
+[3.0.15](https://github.com/telemt/telemt/releases/tag/3.0.15)
---
diff --git a/config.full.toml b/config.full.toml
new file mode 100644
index 0000000..44db620
--- /dev/null
+++ b/config.full.toml
@@ -0,0 +1,204 @@
+# Telemt full config with default values.
+# Examples are kept in comments after '#'.
+
+# Top-level legacy field.
+show_link = [] # example: "*" or ["alice", "bob"]
+# default_dc = 2 # example: default DC for unmapped non-standard DCs
+
+[general]
+fast_mode = true
+use_middle_proxy = false
+# ad_tag = "00000000000000000000000000000000" # example
+# proxy_secret_path = "proxy-secret" # example custom path
+# middle_proxy_nat_ip = "203.0.113.10" # example public NAT IP override
+middle_proxy_nat_probe = true
+# middle_proxy_nat_stun = "stun.l.google.com:19302" # example
+# middle_proxy_nat_stun_servers = [] # example: ["stun1.l.google.com:19302", "stun2.l.google.com:19302"]
+middle_proxy_pool_size = 8
+middle_proxy_warm_standby = 16
+me_keepalive_enabled = true
+me_keepalive_interval_secs = 25
+me_keepalive_jitter_secs = 5
+me_keepalive_payload_random = true
+crypto_pending_buffer = 262144
+max_client_frame = 16777216
+desync_all_full = false
+beobachten = true
+beobachten_minutes = 10
+beobachten_flush_secs = 15
+beobachten_file = "cache/beobachten.txt"
+hardswap = true
+me_warmup_stagger_enabled = true
+me_warmup_step_delay_ms = 500
+me_warmup_step_jitter_ms = 300
+me_reconnect_max_concurrent_per_dc = 8
+me_reconnect_backoff_base_ms = 500
+me_reconnect_backoff_cap_ms = 30000
+me_reconnect_fast_retry_count = 12
+stun_iface_mismatch_ignore = false
+unknown_dc_log_path = "unknown-dc.txt" # to disable: set to null
+log_level = "normal" # debug | verbose | normal | silent
+disable_colors = false
+fast_mode_min_tls_record = 0
+update_every = 300
+me_reinit_every_secs = 900
+me_hardswap_warmup_delay_min_ms = 1000
+me_hardswap_warmup_delay_max_ms = 2000
+me_hardswap_warmup_extra_passes = 3
+me_hardswap_warmup_pass_backoff_base_ms = 500
+me_config_stable_snapshots = 2
+me_config_apply_cooldown_secs = 300
+proxy_secret_stable_snapshots = 2
+proxy_secret_rotate_runtime = true
+proxy_secret_len_max = 256
+me_pool_drain_ttl_secs = 90
+me_pool_min_fresh_ratio = 0.8
+me_reinit_drain_timeout_secs = 120
+# Legacy compatibility fields used when update_every is omitted.
+proxy_secret_auto_reload_secs = 3600
+proxy_config_auto_reload_secs = 3600
+ntp_check = true
+ntp_servers = ["pool.ntp.org"] # example: ["pool.ntp.org", "time.cloudflare.com"]
+auto_degradation_enabled = true
+degradation_min_unavailable_dc_groups = 2
+
+[general.modes]
+classic = false
+secure = false
+tls = true
+
+[general.links]
+show ="*" # example: "*" or ["alice", "bob"]
+# public_host = "proxy.example.com" # example explicit host/IP for tg:// links
+# public_port = 443 # example explicit port for tg:// links
+
+[network]
+ipv4 = true
+ipv6 = false # set true to enable IPv6
+prefer = 4 # 4 or 6
+multipath = false
+stun_servers = [
+ "stun.l.google.com:5349",
+ "stun1.l.google.com:3478",
+ "stun.gmx.net:3478",
+ "stun.l.google.com:19302",
+ "stun.1und1.de:3478",
+ "stun1.l.google.com:19302",
+ "stun2.l.google.com:19302",
+ "stun3.l.google.com:19302",
+ "stun4.l.google.com:19302",
+ "stun.services.mozilla.com:3478",
+ "stun.stunprotocol.org:3478",
+ "stun.nextcloud.com:3478",
+ "stun.voip.eutelia.it:3478",
+]
+stun_tcp_fallback = true
+http_ip_detect_urls = ["https://ifconfig.me/ip", "https://api.ipify.org"]
+cache_public_ip_path = "cache/public_ip.txt"
+
+[server]
+port = 443
+listen_addr_ipv4 = "0.0.0.0"
+listen_addr_ipv6 = "::"
+# listen_unix_sock = "/var/run/telemt.sock" # example
+# listen_unix_sock_perm = "0660" # example unix socket mode
+# listen_tcp = true # example explicit override (auto-detected when omitted)
+proxy_protocol = false
+# metrics_port = 9090 # example
+metrics_whitelist = ["127.0.0.1/32", "::1/128"]
+# Example explicit listeners (default: omitted, auto-generated from listen_addr_*):
+# [[server.listeners]]
+# ip = "0.0.0.0"
+# announce = "proxy-v4.example.com"
+# # announce_ip = "203.0.113.10" # deprecated alias
+# proxy_protocol = false
+# reuse_allow = false
+#
+# [[server.listeners]]
+# ip = "::"
+# announce = "proxy-v6.example.com"
+# proxy_protocol = false
+# reuse_allow = false
+
+[timeouts]
+client_handshake = 15
+tg_connect = 10
+client_keepalive = 60
+client_ack = 300
+me_one_retry = 3
+me_one_timeout_ms = 1500
+
+[censorship]
+tls_domain = "petrovich.ru"
+# tls_domains = ["example.com", "cdn.example.net"] # Additional domains for EE links
+mask = true
+# mask_host = "www.google.com" # example, defaults to tls_domain when both mask_host/mask_unix_sock are unset
+# mask_unix_sock = "/var/run/nginx.sock" # example, mutually exclusive with mask_host
+mask_port = 443
+fake_cert_len = 2048 # if tls_emulation=false and default value is used, loader may randomize this value at runtime
+tls_emulation = true
+tls_front_dir = "tlsfront"
+server_hello_delay_min_ms = 0
+server_hello_delay_max_ms = 0
+tls_new_session_tickets = 0
+tls_full_cert_ttl_secs = 90
+alpn_enforce = true
+
+[access]
+replay_check_len = 65536
+replay_window_secs = 1800
+ignore_time_skew = false
+
+[access.users]
+# format: "username" = "32_hex_chars_secret"
+hello = "00000000000000000000000000000000"
+# alice = "11111111111111111111111111111111" # example
+
+[access.user_max_tcp_conns]
+# alice = 100 # example
+
+[access.user_expirations]
+# alice = "2078-01-01T00:00:00Z" # example
+
+[access.user_data_quota]
+# hello = 10737418240 # example bytes
+# alice = 10737418240 # example bytes
+
+[access.user_max_unique_ips]
+# hello = 10 # example
+# alice = 100 # example
+
+# Default behavior if [[upstreams]] is omitted: loader injects one direct upstream.
+# Example explicit upstreams:
+# [[upstreams]]
+# type = "direct"
+# interface = "eth0"
+# bind_addresses = ["192.0.2.10"]
+# weight = 1
+# enabled = true
+# scopes = "*"
+#
+# [[upstreams]]
+# type = "socks4"
+# address = "198.51.100.20:1080"
+# interface = "eth0"
+# user_id = "telemt"
+# weight = 1
+# enabled = true
+# scopes = "*"
+#
+# [[upstreams]]
+# type = "socks5"
+# address = "198.51.100.30:1080"
+# interface = "eth0"
+# username = "proxy-user"
+# password = "proxy-pass"
+# weight = 1
+# enabled = true
+# scopes = "*"
+
+# === DC Address Overrides ===
+# [dc_overrides]
+# "201" = "149.154.175.50:443" # example
+# "202" = ["149.154.167.51:443", "149.154.175.100:443"] # example
+# "203" = "91.105.192.100:443" # loader auto-adds this one when omitted
diff --git a/config.toml b/config.toml
index 375cd7f..b280234 100644
--- a/config.toml
+++ b/config.toml
@@ -1,11 +1,11 @@
+### Telemt Based Config.toml
+# We believe that these settings are sufficient for most scenarios
+# where cutting-egde methods and parameters or special solutions are not needed
+
# === General Settings ===
[general]
-fast_mode = true
-use_middle_proxy = true
+use_middle_proxy = false
# ad_tag = "00000000000000000000000000000000"
-# Path to proxy-secret binary (auto-downloaded if missing).
-proxy_secret_path = "proxy-secret"
-# disable_colors = false # Disable colored output in logs (useful for files/systemd)
# === Log Level ===
# Log level: debug | verbose | normal | silent
@@ -13,47 +13,6 @@ proxy_secret_path = "proxy-secret"
# RUST_LOG env var takes absolute priority over all of these
log_level = "normal"
-# === 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 = 4 # 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
-update_every = 7200 # Resolve the active updater interval for ME infrastructure refresh tasks.
-crypto_pending_buffer = 262144 # Max pending ciphertext buffer per client writer (bytes). Controls FakeTLS backpressure vs throughput.
-max_client_frame = 16777216 # Maximum allowed client MTProto frame size (bytes).
-desync_all_full = false # Emit full crypto-desync forensic logs for every event. When false, full forensic details are emitted once per key window.
-me_reinit_drain_timeout_secs = 300 # Drain timeout in seconds for stale ME writers after endpoint map changes. Set to 0 to keep stale writers draining indefinitely (no force-close).
-auto_degradation_enabled = true # Enable auto-degradation from ME to Direct-DC.
-degradation_min_unavailable_dc_groups = 2 # Minimum unavailable ME DC groups before degrading.
-hardswap = true # Enable C-like hard-swap for ME pool generations. When true, Telemt prewarms a new generation and switches once full coverage is reached.
-me_pool_drain_ttl_secs = 90 # Drain-TTL in seconds for stale ME writers after endpoint map changes. During TTL, stale writers may be used only as fallback for new bindings.
-me_pool_min_fresh_ratio = 0.8 # Minimum desired-DC coverage ratio required before draining stale writers. Range: 0.0..=1.0.
-me_reinit_drain_timeout_secs = 120 # Drain timeout in seconds for stale ME writers after endpoint map changes. Set to 0 to keep stale writers draining indefinitely (no force-close).
-
[general.modes]
classic = false
secure = false
@@ -66,93 +25,24 @@ show = "*"
# public_host = "proxy.example.com" # Host (IP or domain) for tg:// links
# public_port = 443 # Port for tg:// links (default: server.port)
-# === Network Parameters ===
-[network]
-# Enable/disable families: true/false/auto(None)
-ipv4 = true
-ipv6 = false # UNSTABLE WITH ME
-# prefer = 4 or 6
-prefer = 4
-multipath = false # EXPERIMENTAL!
-
# === Server Binding ===
[server]
port = 443
-listen_addr_ipv4 = "0.0.0.0"
-listen_addr_ipv6 = "::"
-# listen_unix_sock = "/var/run/telemt.sock" # Unix socket
-# listen_unix_sock_perm = "0666" # Socket file permissions
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
# metrics_port = 9090
-# metrics_whitelist = ["127.0.0.1", "::1"]
+# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"]
# Listen on multiple interfaces/IPs - IPv4
[[server.listeners]]
ip = "0.0.0.0"
-# Listen on multiple interfaces/IPs - IPv6
-[[server.listeners]]
-ip = "::"
-
-# === Timeouts (in seconds) ===
-[timeouts]
-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]
tls_domain = "petrovich.ru"
-# tls_domains = ["example.com", "cdn.example.net"] # Additional domains for EE links
mask = true
-mask_port = 443
-# mask_host = "petrovich.ru" # Defaults to tls_domain if not set
-# mask_unix_sock = "/var/run/nginx.sock" # Unix socket (mutually exclusive with mask_host)
-fake_cert_len = 2048
-# tls_emulation = false # Fetch real cert lengths and emulate TLS records
-# tls_front_dir = "tlsfront" # Cache directory for TLS emulation
-
-# === Access Control & Users ===
-[access]
-replay_check_len = 65536
-replay_window_secs = 1800
-ignore_time_skew = false
+tls_emulation = true # Fetch real cert lengths and emulate TLS records
+tls_front_dir = "tlsfront" # Cache directory for TLS emulation
[access.users]
# format: "username" = "32_hex_chars_secret"
hello = "00000000000000000000000000000000"
-
-# [access.user_max_tcp_conns]
-# hello = 50
-
-# [access.user_max_unique_ips]
-# hello = 5
-
-# [access.user_data_quota]
-# hello = 1073741824 # 1 GB
-
-# [access.user_expirations]
-# format: username = "[year]-[month]-[day]T[hour]:[minute]:[second]Z" UTC
-# hello = "2027-01-01T00:00:00Z"
-
-# === Upstreams & Routing ===
-[[upstreams]]
-type = "direct"
-enabled = true
-weight = 10
-# interface = "192.168.1.100" # Bind outgoing to specific IP or iface name
-# bind_addresses = ["192.168.1.100"] # List for round-robin binding (family must match target)
-
-# [[upstreams]]
-# type = "socks5"
-# address = "127.0.0.1:1080"
-# enabled = false
-# weight = 1
-
-# === DC Address Overrides ===
-# [dc_overrides]
-# "203" = "91.105.192.100:443"
diff --git a/proxy-secret b/proxy-secret
deleted file mode 100644
index ef77163..0000000
--- a/proxy-secret
+++ /dev/null
@@ -1 +0,0 @@
-ʖxHl~,D0d]UJUAM'!FnRZD>ϳF>yZfa*ߜڋ
o8zM:dq>\3w}n\TĐy'VIil&]
\ No newline at end of file
diff --git a/src/config/defaults.rs b/src/config/defaults.rs
index a0443fc..51abe65 100644
--- a/src/config/defaults.rs
+++ b/src/config/defaults.rs
@@ -3,6 +3,15 @@ use ipnetwork::IpNetwork;
use serde::Deserialize;
// Helper defaults kept private to the config module.
+const DEFAULT_NETWORK_IPV6: Option = Some(false);
+const DEFAULT_STUN_TCP_FALLBACK: bool = true;
+const DEFAULT_MIDDLE_PROXY_WARM_STANDBY: usize = 16;
+const DEFAULT_ME_RECONNECT_MAX_CONCURRENT_PER_DC: u32 = 8;
+const DEFAULT_ME_RECONNECT_FAST_RETRY_COUNT: u32 = 12;
+const DEFAULT_LISTEN_ADDR_IPV6: &str = "::";
+const DEFAULT_ACCESS_USER: &str = "default";
+const DEFAULT_ACCESS_SECRET: &str = "00000000000000000000000000000000";
+
pub(crate) fn default_true() -> bool {
true
}
@@ -77,6 +86,14 @@ pub(crate) fn default_prefer_4() -> u8 {
4
}
+pub(crate) fn default_network_ipv6() -> Option {
+ DEFAULT_NETWORK_IPV6
+}
+
+pub(crate) fn default_stun_tcp_fallback() -> bool {
+ DEFAULT_STUN_TCP_FALLBACK
+}
+
pub(crate) fn default_unknown_dc_log_path() -> Option {
Some("unknown-dc.txt".to_string())
}
@@ -85,6 +102,10 @@ pub(crate) fn default_pool_size() -> usize {
8
}
+pub(crate) fn default_middle_proxy_warm_standby() -> usize {
+ DEFAULT_MIDDLE_PROXY_WARM_STANDBY
+}
+
pub(crate) fn default_keepalive_interval() -> u64 {
25
}
@@ -109,6 +130,14 @@ pub(crate) fn default_reconnect_backoff_cap_ms() -> u64 {
30_000
}
+pub(crate) fn default_me_reconnect_max_concurrent_per_dc() -> u32 {
+ DEFAULT_ME_RECONNECT_MAX_CONCURRENT_PER_DC
+}
+
+pub(crate) fn default_me_reconnect_fast_retry_count() -> u32 {
+ DEFAULT_ME_RECONNECT_FAST_RETRY_COUNT
+}
+
pub(crate) fn default_crypto_pending_buffer() -> usize {
256 * 1024
}
@@ -121,6 +150,18 @@ pub(crate) fn default_desync_all_full() -> bool {
false
}
+pub(crate) fn default_beobachten_minutes() -> u64 {
+ 10
+}
+
+pub(crate) fn default_beobachten_flush_secs() -> u64 {
+ 15
+}
+
+pub(crate) fn default_beobachten_file() -> String {
+ "cache/beobachten.txt".to_string()
+}
+
pub(crate) fn default_tls_new_session_tickets() -> u8 {
0
}
@@ -171,15 +212,59 @@ pub(crate) fn default_cache_public_ip_path() -> String {
}
pub(crate) fn default_proxy_secret_reload_secs() -> u64 {
- 1 * 60 * 60
+ 60 * 60
}
pub(crate) fn default_proxy_config_reload_secs() -> u64 {
- 1 * 60 * 60
+ 60 * 60
}
pub(crate) fn default_update_every_secs() -> u64 {
- 1 * 30 * 60
+ 5 * 60
+}
+
+pub(crate) fn default_update_every() -> Option {
+ Some(default_update_every_secs())
+}
+
+pub(crate) fn default_me_reinit_every_secs() -> u64 {
+ 15 * 60
+}
+
+pub(crate) fn default_me_hardswap_warmup_delay_min_ms() -> u64 {
+ 1000
+}
+
+pub(crate) fn default_me_hardswap_warmup_delay_max_ms() -> u64 {
+ 2000
+}
+
+pub(crate) fn default_me_hardswap_warmup_extra_passes() -> u8 {
+ 3
+}
+
+pub(crate) fn default_me_hardswap_warmup_pass_backoff_base_ms() -> u64 {
+ 500
+}
+
+pub(crate) fn default_me_config_stable_snapshots() -> u8 {
+ 2
+}
+
+pub(crate) fn default_me_config_apply_cooldown_secs() -> u64 {
+ 300
+}
+
+pub(crate) fn default_proxy_secret_stable_snapshots() -> u8 {
+ 2
+}
+
+pub(crate) fn default_proxy_secret_rotate_runtime() -> bool {
+ true
+}
+
+pub(crate) fn default_proxy_secret_len_max() -> usize {
+ 256
}
pub(crate) fn default_me_reinit_drain_timeout_secs() -> u64 {
@@ -214,6 +299,17 @@ pub(crate) fn default_degradation_min_unavailable_dc_groups() -> u8 {
2
}
+pub(crate) fn default_listen_addr_ipv6() -> String {
+ DEFAULT_LISTEN_ADDR_IPV6.to_string()
+}
+
+pub(crate) fn default_access_users() -> HashMap {
+ HashMap::from([(
+ DEFAULT_ACCESS_USER.to_string(),
+ DEFAULT_ACCESS_SECRET.to_string(),
+ )])
+}
+
// Custom deserializer helpers
#[derive(Deserialize)]
diff --git a/src/config/load.rs b/src/config/load.rs
index dce5fbc..be6759e 100644
--- a/src/config/load.rs
+++ b/src/config/load.rs
@@ -147,6 +147,74 @@ impl ProxyConfig {
}
}
+ if config.general.me_reinit_every_secs == 0 {
+ return Err(ProxyError::Config(
+ "general.me_reinit_every_secs must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.beobachten_minutes == 0 {
+ return Err(ProxyError::Config(
+ "general.beobachten_minutes must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.beobachten_flush_secs == 0 {
+ return Err(ProxyError::Config(
+ "general.beobachten_flush_secs must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.beobachten_file.trim().is_empty() {
+ return Err(ProxyError::Config(
+ "general.beobachten_file cannot be empty".to_string(),
+ ));
+ }
+
+ if config.general.me_hardswap_warmup_delay_max_ms == 0 {
+ return Err(ProxyError::Config(
+ "general.me_hardswap_warmup_delay_max_ms must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.me_hardswap_warmup_delay_min_ms
+ > config.general.me_hardswap_warmup_delay_max_ms
+ {
+ return Err(ProxyError::Config(
+ "general.me_hardswap_warmup_delay_min_ms must be <= general.me_hardswap_warmup_delay_max_ms".to_string(),
+ ));
+ }
+
+ if config.general.me_hardswap_warmup_extra_passes > 10 {
+ return Err(ProxyError::Config(
+ "general.me_hardswap_warmup_extra_passes must be within [0, 10]".to_string(),
+ ));
+ }
+
+ if config.general.me_hardswap_warmup_pass_backoff_base_ms == 0 {
+ return Err(ProxyError::Config(
+ "general.me_hardswap_warmup_pass_backoff_base_ms must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.me_config_stable_snapshots == 0 {
+ return Err(ProxyError::Config(
+ "general.me_config_stable_snapshots must be > 0".to_string(),
+ ));
+ }
+
+ if config.general.proxy_secret_stable_snapshots == 0 {
+ return Err(ProxyError::Config(
+ "general.proxy_secret_stable_snapshots must be > 0".to_string(),
+ ));
+ }
+
+ if !(32..=4096).contains(&config.general.proxy_secret_len_max) {
+ return Err(ProxyError::Config(
+ "general.proxy_secret_len_max must be within [32, 4096]".to_string(),
+ ));
+ }
+
if !(0.0..=1.0).contains(&config.general.me_pool_min_fresh_ratio) {
return Err(ProxyError::Config(
"general.me_pool_min_fresh_ratio must be within [0.0, 1.0]".to_string(),
@@ -278,23 +346,25 @@ impl ProxyConfig {
reuse_allow: false,
});
}
- if let Some(ipv6_str) = &config.server.listen_addr_ipv6 {
- if let Ok(ipv6) = ipv6_str.parse::() {
- config.server.listeners.push(ListenerConfig {
- ip: ipv6,
- announce: None,
- announce_ip: None,
- proxy_protocol: None,
- reuse_allow: false,
- });
- }
+ if let Some(ipv6_str) = &config.server.listen_addr_ipv6
+ && let Ok(ipv6) = ipv6_str.parse::()
+ {
+ config.server.listeners.push(ListenerConfig {
+ ip: ipv6,
+ announce: None,
+ announce_ip: None,
+ proxy_protocol: None,
+ reuse_allow: false,
+ });
}
}
// Migration: announce_ip → announce for each listener.
for listener in &mut config.server.listeners {
- if listener.announce.is_none() && listener.announce_ip.is_some() {
- listener.announce = Some(listener.announce_ip.unwrap().to_string());
+ if listener.announce.is_none()
+ && let Some(ip) = listener.announce_ip.take()
+ {
+ listener.announce = Some(ip.to_string());
}
}
@@ -357,6 +427,55 @@ impl ProxyConfig {
mod tests {
use super::*;
+ #[test]
+ fn serde_defaults_remain_unchanged_for_present_sections() {
+ let toml = r#"
+ [network]
+ [general]
+ [server]
+ [access]
+ "#;
+ let cfg: ProxyConfig = toml::from_str(toml).unwrap();
+
+ assert_eq!(cfg.network.ipv6, None);
+ assert!(!cfg.network.stun_tcp_fallback);
+ assert_eq!(cfg.general.middle_proxy_warm_standby, 0);
+ assert_eq!(cfg.general.me_reconnect_max_concurrent_per_dc, 0);
+ assert_eq!(cfg.general.me_reconnect_fast_retry_count, 0);
+ assert_eq!(cfg.general.update_every, None);
+ assert_eq!(cfg.server.listen_addr_ipv4, None);
+ assert_eq!(cfg.server.listen_addr_ipv6, None);
+ assert!(cfg.access.users.is_empty());
+ }
+
+ #[test]
+ fn impl_defaults_are_sourced_from_default_helpers() {
+ let network = NetworkConfig::default();
+ assert_eq!(network.ipv6, default_network_ipv6());
+ assert_eq!(network.stun_tcp_fallback, default_stun_tcp_fallback());
+
+ let general = GeneralConfig::default();
+ assert_eq!(
+ general.middle_proxy_warm_standby,
+ default_middle_proxy_warm_standby()
+ );
+ assert_eq!(
+ general.me_reconnect_max_concurrent_per_dc,
+ default_me_reconnect_max_concurrent_per_dc()
+ );
+ assert_eq!(
+ general.me_reconnect_fast_retry_count,
+ default_me_reconnect_fast_retry_count()
+ );
+ assert_eq!(general.update_every, default_update_every());
+
+ let server = ServerConfig::default();
+ assert_eq!(server.listen_addr_ipv6, Some(default_listen_addr_ipv6()));
+
+ let access = AccessConfig::default();
+ assert_eq!(access.users, default_access_users());
+ }
+
#[test]
fn dc_overrides_allow_string_and_array() {
let toml = r#"
@@ -460,6 +579,221 @@ mod tests {
let _ = std::fs::remove_file(path);
}
+ #[test]
+ fn me_reinit_every_default_is_set() {
+ let toml = r#"
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_reinit_every_default_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let cfg = ProxyConfig::load(&path).unwrap();
+ assert_eq!(
+ cfg.general.me_reinit_every_secs,
+ default_me_reinit_every_secs()
+ );
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_reinit_every_zero_is_rejected() {
+ let toml = r#"
+ [general]
+ me_reinit_every_secs = 0
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_reinit_every_zero_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.me_reinit_every_secs must be > 0"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_hardswap_warmup_defaults_are_set() {
+ let toml = r#"
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_hardswap_warmup_defaults_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let cfg = ProxyConfig::load(&path).unwrap();
+ assert_eq!(
+ cfg.general.me_hardswap_warmup_delay_min_ms,
+ default_me_hardswap_warmup_delay_min_ms()
+ );
+ assert_eq!(
+ cfg.general.me_hardswap_warmup_delay_max_ms,
+ default_me_hardswap_warmup_delay_max_ms()
+ );
+ assert_eq!(
+ cfg.general.me_hardswap_warmup_extra_passes,
+ default_me_hardswap_warmup_extra_passes()
+ );
+ assert_eq!(
+ cfg.general.me_hardswap_warmup_pass_backoff_base_ms,
+ default_me_hardswap_warmup_pass_backoff_base_ms()
+ );
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_hardswap_warmup_delay_range_is_validated() {
+ let toml = r#"
+ [general]
+ me_hardswap_warmup_delay_min_ms = 2001
+ me_hardswap_warmup_delay_max_ms = 2000
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_hardswap_warmup_delay_range_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains(
+ "general.me_hardswap_warmup_delay_min_ms must be <= general.me_hardswap_warmup_delay_max_ms"
+ ));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_hardswap_warmup_delay_max_zero_is_rejected() {
+ let toml = r#"
+ [general]
+ me_hardswap_warmup_delay_max_ms = 0
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_hardswap_warmup_delay_max_zero_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.me_hardswap_warmup_delay_max_ms must be > 0"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_hardswap_warmup_extra_passes_out_of_range_is_rejected() {
+ let toml = r#"
+ [general]
+ me_hardswap_warmup_extra_passes = 11
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_hardswap_warmup_extra_passes_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.me_hardswap_warmup_extra_passes must be within [0, 10]"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_hardswap_warmup_pass_backoff_zero_is_rejected() {
+ let toml = r#"
+ [general]
+ me_hardswap_warmup_pass_backoff_base_ms = 0
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_hardswap_warmup_backoff_zero_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.me_hardswap_warmup_pass_backoff_base_ms must be > 0"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn me_config_stable_snapshots_zero_is_rejected() {
+ let toml = r#"
+ [general]
+ me_config_stable_snapshots = 0
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_me_config_stable_snapshots_zero_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.me_config_stable_snapshots must be > 0"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn proxy_secret_stable_snapshots_zero_is_rejected() {
+ let toml = r#"
+ [general]
+ proxy_secret_stable_snapshots = 0
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_proxy_secret_stable_snapshots_zero_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.proxy_secret_stable_snapshots must be > 0"));
+ let _ = std::fs::remove_file(path);
+ }
+
+ #[test]
+ fn proxy_secret_len_max_out_of_range_is_rejected() {
+ let toml = r#"
+ [general]
+ proxy_secret_len_max = 16
+
+ [censorship]
+ tls_domain = "example.com"
+
+ [access.users]
+ user = "00000000000000000000000000000000"
+ "#;
+ let dir = std::env::temp_dir();
+ let path = dir.join("telemt_proxy_secret_len_max_out_of_range_test.toml");
+ std::fs::write(&path, toml).unwrap();
+ let err = ProxyConfig::load(&path).unwrap_err().to_string();
+ assert!(err.contains("general.proxy_secret_len_max must be within [32, 4096]"));
+ let _ = std::fs::remove_file(path);
+ }
+
#[test]
fn me_pool_min_fresh_ratio_out_of_range_is_rejected() {
let toml = r#"
diff --git a/src/config/types.rs b/src/config/types.rs
index 8d573df..1302a97 100644
--- a/src/config/types.rs
+++ b/src/config/types.rs
@@ -76,7 +76,7 @@ impl Default for ProxyModes {
Self {
classic: false,
secure: false,
- tls: true,
+ tls: default_true(),
}
}
}
@@ -117,12 +117,12 @@ pub struct NetworkConfig {
impl Default for NetworkConfig {
fn default() -> Self {
Self {
- ipv4: true,
- ipv6: Some(false),
- prefer: 4,
+ ipv4: default_true(),
+ ipv6: default_network_ipv6(),
+ prefer: default_prefer_4(),
multipath: false,
stun_servers: default_stun_servers(),
- stun_tcp_fallback: true,
+ stun_tcp_fallback: default_stun_tcp_fallback(),
http_ip_detect_urls: default_http_ip_detect_urls(),
cache_public_ip_path: default_cache_public_ip_path(),
}
@@ -206,6 +206,22 @@ pub struct GeneralConfig {
#[serde(default = "default_desync_all_full")]
pub desync_all_full: bool,
+ /// Enable per-IP forensic observation buckets for scanners and handshake failures.
+ #[serde(default)]
+ pub beobachten: bool,
+
+ /// Observation retention window in minutes for per-IP forensic buckets.
+ #[serde(default = "default_beobachten_minutes")]
+ pub beobachten_minutes: u64,
+
+ /// Snapshot flush interval in seconds for beob output file.
+ #[serde(default = "default_beobachten_flush_secs")]
+ pub beobachten_flush_secs: u64,
+
+ /// Snapshot file path for beob output.
+ #[serde(default = "default_beobachten_file")]
+ pub beobachten_file: String,
+
/// Enable C-like hard-swap for ME pool generations.
/// When true, Telemt prewarms a new generation and switches once full coverage is reached.
#[serde(default = "default_hardswap")]
@@ -267,6 +283,46 @@ pub struct GeneralConfig {
#[serde(default)]
pub update_every: Option,
+ /// Periodic ME pool reinitialization interval in seconds.
+ #[serde(default = "default_me_reinit_every_secs")]
+ pub me_reinit_every_secs: u64,
+
+ /// Minimum delay in ms between hardswap warmup connect attempts.
+ #[serde(default = "default_me_hardswap_warmup_delay_min_ms")]
+ pub me_hardswap_warmup_delay_min_ms: u64,
+
+ /// Maximum delay in ms between hardswap warmup connect attempts.
+ #[serde(default = "default_me_hardswap_warmup_delay_max_ms")]
+ pub me_hardswap_warmup_delay_max_ms: u64,
+
+ /// Additional warmup passes in the same hardswap cycle after the base pass.
+ #[serde(default = "default_me_hardswap_warmup_extra_passes")]
+ pub me_hardswap_warmup_extra_passes: u8,
+
+ /// Base backoff in ms between hardswap warmup passes when floor is still incomplete.
+ #[serde(default = "default_me_hardswap_warmup_pass_backoff_base_ms")]
+ pub me_hardswap_warmup_pass_backoff_base_ms: u64,
+
+ /// Number of identical getProxyConfig snapshots required before applying ME map updates.
+ #[serde(default = "default_me_config_stable_snapshots")]
+ pub me_config_stable_snapshots: u8,
+
+ /// Cooldown in seconds between applied ME map updates.
+ #[serde(default = "default_me_config_apply_cooldown_secs")]
+ pub me_config_apply_cooldown_secs: u64,
+
+ /// Number of identical getProxySecret snapshots required before runtime secret rotation.
+ #[serde(default = "default_proxy_secret_stable_snapshots")]
+ pub proxy_secret_stable_snapshots: u8,
+
+ /// Enable runtime proxy-secret rotation from getProxySecret.
+ #[serde(default = "default_proxy_secret_rotate_runtime")]
+ pub proxy_secret_rotate_runtime: bool,
+
+ /// Maximum allowed proxy-secret length in bytes for startup and runtime refresh.
+ #[serde(default = "default_proxy_secret_len_max")]
+ pub proxy_secret_len_max: usize,
+
/// Drain-TTL in seconds for stale ME writers after endpoint map changes.
/// During TTL, stale writers may be used only as fallback for new bindings.
#[serde(default = "default_me_pool_drain_ttl_secs")]
@@ -314,27 +370,27 @@ impl Default for GeneralConfig {
Self {
modes: ProxyModes::default(),
prefer_ipv6: false,
- fast_mode: true,
+ fast_mode: default_true(),
use_middle_proxy: false,
ad_tag: None,
proxy_secret_path: None,
middle_proxy_nat_ip: None,
- middle_proxy_nat_probe: false,
+ middle_proxy_nat_probe: true,
middle_proxy_nat_stun: None,
middle_proxy_nat_stun_servers: Vec::new(),
middle_proxy_pool_size: default_pool_size(),
- middle_proxy_warm_standby: 16,
- me_keepalive_enabled: true,
+ middle_proxy_warm_standby: default_middle_proxy_warm_standby(),
+ me_keepalive_enabled: default_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_keepalive_payload_random: default_true(),
+ me_warmup_stagger_enabled: default_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: 8,
+ me_reconnect_max_concurrent_per_dc: default_me_reconnect_max_concurrent_per_dc(),
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: 8,
+ me_reconnect_fast_retry_count: default_me_reconnect_fast_retry_count(),
stun_iface_mismatch_ignore: false,
unknown_dc_log_path: default_unknown_dc_log_path(),
log_level: LogLevel::Normal,
@@ -343,9 +399,23 @@ impl Default for GeneralConfig {
crypto_pending_buffer: default_crypto_pending_buffer(),
max_client_frame: default_max_client_frame(),
desync_all_full: default_desync_all_full(),
+ beobachten: true,
+ beobachten_minutes: default_beobachten_minutes(),
+ beobachten_flush_secs: default_beobachten_flush_secs(),
+ beobachten_file: default_beobachten_file(),
hardswap: default_hardswap(),
fast_mode_min_tls_record: default_fast_mode_min_tls_record(),
- update_every: Some(default_update_every_secs()),
+ update_every: default_update_every(),
+ me_reinit_every_secs: default_me_reinit_every_secs(),
+ me_hardswap_warmup_delay_min_ms: default_me_hardswap_warmup_delay_min_ms(),
+ me_hardswap_warmup_delay_max_ms: default_me_hardswap_warmup_delay_max_ms(),
+ me_hardswap_warmup_extra_passes: default_me_hardswap_warmup_extra_passes(),
+ me_hardswap_warmup_pass_backoff_base_ms: default_me_hardswap_warmup_pass_backoff_base_ms(),
+ me_config_stable_snapshots: default_me_config_stable_snapshots(),
+ me_config_apply_cooldown_secs: default_me_config_apply_cooldown_secs(),
+ proxy_secret_stable_snapshots: default_proxy_secret_stable_snapshots(),
+ proxy_secret_rotate_runtime: default_proxy_secret_rotate_runtime(),
+ proxy_secret_len_max: default_proxy_secret_len_max(),
me_pool_drain_ttl_secs: default_me_pool_drain_ttl_secs(),
me_pool_min_fresh_ratio: default_me_pool_min_fresh_ratio(),
me_reinit_drain_timeout_secs: default_me_reinit_drain_timeout_secs(),
@@ -353,7 +423,7 @@ impl Default for GeneralConfig {
proxy_config_auto_reload_secs: default_proxy_config_reload_secs(),
ntp_check: default_ntp_check(),
ntp_servers: default_ntp_servers(),
- auto_degradation_enabled: true,
+ auto_degradation_enabled: default_true(),
degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(),
}
}
@@ -367,6 +437,11 @@ impl GeneralConfig {
.unwrap_or_else(|| self.proxy_secret_auto_reload_secs.min(self.proxy_config_auto_reload_secs))
}
+ /// Resolve periodic zero-downtime reinit interval for ME writers.
+ pub fn effective_me_reinit_every_secs(&self) -> u64 {
+ self.me_reinit_every_secs
+ }
+
/// Resolve force-close timeout for stale writers.
/// `me_reinit_drain_timeout_secs` remains backward-compatible alias.
pub fn effective_me_pool_force_close_secs(&self) -> u64 {
@@ -435,7 +510,7 @@ impl Default for ServerConfig {
Self {
port: default_port(),
listen_addr_ipv4: Some(default_listen_addr()),
- listen_addr_ipv6: Some("::".to_string()),
+ listen_addr_ipv6: Some(default_listen_addr_ipv6()),
listen_unix_sock: None,
listen_unix_sock_perm: None,
listen_tcp: None,
@@ -543,12 +618,12 @@ impl Default for AntiCensorshipConfig {
Self {
tls_domain: default_tls_domain(),
tls_domains: Vec::new(),
- mask: true,
+ mask: default_true(),
mask_host: None,
mask_port: default_mask_port(),
mask_unix_sock: None,
fake_cert_len: default_fake_cert_len(),
- tls_emulation: false,
+ tls_emulation: true,
tls_front_dir: default_tls_front_dir(),
server_hello_delay_min_ms: default_server_hello_delay_min_ms(),
server_hello_delay_max_ms: default_server_hello_delay_max_ms(),
@@ -588,13 +663,8 @@ pub struct AccessConfig {
impl Default for AccessConfig {
fn default() -> Self {
- let mut users = HashMap::new();
- users.insert(
- "default".to_string(),
- "00000000000000000000000000000000".to_string(),
- );
Self {
- users,
+ users: default_access_users(),
user_max_tcp_conns: HashMap::new(),
user_expirations: HashMap::new(),
user_data_quota: HashMap::new(),
@@ -677,9 +747,10 @@ pub struct ListenerConfig {
/// - `show_link = "*"` — show links for all users
/// - `show_link = ["a", "b"]` — show links for specific users
/// - omitted — show no links (default)
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Default)]
pub enum ShowLink {
/// Don't show any links (default when omitted).
+ #[default]
None,
/// Show links for all configured users.
All,
@@ -687,12 +758,6 @@ pub enum ShowLink {
Specific(Vec),
}
-impl Default for ShowLink {
- fn default() -> Self {
- ShowLink::None
- }
-}
-
impl ShowLink {
/// Returns true if no links should be shown.
pub fn is_empty(&self) -> bool {
diff --git a/src/crypto/aes.rs b/src/crypto/aes.rs
index 674e4cb..deda730 100644
--- a/src/crypto/aes.rs
+++ b/src/crypto/aes.rs
@@ -23,13 +23,13 @@ type Aes256Ctr = Ctr128BE;
// ============= AES-256-CTR =============
/// AES-256-CTR encryptor/decryptor
-///
+///
/// CTR mode is symmetric — encryption and decryption are the same operation.
///
/// **Zeroize note:** The inner `Aes256Ctr` cipher state (expanded key schedule
-/// + counter) is opaque and cannot be zeroized. If you need to protect key
-/// material, zeroize the `[u8; 32]` key and `u128` IV at the call site
-/// before dropping them.
+/// + counter) is opaque and cannot be zeroized. If you need to protect key
+/// material, zeroize the `[u8; 32]` key and `u128` IV at the call site
+/// before dropping them.
pub struct AesCtr {
cipher: Aes256Ctr,
}
@@ -149,7 +149,7 @@ impl AesCbc {
///
/// CBC Encryption: C[i] = AES_Encrypt(P[i] XOR C[i-1]), where C[-1] = IV
pub fn encrypt(&self, data: &[u8]) -> Result> {
- if data.len() % Self::BLOCK_SIZE != 0 {
+ if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
return Err(ProxyError::Crypto(
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
));
@@ -180,7 +180,7 @@ impl AesCbc {
///
/// CBC Decryption: P[i] = AES_Decrypt(C[i]) XOR C[i-1], where C[-1] = IV
pub fn decrypt(&self, data: &[u8]) -> Result> {
- if data.len() % Self::BLOCK_SIZE != 0 {
+ if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
return Err(ProxyError::Crypto(
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
));
@@ -209,7 +209,7 @@ impl AesCbc {
/// Encrypt data in-place
pub fn encrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
- if data.len() % Self::BLOCK_SIZE != 0 {
+ if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
return Err(ProxyError::Crypto(
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
));
@@ -242,7 +242,7 @@ impl AesCbc {
/// Decrypt data in-place
pub fn decrypt_in_place(&self, data: &mut [u8]) -> Result<()> {
- if data.len() % Self::BLOCK_SIZE != 0 {
+ if !data.len().is_multiple_of(Self::BLOCK_SIZE) {
return Err(ProxyError::Crypto(
format!("CBC data must be aligned to 16 bytes, got {}", data.len())
));
diff --git a/src/crypto/hash.rs b/src/crypto/hash.rs
index d3f6f55..fa3e441 100644
--- a/src/crypto/hash.rs
+++ b/src/crypto/hash.rs
@@ -64,6 +64,7 @@ pub fn crc32c(data: &[u8]) -> u32 {
///
/// Returned buffer layout (IPv4):
/// nonce_srv | nonce_clt | clt_ts | srv_ip | clt_port | purpose | clt_ip | srv_port | secret | nonce_srv | [clt_v6 | srv_v6] | nonce_clt
+#[allow(clippy::too_many_arguments)]
pub fn build_middleproxy_prekey(
nonce_srv: &[u8; 16],
nonce_clt: &[u8; 16],
@@ -108,6 +109,7 @@ pub fn build_middleproxy_prekey(
/// Uses MD5 + SHA-1 as mandated by the Telegram Middle Proxy protocol.
/// These algorithms are NOT replaceable here — changing them would break
/// interoperability with Telegram's middle proxy infrastructure.
+#[allow(clippy::too_many_arguments)]
pub fn derive_middleproxy_keys(
nonce_srv: &[u8; 16],
nonce_clt: &[u8; 16],
diff --git a/src/crypto/random.rs b/src/crypto/random.rs
index 0dd5f1a..6313610 100644
--- a/src/crypto/random.rs
+++ b/src/crypto/random.rs
@@ -95,7 +95,7 @@ impl SecureRandom {
return 0;
}
- let bytes_needed = (k + 7) / 8;
+ let bytes_needed = k.div_ceil(8);
let bytes = self.bytes(bytes_needed.min(8));
let mut result = 0u64;
diff --git a/src/error.rs b/src/error.rs
index eaebd88..e4d66b9 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -91,7 +91,7 @@ impl From for std::io::Error {
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, err)
}
StreamError::Poisoned { .. } => {
- std::io::Error::new(std::io::ErrorKind::Other, err)
+ std::io::Error::other(err)
}
StreamError::BufferOverflow { .. } => {
std::io::Error::new(std::io::ErrorKind::OutOfMemory, err)
@@ -100,7 +100,7 @@ impl From for std::io::Error {
std::io::Error::new(std::io::ErrorKind::InvalidData, err)
}
StreamError::PartialRead { .. } | StreamError::PartialWrite { .. } => {
- std::io::Error::new(std::io::ErrorKind::Other, err)
+ std::io::Error::other(err)
}
}
}
@@ -135,12 +135,7 @@ impl Recoverable for StreamError {
}
fn can_continue(&self) -> bool {
- match self {
- Self::Poisoned { .. } => false,
- Self::UnexpectedEof => false,
- Self::BufferOverflow { .. } => false,
- _ => true,
- }
+ !matches!(self, Self::Poisoned { .. } | Self::UnexpectedEof | Self::BufferOverflow { .. })
}
}
diff --git a/src/main.rs b/src/main.rs
index 0d1eccc..c2b8c34 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,6 +35,7 @@ use crate::crypto::SecureRandom;
use crate::ip_tracker::UserIpTracker;
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
use crate::proxy::ClientHandler;
+use crate::stats::beobachten::BeobachtenStore;
use crate::stats::{ReplayChecker, Stats};
use crate::stream::BufferPool;
use crate::transport::middle_proxy::{
@@ -159,6 +160,15 @@ fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
info!(target: "telemt::links", "------------------------");
}
+async fn write_beobachten_snapshot(path: &str, payload: &str) -> std::io::Result<()> {
+ if let Some(parent) = std::path::Path::new(path).parent()
+ && !parent.as_os_str().is_empty()
+ {
+ tokio::fs::create_dir_all(parent).await?;
+ }
+ tokio::fs::write(path, payload).await
+}
+
#[tokio::main]
async fn main() -> std::result::Result<(), Box> {
let (config_path, cli_silent, cli_log_level) = parse_cli();
@@ -193,14 +203,14 @@ async fn main() -> std::result::Result<(), Box> {
};
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new("info"));
-
+
// Configure color output based on config
let fmt_layer = if config.general.disable_colors {
fmt::Layer::default().with_ansi(false)
} else {
fmt::Layer::default().with_ansi(true)
};
-
+
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
@@ -256,6 +266,7 @@ async fn main() -> std::result::Result<(), Box> {
let prefer_ipv6 = decision.prefer_ipv6();
let mut use_middle_proxy = config.general.use_middle_proxy && (decision.ipv4_me || decision.ipv6_me);
let stats = Arc::new(Stats::new());
+ let beobachten = Arc::new(BeobachtenStore::new());
let rng = Arc::new(SecureRandom::new());
// IP Tracker initialization
@@ -298,25 +309,30 @@ 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() 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"
- );
+ match crate::transport::middle_proxy::fetch_proxy_secret(
+ proxy_secret_path,
+ config.general.proxy_secret_len_max,
+ )
+ .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"
+ );
// Load ME config (v4/v6) + default DC
let mut cfg_v4 = fetch_proxy_config(
@@ -368,6 +384,10 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
config.general.me_pool_drain_ttl_secs,
config.general.effective_me_pool_force_close_secs(),
config.general.me_pool_min_fresh_ratio,
+ config.general.me_hardswap_warmup_delay_min_ms,
+ config.general.me_hardswap_warmup_delay_max_ms,
+ config.general.me_hardswap_warmup_extra_passes,
+ config.general.me_hardswap_warmup_pass_backoff_base_ms,
);
let pool_size = config.general.middle_proxy_pool_size.max(1);
@@ -386,18 +406,6 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
.await;
});
- // Periodic ME connection rotation
- let pool_clone_rot = pool.clone();
- let rng_clone_rot = rng.clone();
- tokio::spawn(async move {
- crate::transport::middle_proxy::me_rotation_task(
- pool_clone_rot,
- rng_clone_rot,
- std::time::Duration::from_secs(1800),
- )
- .await;
- });
-
Some(pool)
}
Err(e) => {
@@ -597,14 +605,12 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
} else {
info!(" IPv4 in use / 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 DC connectivity");
- }
+ } 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 DC connectivity");
}
info!(" via {}", upstream_result.upstream_name);
@@ -671,14 +677,8 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
rc_clone.run_periodic_cleanup().await;
});
- let detected_ip_v4: Option = probe
- .reflected_ipv4
- .map(|s| s.ip())
- .or_else(|| probe.detected_ipv4.map(std::net::IpAddr::V4));
- let detected_ip_v6: Option = probe
- .reflected_ipv6
- .map(|s| s.ip())
- .or_else(|| probe.detected_ipv6.map(std::net::IpAddr::V6));
+ let detected_ip_v4: Option = probe.detected_ipv4.map(std::net::IpAddr::V4);
+ let detected_ip_v6: Option = probe.detected_ipv6.map(std::net::IpAddr::V6);
debug!(
"Detected IPs: v4={:?} v6={:?}",
detected_ip_v4, detected_ip_v6
@@ -697,6 +697,26 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
detected_ip_v6,
);
+ let beobachten_writer = beobachten.clone();
+ let config_rx_beobachten = config_rx.clone();
+ tokio::spawn(async move {
+ loop {
+ let cfg = config_rx_beobachten.borrow().clone();
+ let sleep_secs = cfg.general.beobachten_flush_secs.max(1);
+
+ if cfg.general.beobachten {
+ let ttl = Duration::from_secs(cfg.general.beobachten_minutes.saturating_mul(60));
+ let path = cfg.general.beobachten_file.clone();
+ let snapshot = beobachten_writer.snapshot_text(ttl);
+ if let Err(e) = write_beobachten_snapshot(&path, &snapshot).await {
+ warn!(error = %e, path = %path, "Failed to flush beobachten snapshot");
+ }
+ }
+
+ tokio::time::sleep(Duration::from_secs(sleep_secs)).await;
+ }
+ });
+
if let Some(ref pool) = me_pool {
let pool_clone = pool.clone();
let rng_clone = rng.clone();
@@ -709,6 +729,18 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
)
.await;
});
+
+ let pool_clone_rot = pool.clone();
+ let rng_clone_rot = rng.clone();
+ let config_rx_clone_rot = config_rx.clone();
+ tokio::spawn(async move {
+ crate::transport::middle_proxy::me_rotation_task(
+ pool_clone_rot,
+ rng_clone_rot,
+ config_rx_clone_rot,
+ )
+ .await;
+ });
}
let mut listeners = Vec::new();
@@ -853,6 +885,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone();
+ let beobachten = beobachten.clone();
let max_connections_unix = max_connections.clone();
tokio::spawn(async move {
@@ -880,6 +913,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone();
+ let beobachten = beobachten.clone();
let proxy_protocol_enabled = config.server.proxy_protocol;
tokio::spawn(async move {
@@ -887,7 +921,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
if let Err(e) = crate::proxy::client::handle_client_stream(
stream, fake_peer, config, stats,
upstream_manager, replay_checker, buffer_pool, rng,
- me_pool, tls_cache, ip_tracker, proxy_protocol_enabled,
+ me_pool, tls_cache, ip_tracker, beobachten, proxy_protocol_enabled,
).await {
debug!(error = %e, "Unix socket connection error");
}
@@ -935,9 +969,11 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
if let Some(port) = config.server.metrics_port {
let stats = stats.clone();
+ let beobachten = beobachten.clone();
+ let config_rx_metrics = config_rx.clone();
let whitelist = config.server.metrics_whitelist.clone();
tokio::spawn(async move {
- metrics::serve(port, stats, whitelist).await;
+ metrics::serve(port, stats, beobachten, config_rx_metrics, whitelist).await;
});
}
@@ -951,6 +987,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone();
+ let beobachten = beobachten.clone();
let max_connections_tcp = max_connections.clone();
tokio::spawn(async move {
@@ -973,6 +1010,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
let me_pool = me_pool.clone();
let tls_cache = tls_cache.clone();
let ip_tracker = ip_tracker.clone();
+ let beobachten = beobachten.clone();
let proxy_protocol_enabled = listener_proxy_protocol;
tokio::spawn(async move {
@@ -989,6 +1027,7 @@ match crate::transport::middle_proxy::fetch_proxy_secret(proxy_secret_path).awai
me_pool,
tls_cache,
ip_tracker,
+ beobachten,
proxy_protocol_enabled,
)
.run()
diff --git a/src/metrics.rs b/src/metrics.rs
index 53ddd5d..08abb2d 100644
--- a/src/metrics.rs
+++ b/src/metrics.rs
@@ -1,6 +1,7 @@
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
+use std::time::Duration;
use http_body_util::Full;
use hyper::body::Bytes;
@@ -11,9 +12,17 @@ use ipnetwork::IpNetwork;
use tokio::net::TcpListener;
use tracing::{info, warn, debug};
+use crate::config::ProxyConfig;
+use crate::stats::beobachten::BeobachtenStore;
use crate::stats::Stats;
-pub async fn serve(port: u16, stats: Arc, whitelist: Vec) {
+pub async fn serve(
+ port: u16,
+ stats: Arc,
+ beobachten: Arc,
+ config_rx: tokio::sync::watch::Receiver>,
+ whitelist: Vec,
+) {
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
@@ -22,7 +31,7 @@ pub async fn serve(port: u16, stats: Arc, whitelist: Vec) {
return;
}
};
- info!("Metrics endpoint: http://{}/metrics", addr);
+ info!("Metrics endpoint: http://{}/metrics and /beobachten", addr);
loop {
let (stream, peer) = match listener.accept().await {
@@ -39,10 +48,14 @@ pub async fn serve(port: u16, stats: Arc, whitelist: Vec) {
}
let stats = stats.clone();
+ let beobachten = beobachten.clone();
+ let config_rx_conn = config_rx.clone();
tokio::spawn(async move {
let svc = service_fn(move |req| {
let stats = stats.clone();
- async move { handle(req, &stats) }
+ let beobachten = beobachten.clone();
+ let config = config_rx_conn.borrow().clone();
+ async move { handle(req, &stats, &beobachten, &config) }
});
if let Err(e) = http1::Builder::new()
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
@@ -54,24 +67,48 @@ pub async fn serve(port: u16, stats: Arc, whitelist: Vec) {
}
}
-fn handle(req: Request, stats: &Stats) -> Result>, Infallible> {
- if req.uri().path() != "/metrics" {
+fn handle(
+ req: Request,
+ stats: &Stats,
+ beobachten: &BeobachtenStore,
+ config: &ProxyConfig,
+) -> Result>, Infallible> {
+ if req.uri().path() == "/metrics" {
+ let body = render_metrics(stats);
let resp = Response::builder()
- .status(StatusCode::NOT_FOUND)
- .body(Full::new(Bytes::from("Not Found\n")))
+ .status(StatusCode::OK)
+ .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
+ .body(Full::new(Bytes::from(body)))
+ .unwrap();
+ return Ok(resp);
+ }
+
+ if req.uri().path() == "/beobachten" {
+ let body = render_beobachten(beobachten, config);
+ let resp = Response::builder()
+ .status(StatusCode::OK)
+ .header("content-type", "text/plain; charset=utf-8")
+ .body(Full::new(Bytes::from(body)))
.unwrap();
return Ok(resp);
}
- let body = render_metrics(stats);
let resp = Response::builder()
- .status(StatusCode::OK)
- .header("content-type", "text/plain; version=0.0.4; charset=utf-8")
- .body(Full::new(Bytes::from(body)))
+ .status(StatusCode::NOT_FOUND)
+ .body(Full::new(Bytes::from("Not Found\n")))
.unwrap();
Ok(resp)
}
+fn render_beobachten(beobachten: &BeobachtenStore, config: &ProxyConfig) -> String {
+ if !config.general.beobachten {
+ return "beobachten disabled\n".to_string();
+ }
+
+ let ttl = Duration::from_secs(config.general.beobachten_minutes.saturating_mul(60));
+ beobachten.snapshot_text(ttl)
+}
+
fn render_metrics(stats: &Stats) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(4096);
@@ -199,6 +236,95 @@ fn render_metrics(stats: &Stats) -> String {
stats.get_pool_stale_pick_total()
);
+ let _ = writeln!(out, "# HELP telemt_me_writer_removed_total Total ME writer removals");
+ let _ = writeln!(out, "# TYPE telemt_me_writer_removed_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_writer_removed_total {}",
+ stats.get_me_writer_removed_total()
+ );
+
+ let _ = writeln!(
+ out,
+ "# HELP telemt_me_writer_removed_unexpected_total Unexpected ME writer removals that triggered refill"
+ );
+ let _ = writeln!(out, "# TYPE telemt_me_writer_removed_unexpected_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_writer_removed_unexpected_total {}",
+ stats.get_me_writer_removed_unexpected_total()
+ );
+
+ let _ = writeln!(out, "# HELP telemt_me_refill_triggered_total Immediate ME refill runs started");
+ let _ = writeln!(out, "# TYPE telemt_me_refill_triggered_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_refill_triggered_total {}",
+ stats.get_me_refill_triggered_total()
+ );
+
+ let _ = writeln!(
+ out,
+ "# HELP telemt_me_refill_skipped_inflight_total Immediate ME refill skips due to inflight dedup"
+ );
+ let _ = writeln!(out, "# TYPE telemt_me_refill_skipped_inflight_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_refill_skipped_inflight_total {}",
+ stats.get_me_refill_skipped_inflight_total()
+ );
+
+ let _ = writeln!(out, "# HELP telemt_me_refill_failed_total Immediate ME refill failures");
+ let _ = writeln!(out, "# TYPE telemt_me_refill_failed_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_refill_failed_total {}",
+ stats.get_me_refill_failed_total()
+ );
+
+ let _ = writeln!(
+ out,
+ "# HELP telemt_me_writer_restored_same_endpoint_total Refilled ME writer restored on the same endpoint"
+ );
+ let _ = writeln!(out, "# TYPE telemt_me_writer_restored_same_endpoint_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_writer_restored_same_endpoint_total {}",
+ stats.get_me_writer_restored_same_endpoint_total()
+ );
+
+ let _ = writeln!(
+ out,
+ "# HELP telemt_me_writer_restored_fallback_total Refilled ME writer restored via fallback endpoint"
+ );
+ let _ = writeln!(out, "# TYPE telemt_me_writer_restored_fallback_total counter");
+ let _ = writeln!(
+ out,
+ "telemt_me_writer_restored_fallback_total {}",
+ stats.get_me_writer_restored_fallback_total()
+ );
+
+ let unresolved_writer_losses = stats
+ .get_me_writer_removed_unexpected_total()
+ .saturating_sub(
+ stats
+ .get_me_writer_restored_same_endpoint_total()
+ .saturating_add(stats.get_me_writer_restored_fallback_total()),
+ );
+ let _ = writeln!(
+ out,
+ "# HELP telemt_me_writer_removed_unexpected_minus_restored_total Unexpected writer removals not yet compensated by restore"
+ );
+ let _ = writeln!(
+ out,
+ "# TYPE telemt_me_writer_removed_unexpected_minus_restored_total gauge"
+ );
+ let _ = writeln!(
+ out,
+ "telemt_me_writer_removed_unexpected_minus_restored_total {}",
+ unresolved_writer_losses
+ );
+
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");
@@ -229,6 +355,7 @@ fn render_metrics(stats: &Stats) -> String {
#[cfg(test)]
mod tests {
use super::*;
+ use std::net::IpAddr;
use http_body_util::BodyExt;
#[test]
@@ -277,11 +404,17 @@ mod tests {
assert!(output.contains("# TYPE telemt_connections_total counter"));
assert!(output.contains("# TYPE telemt_connections_bad_total counter"));
assert!(output.contains("# TYPE telemt_handshake_timeouts_total counter"));
+ assert!(output.contains("# TYPE telemt_me_writer_removed_total counter"));
+ assert!(output.contains(
+ "# TYPE telemt_me_writer_removed_unexpected_minus_restored_total gauge"
+ ));
}
#[tokio::test]
async fn test_endpoint_integration() {
let stats = Arc::new(Stats::new());
+ let beobachten = Arc::new(BeobachtenStore::new());
+ let mut config = ProxyConfig::default();
stats.increment_connects_all();
stats.increment_connects_all();
stats.increment_connects_all();
@@ -290,16 +423,34 @@ mod tests {
.uri("/metrics")
.body(())
.unwrap();
- let resp = handle(req, &stats).unwrap();
+ let resp = handle(req, &stats, &beobachten, &config).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.into_body().collect().await.unwrap().to_bytes();
assert!(std::str::from_utf8(body.as_ref()).unwrap().contains("telemt_connections_total 3"));
+ config.general.beobachten = true;
+ config.general.beobachten_minutes = 10;
+ beobachten.record(
+ "TLS-scanner",
+ "203.0.113.10".parse::().unwrap(),
+ Duration::from_secs(600),
+ );
+ let req_beob = Request::builder()
+ .uri("/beobachten")
+ .body(())
+ .unwrap();
+ let resp_beob = handle(req_beob, &stats, &beobachten, &config).unwrap();
+ assert_eq!(resp_beob.status(), StatusCode::OK);
+ let body_beob = resp_beob.into_body().collect().await.unwrap().to_bytes();
+ let beob_text = std::str::from_utf8(body_beob.as_ref()).unwrap();
+ assert!(beob_text.contains("[TLS-scanner]"));
+ assert!(beob_text.contains("203.0.113.10-1"));
+
let req404 = Request::builder()
.uri("/other")
.body(())
.unwrap();
- let resp404 = handle(req404, &stats).unwrap();
+ let resp404 = handle(req404, &stats, &beobachten, &config).unwrap();
assert_eq!(resp404.status(), StatusCode::NOT_FOUND);
}
}
diff --git a/src/network/probe.rs b/src/network/probe.rs
index eda69b8..c52b340 100644
--- a/src/network/probe.rs
+++ b/src/network/probe.rs
@@ -95,23 +95,21 @@ pub async fn run_probe(config: &NetworkConfig, stun_addr: Option, nat_pr
}
pub fn decide_network_capabilities(config: &NetworkConfig, probe: &NetworkProbe) -> NetworkDecision {
- let mut decision = NetworkDecision::default();
+ let ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
+ let ipv6_dc = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
- decision.ipv4_dc = config.ipv4 && probe.detected_ipv4.is_some();
- decision.ipv6_dc = config.ipv6.unwrap_or(probe.detected_ipv6.is_some()) && probe.detected_ipv6.is_some();
-
- decision.ipv4_me = config.ipv4
+ let ipv4_me = config.ipv4
&& probe.detected_ipv4.is_some()
&& (!probe.ipv4_is_bogon || probe.reflected_ipv4.is_some());
let ipv6_enabled = config.ipv6.unwrap_or(probe.detected_ipv6.is_some());
- decision.ipv6_me = ipv6_enabled
+ let ipv6_me = ipv6_enabled
&& probe.detected_ipv6.is_some()
&& (!probe.ipv6_is_bogon || probe.reflected_ipv6.is_some());
- decision.effective_prefer = match config.prefer {
- 6 if decision.ipv6_me || decision.ipv6_dc => 6,
- 4 if decision.ipv4_me || decision.ipv4_dc => 4,
+ let effective_prefer = match config.prefer {
+ 6 if ipv6_me || ipv6_dc => 6,
+ 4 if ipv4_me || ipv4_dc => 4,
6 => {
warn!("prefer=6 requested but IPv6 unavailable; falling back to IPv4");
4
@@ -119,10 +117,17 @@ pub fn decide_network_capabilities(config: &NetworkConfig, probe: &NetworkProbe)
_ => 4,
};
- let me_families = decision.ipv4_me as u8 + decision.ipv6_me as u8;
- decision.effective_multipath = config.multipath && me_families >= 2;
+ let me_families = ipv4_me as u8 + ipv6_me as u8;
+ let effective_multipath = config.multipath && me_families >= 2;
- decision
+ NetworkDecision {
+ ipv4_dc,
+ ipv6_dc,
+ ipv4_me,
+ ipv6_me,
+ effective_prefer,
+ effective_multipath,
+ }
}
fn detect_local_ip_v4() -> Option {
diff --git a/src/network/stun.rs b/src/network/stun.rs
index c47aa49..5bda495 100644
--- a/src/network/stun.rs
+++ b/src/network/stun.rs
@@ -198,16 +198,11 @@ async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result