High-performance Telegram MTProto proxy written in Zig
Disguises Telegram traffic as standard TLS 1.3 HTTPS to bypass network censorship.
126 KB binary. ~120 KB RAM. Boots in <2 ms. Zero dependencies.
Features • How It Works • Quick Start • Deploy • Configuration • Security • Project Structure
| Feature | Description | |
|---|---|---|
| TLS 1.3 | Fake Handshake | Connections are indistinguishable from normal HTTPS to DPI systems |
| MTProto v2 | Obfuscation | AES-256-CTR encrypted tunneling (abridged, intermediate, secure) |
| DRS | Dynamic Record Sizing | Mimics real browser TLS behavior (Chrome/Firefox) to resist fingerprinting |
| Multi-user | Access Control | Independent secret-based authentication per user |
| Anti-replay | Timestamp Validation | Rejects replayed handshakes outside a +/- 2 min window |
| Masking | Connection Cloaking | Forwards unauthenticated clients to a real domain |
| Fast Mode | Zero-copy S2C | Drastically reduces CPU usage by delegating Server-to-Client AES encryption to the DC |
| Promotion | Tag Support | Optional promotion tag for sponsored proxy channel registration |
| 0 deps | Stdlib Only | Built entirely on the Zig standard library |
| 0 globals | Thread Safety | Dependency injection -- no global mutable state |
%%{init: {'theme': 'dark'}}%%
sequenceDiagram
participant C as Client
participant P as Proxy
participant DC as Telegram DC
rect rgb(30, 30, 80)
Note over C,P: Layer 1 — Fake TLS 1.3
C->>P: TLS ClientHello (HMAC-SHA256 in random)
P-->>C: TLS ServerHello + ChangeCipherSpec
end
rect rgb(20, 70, 30)
Note over C,DC: Layer 2 — MTProto Obfuscation
C->>P: TLS AppData ← 64-byte obfuscated handshake
P->>DC: Obfuscated handshake (AES-256-CTR keys derived)
DC-->>P: Obfuscated response
end
rect rgb(90, 30, 30)
Note over C,DC: Layer 3 — Encrypted Relay
C->>P: TLS( AES-CTR( data ) )
P->>DC: AES-CTR( data )
DC-->>P: AES-CTR( data )
P-->>C: TLS( AES-CTR( data ) )
end
Layer 1 -- Fake TLS 1.3 The client embeds an HMAC-SHA256 digest (derived from its secret) in the ClientHello
randomfield. The proxy validates it and responds with an indistinguishable ServerHello.
Layer 2 -- MTProto Obfuscation Inside the TLS tunnel, a 64-byte obfuscated handshake is exchanged. AES-256-CTR keys are derived via SHA-256 for bidirectional encryption.
Layer 3 -- DC Relay The proxy connects to the target Telegram datacenter (DC1-DC5), performs its own obfuscated handshake, and relays traffic between client and DC with re-encryption.
Anti-censorship -- Masking When an unauthenticated client connects (e.g. a DPI active probe), the proxy transparently forwards the connection to the real
tls_domain(e.g.wb.ru). The prober receives a genuine TLS certificate and HTTP response, making the proxy indistinguishable from a real web server.
Measured locally (ReleaseSmall) and on a 1 vCPU Linux VPS under load.
| mtprotoproxy | telemt | mtproto.zig | |
|---|---|---|---|
| Language | Python | Rust | Zig |
| RAM (Peak) | > 50 MB | ~11.6 MB | ~6.8 MB |
| RAM (Idle) | ~30 MB | ~3.0 MB | ~120 KB |
| Binary Size | N/A (Scripts) | ~17.0 MB | 126 KB |
| Dependencies | cryptography, uvloop |
150+ Crates | 0 (None) |
Measured via systemd status and
/usr/bin/time -von an Ubuntu 24.04 server.mtproto.zigis compiled statically viazig build -Doptimize=ReleaseSmall -Dtarget=x86_64-linuxand uses the standard library entirely for its cryptography, handshakes, and event loops.
- Zig 0.15.2 or later
# Clone
git clone https://github.com/sleep3r/mtproto.zig.git
cd mtproto.zig
# Build (debug)
make build
# Build (optimized for production)
make release
# Run with default config.toml
make runmake testAll Make targets
| Target | Description |
|---|---|
make build |
Debug build |
make release |
Optimized build (ReleaseFast) |
make run CONFIG=<path> |
Run proxy (default: config.toml) |
make test |
Run unit tests |
make clean |
Remove build artifacts |
make fmt |
Format all Zig source files |
make deploy |
Cross-compile, upload to VPS, restart service |
make deploy SERVER=<ip> |
Deploy to a specific server |
curl -sSf https://raw.githubusercontent.com/sleep3r/mtproto.zig/main/deploy/install.sh | sudo bashThis will:
- Install Zig 0.15.2 (if not present)
- Clone and build the proxy with
ReleaseFast - Generate a random 16-byte secret
- Create a
systemdservice (mtproto-proxy) - Open port 443 in
ufw(if active) - Print a ready-to-use
tg://connection link
Step-by-step instructions
1. Install Zig on the server
# x86_64
curl -sSfL https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz | \
sudo tar xJ -C /usr/local
sudo ln -sf /usr/local/zig-linux-x86_64-0.15.2/zig /usr/local/bin/zig
# Verify
zig version # → 0.15.22. Build the proxy
git clone https://github.com/sleep3r/mtproto.zig.git
cd mtproto.zig
zig build -Doptimize=ReleaseFastOr cross-compile on your Mac:
zig build -Doptimize=ReleaseFast -Dtarget=x86_64-linux
scp zig-out/bin/mtproto-proxy root@<SERVER_IP>:/opt/mtproto-proxy/3. Configure
sudo mkdir -p /opt/mtproto-proxy
sudo cp zig-out/bin/mtproto-proxy /opt/mtproto-proxy/
# Generate a random secret
SECRET=$(openssl rand -hex 16)
echo $SECRET
sudo tee /opt/mtproto-proxy/config.toml <<EOF
[server]
port = 443
# tag = "<your-promotion-tag>" # Optional: 32 hex-char promotion tag from @MTProxybot
[censorship]
tls_domain = "wb.ru"
mask = true
fast_mode = true
[access.users]
user = "$SECRET"
EOF4. Install the systemd service
sudo cp deploy/mtproto-proxy.service /etc/systemd/system/
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mtproto
sudo chown -R mtproto:mtproto /opt/mtproto-proxy
sudo systemctl daemon-reload
sudo systemctl enable mtproto-proxy
sudo systemctl start mtproto-proxy5. Open port 443
sudo ufw allow 443/tcp6. Generate connection link
The proxy prints links on startup. Check them with:
journalctl -u mtproto-proxy | head -30Or build it manually:
tg://proxy?server=<SERVER_IP>&port=443&secret=ee<SECRET><HEX_DOMAIN>
Where <HEX_DOMAIN> is your tls_domain encoded as hex:
echo -n "wb.ru" | xxd -p # → 77622e7275# Status
sudo systemctl status mtproto-proxy
# Live logs
sudo journalctl -u mtproto-proxy -f
# Restart (e.g., after config change)
sudo systemctl restart mtproto-proxy
# Stop
sudo systemctl stop mtproto-proxyCreate a config.toml in the project root:
[server]
port = 443
tag = "1234567890abcdef1234567890abcdef" # Optional: promotion tag from @MTProxybot
[censorship]
tls_domain = "wb.ru"
mask = true
fast_mode = true
[access.users]
alice = "00112233445566778899aabbccddeeff"
bob = "ffeeddccbbaa99887766554433221100"Configuration reference
| Section | Key | Default | Description |
|---|---|---|---|
[server] |
port |
443 |
TCP port to listen on |
[server] |
tag |
(none) | Optional 32 hex-char promotion tag from @MTProxybot |
[censorship] |
tls_domain |
"wb.ru" |
Domain to impersonate / forward bad clients to |
[censorship] |
mask |
true |
Forward unauthenticated connections to tls_domain to defeat DPI |
[censorship] |
fast_mode |
false |
Recommended. Drastically reduces RAM/CPU usage by natively delegating S2C AES encryption to the Telegram DC |
[access.users] |
<name> |
-- | 32 hex-char secret (16 bytes) per user |
Tip Generate a random secret:
openssl rand -hex 16
Note The configuration format is compatible with the Rust-based
telemtproxy.
| Measure | Details |
|---|---|
| Constant-time comparison | HMAC validation uses constant-time byte comparison to prevent timing attacks |
| Key wiping | All key material is zeroed from memory after use |
| Secure randomness | Cryptographically secure RNG for all nonces and key generation |
| Anti-replay | Embedded timestamp validation rejects handshakes outside +/- 2 min window |
| Nonce validation | Rejects nonces matching HTTP, plain MTProto, or TLS patterns |
| Dynamic Record Sizing | TLS record sizes mimic real browsers, preventing traffic fingerprinting |
| Connection masking | Invalid clients are proxied to the real tls_domain, defeating DPI active probes |
| Systemd hardening | Runs as unprivileged user with NoNewPrivileges, ProtectSystem=strict |
├── deploy/
│ ├── install.sh One-line installer for Linux
│ └── mtproto-proxy.service Systemd unit file
│
└── src/
├── main.zig Entry point, banner, IP detection
├── config.zig TOML-like configuration parser
│
├── crypto/
│ └── crypto.zig AES-256-CTR/CBC, SHA-256, HMAC, SHA-1, MD5
│
├── protocol/
│ ├── constants.zig DC addresses, protocol tags, TLS constants
│ ├── tls.zig Fake TLS 1.3 (ClientHello validation, ServerHello)
│ └── obfuscation.zig MTProto v2 obfuscation & key derivation
│
└── proxy/
└── proxy.zig TCP listener, connection handler, relay, DRS
The proxy includes specific handling for iOS Telegram clients:
-
Fast Mode (
fast_mode = true) — Highly recommended for iOS clients to fix the "Updating..." connection loop. This bypasses proxy S2C encryption and relies on the Telegram DC directly. -
Fragmented handshake assembly — iOS may split the 64-byte MTProto handshake across multiple TLS AppData records or interleave CCS records
-
Two-stage timeouts — idle pool connections (common on iOS) get a generous 5-minute poll timeout; active data gets a tight 10s
SO_RCVTIMEO -
Generous handshake timeout — 60s timeout during handshake assembly (iOS may delay after ServerHello)
-
Fixed record sizing — TLS records are kept at MSS-sized 1369 bytes for maximum compatibility
Important Many Russian ISPs (via TSPU/DPI) block known VPS IP ranges at the network level. If the proxy appears to connect but the app stays on "Updating...", try a server in a different country/provider. The
mask = truesetting helps prevent your IP from being flagged in the first place.
If you run both the proxy and AmneziaVPN (or any WireGuard-based VPN) on the same server, iOS clients connected through the VPN will not be able to reach the proxy by default.
The problem: iOS routes all traffic (including proxy connections) through the VPN tunnel. The packets exit the tunnel inside a Docker network (e.g. 172.29.172.0/24), but Docker's default FORWARD policy DROP silently blocks them from reaching port 443 on the host. macOS VPN clients are not affected because they route traffic to the VPN server's own IP outside the tunnel.
The fix — allow VPN clients to reach the proxy:
# Allow traffic from the VPN Docker subnet to the proxy port
iptables -I DOCKER-USER -s 172.29.172.0/24 -p tcp --dport 443 -j ACCEPT
# Make the rule persistent across reboots
apt-get install -y iptables-persistent
netfilter-persistent saveNote Replace
172.29.172.0/24with your AmneziaVPN Docker subnet. For standard WireGuard without Docker, you might need to allow traffic from10.8.1.0/24(or whatever yourAllowedIPssubnet is).
If your iPhone is connected to a commercial/premium VPN (even if using the Amnezia client or WireGuard protocol) and your MTProto proxy is stuck on "Updating...", the VPN provider is likely blocking the traffic.
- DPI & TLS Inspection: Commercial VPNs often use DPI to inspect TLS traffic. FakeTLS mimics a normal HTTPS handshake, but if the VPN actively probes the connection or analyzes the payload structure, they may detect MTProto traffic and drop it.
- IP Blocking: The VPN may outright block connections to Telegram datacenter IP ranges.
Solution: Use a self-hosted VPN (like AmneziaWG) on your own server. Your own server won't restrict outbound MTProto traffic.
MIT © 2026 Aleksandr Kalashnikov