tg-ws-proxy-rs
Telegram MTProto WebSocket Bridge Proxy — a Rust vibecoded port of Flowseal/tg-ws-proxy
2.5K
Currently https://github.com/valnesfjord/tg-ws-proxy-rs is shadowed in GitHub. You can download linux images from this hub. We are already working on restoring the repository.
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)
↘ CF Worker (*.workers.dev) → Telegram DC (TCP tunnel via Cloudflare)
↘ upstream MTProto proxy → Telegram DC (WS fallback)
↘ direct TCP :443 → Telegram DC (last resort)
| 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) |
Download from the Releases page.
# Debug build
cargo build
# Optimised release build
cargo build --release
# Static binary for Linux x86_64 (e.g. for Docker scratch images)
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
The release binary is at target/release/tg-ws-proxy (or
target/<target>/release/tg-ws-proxy for cross-compiled targets).
cargo-zigbuildcargo-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
pip install ziglang # or: brew install zig
cargo install cargo-zigbuild
# Add all required Rust targets in one shot
rustup target add \
x86_64-unknown-linux-musl \
aarch64-unknown-linux-musl \
armv7-unknown-linux-musleabihf \
mipsel-unknown-linux-musl \
x86_64-apple-darwin \
aarch64-apple-darwin \
x86_64-pc-windows-gnu
# Build for all platforms
cargo zigbuild --release --target x86_64-unknown-linux-musl # Linux x86-64 (musl static)
cargo zigbuild --release --target aarch64-unknown-linux-musl # Linux / OpenWrt ARM64
cargo zigbuild --release --target armv7-unknown-linux-musleabihf # OpenWrt ARMv7
cargo zigbuild --release --target mipsel-unknown-linux-musl # OpenWrt MIPS LE
cargo zigbuild --release --target x86_64-apple-darwin # macOS Intel
cargo zigbuild --release --target aarch64-apple-darwin # macOS Apple Silicon
cargo zigbuild --release --target x86_64-pc-windows-gnu # Windows x86-64
Note: Building macOS targets (
*-apple-darwin) requires the macOS SDK (XCode Command Line Tools). On Linux you can useosxcross to supply the SDK and then setSDKROOT/MACOSX_DEPLOYMENT_TARGETappropriately before runningcargo zigbuild.
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 (repeatable / comma-separated for per-user secrets) |
--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) |
--cf-worker-domain <DOMAIN> | — | Cloudflare Worker domain(s) for TCP-tunnel fallback, comma-separated/repeatable (see Cloudflare Worker) |
--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 and --cf-worker-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) |
--outbound-proxy <URL> | — | Proxy for all outgoing connections: http://, socks5://, or socks5h://; https:// proxy URLs are not supported |
--no-outbound-proxy | off | Ignore standard outbound proxy environment variables |
--no-proxy <LIST> | — | Comma-separated host bypass list for --outbound-proxy |
--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_WORKER_DOMAIN, TG_CF_PRIORITY,
TG_CF_BALANCE, TG_DEFAULT_DOMAINS, TG_OUTBOUND_PROXY, TG_NO_OUTBOUND_PROXY, TG_NO_PROXY).
When TG_OUTBOUND_PROXY is not set, standard HTTPS_PROXY, ALL_PROXY,
HTTP_PROXY and NO_PROXY environment variables are also honored. https://
proxy URLs are skipped when discovered from the standard environment if a later
supported http://, socks5://, or socks5h:// fallback is present.
# Standard run (random secret, DC 2 + 4)
tg-ws-proxy
# Custom port and extra DCs
tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
# With upstream MTProto proxy fallback
tg-ws-proxy --mtproto-proxy proxy.example.com:443:ddabcdef1234567890abcdef1234567890
# With Cloudflare proxy domain (WS fallback via Cloudflare CDN)
tg-ws-proxy --cf-domain yourdomain.com
# With Cloudflare Worker (free workers.dev TCP tunnel fallback)
tg-ws-proxy --cf-worker-domain random-symbols-1234.username.workers.dev
# CF proxy only: omit --dc-ip so CF proxy handles all DCs
tg-ws-proxy --cf-domain yourdomain.com --cf-priority
# Use default CF domains from GitHub — no Cloudflare setup required
tg-ws-proxy --default-domains
# Default domains + CF priority (try CF first, fall back to direct WS)
tg-ws-proxy --default-domains --cf-priority
# Default domains + your own domain (yours goes first)
tg-ws-proxy --cf-domain yourdomain.com --default-domains
# Multiple CF domains (tried in order) with CF priority over direct WS
tg-ws-proxy --cf-domain proxy.net,example.com --cf-priority
# Multiple CF domains with round-robin load balancing
tg-ws-proxy --cf-domain proxy.net,example.com --cf-balance
# CF balance + priority: round-robin across CF domains, tried before direct WS
tg-ws-proxy --cf-domain proxy.net,example.com --cf-balance --cf-priority
# CF priority: CF proxy is tried first, falls back to direct WS on failure
tg-ws-proxy --dc-ip 2:149.154.167.220 --cf-domain yourdomain.com --cf-priority
# Multiple upstream proxies (tried in order until one succeeds)
tg-ws-proxy \
--mtproto-proxy proxy.example.com:443:ddabcdef1234567890abcdef1234567890 \
--mtproto-proxy other.example.net:8888:dddeadbeef01234567deadbeef01234567
# Route all outbound connections through an HTTP CONNECT proxy
tg-ws-proxy --outbound-proxy http://user:[email protected]:3128
# Route all outbound connections through a SOCKS5 proxy with remote DNS
tg-ws-proxy --outbound-proxy socks5h://user:[email protected]:1080
# Router deployment: listen on all interfaces, let all LAN devices use the proxy
tg-ws-proxy --host 0.0.0.0
# Public home server: inbound ee FakeTLS, backend still WSS to Telegram Web
tg-ws-proxy --host 0.0.0.0 --port 443 --listen-faketls-domain www.yandex.ru
# Equivalent: pass a full ee secret directly
tg-ws-proxy --host 0.0.0.0 --port 443 --secret ee<32-hex-key><hex-encoded-domain>
# Verbose logging
tg-ws-proxy -v
# Log to a file instead of stderr (no garbled ANSI codes — useful on Windows)
tg-ws-proxy --log-file proxy.log
# All options via environment variables (useful for Docker / systemd)
TG_PORT=1443 TG_SECRET=deadbeef... tg-ws-proxy
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.
To issue separate credentials per user, pass multiple --secret values:
tg-ws-proxy --host 0.0.0.0 --port 443 \
--secret 11111111111111111111111111111111 \
--secret 22222222222222222222222222222222
The proxy will accept all configured secrets and print an individual tg://
link for each additional secret.
For public home servers where DPI blocks raw inbound MTProto, enable inbound FakeTLS:
tg-ws-proxy --host 0.0.0.0 --port 443 --listen-faketls-domain www.yandex.ru
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.
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)
tg-ws-proxy --mtproto-proxy proxy.example.com:443:ddabcdef1234567890abcdef1234567890
# FakeTLS proxy (ee prefix — domain-fronting transport)
tg-ws-proxy --mtproto-proxy proxy.example.com:443:ee<32-hex-key><hex-encoded-hostname>
# Multiple proxies (tried in order until one succeeds)
tg-ws-proxy \
--mtproto-proxy proxy.example.com:443:ddabcdef1234567890abcdef1234567890 \
--mtproto-proxy other.example.net:8888:dddeadbeef01234567deadbeef01234567
# Or via environment variable (comma-separated)
TG_MTPROTO_PROXY="proxy.example.com:443:ddabcdef1234...,other.example.net:8888:dddeadbeef..." tg-ws-proxy
ℹ️ 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.
If the host running tg-ws-proxy-rs can reach the internet only through another
proxy, route all outgoing connections through --outbound-proxy:
tg-ws-proxy --outbound-proxy http://user:[email protected]:3128
tg-ws-proxy --outbound-proxy socks5://user:[email protected]:1080
tg-ws-proxy --outbound-proxy socks5h://user:[email protected]:1080
socks5:// resolves hostnames locally before sending the CONNECT request.
socks5h:// sends the hostname to the SOCKS proxy and lets it resolve DNS
remotely. The setting applies to direct Telegram WS, Cloudflare proxy,
Cloudflare Worker, upstream MTProto proxies, direct TCP fallback, --check,
and the --default-domains fetch.
The same setting can be supplied through TG_OUTBOUND_PROXY. If it is not
set, standard HTTPS_PROXY, ALL_PROXY, HTTP_PROXY variables are used in
that order. https:// proxy URLs from those standard environment variables
are ignored when a later supported fallback exists; an explicit
--outbound-proxy https://... remains an error.
Use TG_OUTBOUND_PROXY=direct (also accepts none or off) or
--no-outbound-proxy to disable environment proxy discovery explicitly. Use
--no-proxy / TG_NO_PROXY / NO_PROXY to bypass the proxy for specific
hosts. The bypass list follows standard NO_PROXY domain semantics: bare
domain entries such as example.com may match both example.com and
subdomains such as api.example.com. It also accepts *, suffix entries such
as .example.com or *.example.com, IP/CIDR entries such as 127.0.0.0/8,
and bracketed IPv6 entries. Host and IP entries may include a port, for example
example.com:443 or [2001:db8::1]:443; when a port is present, only that
target port bypasses the proxy.
TG_OUTBOUND_PROXY=socks5h://user:[email protected]:1080 \
TG_NO_PROXY=localhost,127.0.0.1,127.0.0.0/8,.lan,example.com:443 \
tg-ws-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
tg-ws-proxy --cf-domain yourdomain.com
# Multiple domains (tried in order, first has highest priority)
tg-ws-proxy --cf-domain primary.net,backup.com
# Multiple domains with round-robin load balancing
tg-ws-proxy --cf-domain primary.net,backup.com --cf-balance
# CF-only mode: omit --dc-ip so CF proxy handles all DCs
tg-ws-proxy --cf-domain yourdomain.com
# CF priority: try CF proxy before direct WS, with WS as fallback
tg-ws-proxy --dc-ip 2:149.154.167.220 --cf-domain yourdomain.com --cf-priority
# Or via environment variable
TG_CF_DOMAIN=yourdomain.com tg-ws-proxy
The proxy will try the CF path as a fallback after direct WebSocket fails.
With --cf-priority, the CF proxy is tried before direct WebSocket for all
DCs.
--cf-balance — round-robin load balancingWhen 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:
tg-ws-proxy --cf-domain d1.example.com,d2.example.com,d3.example.com --cf-balance
# 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
tg-ws-proxy --cf-domain d1.example.com,d2.example.com --cf-balance --cf-priority
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 |
|---|---|
kws1 | 149.154.175.50 |
kws1-1 | 149.154.175.50 |
kws2 | 149.154.167.51 |
kws2-1 | 149.154.167.51 |
kws3 | 149.154.175.100 |
kws3-1 | 149.154.175.100 |
kws4 | 149.154.167.91 |
kws4-1 | 149.154.167.91 |
kws5 | 149.154.171.5 |
kws5-1 | 149.154.171.5 |
kws203 | 91.105.192.100 |
kws203-1 | 91.105.192.100 |
See docs/CfProxy.md for full instructions.
Cloudflare Worker mode is an alternative to --cf-domain when you do not own
a domain. Deploy the Worker script from docs/CfWorker.md,
copy its *.workers.dev domain, and pass it to the proxy:
tg-ws-proxy --cf-worker-domain random-symbols-1234.username.workers.dev
Multiple Worker domains are supported (comma-separated or repeated flag).
With --cf-balance, each new connection starts from a different Worker and
falls back to the remaining Workers if the first one fails.
tg-ws-proxy --cf-worker-domain w1.user.workers.dev,w2.user.workers.dev --cf-balance
Or via environment variable:
TG_CF_WORKER_DOMAIN=random-symbols-1234.username.workers.dev tg-ws-proxy
The Worker accepts an outer WebSocket connection from tg-ws-proxy-rs, opens a
raw TCP connection to the selected Telegram DC IP, and forwards WebSocket
message payloads as TCP bytes:
tg-ws-proxy-rs → wss://<worker>/apiws?dst=<dc-ip>&dc=<dc>&media=<0|1>
Cloudflare Worker → TCP <dc-ip>:443
For DCs with a configured direct WebSocket target, direct WS is tried first and the Worker is used only after that path fails. For DCs without a direct WS target, the Worker is tried before the regular Cloudflare proxy/default domains and the remaining fallbacks.
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
tg-ws-proxy --default-domains
# Enable CF priority so CF path is tried first
tg-ws-proxy --default-domains --cf-priority
# Combine with your own domain (yours gets highest priority)
tg-ws-proxy --cf-domain yourdomain.com --default-domains
# Test the fetched domains before starting the proxy
tg-ws-proxy --default-domains --check
Or via environment variable:
TG_DEFAULT_DOMAINS=true tg-ws-proxy
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).
Run the proxy on your router with --host 0.0.0.0 so it accepts connections
from all LAN devices:
tg-ws-proxy --host 0.0.0.0 --port 1443
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:
tg-ws-proxy --host 0.0.0.0 --link-ip 192.168.1.1
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).
127.0.0.11443 (or your --port)Or use the tg://proxy?... link that is printed on startup.
127.0.0.1:1443.wss://kwsN.web.telegram.org/apiws
(using the DC-specific domain as TLS SNI but routing TCP to the configured
IP).wss://<worker>/apiws?dst=<dc-ip> and tunnel raw TCP traffic to the DC via
the Worker.wss://kwsN.{cf-domain}/apiws) if --cf-domain is configured.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
docs/
CfProxy.md — Cloudflare DNS proxy setup
CfWorker.md — Cloudflare Worker TCP tunnel setup
.cargo/
config.toml — Cross-compilation target presets (commented out)
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_WORKER_DOMAIN=random-symbols-1234.username.workers.dev
TG_CF_PRIORITY=false
TG_CF_BALANCE=false
TG_DEFAULT_DOMAINS=false
TG_OUTBOUND_PROXY=socks5h://user:[email protected]:1080
TG_NO_OUTBOUND_PROXY=false
TG_NO_PROXY=localhost,127.0.0.1
TG_LOG_FILE=/var/log/tg-ws-proxy.log
TG_MTPROTO_PROXY=proxy.example.com:443:ddabcdef1234567890abcdef1234567890
If you prefer completely clean logs or want to capture them to a file, use
--log-file:
tg-ws-proxy --log-file proxy.log
# or
set TG_LOG_FILE=proxy.log && tg-ws-proxy
MIT
Content type
Image
Digest
sha256:8540e5004…
Size
2 MB
Last updated
17 days ago
docker pull valnesfjord/tg-ws-proxy-rs