Threat Model
July 7, 2026 · View on GitHub
This document describes what mtproto.zig is designed to protect against, what it does not protect against, and the operational constraints you should expect in production.
Scope
In scope:
mtproto-proxydata planemtbuddyinstall/update/control workflows- default masking and upstream routing modes
Out of scope:
- Telegram protocol internals and Telegram backend security
- host OS hardening beyond what project configs apply
- physical access, hypervisor compromise, or root compromise on your VPS
Assets
Primary assets:
- availability of proxy service
- confidentiality of user secrets and runtime config
- integrity of update/install artifacts
Secondary assets:
- operational privacy (traffic shape camouflage, anti-probe behavior)
- predictable behavior under load and hostile input
Adversaries
- passive DPI observers
- active probing systems (invalid handshake probes, replay probes)
- network attackers causing packet loss, fragmentation, and reset patterns
- opportunistic abuse (scanners, connection floods)
Security Goals
mtproto.zig aims to:
- make MTProto traffic resemble common TLS traffic
- reduce fingerprinting by DPI and active probes
- enforce connection caps; provide an opt-in per-subnet rate limiter and a per-IP handshake flood guard (both disabled by default to avoid carrier-NAT false positives), plus an opt-in kernel-level per-IP SYN rate-limiter (
mtbuddy setup syn-limit) that drops abusive first-SYN bursts beforeaccept()— installed as a separate systemd unit soCAP_NET_ADMINis not granted to the proxy - fail safely on invalid handshakes and malformed frames
- verify release artifacts (signature + checksum) in default install/update flows
Non-Goals
mtproto.zig does not aim to:
- provide anonymity (it is not Tor)
- hide destination IP from your hosting provider
- defend against compromised client devices
- guarantee bypass in every country/network forever
- guarantee zero downtime during all upgrades on every deployment model
Known Limitations
- Censorship techniques evolve quickly; bypass methods can degrade without prior notice.
- Traffic camouflage can be weakened by network-level heuristics outside proxy control.
- FakeTLS with a borrowed
tls_domain(a domain that doesn't resolve to this server's IP) is detectable passively by SNI↔IP correlation, independent of handshake quality. See FakeTLS borrowed-SNI limitation. - Some mitigations depend on host networking setup (iptables/nftables, kernel routing, NIC offload behavior).
- The dashboard requires HTTP Basic auth (auto-generated token at
/opt/mtproto-proxy/monitor/dashboard.token) and pins the Host header on loopback binds, but it is still plain HTTP and runs a root-privileged control plane — reach it over an SSH tunnel and only ever expose it behind HTTPS + a reverse proxy. The Prometheus/metricsendpoint has no auth; keep it on loopback. - Proxy behavior depends on Telegram DC availability and protocol expectations that can change.
- Telegram calls are out of scope and do not work through this proxy. Calls use Telegram's SOCKS-style call path, which is outside the MTProto/TLS-masking model and cannot be disguised cleanly as normal HTTPS here.
- Media for non-Premium accounts requires MiddleProxy (
[general].use_middle_proxy = true). Without it, photos, videos, stories, and other media on non-Premium accounts should be considered unavailable.
Client Transports: FakeTLS vs direct-obfuscated (dd)
The proxy accepts two client transports on the same port:
- FakeTLS (
ee...secret, recommended): the obfuscated MTProto handshake is wrapped in a TLS 1.3 ClientHello/ServerHello so the connection looks like normal HTTPS to DPI. This is the transport the camouflage design (FakeTLS, DRS, desync, masking) is built around. - Direct-obfuscated (
dd...secret, "secure"/padded mode): plain obfuscated MTProto with random padding and no TLS wrapper. It still requires a valid user secret, but it is not disguised as TLS and is therefore directly fingerprintable as MTProto by DPI. Theddname refers to the padding mode, not to any DPI-resistance property.
Implications:
- The
ddtransport is rejected by default ([censorship].fake_tls_only = true): non-TLS first bytes are masked immediately (matching the masking target) instead of being parsed as an obfuscated handshake, so there is no dd active-probe timing difference and the proxy only ever speaks FakeTLS on the wire. This is the secure default for a TLS-camouflage proxy. - To hand out
ddlinks (lower-DPI / compatibility scenarios), setfake_tls_only = false. Only then doesmtbuddy linksprint dd links, and only then does the proxy accept the DPI-fingerprintable dd transport. Preferee/FakeTLS links wherever active DPI is a concern.
FakeTLS borrowed-SNI limitation: IP ↔ SNI correlation
FakeTLS makes the proxy look like an HTTPS server for tls_domain (e.g. wb.ru):
the client sends a ClientHello with SNI = tls_domain, and the proxy answers with a
byte-compatible TLS 1.3 ServerHello. This defeats two DPI strategies well:
- Handshake fingerprinting — the ClientHello/ServerHello match a real nginx + OpenSSL stack (extension order, key_share group, fixed cert-record size), so the flow is not distinguishable as MTProto by shape alone.
- Active probing — an unauthenticated prober is forwarded to the masking backend (a real TLS server) instead of getting a tell-tale proxy error.
Post-quantum key_share (X25519MLKEM768 / 0x11ec). Modern browsers and CDNs now
negotiate the hybrid group X25519MLKEM768; by 2026 a majority of real browser→CDN
ClientHellos carry a 0x11ec key_share. A FakeTLS proxy that always answers with a
classical x25519 (0x001d) key_share is a passive group-downgrade tell. The
proxy therefore echoes a correctly-sized 0x11ec ServerHello key_share whenever the
client offers 0x11ec. (The share is high-entropy bytes, not a real ML-KEM
encapsulation: FakeTLS clients validate only record framing + the HMAC in
server-random, passive fingerprinting keys on the group and size, and we are not a
TLS terminator — so a cryptographically valid ciphertext is unnecessary.)
The tls_domain must itself support X25519MLKEM768 (June-2026 TSPU marker). The
echo above fixes the connection-level downgrade tell, but there is a second,
domain-level check the TSPU rolled out the night of 4→5 June 2026 that the echo does
not address. The censor appears to probe the SNI domain out-of-band for
post-quantum support and blocks the flow when the domain lacks it: a tls_domain
whose real TLS 1.3 negotiates only classical x25519 (no X25519MLKEM768) is a passive
marker, and iOS clients — plus everyone sharing their NAT egress IP — fronting
such a domain get dropped. Because the signal is a property of the domain (evidence:
the community's fix is "change the domain", @Sni_checker_bot checks a domain, a
self-signed backend must run OpenSSL 3.5+ to offer the group, and one domain's two IPs
can score differently), our own ServerHello can't buy it back. The only lever is
choosing a tls_domain that genuinely negotiates X25519MLKEM768 in a single round —
which our FakeTLS then mimics correctly via the 0x11ec echo. mtbuddy install /
setup masking now probe for this and warn. This collides with tls_domain
immutability: a live deploy already pinned to a classical-x25519 domain cannot migrate
without invalidating every distributed link. (See src/ctl/fronting_domain.zig.)
It does not defend against a third strategy that DPI vendors are now leaning on:
SNI ↔ destination-IP consistency. When tls_domain is a well-known third-party
domain that does not resolve to your proxy's IP, a DPI box can flag the flow passively,
with no active probe: "this flow claims SNI = wb.ru, but this IP has never been
observed serving wb.ru (no matching A-record / certificate / passive-DNS history) →
fake SNI." Recent fastDPI / TSPU builds make this explicit — a dedicated FakeSNI check
plus IP-vs-SNI reconciliation that runs when the destination IP does not already
classify the protocol. The borrowed SNI itself is the signal.
This is an inherent property of borrowed-domain FakeTLS, not a fixable bug. Mitigations, strongest to most practical:
- Use a domain that genuinely resolves to this server's IP, with a real certificate
(the
mtbuddy setup masking+ certbot path). Then SNI ↔ IP is consistent and there is nothing fake about the SNI — the only full mitigation. It is also operationally heavy (you need a domain, DNS control, and a cert), and changingtls_domainon a live deploy invalidates every distributed share link, so in practice most operators keep a borrowed domain and accept the residual exposure. - Prefer a plausible, less-prominent borrowed domain over a globally famous one. A massively popular CDN-hosted domain is the easiest IP↔SNI mismatch to catch; a regional / less-watched HTTPS site is a weaker signal. It must still pass the single-round X25519MLKEM768 ServerHello check below.
- IP and egress hygiene. Borrowed-SNI FakeTLS is worth most when the IP itself
isn't already burned: prefer a fresh address, and use IPv6 rotation
(
mtbuddy ipv6-hop) and/or tunnel egress so the visible endpoint isn't a long-lived, reputation-flagged IP.
Separate but related — ServerHello suitability. The synthetic ServerHello is a
single-round exchange with no HelloRetryRequest, emitting either an X25519MLKEM768
(0x11ec) or a classical x25519 key_share depending on what the client offered. A
tls_domain whose real TLS 1.3 server issues an HRR or prefers a group we can't emit
(e.g. wb.ru, mail.ru select secp521r1) produces a passive ServerHello mismatch
independent of the IP↔SNI issue. Combined with the June-2026 domain marker above, the
target to prefer is a domain that negotiates X25519MLKEM768 in a single round;
mtbuddy install / setup masking warn when a domain does only classical x25519 or
does an HRR.
Bottom line: FakeTLS with a borrowed SNI is strong camouflage against handshake
fingerprinting and active probing, but it is not a guarantee against IP↔SNI
reputation correlation. Treat the choice of tls_domain and the reputation of the proxy
IP as part of the threat model, not as cosmetic settings.
Region-Specific Caveats
- Blocking patterns differ by ISP and country. A configuration that works in one region can fail in another.
- IPv6 behavior is especially region/provider dependent; dual-stack DNS can cause client-side delays when AAAA is published but upstream IPv6 is broken.
- Tunnel mode success depends on local policy routing support and allowed VPN protocols in that region.
Compatibility Matrix
Telegram clients
| Client family | Status | Notes |
|---|---|---|
| Official Telegram Android | expected to work | test with latest stable app before rollout |
| Official Telegram iOS | expected to work | IPv6/AAAA issues are a common deployment pitfall |
| Telegram Desktop | expected to work | verify with your selected masking domain |
| Third-party Telegram clients | best effort | protocol edge cases may differ |
Compatibility here covers chat and media transport. Telegram calls are unsupported in both direct and MiddleProxy modes.
OS / kernel
| Platform | Status | Notes |
|---|---|---|
| Linux x86_64 | supported | primary production target |
| Linux aarch64 | supported | verify release artifact/CPU compatibility on target host |
| Linux in Docker | supported with caveats | OS-level DPI modules are not applied inside container by default |
| macOS / Windows runtime | not supported | cross-compilation host is fine; runtime target is Linux |
What Can Break After Telegram/DC Changes
Potential breakage vectors:
- DC endpoint changes and transport policy updates
- MiddleProxy metadata format or refresh endpoint changes
- handshake/timing expectations used by client versions
- media path specifics (for example DC203 behavior)
Operational guidance:
- keep to latest release
- monitor logs after each Telegram client or DC behavior shift
- keep fallback paths tested (direct/tunnel/upstream)
- run
mtbuddy config doctorand E2E checks after major updates
Residual Risk
Even with all mitigations enabled, this project cannot guarantee uninterrupted bypass against adaptive nation-state censorship systems. Treat this as a hardened transport tool, not a universal censorship-proof channel.