tg-ws-proxy-rs
Telegram MTProto WebSocket Bridge Proxy — a Rust vibecoded port of Flowseal/tg-ws-proxy.
Listens for Telegram Desktop's MTProto connections on a local port and tunnels them through WebSocket (TLS) connections to Telegram's DC servers.
Telegram Desktop → MTProto (TCP 1443) → tg-ws-proxy-rs → WS (TLS 443) → Telegram DC
↘ CF proxy (kws{N}.{cf-domain}) → Telegram DC (WS via Cloudflare)
↘ upstream MTProto proxy → Telegram DC (WS fallback)
↘ direct TCP :443 → Telegram DC (last resort)
Why Rust?
| Python original | This port | |
|---|---|---|
| Runtime | CPython required | Single static binary |
| Memory | ~30–50 MB | ~3–5 MB |
| CPU | Higher | Lower (compiled) |
| OpenWrt | Needs Python install | Just copy the binary |
| Static build | No | Yes (musl) |
Quick Start
Pre-built binaries
Download from the Releases page.
Build from source
# Debug build
# Optimised release build
# Static binary for Linux x86_64 (e.g. for Docker scratch images)
The release binary is at target/release/tg-ws-proxy (or
target/<target>/release/tg-ws-proxy for cross-compiled targets).
Cross-platform builds with cargo-zigbuild
cargo-zigbuild uses the Zig
compiler as a drop-in C cross-linker so you can build for every platform from
a single Linux or macOS host without installing any platform SDKs.
# Install cargo-zigbuild and Zig
# Add all required Rust targets in one shot
# Build for all platforms
Note: Building macOS targets (
*-apple-darwin) requires the macOS SDK (XCode Command Line Tools). On Linux you can useosxcrossto supply the SDK and then setSDKROOT/MACOSX_DEPLOYMENT_TARGETappropriately before runningcargo zigbuild.
Usage
tg-ws-proxy [OPTIONS]
| Flag | Default | Description |
|---|---|---|
--port <PORT> |
1443 |
Listen port |
--host <HOST> |
127.0.0.1 |
Listen address |
--link-ip <IP> |
auto-detected | IP shown in the tg:// link (see Router deployment) |
--secret <HEX> |
random | 32 hex-char MTProto secret |
--listen-faketls-domain <DOMAIN> |
— | Accept inbound clients with ee FakeTLS and advertise this SNI domain in the link |
--dc-ip <DC:IP> |
DC2 + DC4 | Target IP per DC (repeatable); omit when using --cf-domain to let CF proxy handle all DCs |
--buf-kb <KB> |
256 |
Socket buffer size |
--pool-size <N> |
4 |
Pre-warmed WS connections per DC |
--cf-domain <DOMAIN> |
— | Cloudflare-proxied domain(s) for alternative WS routing, comma-separated (see CF Proxy) |
--default-domains |
off | Fetch and use the built-in CF proxy domain list from GitHub (no Cloudflare setup needed, see Default domains) |
--cf-priority |
off | Try CF proxy before direct WS for all DCs (see CF Proxy) |
--cf-balance |
off | Round-robin load balance across multiple --cf-domain values (see CF Proxy) |
--max-connections <N> |
auto | Max concurrent client connections (auto-computed from ulimit -n) |
--mtproto-proxy <HOST:PORT:SECRET> |
— | Upstream MTProto proxy fallback (repeatable) |
--log-file <PATH> |
— | Write logs to a file instead of stderr (no ANSI color codes) |
-q / --quiet |
off | Suppress all log output |
-v / --verbose |
off | Debug logging |
--danger-accept-invalid-certs |
off | Skip TLS verification |
Every flag has a matching environment variable (TG_PORT, TG_HOST,
TG_SECRET, TG_BUF_KB, TG_POOL_SIZE, TG_MAX_CONNECTIONS, TG_QUIET,
TG_VERBOSE, TG_SKIP_TLS_VERIFY, TG_LINK_IP, TG_LISTEN_FAKETLS_DOMAIN, TG_MTPROTO_PROXY,
TG_LOG_FILE, TG_CF_DOMAIN, TG_CF_PRIORITY, TG_CF_BALANCE, TG_DEFAULT_DOMAINS).
Examples
# Standard run (random secret, DC 2 + 4)
# Custom port and extra DCs
# With upstream MTProto proxy fallback
# With Cloudflare proxy domain (WS fallback via Cloudflare CDN)
# CF proxy only: omit --dc-ip so CF proxy handles all DCs
# Use default CF domains from GitHub — no Cloudflare setup required
# Default domains + CF priority (try CF first, fall back to direct WS)
# Default domains + your own domain (yours goes first)
# Multiple CF domains (tried in order) with CF priority over direct WS
# Multiple CF domains with round-robin load balancing
# CF balance + priority: round-robin across CF domains, tried before direct WS
# CF priority: CF proxy is tried first, falls back to direct WS on failure
# Multiple upstream proxies (tried in order until one succeeds)
# Router deployment: listen on all interfaces, let all LAN devices use the proxy
# Public home server: inbound ee FakeTLS, backend still WSS to Telegram Web
# Equivalent: pass a full ee secret directly
# Verbose logging
# Log to a file instead of stderr (no garbled ANSI codes — useful on Windows)
# All options via environment variables (useful for Docker / systemd)
TG_PORT=1443 TG_SECRET=deadbeef...
On startup the proxy prints a tg://proxy?... link you can paste into
Telegram Desktop to configure it automatically. With --listen-faketls-domain,
the printed link uses secret=ee<key><domain_hex>; otherwise it uses the
classic dd<key> padded MTProto secret.
Inbound FakeTLS listener
For public home servers where DPI blocks raw inbound MTProto, enable inbound FakeTLS:
This changes only the client-facing transport:
Telegram client → ee FakeTLS → tg-ws-proxy-rs → WSS/TLS → kws*.web.telegram.org
The proxy accepts the TLS ClientHello, validates the FakeTLS HMAC, sends a synthetic TLS ServerHello, unwraps TLS Application Data records, and then passes the recovered MTProto init into the existing WebSocket backend path.
Upstream MTProto proxy fallback
When WebSocket connections to Telegram are blocked, the proxy can route traffic through an external MTProto proxy before falling back to direct TCP:
WS (preferred) → upstream MTProto proxy → direct TCP (last resort)
Pass one or more --mtproto-proxy HOST:PORT:SECRET flags (or a
comma-separated list in TG_MTPROTO_PROXY). Proxies are tried in the order
given; if one fails it enters a 60-second cooldown so subsequent connections
skip it without delay.
# Padded-intermediate proxy (dd prefix)
# FakeTLS proxy (ee prefix — domain-fronting transport)
# Multiple proxies (tried in order until one succeeds)
# Or via environment variable (comma-separated)
TG_MTPROTO_PROXY="proxy.example.com:443:ddabcdef1234...,other.example.net:8888:dddeadbeef..."
ℹ️ Secret format — pass the secret exactly as shown in the
tg://proxylinkPublic MTProto proxies advertise secrets with a 1-byte prefix that tells the proxy which transport mode to use. Copy the full secret as-is — prefix included:
Prefix Meaning Example ddPadded-intermediate transport ddabcdef1234567890abcdef1234567890eeFakeTLS (domain-fronting) transport ee+ 32 hex key chars + hex-encoded hostname(none) Plain transport (legacy, 32 hex chars) abcdef1234567890abcdef1234567890In the
tg://proxy?server=...&secret=link thesecret=value already contains the correct prefix. Copy everything aftersecret=and pass it directly to--mtproto-proxy.
Cloudflare Proxy
When Telegram's IP ranges are blocked by your ISP, you can route WebSocket
traffic through Cloudflare using --cf-domain. This requires only a domain
name — no server-side component.
# Use your own Cloudflare-proxied domain
# Multiple domains (tried in order, first has highest priority)
# Multiple domains with round-robin load balancing
# CF-only mode: omit --dc-ip so CF proxy handles all DCs
# CF priority: try CF proxy before direct WS, with WS as fallback
# Or via environment variable
TG_CF_DOMAIN=yourdomain.com
The proxy will try the CF path as a fallback after direct WebSocket fails, and
as the primary path for DCs that have no --dc-ip configured. With
--cf-priority, the CF proxy is tried before direct WebSocket for all DCs.
--cf-balance — round-robin load balancing
When multiple --cf-domain values are given, connections normally always start
with the first domain. Adding --cf-balance distributes connections evenly
across all configured CF domains using round-robin selection:
# connection 0 → tries d1, then d2, then d3
# connection 1 → tries d2, then d3, then d1
# connection 2 → tries d3, then d1, then d2
The remaining domains still serve as ordered fallbacks if the primary one
fails, so resilience is unchanged. Has no effect when only one CF domain is
configured. Can be combined with --cf-priority:
# Round-robin CF load balancing, tried before direct WS
One-time domain setup (do this in the Cloudflare dashboard):
-
In SSL/TLS → Overview set mode to Flexible.
-
In DNS → Records add these proxied (
🔶) A records:Name IPv4 address kws1149.154.175.50kws1-1149.154.175.50kws2149.154.167.51kws2-1149.154.167.51kws3149.154.175.100kws3-1149.154.175.100kws4149.154.167.91kws4-1149.154.167.91kws5149.154.171.5kws5-1149.154.171.5kws20391.105.192.100kws203-191.105.192.100
See docs/CfProxy.md for full instructions.
Default domains
Don't want to configure your own Cloudflare DNS zone? Use --default-domains
to automatically fetch a pre-configured, working list of CF proxy domains from
the upstream repository:
# No Cloudflare account or DNS setup required
# Enable CF priority so CF path is tried first
# Combine with your own domain (yours gets highest priority)
# Test the fetched domains before starting the proxy
Or via environment variable:
TG_DEFAULT_DOMAINS=true
At startup the proxy fetches an obfuscated domain list from
Flowseal/tg-ws-proxy,
deobfuscates it, and appends the decoded domains after any explicit
--cf-domain entries. If the fetch fails (network not yet available,
GitHub unreachable) the proxy falls back to a small built-in list and logs a
warning — it will still start normally.
Note: These are community-maintained domains; availability may change over time. For maximum reliability, consider setting up your own Cloudflare zone (see docs/CfProxy.md).
Router deployment
Run the proxy on your router with --host 0.0.0.0 so it accepts connections
from all LAN devices:
When --host 0.0.0.0 is used, the proxy auto-detects the router's LAN IP
address and uses it in the generated tg:// link, so you can share the same
link with every device on your network.
If auto-detection picks the wrong interface, override it explicitly:
Note: The default
--host 127.0.0.1only accepts connections from the machine running the proxy. Other devices on the network will not be able to connect unless you change this to0.0.0.0(or the router's LAN IP).
Telegram Desktop Setup
- Settings → Advanced → Connection type → Use custom proxy
- Add MTProto proxy:
- Server:
127.0.0.1 - Port:
1443(or your--port) - Secret: shown in the proxy startup log
- Server:
Or use the tg://proxy?... link that is printed on startup.
Cross-compilation for OpenWrt
OpenWrt uses musl libc and runs on MIPS, ARM, and ARM64 CPUs. Building a fully static Rust binary requires:
- A C cross-compiler for your target (used by
ring/aws-lc-sys) - The matching Rust target
ARM64 (aarch64) — e.g. GL.iNet MT6000, Banana Pi R4
# Install the cross toolchain (Ubuntu/Debian)
# Add the Rust target
# Uncomment the [target.aarch64-unknown-linux-musl] section in .cargo/config.toml,
# then build:
ARM (armv7) — e.g. older GL.iNet routers, some TP-Link models
# Uncomment the armv7 section in .cargo/config.toml
MIPS LE — e.g. TP-Link WR series
# Uncomment the mipsel section in .cargo/config.toml
Using cross (easier alternative)
cross uses Docker to manage toolchains:
OpenWrt procd init script
Create /etc/init.d/tg-ws-proxy:
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=90
STOP=10
PROG=/usr/local/bin/tg-ws-proxy
How it works
- Telegram Desktop connects to the proxy on
127.0.0.1:1443. - The proxy reads the 64-byte MTProto obfuscation handshake, validates the secret, and extracts the target DC id and transport protocol.
- A WebSocket connection is opened to
wss://kwsN.web.telegram.org/apiws(using the DC-specific domain as TLS SNI but routing TCP to the configured IP). - The relay init packet is sent to Telegram, and bidirectional bridging begins with AES-256-CTR re-encryption (client keys ↔ relay keys).
- If WebSocket is unavailable, the proxy tries the Cloudflare proxy path
(
wss://kwsN.{cf-domain}/apiws) if--cf-domainis configured — Cloudflare terminates TLS and forwards plain WebSocket traffic to Telegram. - If CF proxy is unavailable or not configured, each upstream MTProto proxy is tried in order (generating a fresh client handshake with the upstream's secret so it can route to the correct DC).
- If no upstream proxy is configured or all fail, the proxy falls back to direct TCP on port 443.
- A small pool of pre-connected WebSocket connections is maintained per DC to reduce connection latency for subsequent clients.
Project structure
src/
main.rs — Entry point, CLI parsing, server startup, banner
config.rs — ProxyConfig struct, argument parsing, env-var aliases
crypto.rs — MTProto obfuscation: handshake parsing, relay init generation,
AES-256-CTR key derivation and cipher construction
splitter.rs — MTProto packet splitter for correct WebSocket framing
ws_client.rs — WebSocket client for Telegram DC connections (IP routing + SNI)
pool.rs — Pre-warmed WebSocket connection pool per DC
proxy.rs — Client handler, re-encryption bridge, TCP fallback logic
default_domains.rs — Fetches and deobfuscates the default CF proxy domain list
.cargo/
config.toml — Cross-compilation target presets (commented out)
Configuration via environment
TG_HOST=0.0.0.0
TG_PORT=1443
TG_SECRET=0123456789abcdef0123456789abcdef
TG_POOL_SIZE=4
TG_BUF_KB=256
TG_MAX_CONNECTIONS=64
TG_QUIET=true
TG_VERBOSE=false
TG_CF_DOMAIN=yourdomain.com
TG_CF_PRIORITY=false
TG_CF_BALANCE=false
TG_DEFAULT_DOMAINS=false
TG_LOG_FILE=/var/log/tg-ws-proxy.log
TG_MTPROTO_PROXY=proxy.example.com:443:ddabcdef1234567890abcdef1234567890
Windows console — no garbled characters
On Windows the console does not enable ANSI/VT colour codes by default, which
caused log lines to show symbols like ←[32m around the log level. This is
fixed: ANSI escape codes are automatically disabled when running on Windows or
when stderr is not a terminal (e.g. output is piped or redirected).
If you prefer completely clean logs or want to capture them to a file, use
--log-file:
# or
&&
License
MIT