Русский | فارسی | Tiếng Việt
High-performance MT-Proto proxy for Telegram with DPI resistance, fake-TLS camouflage, and production-grade monitoring.
Features: Fake-TLS (EE mode), Direct-to-DC, Dynamic Record Sizing, IP access control, Prometheus metrics, multi-secret support with labels, static binaries, Docker, ARM64.
Comparison with TelegramMessenger/MTProxy (upstream, abandoned) and the main third-party alternatives.
| Feature | Original | Teleproxy | mtg | telemt |
|---|---|---|---|---|
| Language | C | C | Go | Rust |
| Protocol | ||||
| Fake-TLS (EE mode) | Yes | Yes | Yes | Yes |
| Direct-to-DC mode | No | Yes | Yes | Yes |
| Ad proxy tag | Yes | Yes | No | Yes |
| Multiple secrets | Yes | Yes (up to 16, with labels) | No | Yes |
| Anti-replay protection | Weak | Yes | Yes | Partial |
| Constant-time HMAC | No | Yes | — | Yes |
| DPI resistance | ||||
| Custom TLS backend (TCP splitting) | Yes | Yes | No | Yes |
| Dynamic Record Sizing (DRS) | No | Yes | Yes | No |
| Traffic mimicry (DRS + timing) | No | Yes | Yes | No |
| SOCKS5 upstream proxy | No | No | Yes | Yes |
| Access control | ||||
| IP blocklist / allowlist | No | Yes | Yes | No |
| Per-user unique IP limits | No | No | No | Yes |
| Proxy Protocol v1/v2 | No | No | Yes | Yes |
| Deployment | ||||
| Docker image | ~57 MB | ~8 MB | ~3.5 MB | ~5 MB |
| ARM64 / Apple Silicon | No | Yes | Yes | Yes |
| IPv6 | Yes | Yes | Yes | Yes |
| Multi-worker processes | Yes | Yes | — | — |
| Static binary releases | No | Yes | Yes | Yes |
| RPM packages | No | Yes | No | No |
| Systemd integration | Partial | Yes | — | Yes |
| Monitoring & management | ||||
| Prometheus metrics | No | Yes | Yes | Yes |
| HTTP stats endpoint | Yes | Yes | — | Yes |
| REST management API | No | No | No | Yes |
| Auto config refresh | No | Yes | Yes | Yes |
| Health checks | No | Yes | Yes | Yes |
| Testing & quality | ||||
| Fuzz testing (CI) | No | Yes | No | Partial |
| E2E tests (real Telegram clients) | No | Yes | No | No |
| Static analysis (CI) | No | Yes | Yes | — |
Pre-built static binaries (musl libc, zero dependencies) are available for every release:
# Download (choose amd64 or arm64)
curl -Lo teleproxy https://github.com/teleproxy/teleproxy/releases/latest/download/teleproxy-linux-amd64
chmod +x teleproxy
# Generate a secret
SECRET=$(head -c 16 /dev/urandom | xxd -ps)
# Run in direct mode (simplest — no config files needed)
./teleproxy -S "$SECRET" -H 443 --direct -p 8888 --aes-pwd /dev/nullBinaries are available for linux/amd64 and linux/arm64. SHA256 checksums are published alongside each release.
Install dependencies, you would need common set of tools for building from source, and development packages for openssl and zlib.
On Debian/Ubuntu:
apt install git curl build-essential libssl-dev zlib1g-devOn CentOS/RHEL (not advisable, use packages mentioned above instead):
yum install openssl-devel zlib-devel
yum groupinstall "Development Tools"Clone the repo:
git clone https://github.com/teleproxy/teleproxy
cd teleproxyTo build, simply run make, the binary will be in objs/bin/teleproxy:
make && cd objs/binIf the build has failed, you should run make clean before building it again.
This repository includes a comprehensive test suite. For detailed instructions, see TESTING.md.
To run the tests using Docker:
# Export environment variables (see TESTING.md)
export TELEPROXY_SECRET=...
make test- Obtain a secret, used to connect to telegram servers.
curl --connect-timeout 10 --max-time 30 --retry 3 -fsSL https://core.telegram.org/getProxySecret -o proxy-secret- Obtain current telegram configuration. It can change (occasionally), so we encourage you to update it once per day.
curl --connect-timeout 10 --max-time 30 --retry 3 -fsSL https://core.telegram.org/getProxyConfig -o proxy-multi.conf- Generate a secret to be used by users to connect to your proxy.
head -c 16 /dev/urandom | xxd -ps- Run
teleproxy:
./teleproxy -u nobody -p 8888 -H 443 -S <secret> --http-stats --aes-pwd proxy-secret proxy-multi.conf -M 1... where:
nobodyis the username.teleproxycallssetuid()to drop privilegies.443is the port, used by clients to connect to the proxy.8888is the local port for statistics (requires--http-stats). Likecurl http://localhost:8888/stats. Stats are accessible from private networks (loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) but not from public IPs.<secret>is the secret generated at step 3. Also you can set multiple secrets:-S <secret1> -S <secret2>. Each secret can have an optional label:-S <secret>:family -S <secret>:friends. Labels appear in logs and stats instead of raw secrets, making it easy to identify which secret a connection used. You can also set a per-secret connection limit:-S <secret>:family:1000(see Per-Secret Connection Limits).--aes-pwd proxy-secretpoints to theproxy-secretfile downloaded at step 1, which contains the encryption key used for MTProto key exchange with Telegram DCs.proxy-secretandproxy-multi.confare obtained at steps 1 and 2.1is the number of workers. You can increase the number of workers, if you have a powerful server.
Also feel free to check out other options using teleproxy --help.
Restrict client connections by IP address using CIDR-based blocklist/allowlist files:
./teleproxy ... --ip-blocklist blocklist.txt --ip-allowlist allowlist.txtFile format (one CIDR range per line, # comments allowed):
# Block known scanner ranges
185.220.101.0/24
2001:db8::/32
- Both IPv4 and IPv6 CIDR notation supported
--ip-allowlist: only matching IPs are accepted (whitelist mode)--ip-blocklist: matching IPs are rejected- Files are reloaded on
SIGHUP— update rules without restarting the proxy - Rejected connections are tracked via Prometheus metric
teleproxy_ip_acl_rejected_total
- Generate the link with following schema:
tg://proxy?server=SERVER_NAME&port=PORT&secret=SECRET(or let the official bot generate it for you). - Register your proxy with @MTProxybot on Telegram.
- Set received tag with arguments:
-P <proxy tag> - Enjoy.
By default, Teleproxy routes traffic through Telegram's middle-end (ME) relay servers listed in proxy-multi.conf. Direct mode bypasses the ME relays and connects straight to Telegram data centers, reducing latency and simplifying deployment:
Default: Client → Teleproxy → ME relay (proxy-multi.conf) → Telegram DC
Direct: Client → Teleproxy → Telegram DC
To enable direct mode, use --direct:
./teleproxy -u nobody -p 8888 -H 443 -S <secret> --http-stats --directIn direct mode:
- No
proxy-multi.conforproxy-secretfiles are needed - No config file argument is required
- The proxy connects directly to well-known Telegram DC addresses
- Incompatible with
-P(proxy tag) — promoted channels require ME relays
Teleproxy supports IPv6. To enable it, pass the -6 flag and specify only port numbers to -H (do not include an address like [::]:443).
./teleproxy -6 -u nobody -p 8888 -H 443 -S <secret> --http-stats --aes-pwd proxy-secret proxy-multi.conf -M 1-6enables IPv6 listening. The proxy binds to::(all IPv6 interfaces). On most systems this also accepts IPv4 connections on the same port (dual-stack), unless the kernel forces IPv6-only.-Haccepts a comma-separated list of ports only (e.g.,-H 80,443). Do not pass IP literals to-H.- Binding to a specific IPv6 address is not currently supported. If you must restrict which address is reachable, use a firewall (ip6tables/nftables/security groups).
- You can use either a hostname with an AAAA record or a raw IPv6 address.
- When sharing links, prefer a hostname with an AAAA record. Some clients may not accept raw IPv6 literals in
tg://links.
Examples:
- Hostname:
tg://proxy?server=proxy.example.com&port=443&secret=<secret>(with AAAA record onproxy.example.com). - Raw IPv6 (may not be supported by all clients):
tg://proxy?server=[2001:db8::1]&port=443&secret=<secret>
- Verify the proxy listens on IPv6:
ss -ltnp | grep :443 # Expect to see :::443 among listeners
- Test locally (stats endpoint):
curl -6 http://[::1]:8888/stats
- Ensure IPv6 is enabled on the host:
sysctl net.ipv6.conf.all.disable_ipv6 # should be 0 - Firewalls/security groups must allow IPv6 on the chosen port (separate from IPv4 rules).
- If IPv4 stops working after enabling
-6, your system might enforce IPv6-only sockets (V6ONLY). Checknet.ipv6.bindv6onlyand firewall rules. - Use a hostname with an AAAA record to avoid client parsing issues with IPv6 literals in links.
[Unit]
Description=Teleproxy (IPv6)
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/teleproxy
ExecStart=/opt/teleproxy/teleproxy -6 -u nobody -p 8888 -H 443 -S <secret> --http-stats -P <proxy tag> --aes-pwd proxy-secret proxy-multi.conf -M 1
Restart=on-failure
[Install]
WantedBy=multi-user.target- Ensure Docker daemon has IPv6 enabled and the host has routable IPv6.
- Port publishing must include IPv6 (Docker binds to IPv6 only if daemon IPv6 is enabled). See Docker docs for
daemon.json("ipv6": true,"fixed-cidr-v6": "…/64"). - The provided image’s default entrypoint does not add
-6. To use IPv6, either run Teleproxy on the host, or build/override the container command to include-6.
Teleproxy supports different transport modes that provide various levels of obfuscation:
Due to some ISPs detecting MTProto proxy by packet sizes, random padding is added to packets when this mode is enabled.
Client Setup: Add dd prefix to secret (cafe...babe => ddcafe...babe)
Server Setup: Use -R argument to allow only clients with random padding enabled
EE mode provides enhanced obfuscation by mimicking TLS 1.3 connections, making proxy traffic harder to detect and block.
Server Setup:
-
Add domain configuration: Choose a website that supports TLS 1.3 (e.g.,
www.google.com,www.cloudflare.com)./teleproxy -u nobody -p 8888 -H 443 -S <secret> -D www.google.com --http-stats --aes-pwd proxy-secret proxy-multi.conf -M 1
-
Get domain HEX dump:
echo -n www.google.com | xxd -plain # Output: 7777772e676f6f676c652e636f6d
Client Setup:
Use the format: ee + server_secret + domain_hex
Example:
- Server secret:
cafe1234567890abcdef1234567890ab - Domain:
www.google.com - Domain HEX:
7777772e676f6f676c652e636f6d - Client secret:
eecafe1234567890abcdef1234567890ab7777772e676f6f676c652e636f6d
Quick Generation:
# Generate complete client secret automatically
SECRET="cafe1234567890abcdef1234567890ab"
DOMAIN="www.google.com"
echo -n "ee${SECRET}" && echo -n $DOMAIN | xxd -plainBenefits:
- ✅ Traffic appears as TLS 1.3: Harder to detect and block
- ✅ Works with modern clients: Desktop, mobile, and web clients
- ✅ Domain flexibility: Choose any TLS 1.3-capable domain
- ✅ DPI resistant: Traffic indistinguishable from standard TLS 1.3
Instead of mimicking a public website, you can run your own web server (e.g., nginx) behind Teleproxy with a real TLS certificate for your domain. Non-proxy visitors see a fully functioning HTTPS website, making the server indistinguishable from a normal web server.
How it works:
- Teleproxy listens on port 443
- nginx runs on a non-standard port (e.g., 8443) with a valid certificate for your domain
- The domain's DNS A record points to the Teleproxy server
- Valid Teleproxy clients connect normally; all other traffic is forwarded to nginx
Active probing resistance: Every connection that fails Teleproxy validation — wrong secret, expired timestamp, unknown SNI, replayed handshake, malformed ClientHello, or plain non-TLS traffic — is transparently forwarded to the backend rather than rejected with a TLS error. Anyone probing the server with a standard browser sees a real HTTPS website, making the proxy indistinguishable from a normal web server under active probing.
Dynamic Record Sizing (DRS): TLS connections automatically use graduated record sizes that mimic real HTTPS servers (Cloudflare, Go, Caddy): small MTU-sized records during TCP slow-start (~1450 bytes), ramping to ~4096 bytes, then max TLS payload (~16144 bytes). This defeats statistical traffic analysis that fingerprints proxy traffic by its uniform record sizes. No configuration needed — DRS activates automatically for all TLS connections.
Requirements:
- The backend must support TLS 1.3 (Teleproxy verifies this at startup)
- The
-Dvalue must be a hostname, not a raw IP address. Using an IP address (e.g.,-D 127.0.0.1:8443) breakseesecrets because TLS SNI does not support IP addresses (RFC 6066)
Setup:
-
Configure nginx to listen on a local port with TLS 1.3:
server { listen 127.0.0.1:8443 ssl default_server; server_name mywebsite.com; ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem; ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; root /var/www/html; location / { try_files $uri $uri/ =404; } }
Certificate renewal: Use certbot with DNS-01 challenge (
--preferred-challenges dns). HTTP-01 challenge will not work because Teleproxy occupies port 443. -
Add an
/etc/hostsentry so Teleproxy resolves the domain to loopback (needed when nginx only listens on127.0.0.1):127.0.0.1 mywebsite.comNote: If nginx listens on all interfaces (
0.0.0.0:8443) and the domain's DNS already points to this server, you can skip the/etc/hostsentry. -
Run Teleproxy with the domain and port:
./teleproxy -u nobody -p 8888 -H 443 -S <secret> -D mywebsite.com:8443 --http-stats --aes-pwd proxy-secret proxy-multi.conf -M 1
-
Generate the client
eesecret as usual (usingmywebsite.comas the domain):SECRET="<your_32_hex_secret>" echo -n "ee${SECRET}" && echo -n mywebsite.com | xxd -plain
- Create systemd service file (it's standard path for the most Linux distros, but you should check it before):
nano /etc/systemd/system/teleproxy.service- Edit this basic service (especially paths and params):
[Unit]
Description=Teleproxy
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/teleproxy
ExecStart=/opt/teleproxy/teleproxy -u nobody -p 8888 -H 443 -S <secret> -P <proxy tag> <other params>
ExecStart=/opt/teleproxy/teleproxy -u nobody -p 8888 -H 443 -S <secret> --http-stats -P <proxy tag> <other params>
Restart=on-failure
[Install]
WantedBy=multi-user.target- Reload daemons:
systemctl daemon-reload- Test fresh Teleproxy service:
systemctl restart teleproxy.service
# Check status, it should be active
systemctl status teleproxy.service- Enable it, to autostart service after reboot:
systemctl enable teleproxy.serviceThe simplest way to run Teleproxy - no configuration needed:
docker run -d \
--name teleproxy \
-p 443:443 \
-p 8888:8888 \
--restart unless-stopped \
ghcr.io/teleproxy/teleproxy:latestThe container automatically:
- Downloads the latest proxy configuration from Telegram
- Generates a random secret if none is provided
- Starts the proxy on port 443
Connection Links at Startup:
The container prints ready-to-share connection links in the logs:
docker logs teleproxy
# ===== Connection Links =====
# https://t.me/proxy?server=203.0.113.1&port=443&secret=eecafe...
# =============================If external IP detection fails, links show <YOUR_SERVER_IP> — set EXTERNAL_IP to fix.
For more control, specify environment variables:
docker run -d \
--name teleproxy \
-p 443:443 \
-p 8888:8888 \
-e SECRET=$(head -c 16 /dev/urandom | xxd -ps) \
-e PROXY_TAG=your_proxy_tag_here \
-v teleproxy-data:/opt/teleproxy/data \
--restart unless-stopped \
ghcr.io/teleproxy/teleproxy:latestSECRET: Proxy secret(s) — 32 hex characters each (auto-generated if not provided)- Single:
SECRET=cafe1234567890abcdef1234567890ab - Multiple (comma-separated):
SECRET=secret1,secret2,secret3 - With labels:
SECRET=hex1:family,hex2:friends(see Secret Labels) - Multiple (numbered):
SECRET_1=aabb...,SECRET_2=ccdd...(up toSECRET_16) - If both
SECRETandSECRET_Nare set, all are combined - Maximum 16 secrets (binary limit)
- Single:
SECRET_LABEL_1,SECRET_LABEL_2, ...: Optional labels for numbered secrets (e.g.SECRET_LABEL_1=family). See Secret LabelsSECRET_LIMIT_1,SECRET_LIMIT_2, ...: Optional per-secret connection limits (e.g.SECRET_LIMIT_1=1000). See Per-Secret Connection LimitsPORT: Port for client connections (default: 443)STATS_PORT: Port for statistics endpoint (default: 8888)WORKERS: Number of worker processes (default: 1)PROXY_TAG: Proxy tag from @MTProxybot (optional, for channel promotion)DIRECT_MODE: Connect directly to Telegram DCs instead of through ME relays (true/false, default: false). Incompatible withPROXY_TAG. See Direct-to-DC ModeRANDOM_PADDING: Enable random padding only mode (true/false, default: false)EXTERNAL_IP: Your public IP address for NAT environments (optional)EE_DOMAIN: Domain for EE Mode (Fake-TLS + Padding), e.g.www.google.com. Acceptshost:portfor custom TLS backends (e.g.,mywebsite.com:8443). See Custom TLS BackendIP_BLOCKLIST: Path (inside container) to a file with CIDR ranges to reject (one per line,#comments allowed). Example:--ip-blocklist /opt/teleproxy/blocklist.txtIP_ALLOWLIST: Path (inside container) to a file with CIDR ranges to exclusively allow. When set, only matching IPs are accepted. Both IPv4 and IPv6 CIDR notation supported. Files are reloaded onSIGHUP.
curl http://localhost:8888/statscurl http://localhost:8888/metricsReturns metrics in Prometheus exposition format, ready for scraping. Available on the same --http-stats port, restricted to private networks. Includes per-secret connection metrics when secret labels are configured.
The simplest Docker Compose setup (create docker-compose.yml):
services:
teleproxy:
image: ghcr.io/teleproxy/teleproxy:latest
ports:
- "443:443"
- "8888:8888"
restart: unless-stoppedThen run:
docker-compose up -d
docker-compose logs teleproxy | grep "Generated secret"For custom configuration, create a .env file:
SECRET=your_secret_here
PROXY_TAG=your_proxy_tag_here
RANDOM_PADDING=falseFor multiple secrets (per-group access control):
# Option A: comma-separated
SECRET=family_secret_hex,friends_secret_hex,public_secret_hex
# Option B: numbered variables
SECRET_1=family_secret_hex
SECRET_2=friends_secret_hex
SECRET_3=public_secret_hexLabels let you identify which secret a connection is using — useful for revoking leaked secrets or monitoring per-group traffic:
# Inline labels (CLI)
./teleproxy ... -S cafe1234567890abcdef1234567890ab:family -S dead1234567890abcdef1234567890ef:friends
# Inline labels (Docker)
SECRET=cafe1234567890abcdef1234567890ab:family,dead1234567890abcdef1234567890ef:friends
# Separate label env vars (Docker)
SECRET_1=cafe1234567890abcdef1234567890ab
SECRET_LABEL_1=family
SECRET_2=dead1234567890abcdef1234567890ef
SECRET_LABEL_2=friendsLabels appear in:
- Logs:
TLS handshake matched secret [family] from 1.2.3.4:12345 - Prometheus (
/metrics):teleproxy_secret_connections{secret="family"} 3 - Stats (
/stats):secret_family_connections 3
If no label is given, secrets are auto-labeled secret_0, secret_1, etc.
Label rules: max 32 characters, alphanumeric plus _ and - only.
Prevent a leaked or widely-shared secret from consuming all proxy resources by setting a maximum number of concurrent connections per secret:
# CLI: append :LIMIT after the label
./teleproxy ... -S cafe...90ab:family:1000 -S dead...90ef:public:200
# Without a label, use an empty label field
./teleproxy ... -S cafe...90ab::500
# Docker: numbered env vars
SECRET_1=cafe1234567890abcdef1234567890ab
SECRET_LABEL_1=family
SECRET_LIMIT_1=1000
SECRET_2=dead1234567890abcdef1234567890ef
SECRET_LABEL_2=public
SECRET_LIMIT_2=200
# Docker: inline (comma-separated)
SECRET=cafe...90ab:family:1000,dead...90ef:public:200When the limit is reached, new connections using that secret are rejected:
- Fake-TLS (EE mode): rejected during the TLS handshake — the client is proxied to the configured domain, so it sees a normal website (indistinguishable from a non-proxy server).
- Obfuscated2 (DD mode): the connection is silently dropped.
Existing connections are not affected. Other secrets continue operating normally.
Multi-worker note: with -M N workers, each worker enforces limit / N independently.
For single-worker mode (-M 0 or -M 1), the limit is exact.
Limits appear in stats and Prometheus metrics:
- Stats (
/stats):secret_family_limit 1000,secret_family_rejected 42 - Prometheus (
/metrics):teleproxy_secret_connection_limit{secret="family"} 1000,teleproxy_secret_connections_rejected_total{secret="family"} 42
Secrets without a limit are unlimited (the default, backward-compatible behavior).
And reference it in your docker-compose.yml:
services:
teleproxy:
image: ghcr.io/teleproxy/teleproxy:latest
ports:
- "443:443"
- "8888:8888"
environment:
- SECRET=${SECRET}
- PROXY_TAG=${PROXY_TAG}
- RANDOM_PADDING=${RANDOM_PADDING}
restart: unless-stoppedIf you want to build the image yourself:
docker build -t teleproxy .
docker run -d \
--name teleproxy \
-p 443:443 \
-p 8888:8888 \
teleproxyCheck the logs to find your auto-generated secret:
docker logs teleproxy 2>&1 | grep "Generated secret"The Docker container includes a health check that monitors the statistics endpoint. You can check the container health with:
docker ps
# Look for the health status in the STATUS columnThe container includes a daily cron job that automatically refreshes the Telegram DC configuration (proxy-multi.conf). This prevents the proxy from becoming unavailable due to stale server addresses — Telegram periodically rotates DC IPs, and without refresh the proxy may silently lose connectivity.
The refresh process:
- Downloads the latest config from
core.telegram.org - Validates the downloaded file
- Compares with the current config — skips if unchanged
- Replaces the config and sends
SIGHUPto reload without downtime
No user configuration is needed — this runs automatically.
The container stores proxy-multi.conf (Telegram DC addresses) in /opt/teleproxy/data/. Mount a volume to persist this configuration across container restarts. The proxy-secret file is baked into the image at build time and does not require persistence.
If core.telegram.org is unreachable (e.g., due to network restrictions), the container will use a cached proxy-multi.conf from the data volume when available. On first run without network access, you must manually place proxy-multi.conf in the data volume.
Mount a volume:
-v /path/to/host/data:/opt/teleproxy/dataghcr.io/teleproxy/teleproxy:latest- Latest stable build from master branchghcr.io/teleproxy/teleproxy:master- Latest build from master branchghcr.io/teleproxy/teleproxy:v*- Specific version tags (when available)