High-performance Telegram MTProxy in Go with TLS fronting, Telegram Middle-End, and native WEB protocol support
Quick Install • Features • Installation • Quick Start • Configuration • Middle-End • WEB Proxy • Docker • Nginx + LE • Performance
English | Русский
Telegram blocked in your country? telEgo's TLS fronting makes your proxy indistinguishable from regular HTTPS traffic to censors. Get started in 2 minutes
Before installation, point a DNS hostname at the VPS.
Make sure that Docker is available and public TCP ports 80 and 443 are free. Then run:
curl -fsSL https://raw.githubusercontent.com/Scratch-net/telego/main/examples/gateway/install.sh \
| sh -s -- --domain proxy.example.com --email admin@example.comThe installer configures Telego, Nginx, the TLS certificate, automatic renewal, and persistent host storage.
If you already run Telego, copy the existing users into the generated [secrets] section.
Read the managed gateway guide for updates, backups, carrier selection, and removal.
- Event-driven I/O — Built on gnet with epoll/kqueue for maximum efficiency
- Telegram Middle-End — Routes authenticated MTProxy and native WEB streams through persistent gnet link pools
- Native WEB Proxy — Optional gnet HTTPS or WebSocket carrier for Telegram Desktop behind Nginx
- Zero-copy relaying — Direct buffer manipulation without intermediate copies
- Buffer pooling — Striped sync.Pool design eliminates allocations in hot paths
- Optimized TCP —
TCP_NODELAY,TCP_QUICKACK, 64KB buffers,SO_REUSEPORT
- TLS Fronting — Fetches real certificates from mask host for perfect camouflage
- Probe Resistance — Forwards unrecognized clients to mask host (indistinguishable from HTTPS)
- Replay Protection — 64-shard LRU cache with TTL expiration (hashicorp/golang-lru)
- Key Zeroization — Sensitive data (session IDs, random bytes) zeroed on connection close
- Desync Detection — Detects crypto state divergence via abnormal frame sizes
- Dual Protocol Support — FakeTLS (ee) and raw Obfuscated2 (dd) with auto-detection on single port
- Dynamic Record Sizer (DRS) — Outbound TLS ApplicationData starts with 1369-byte records and ramps to full size after 8 records or 128 KB, mimicking Chrome/Firefox steady-state record patterns
- Split-TLS — First outbound ApplicationData record is emitted as a 1-byte record to defeat passive signatures keyed on the first record
- Profile-matched cert record — The fake certificate record in the FakeTLS ServerHello is sized to match the mask host's real certificate record, so the accept path and the probe (splice) path present identical TLS record lengths
- Post-quantum key share — When a client offers the hybrid X25519MLKEM768 group, the synthetic ServerHello answers with a matching key share instead of downgrading to classical x25519 (a passive group-downgrade tell)
- SNI-following mask — Optional safelist of domains; an unrecognized probe whose SNI is safelisted is forwarded to that domain's own server, so the on-wire conversation matches the SNI it claimed
- Multi-user Support — Named secrets with per-user tracking and logging
- ME Link Repair — Replaces failed physical links without moving healthy bindings or rebuilding every DC pool
- Connection Tracking — Unique connection IDs for easy log correlation
- Connection Limits — Per-IP connection limits and per-user IP limits with smart blocking (only blocks IPs with active connections when evicted, allowing legitimate reconnections)
- Prometheus Metrics — Per-user connection counts, traffic, and blocked IP statistics
- DC Probing — Automatic RTT-based DC address sorting at startup
- Config Hot-Reload — SIGHUP and file watching for runtime config changes
- Clock Self-Correction — Optional startup sync of the handshake time-skew window from a remote HTTP
Dateheader, so a wrong server clock doesn't reject every client - Stale-Connection Recovery — Optional close of relays whose server reply goes unanswered by the client, recovering iOS clients stuck on "Updating" after a long background
- Graceful Shutdown — Clean connection draining on SIGTERM/SIGINT
- Structured Logging — JSON and text output with configurable levels
- Unix Socket Support — Bind to Unix sockets for reverse proxy setups
- PROXY Protocol — Accept v1/v2 headers from HAProxy/nginx to preserve client IPs
- SOCKS5 Upstream — Route direct DC traffic, ME links, and artifact requests through a SOCKS5 proxy
git clone https://github.com/Scratch-net/telego.git
cd telego
make buildDownload from Releases.
go install github.com/scratch-net/telego/cmd/telego@latest1. Generate a secret:
telego generate www.google.com
# secret=0123456789abcdef0123456789abcdef <- put this in config
# ee_link=tg://proxy?server=YOUR_IP&port=443&secret=ee... <- FakeTLS (recommended)
# dd_link=tg://proxy?server=YOUR_IP&port=443&secret=dd... <- raw Obfuscated22. Create config.toml:
[general]
bind-to = "0.0.0.0:443"
[secrets]
alice = "0123456789abcdef0123456789abcdef"
[tls-fronting]
mask-host = "www.google.com"3. Run:
telego run -c config.toml -lThe -l flag prints Telegram proxy links with auto-detected public IP.
telEgo supports two MTProxy protocol variants on a single port with auto-detection:
| Mode | Secret Prefix | Description | Best For |
|---|---|---|---|
| FakeTLS (ee) | ee... |
TLS-wrapped Obfuscated2 | Maximum stealth, censorship bypass |
| Raw (dd) | dd... |
Plain Obfuscated2 | Compatibility, lower overhead |
FakeTLS (ee) wraps traffic in TLS 1.3 records, making it indistinguishable from HTTPS. The secret includes the mask hostname for SNI validation. Unrecognized connections are forwarded to the mask host for probe resistance.
Raw (dd) sends Obfuscated2 directly without TLS wrapping. Lower overhead but easier to fingerprint. Useful for compatibility with older clients or when TLS fronting isn't needed.
Both modes use the same 16-byte secret key. The ee or dd prefix in the client link determines which mode the client uses. telEgo auto-detects the protocol from the first bytes of each connection.
Telegram Middle-End (ME) is Telegram's upstream transport for MTProxy servers. Telego routes authenticated MTProxy and native WEB streams through ME.
Both the public listener and the ME link engine use gnet. Telego keeps four physical links for each signed Telegram DC.
Each ME request uses the public source IP of its selected link. This behavior supports SOCKS5 egress and hosts with multiple public IPs.
Telegram proxy registration and proxy-tag are optional. Without a tag, Telego sends untagged ME requests and ME still works.
ME stays off by default because it changes the outbound topology and reserves persistent links and bounded queues. An upgrade does not enable these requirements silently.
Before you enable ME, allow HTTPS to core.telegram.org and TCP to the signed ME endpoints. Private direct sockets also need UDP STUN access or a correct nat-ip. Any NAT on direct links must preserve TCP source ports.
Add this section to enable ME:
[middle-end]
enabled = true
# proxy-tag = "0123456789abcdef0123456789abcdef" # Optional tag issued by Telegram
# socks5 = "127.0.0.1:1080"
# nat-ip = "YOUR_PUBLIC_IP" # Usually empty. Automatic STUN supports Docker bridge networks.Restart Telego after a change to this section. If the section is absent or enabled is false, ME stays disabled.
Telego replaces a failed link in place. Healthy bindings and healthy DC pools stay on their existing physical links.
Before ME is ready, the direct DC path stays available. If ME cannot accept a binding, the direct DC path also stays available.
Each connection keeps its selected route until it closes.
Read the Middle-End operator guide for topology, failure behavior, limits, metrics, and proxy configuration.
Telego includes an optional WEB proxy for Telegram Desktop. It uses a separate private gnet HTTP/1.1 listener behind Nginx.
Nginx keeps the real TLS certificate and the ordinary website. The existing MTProxy listener stays on public port 443.
Add this section to enable WEB for existing secrets:
[web-proxy]
enabled = true
hostname = "proxy.example.com"
carrier = "https-lanes"
bind-to = "127.0.0.1:8080"
trusted-proxy-cidrs = ["127.0.0.1/32"]The carrier value selects one of four transports:
| Value | Transport | Requirements |
|---|---|---|
https |
One serialized fetch and long-poll carrier | This value is the default when carrier is empty or absent. |
https-lanes |
One fetch and long-poll lane for each Telegram stream | Enable HTTP/2 on the public Nginx server. This value is the conservative recommendation until comparative benchmarks exist. |
websocket |
One multiplexed WebSocket for the WEB session | Forward HTTP/1.1 Upgrade and Connection headers to Telego. |
websocket-lanes |
One WebSocket for each Telegram stream | Use this value for the official WebSocket lane option. Forward HTTP/1.1 upgrade headers to Telego. |
Current Telegram Desktop manages the WEB carrier. The user does not need to open or keep a browser tab.
In WebSocket modes, the carrier opens same-host wss connections through Nginx. In HTTPS modes, it uses fetch requests and long polls.
You must route every request from the Nginx TLS server to this listener. This rule prevents carrier headers from bypassing Telego.
The setup uses two private fallback statuses.
- Status 418 preserves an ordinary website request.
- Status 419 removes carrier credentials before public-site delivery.
Run Telego with -l to print the plain and dd WEB links. Telego derives both links from each existing base secret.
Use --web-host to print WEB links when you generate a new secret:
telego generate www.google.com --web-host proxy.example.comThe same command works through Docker because the image runs Telego as its entry point:
docker run --rm scratchnet/telego:latest \
generate www.google.com --web-host proxy.example.comThe positional hostname is the FakeTLS mask hostname. The --web-host value is the public WEB proxy hostname.
The --web-host value must match [web-proxy].hostname and the TLS certificate in Nginx.
Read the native WEB proxy setup guide for the complete Nginx configuration, Docker setup, and rollback procedure.
For a new VPS, use the quick managed install. Read the gateway guide for full instructions.
Use the manual example for an existing website or a custom Nginx layout.
The WEB configuration is inactive by default. Configurations without [web-proxy] continue to use the existing MTProxy startup path.
[general]
# Network binding (TCP or Unix socket)
bind-to = "0.0.0.0:443"
# bind-to = "/run/telego/telego.sock" # Unix socket
# Log level: trace, debug, info, warn, error
log-level = "info"
# Accept incoming PROXY protocol headers (from HAProxy/nginx)
# proxy-protocol = false
# Maximum connections per IP (0 = unlimited)
# Applies to ALL connections (including unauthenticated) from a single IP
# max-connections-per-ip = 100
# Max time for handshake before dropping connection (default: 5s)
# handshake-timeout = "5s"
# Maximum unique IPs per user (0 = unlimited)
# Prevents secret sharing - limits how many devices/locations can use one secret
# max-ips-per-user = 3
# How long blocked IPs stay blocked (for max-ips-per-user)
# ip-block-timeout = "5m"
# Correct a skewed server clock at startup from a remote HTTP Date header
# (offset clamped to +-1 day). A wrong VPS clock otherwise rejects every client.
# clock-sync-url = "https://www.cloudflare.com"
# Named secrets (hex format, 32 chars = 16 bytes)
# Generate with: telego generate <hostname>
[secrets]
user1 = "0123456789abcdef0123456789abcdef"
user2 = "fedcba9876543210fedcba9876543210"
# TLS fronting configuration
[tls-fronting]
mask-host = "www.google.com" # Host to mimic (SNI validation, proxy links)
# mask-port = 443 # Port for mask-host (default: 443)
# cert-host = "127.0.0.1" # Where to fetch TLS cert (default: mask-host)
# cert-port = 8443 # Cert fetch port (default: mask-port)
# splice-host = "127.0.0.1" # Forward unrecognized clients here (default: mask-host)
# splice-port = 8080 # Splice port (default: mask-port)
# splice-proxy-protocol = 1 # PROXY protocol to splice: 0=off, 1=v1(text), 2=v2(binary)
# splice-idle-timeout = "30s" # Idle timeout for spliced connections (default: 30s)
# fake-cert-size = 0 # Fake cert record size; 0 = auto-match mask host (clamp 256..16384)
# mask-sni-safelist = ["www.microsoft.com"] # Probes with these SNIs front to that domain (off = empty)
# Anti-DPI record shaping (proxy -> client direction)
# enable-drs = true # Probe-then-ramp record sizing (1369 -> 16384 bytes after 8 records or 128 KB)
# enable-split-tls = true # Emit first ApplicationData record as 1 byte
# Native Telegram Desktop WEB proxy (optional; requires Nginx with real TLS)
[web-proxy]
enabled = false
# carrier = "https-lanes" # Conservative recommendation. Requires public HTTP/2.
# Other values: https, websocket, websocket-lanes
# hostname = "proxy.example.com" # Required public certificate hostname
# bind-to = "127.0.0.1:8080" # Private HTTP/1.1 listener
# backend = "127.0.0.1:443" # Derived TCP or Unix MTProxy backend
# trusted-proxy-cidrs = ["127.0.0.1/32"] # Nginx peers allowed to send X-Forwarded-For
# num-event-loops = 0 # 0 = automatic
# Telegram Middle-End transport (optional; requires a restart)
[middle-end]
enabled = false
# proxy-tag = "0123456789abcdef0123456789abcdef" # Optional tag issued by Telegram
# socks5 = "127.0.0.1:1080" # Defaults to [upstream].socks5
# socks5-username = "proxy-user"
# socks5-password = "proxy-password"
# artifact-proxy = "http://127.0.0.1:3128" # Overrides the artifact proxy only
# nat-ip = "YOUR_PUBLIC_IP" # Usually empty. Overrides automatic STUN.
# max-connections = 0 # 0 = 10,000. An override can only reduce it.
# queue-budget-mb = 0 # 0 = 32 MiB plus 16 KiB
# Performance tuning (all optional)
[performance]
prefer-ip = "prefer-ipv4" # prefer-ipv4, prefer-ipv6, only-ipv4, only-ipv6
idle-timeout = "5m" # Connection idle timeout (hot-reloadable)
num-event-loops = 0 # 0 = auto (all CPU cores)
# client-silence-close = "12s" # Close relays whose server reply stays unanswered (fixes iOS "Updating"); 0 = off
# Upstream (DC connection) settings
[upstream]
# socks5 = "127.0.0.1:1080" # Route DC traffic through SOCKS5 proxy
# Prometheus metrics (optional)
[metrics]
# bind-to = "127.0.0.1:9090" # Metrics endpoint (empty = disabled)
# path = "/metrics" # Metrics pathtelego run Start the proxy server
-c, --config Path to config file (required)
-b, --bind Override bind address
-l, --link Print Telegram proxy links on startup (both ee and dd)
telego generate <mask-host> [--web-host <hostname>]
Generate a new secret
Print ee (FakeTLS) and dd (raw) MTProxy links
--web-host <hostname> Print plain and dd WEB proxy links
telego version Show version information
docker run -d \
--name telego \
-p 443:443 \
-v /path/to/config.toml:/config.toml \
scratchnet/telego:latestversion: '3.8'
services:
telego:
image: scratchnet/telego:latest
container_name: telego
restart: unless-stopped
ports:
- "443:443"
volumes:
- ./config.toml:/config.toml:ro
cap_add:
- NET_BIND_SERVICEdocker build -f dist/Dockerfile.build -t telego .
docker run -d -p 443:443 -v ./config.toml:/config.toml telegotelEgo can run behind HAProxy or nginx using Unix sockets and PROXY protocol:
config.toml:
[general]
bind-to = "/run/telego/telego.sock"
proxy-protocol = true
max-connections-per-ip = 100 # DoS protection
max-ips-per-user = 3 # Sharing protection
[secrets]
user1 = "..."
[tls-fronting]
mask-host = "www.google.com"HAProxy example:
backend telego
mode tcp
server telego /run/telego/telego.sock send-proxy-v2
nginx example:
upstream telego {
server unix:/run/telego/telego.sock;
}
server {
listen 443;
proxy_pass telego;
proxy_protocol on;
}Some censorship systems (e.g., TSPU) validate the TLS certificate against the SNI hostname. If your proxy gets blocked on mobile or connects intermittently, you may need a real certificate for your domain instead of borrowing one from a mask host.
The deploy-nginx.sh script sets up nginx with a Let's Encrypt certificate alongside telego in Docker:
Port 443 (host) --> telego (MTProxy TLS fronting)
|-- valid client --> Telegram DCs
|-- cert fetch --> nginx:8443 (real LE cert)
+-- probe/unknown --> nginx:8443 (decoy website)
Port 80 (host) --> nginx (ACME challenges + HTTPS redirect)
Censors and probes see a valid certificate for your domain and a real HTTPS website.
- VPS with Docker
- A domain pointing to your VPS IP (e.g., via FreeDNS)
- Ports 80 and 443 free
- Telego config file already in place
./deploy-nginx.sh -d <domain> -e <email> [options]| Flag | Description | Default |
|---|---|---|
-d |
Domain name (required) | |
-e |
Email for Let's Encrypt (required) | |
-D |
Telego directory | ~/telego |
-c |
Telego config filename | config.toml |
-p |
Telego port | 443 |
Example:
./deploy-nginx.sh -d myproxy.example.com -e admin@example.com -c config.toml- If no telego config exists, creates one with the correct
[tls-fronting]settings and exits -- add your secrets and re-run - Stops any existing
telegoandtelego-nginxcontainers - Obtains a Let's Encrypt certificate (standalone mode, skips if cert exists)
- Starts nginx (port 80 for ACME, internal port 8443 with the real cert)
- Pulls and starts telego on a shared Docker network with nginx
- Adds a daily cron job for certificate renewal
On a fresh server, the first run creates the config and exits:
./deploy-nginx.sh -d myproxy.example.com -e admin@example.com
# Config created at: ~/telego/config.toml
# Add at least one secret before running againGenerate a secret and add it to the config:
docker run --rm scratchnet/telego:latest generate myproxy.example.com
# Paste the hex string into [secrets] section of config.tomlThen run the script again to deploy:
Install as a systemd service:
sudo make install CONFIG=/etc/telego/config.toml
sudo systemctl enable telego
sudo systemctl start telegoService file is installed to /etc/systemd/system/telego.service.
telEgo supports runtime configuration reloading without restart:
Via SIGHUP:
kill -HUP $(pidof telego)Automatic: Config file changes are detected via fsnotify (Linux inotify).
Hot-reloadable fields:
log-level— Applied immediatelyidle-timeout— Applies to new connections
Require restart:
bind-to,secrets,tls-fronting.*,proxy-protocol,web-proxy.*,middle-end.*,max-connections-per-ip,max-ips-per-user,handshake-timeout
Tested on Intel i9-12900K, Linux 6.6:
| Benchmark | Throughput | Allocations |
|---|---|---|
| Raw TCP loopback | 6.0 GB/s | 0 B/op |
| AES-CTR encrypt | 10.5 GB/s | 0 B/op |
| AES-CTR encrypt+decrypt | 5.3 GB/s | 0 B/op |
| Full pipeline (TLS+O2) | 4.6 GB/s | 5 B/op |
| TLS frame parse (pooled) | 35.5 GB/s | 0 B/op |
| Replay cache lookup | 40 ns | 32 B/op |
- Striped locking — 64-shard replay cache, 64-shard user IP limiter
- Buffer pools — 64KB DC buffers, 16KB TLS record buffers
- Zero-copy crypto — XORKeyStream directly into output buffers
- Batched writes — Multiple TLS records coalesced into single syscall
- Lock-free state — Atomic state machine for connection handling
- Backpressure — Flow control with hysteresis (soft/hard limits) prevents OOM on slow clients without oscillation
Connections are tracked with unique IDs for easy correlation:
INF gnet proxy started on 0.0.0.0:443
INF Connection limiter enabled: max 100 connections per IP
INF User IP limiter enabled: max 3 IPs per user, block timeout 5m0s
INF [#1:alice] 203.0.113.5:54321 -> DC 2
INF [#2:bob] 198.51.100.10:12345 -> DC 4
INF [#1:alice] DC 2 closed (45.2s) (active: 1)
WRN [#2:bob] DC 4 closed (30s): i/o timeout (active: 0)
#N— Connection ID (incremental, unique per session)#N:user— Connection ID with matched secret name- Duration shown on close
- Errors on authenticated connections logged as WARN
telEgo exposes Prometheus metrics when configured:
[metrics]
bind-to = "127.0.0.1:9090"| Metric | Type | Description |
|---|---|---|
telego_connections_active |
Gauge | Active connections per user |
telego_ips_active |
Gauge | IPs with active connections per user |
telego_ips_tracked |
Gauge | Unique IPs in LRU cache per user |
telego_ips_blocked |
Gauge | Currently blocked IPs per user |
telego_blocked_total |
Counter | Total IP block events per user |
telego_traffic_in_bytes_total |
Counter | Bytes received from clients |
telego_traffic_out_bytes_total |
Counter | Bytes sent to clients |
telego_handshake_failures_total |
Counter | MTProxy handshake failures by stage |
telego_web_websocket_connections_active |
Gauge | Active WEB WebSocket carrier connections. This metric has no labels. |
telego_web_sessions_active |
Gauge | Active WEB sessions |
telego_web_streams_active |
Gauge | Active WEB backend streams |
telego_web_backend_dials_active |
Gauge | WEB backend dials in progress |
telego_web_pending_bytes |
Gauge | Bytes charged to WEB pending queues |
telego_web_pending_items |
Gauge | Items charged to WEB pending queues |
telego_web_sessions_created_total |
Counter | WEB sessions created |
telego_web_sessions_closed_total |
Counter | WEB sessions closed by reason |
telego_web_carrier_retries_total |
Counter | WEB retries and replays by operation |
telego_web_backpressure_total |
Counter | WEB backpressure events by operation |
telego_middleend_admitting |
Gauge | Whether an active ME generation accepts new bindings |
telego_middleend_links |
Gauge | ME links by generation role, signed DC, and state |
telego_middleend_slot_failure_total |
Counter | Physical-link failures |
telego_middleend_slot_failure_affected_bindings_total |
Counter | Bindings terminated by physical-link failures |
telego_middleend_slot_repair_total |
Counter | Physical-link replacement results |
telego_middleend_frontend_routes_active |
Gauge | Active ME and direct-fallback routes |
telego_middleend_frontend_route_commits_total |
Counter | Lifetime ME and direct-fallback route selections |
Connection, IP, block, and traffic metrics include a user label. Diagnostic metrics use the labels in the table.
telego_web_websocket_connections_active has no labels. It counts accepted WebSocket carriers after the HTTP upgrade response leaves Telego.
┌─────────────┐ ┌───────────────────────────────┐
│ Telegram │────▶│ telEgo │
│ client │◀────│ ee, dd, and native WEB input │
└─────────────┘ └───────────────┬───────────────┘
│ authenticated session
┌──────────┴──────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Telegram ME │ │Direct fallback│
│ gnet link pool│ │ connection │
└───────┬───────┘ └───────┬───────┘
└──────────┬───────────┘
▼
┌─────────────┐
│ Telegram DC │
└─────────────┘
Unauthenticated FakeTLS probes ─────────────▶ mask host
PRs are welcome! Please ensure:
- Tests pass:
go test -race ./... - Benchmarks don't regress:
go test -bench=. ./...
This project was inspired by and builds upon ideas from:
- mtg by Sergey Arkhipov — The original Go MTProxy implementation
- mtprotoproxy by Alexander Borzunov — Python reference implementation
- telemt — High-performance Rust MTProxy implementation
