MTProto proxy for Telegram on your own server
MTProto proxy for Telegram on your own server: set up mtg v2 with fake-TLS and your own domain, share links, scale out, and fix common connection failures.
This is for places where Telegram is blocked or throttled: in Iran it has been blocked since 2018, in Russia it has been throttled since February 2026 and largely blocked since April. Current as of September 2026: mtg 2.2.8, telemt 3.5.9. In Russia, proxies are in an arms race: since April, the country's DPI system (TSPU) has changed how it detects fake-TLS several times, and in June the mtg author advised everyone to switch to a VPN. A proxy is still handy because users install nothing, but keep a fallback such as VLESS Reality.
What you'll need
- A ForestsNet Cloud VPS in the Netherlands or Germany running Ubuntu 22.04/24.04 or Debian 12. Our addresses are checked daily for blocking in Russia.
- A domain or subdomain with an A record pointing to the server's IP — step 1 explains why. How to create it: "Pointing a domain at your server".
- SSH access as
rootor a user withsudo— "VNC console and first connection". - Traffic headroom: everything clients download through the proxy is outbound traffic for the server, and outbound is what counts against your plan.
How an MTProto proxy works and how it differs from a VPN
The Telegram client connects to your server, and the server relays the traffic to Telegram's data centers. Messages stay encrypted with MTProto: the proxy operator sees IP addresses and traffic volume, not content.
- Only Telegram goes through it. The browser and other apps bypass it.
- Nothing to install on the client. Support is built into the official apps: a user opens a
tg://proxy?…orhttps://t.me/proxy?…link and Telegram offers to connect. - No accounts. Access is granted by the secret in the link: whoever has the link can use it.
- Calls bypass the proxy — a limitation of Telegram itself.
Secret types
- Plain — 16 random bytes, 32 hex characters. From the first byte the stream looks like noise.
- Secure —
ddbefore the secret: random padding is added to hide packet sizes. - Fake-TLS —
ee, the secret and the hex-encoded domain: the connection starts as TLS 1.3 with a website name in SNI and looks like ordinary HTTPS.
Today only fake-TLS makes sense: a noise-like stream stands out by itself, and Russia's DPI has been cutting such protocols for years — Shadowsocks since autumn 2023. mtg v2 accepts only ee secrets.
Which implementation to choose
- mtg v2 (Go): fake-TLS only, one secret per process, domain fronting, TLS records shaped after a real website, replay protection, Prometheus metrics, no sponsored channel. Simple and predictable — this is what we install.
- telemt (Rust): many users per process with limits, quotas and expiry dates, an API, a sponsored channel. The choice for serving a large audience.
- The official MTProxy (C): its only release dates from 2018 and the Docker image is outdated. Not a good fit for new installs.
Others are alive too: teleproxy (a fork of the official one), mtproto.zig, alexbers/mtprotoproxy, seriyps/mtproto_proxy.
Step 1. Pick a domain and put up a cover website
The name from the secret goes into SNI in clear text, so DPI sees a "server IP — domain" pair. Checking it is easy: resolve the domain and compare addresses, so a proxy in the Netherlands posing as microsoft.com gives itself away at once. The mtg author's March 2026 best practices are blunt: the server has to be a real website with mtg hiding behind it.
- The domain is yours, its A record points to the server's IP, and Cloudflare proxying is off.
- The server runs a website with a Let's Encrypt certificate and TLS 1.3. Anyone who arrives without the right secret — a browser, a scanner, a DPI probe — is handed over to it by mtg.
- The name is neutral, without
proxy,mtprotoortg. Put a couple of plausible pages on the site. - The domain isn't blocked where your users are: DPI filters by SNI too.
Passing the proxy off as someone else's website is not allowed on ForestsNet Cloud servers — not in the secret's domain, in domain fronting or in the certificate: our Terms of Service prohibit it.
The examples below use the domain www.example.com and the IP 203.0.113.10. mtg takes port 443, so nginx listens on 80 for certificate issuance and on 127.0.0.1:8443 for the site. Put your pages in /var/www/site:
sudo apt update && sudo apt install -y nginx certbot
sudo mkdir -p /var/www/siteCreate /etc/nginx/sites-available/site, with port 80 only for now:
server {
listen 80;
server_name www.example.com;
root /var/www/site;
location /.well-known/acme-challenge/ { }
location / { return 301 https://$host$request_uri; }
}Enable the site and get a certificate. The --nginx plugin won't do here: it would add listen 443 and collide with mtg.
sudo ln -s /etc/nginx/sites-available/site /etc/nginx/sites-enabled/site
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --webroot -w /var/www/site -d www.example.com --deploy-hook "systemctl reload nginx"Append the HTTPS server to the same file and run sudo nginx -t && sudo systemctl reload nginx again:
server {
listen 127.0.0.1:8443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/site;
}More on certbot: "Free SSL with Let's Encrypt".
Step 2. Install mtg
Download the release, verify the checksum, create a user and a config directory:
MTG=2.2.8
cd /tmp
curl -fsSLO https://github.com/9seconds/mtg/releases/download/v$MTG/mtg-$MTG-linux-amd64.tar.gz
curl -fsSLO https://github.com/9seconds/mtg/releases/download/v$MTG/mtg-$MTG-checksums.txt
sha256sum --ignore-missing -c mtg-$MTG-checksums.txt
tar -xzf mtg-$MTG-linux-amd64.tar.gz
sudo install -m 755 mtg-$MTG-linux-amd64/mtg /usr/local/bin/mtg
mtg --version
sudo useradd --system --no-create-home --shell /usr/sbin/nologin mtg
sudo mkdir -p /etc/mtgStep 3. Generate a secret and write the config
mtg generate-secret --hex www.example.comThe output is ee, 32 hex characters and the hex-encoded domain — both the clients' password and the proxy's key. Create /etc/mtg/main.toml:
secret = "ee…"
bind-to = "0.0.0.0:443"
public-ipv4 = "203.0.113.10"
prefer-ip = "prefer-ipv4"
concurrency = 8192
[domain-fronting]
ip = "127.0.0.1"
port = 8443
[stats.prometheus]
enabled = true
bind-to = "127.0.0.1:3129"Run sudo chown root:mtg /etc/mtg/main.toml && sudo chmod 640 /etc/mtg/main.toml.
public-ipv4— the address for links and checks; without it mtg asks an external service.prefer-ip— by default mtg prefers IPv6. Without working IPv6 on the server, keepprefer-ipv4.concurrency— the limit of simultaneous connections; extra ones are dropped.
Don't copy the bundled example.config.toml wholesale: it has debug = true.
Domain fronting: what it does and when to configure it
In mtg it's always on. If a connection fails validation — wrong secret, a replayed handshake, a stale timestamp, or just a browser — mtg connects it to the real website and passes the bytes through as is. Whoever is probing gets the website's response byte for byte.
By default mtg resolves the domain from the secret and goes to its port 443. You need the [domain-fronting] section when that won't work: the domain points to this same server (without the section mtg connects to itself and loops), DNS lookups for the domain are blocked, or a load balancer sits in front of mtg.
Step 4. Start it and check
A template unit lets you add instances later without rework — /etc/systemd/system/[email protected]:
[Unit]
Description=mtg MTProto proxy (%i)
After=network-online.target
Wants=network-online.target
[Service]
User=mtg
ExecStart=/usr/local/bin/mtg run /etc/mtg/%i.toml
Restart=always
RestartSec=3
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now mtg@main
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo mtg doctor /etc/mtg/main.tomlPort 80 is needed for certificate renewal. More on the firewall: "The ufw firewall: managing ports".
mtg doctor checks deprecated options, the clock, reachability of Telegram data centers and of the cover site, and whether the domain's DNS matches the server's IP — every check should pass. Then open https://www.example.com in a browser: your website should load. That's how the server looks to anyone without the secret.
The Docker option
docker run --rm nineseconds/mtg:2.2.8 generate-secret --hex www.example.com
docker run -d --name mtg --restart unless-stopped --network host \
-v /etc/mtg/main.toml:/config.toml:ro nineseconds/mtg:2.2.8
docker exec mtg /mtg doctor /config.tomlPin the tag to a version, not latest. --network host is deliberate: 127.0.0.1 in the config points to the host's nginx, and ufw rules apply — Docker routes ports published with -p around them. You don't need the mtg user; chmod 600 on the config is enough. Installing Docker: "Docker and Docker Compose".
Step 5. Share the link and a QR code
sudo mtg access /etc/mtg/main.tomlThe output has tg_url and tme_url. A tg://proxy?server=…&port=443&secret=… link opens the app directly; https://t.me/proxy?… is clickable in any messenger or email. The secret is base64 by default and hex with the -x flag; clients accept both.
- Put the domain instead of the IP into the
serverparameter: if the address gets blocked, you change the IP and the A record, and the links keep working. Keep the record's TTL short. - Hand out two or three links to different addresses. Since April 2026, Telegram Desktop can switch between proxies on its own.
- Make QR codes locally, with the commands below: the
tg_qrcodeandtme_qrcodefields point to a third-party service, which would receive your link with the secret. - Don't post the link in public channels. Public proxies don't last: on May 19, 2026, every proxy from one popular Telegram channel stopped working in Moscow at once. The mtg author recommends keeping a proxy within a small circle.
sudo apt install -y qrencode
qrencode -t ansiutf8 'https://t.me/proxy?server=www.example.com&port=443&secret=…'
qrencode -o proxy.png 'https://t.me/proxy?server=www.example.com&port=443&secret=…'Step 6. Multiple instances and load
According to the README, a single mtg process handles about 10–20 thousand simultaneous connections. Count connections, not people: a Telegram client opens several, to different data centers and separately for files. Memory is the real limit: the mtg tracker estimated up to 120 KB per connection, and 2.2.5 reduced it — check the process RSS under your own load. A Go process uses all cores on its own.
Instances are for something else. mtg has one secret per process, so different groups of people mean different instances: you can replace one group's leaked link without touching the rest (the dolonet/mtg-multi fork supports many secrets in one process). We run eight instances per node. Ways to lay them out:
- Different IPs, each with its own 443:
bind-to = "203.0.113.11:443". The best option: blocking an address hits one group. - One IP, different ports. The simplest, but guest and corporate networks often allow only 443.
- One IP and a shared 443. An SNI router sits in front of the instances: each gets its own subdomain, nginx reads the name from the ClientHello and hands the connection to the right mtg, and everything unknown goes to the website. The mtg author published the same setup on HAProxy in
contrib/sni-router.
For the third option, install libnginx-mod-stream and append to the end of /etc/nginx/nginx.conf, outside the http block:
stream {
map $ssl_preread_server_name $mtg_backend {
a.example.com 127.0.0.1:4431;
b.example.com 127.0.0.1:4432;
default 127.0.0.1:8443;
}
server {
listen 443;
ssl_preread on;
proxy_pass $mtg_backend;
proxy_protocol on;
}
}Each instance, main included, gets its own file, e.g. /etc/mtg/a.toml: a secret for a.example.com, bind-to = "127.0.0.1:4431", proxy-protocol-listener = true, and proxy-protocol = true in [domain-fronting]. Switch the site to listen 127.0.0.1:8443 ssl proxy_protocol; and add the subdomains to the certificate with -d. Start with systemctl enable --now mtg@a mtg@b; get links with mtg access -p 443 /etc/mtg/a.toml.
Kernel and monitoring
mtg 2.2.8 tries to enable BBR itself, but a process without root can only use algorithms the system allows. Enable BBR globally — it speeds up media over lossy mobile networks. Create /etc/sysctl.d/90-mtg.conf, then run sudo sysctl --system:
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbrWith ufw, every connection lands in conntrack: at tens of thousands of connections, compare nf_conntrack_count with nf_conntrack_max. The general method: "High load on a VPS".
Prometheus metrics are on 127.0.0.1:3129; for a quick look, curl -s 127.0.0.1:3129/ | grep '^mtg_'. What to keep on dashboards:
mtg_client_connections— client connections: a sharp drop while the service is up hints at an IP block.mtg_concurrency_limited— rejections due to the limit: raiseconcurrencyor add an instance.mtg_domain_frontingandmtg_replay_attacks— hand-offs to the cover site and caught replays: spikes mean scanning or clients with an old secret.
Sponsored channel via @MTProxybot
@MTProxybot is Telegram's official bot for registering proxies. It issues a tag (ad tag): everyone connected through a proxy with the tag sees your channel in their chat list, marked as the proxy sponsor.
- In the bot, send
/newproxy, then203.0.113.10:443and the secret: 32 hex characters withouteeand the domain. - The bot returns a tag. In telemt it goes into
ad_tagtogether withuse_middle_proxy = true; in the official MTProxy, into the-Pflag. - In
/myproxies, pick the proxy, tap Set promotion and send a link to a public channel — private ones aren't accepted. Changes take about an hour to reach clients.
Don't hand out the link the bot generates: it lacks the fake-TLS part. The tag's upsides are statistics and promoting your channel, but the downsides outweigh them:
- The tag only works through Telegram's intermediate servers (middle proxies): an extra hop, and uploads are usually slower.
- mtg v2 doesn't support the tag; you need telemt or the official MTProxy.
- On Android the channel can't be removed from the chat list, and a promoted proxy is public — those get blocked faster.
Troubleshooting
By default mtg logs only warnings and errors. To see why a connection is rejected, temporarily set debug = true, restart the instance and watch journalctl -u mtg@main -f.
"Connecting…" forever.
- The service isn't listening or the firewall blocks it:
sudo ss -tlnp | grep ':443',systemctl status mtg@main,sudo ufw status. - The log shows
cannot read client hello.incorrect client randommeans the link has someone else's or an old secret;timestamp … is too oldmeans the server clock is off (the tolerance is 5 seconds);ip was blacklistedmeans the client's address is on the blocklist. - Packets don't arrive: run
sudo tcpdump -ni any 'tcp dst port 443 and tcp[tcpflags] & tcp-syn != 0'and connect from a phone. If SYNs from networks in the restricted country don't arrive while others do, the address is blocked. - An old Telegram version. On April 1, 2026, Russia's DPI started recognizing fake-TLS by a flaw in the official clients' ClientHello; it was fixed within a week, and in August Telegram Desktop 7.1 updated the fingerprint again. Update the app.
- mtg older than 2.2.8. It used to answer with a GREASE value in the ServerHello, which real TLS servers never do, and DPI dropped those replies.
The port disappears for a few minutes, mostly for Android and Desktop.
When connecting, a Telegram client opens about five TLS connections almost at once, while a browser opens one or two. Since June 2026, Russia's DPI on some networks answers such a burst by temporarily blocking the proxy's IP and port. A per-address SYN limit helps: extra attempts get an RST and spread out over time. telemt has the synlimit option for this; for mtg, add the rule below to /etc/ufw/before.rules before the COMMIT line and run sudo ufw reload. The values mirror telemt's synlimit defaults.
-A ufw-before-input -p tcp --dport 443 --syn -m hashlimit --hashlimit-name mtg-syn --hashlimit-mode srcip --hashlimit-above 48/minute --hashlimit-burst 24 -j REJECT --reject-with tcp-resetWorks on Wi-Fi but not on mobile data.
- Mobile networks are filtered differently from wired ones, and differently by each carrier, down to individual cells. Compare with another SIM card.
- Whitelist mode: during restrictions in Russian regions, and in Moscow in spring 2026, mobile internet reaches only resources on an approved list. A foreign proxy isn't on it, and nothing on the server can fix that.
Drops after a few minutes or after the screen locks.
- mtg older than 2.2.8. A mobile carrier's NAT forgets an idle connection after 5–30 minutes, and after the phone woke up Telegram used to hang on "Connecting to proxy" until the proxy was toggled. 2.2.8 added TCP keepalive (
[network.keep-alive]) and a 5-minute idle timeout. - An L4 proxy with a short timeout sits between the client and mtg. The client sends a service ping about once a minute, so a timeout under a couple of minutes kills live sessions. nginx stream's
proxy_timeoutdefaults to 10 minutes — don't lower it. - The conntrack table is full:
dmesg | grep conntrack.
Messages go through, but photos and videos load slowly or not at all.
- The "16 KB" filter: since June 2025, Russia's DPI on some networks stalls connections to foreign hosting providers after about 16 KB of data. Put a file of a few megabytes on the site and download it from a Russian network. If the download freezes right at the start, only another address or another network will help.
- DC 203, Telegram's media delivery network, is unreachable.
mtg doctorchecks it. - Telegram penalizes the server's outbound IP. We caught this on one of our VPN nodes: from one address, about 90% of SYNs to some Telegram data centers were silently lost, while the neighboring address worked fine. Measure the connect time with the command below: 1 or 3 seconds mean SYN retransmissions. A new IP helps, or a second server address set via
srcin the route to Telegram's networks. - Traffic goes through Telegram's intermediate servers, as with a sponsored channel, or BBR isn't enabled.
for ip in 149.154.175.50 149.154.167.51 149.154.175.100 149.154.167.91 149.154.171.5; do
curl -so /dev/null -w "$ip %{time_connect}s\n" --max-time 5 http://$ip:443/
doneSecurity and maintenance
- Server access. Only SSH, 80 and 443 are open; SSH logins use keys only: "How to harden SSH on your VPS", "VPS security checklist".
- Rotate the secret when the link leaks or connections clearly outnumber the people you gave it to. Generate a new secret with the same domain, replace it in the config, run
sudo systemctl restart mtg@mainand send out the new link: the old one stops working immediately. - Change the IP if it's blocked: put the new address into the A record and
public-ipv4; links with the domain survive the move. If addresses get blocked one after another, the mtg author suggests a cascade: mtg on a VPS inside the country, reaching Telegram through a tunnel to a server abroad — sing-box provides a local SOCKS5, and mtg connects to it via theproxiesoption. - Upgrades. Pin the version, read the release notes, run
mtg doctorafter upgrading and restart instances one at a time.
Sources
- 9seconds/mtg: README and example.config.toml v2.2.8
- mtg: deployment best practices, March 2026
- mtg v2.2.8, PR #446, PR #441
- mtg: SNI router in contrib/sni-router
- mtg: "Is MTProto in Russia over?" discussion, June 2026 (in Russian)
- telemt: FAQ and configuration reference
- TelegramMessenger/MTProxy and the MTProxy page on core.telegram.org
- Telegram API: tg://proxy and t.me/proxy links
- Telegram Desktop: ClientHello fix, April 2026
- ntc.party: Telegram handshake blocking (in Russian) and analysis of connection bursts
- Habr: MTProxy and telemt failing en masse under new DPI filters, April 1, 2026 (in Russian)
- ntc.party: connections stalling after 16 KB (in Russian)
- OONI: Telegram blocked in Russia, 2026
- Wikipedia: Censorship of Telegram
- Kommersant: mobile internet whitelists (in Russian)