-
Notifications
You must be signed in to change notification settings - Fork 367
Cloudflare and CDN Deployment
Putting a CDN in front of mtg sounds attractive: hide the origin IP, ride the CDN's IP/ASN reputation, get free DDoS scrubbing. In practice, most of what is sold as "CDN" is incompatible with mtg, and the combinations that do work are a short list.
This page is for operators trying to answer one question: "should I put Cloudflare (or another CDN) in front of this proxy?" — and if yes, how.
Three motivations, roughly in order of how often they come up:
- Hide the origin IP. If the censor only sees Cloudflare anycast IPs, blocking them collaterally damages a large chunk of the public web, which raises the political cost of the block.
-
Borrow shared reputation. A connection to
1.1.1.1or104.16.0.0/13looks far less like "random VPS in Helsinki" than a Hetzner IP does. Censors that score by ASN/geo will rate it lower. - Free DDoS scrubbing and anycast routing. The CDN absorbs L3/L4 floods so the origin VPS doesn't get knocked offline.
-
A CDN does not fix the SNI/IP-mismatch problem. See Surviving
Active Probing. If the secret encodes
www.vk.comand the public IP is Cloudflare anycast, the censor's DNS forwww.vk.comstill returns Akamai/Telegram CDN, not Cloudflare. The mismatch is just relocated, not removed. You still need a domain you control on the CDN. - A CDN does not fix the active-probing problem. When the censor probes the Cloudflare anycast IP with a fresh ClientHello, Cloudflare forwards it to mtg, mtg's domain fronting kicks in, and the fronting domain's TLS handshake is what comes back. This is the same guarantee you have without a CDN — the CDN does not improve it.
- A CDN with the wrong product line breaks mtg outright. Layer-7 HTTP reverse-proxy mode (the default Cloudflare "orange cloud") terminates TLS, expects HTTP, and rejects everything else. See below.
If your only goal is bypassing IP-reputation blocks, a different hosting provider (or a small Wireguard hop) is usually cheaper and simpler than a CDN.
When mtg operators say "put it behind a CDN", they usually conflate two fundamentally different things:
| Mode | What it does | Compatible with mtg? |
|---|---|---|
| Layer-7 HTTP reverse proxy | Terminates TLS, parses HTTP, caches, applies WAF, re-originates request to backend | No. Breaks FakeTLS. |
| Layer-4 TCP passthrough | Anycast accepts a TCP connection on a port and forwards raw bytes to origin without inspecting payload | Yes, this is the only mode that works. |
mtg uses FakeTLS: a TLS-shaped wrapper around MTProto. The payload is not HTTP, the certificate is not real, and the inner protocol is binary. Anything that tries to terminate TLS at the edge will:
- present its own certificate (wrong fingerprint, wrong SAN),
- decrypt and look for HTTP (find garbage),
- reject the connection.
Only a transparent L4 forwarder works. This rules out the default mode of nearly every "CDN" product on the market.
Does not work. This is the layer-7 reverse proxy. When you toggle a DNS record to "proxied", Cloudflare terminates the client's TLS connection at the edge and inspects the request as HTTP (Cloudflare DNS docs).
mtg's first packet to the edge is a TLS ClientHello with a session ID that encodes MTProto authentication. Cloudflare's edge will complete TLS with its own certificate, then try to read HTTP from the client. The client sent encrypted MTProto, not HTTP. The connection is closed.
There is no flag, page rule, or transform rule that makes the orange cloud forward raw TCP.
Works in principle, but plan-gated to Enterprise for our use case. Spectrum is Cloudflare's layer-4 proxy product. It accepts TCP (or UDP) on a configured port at an anycast IP and forwards the byte stream to your origin without parsing it (Cloudflare Spectrum docs).
Per-plan protocol support (Protocols per plan):
| Plan | Spectrum availability |
|---|---|
| Free | Not available |
| Pro | SSH and Minecraft only (one app each) |
| Business | SSH, RDP, Minecraft only (one app each) |
| Enterprise | Generic TCP and UDP on any port — paid add-on on top of Enterprise |
Generic TCP on port 443 — what mtg needs — is Enterprise-only, and sold as a paid add-on on top of the Enterprise plan. Cloudflare does not publish a list price for either Enterprise or the Spectrum add-on; both are quoted by sales (Cloudflare plans). If a third-party blog cites a specific dollar figure, treat it as unverified — contact Cloudflare sales for an actual quote.
So: yes, it works. No, it is not a $20/month add-on. If you are running a community proxy on a $5 VPS, Spectrum is not the answer.
What you need on the dashboard side:
- Add the domain to a Cloudflare account on the Enterprise plan with the Spectrum add-on enabled.
- Open the domain → Spectrum tab → Create an Application.
- Choose TCP, edge port
443, origin = your VPS IP and origin port (e.g.4443) (Get started). - Decide on PROXY protocol:
- Off: simpler. mtg sees Cloudflare's edge IP as the client.
-
On: mtg parses the PROXY-protocol header and recovers the
real client IP. mtg supports this natively via the
proxy-protocol-listenerconfig option (see below).
- Save. The Spectrum app gets a hostname that resolves to
Cloudflare anycast IPs. Use that hostname in the mtg secret and
share the resulting
tg://proxy?...URL.
Does not solve this problem. Cloudflare Tunnel's TCP/non-HTTP support is restricted to private routing — it pairs with Cloudflare Access / WARP for client authentication, not anonymous public clients. The Spectrum limitations page is explicit: "Cloudflare Tunnel integration is restricted to HTTP/HTTPS applications only, not TCP or other protocols" (Spectrum limitations).
A Telegram client cannot authenticate through Cloudflare Access SSO, and ordinary users will not install cloudflared on their phones. Public hostnames in Cloudflare Tunnel route HTTP — same layer-7 incompatibility as the orange cloud.
Workers gained connect()-style outbound TCP socket support, but
that is for outbound connections from a Worker. Incoming traffic
to a Worker comes through the HTTP fetch handler — there is no
mechanism for a Worker to terminate raw inbound TCP on port 443 from
anonymous public clients
(Workers TCP sockets).
mtg does not speak WebSocket either, so a WebSocket-bridged Worker design would require a custom client. Treat Workers-based fronting as not viable without a substantial protocol shim between the client and mtg.
Layer-4 TCP/UDP anycast in front of AWS resources. Does hide the origin AWS IP and does pass TCP through unmodified.
Pricing (AWS Global Accelerator pricing):
- $0.025 per hour per accelerator (≈ $18/month) charged for every full or partial hour the accelerator runs.
- DT-Premium data transfer: $0.007/GB to $0.105/GB depending on source AWS Region and destination edge location. Charged on the dominant direction (whichever of inbound/outbound is larger).
- Standard public IPv4 address charges apply on top.
- Origin must be inside AWS (EC2, NLB, ALB, or Elastic IP).
If your mtg VPS is at Hetzner, you need to either move it into AWS or front it with an AWS NLB whose backend is your external IP, which has its own caveats.
Viable if you are already on AWS. Awkward if you are not.
GCP offers passthrough and proxy network load balancers. Passthrough preserves client source IP and does not hide the backend; the backend has to be a GCE instance or NEG inside GCP (GCP load balancer types).
If your origin is in GCP, an external proxy NLB on TCP/443 will hide the GCE instance behind GFE anycast IPs. Costs include the forwarding rule, GFE data processing, and egress. Viable for GCP-resident origins.
Earlier drafts of this page listed Fastly, Gcore, and BunnyCDN as candidates. None of them publish a self-serve generic-TCP-on-443 passthrough product:
- Fastly sells HTTP/HTTPS delivery, Compute@Edge, and security products; no documented self-serve L4-on-443 passthrough SKU (Fastly products).
- Gcore sells L3/L4 DDoS protection ("starts free with our PAYG plan, or from $275/mo on the Start plan"), but a generic TCP/443 passthrough proxy is not a documented self-serve product — exposing an arbitrary TCP listener on Gcore anycast requires contacting sales (Gcore DDoS protection).
- BunnyCDN is HTTP/HTTPS only.
If you have an enterprise contract with any of these, ask directly. Otherwise, do not plan around them.
Several DDoS-protection vendors sell L4 TCP passthrough over GRE or IP-in-IP tunnels. These do hide the origin IP and pass TCP unmodified. Pricing varies wildly and many only sell non-443 game ports. Verify each one and confirm port 443 is allowed before committing.
Three things matter.
Move mtg off public 443. Pick something unprivileged, e.g. 4443 or 8443, and bind it where the CDN can reach it.
# /etc/mtg.toml — fragment
bind-to = "0.0.0.0:4443"
secret = "ee..."If the CDN is configured to send PROXY-protocol headers, enable mtg's native PROXY-protocol listener so mtg parses the header and recovers the real client IP (mtg config: ProxyProtocolListener):
bind-to = "0.0.0.0:4443"
proxy-protocol-listener = trueFor domain fronting, mtg has a separate
[domain-fronting].proxy-protocol toggle for outbound PROXY
headers to the fronting backend. These two settings are independent.
The whole point of fronting is that the origin IP is not supposed to take direct connections. If you leave port 4443 open to the world, anyone who guesses the IP can talk to mtg directly, and any leak of the origin IP (DNS history, certificate transparency, bug-induced outbound connect) burns the cover.
For Cloudflare, the canonical IP-range sources are:
- IPv4: https://www.cloudflare.com/ips-v4
- IPv6: https://www.cloudflare.com/ips-v6
- API:
https://api.cloudflare.com/client/v4/ips
These change occasionally. Refresh on a schedule, e.g. weekly:
#!/bin/sh
# /usr/local/sbin/refresh-cf-ips.sh
set -e
TMP=$(mktemp -d)
curl -sf https://www.cloudflare.com/ips-v4 -o "$TMP/v4"
curl -sf https://www.cloudflare.com/ips-v6 -o "$TMP/v6"
ipset create cf4 hash:net family inet -exist
ipset create cf6 hash:net family inet6 -exist
ipset flush cf4
ipset flush cf6
while read -r net; do ipset add cf4 "$net"; done < "$TMP/v4"
while read -r net; do ipset add cf6 "$net"; done < "$TMP/v6"
# install rule once:
# iptables -A INPUT -p tcp --dport 4443 -m set ! --match-set cf4 src -j DROP
# ip6tables -A INPUT -p tcp --dport 4443 -m set ! --match-set cf6 src -j DROP
rm -rf "$TMP"Run weekly via cron or systemd timer. Do not bake the list into a firewall rule once and forget it — the list will drift, and you will lose connectivity weeks later for no obvious reason.
For other CDNs: each publishes its own list. AWS publishes
ip-ranges.json
(AWS IP ranges).
Same pattern: fetch, cache, refresh, restrict.
The IP allowlist is necessary but not sufficient. Common leaks:
- The origin's DNS name (
mtg.example.com) once pointed directly at the VPS before you turned proxying on, and the historical record lives in tools like SecurityTrails. - The origin VPS serves a TLS certificate on its public IP for a domain that links it to the proxy (Certificate Transparency logs).
- The origin makes an outbound connection that includes its real IP (a Telegram CDN connection, an SMTP banner, etc.).
- The origin runs another service (SSH on 22, a web admin panel) on the same IP, which lets censors fingerprint the host independently of the proxy.
If you are serious about hiding the origin, rotate to a fresh VPS IP before fronting, and never let it appear in DNS for the proxy hostname.
mtg supports PROXY protocol natively, but only when the listener is
explicitly enabled. If the CDN sends a PROXY header and mtg's
listener is not in PROXY mode, the first bytes mtg sees are
PROXY TCP4 ... instead of a TLS ClientHello, the handshake fails,
and the client sees resets.
Either disable PROXY protocol on the CDN side, or set
proxy-protocol-listener = true in mtg.toml so mtg parses the
header. The two settings must match.
Some CDN UIs, when you toggle the orange cloud, will let you choose "Full" or "Full (strict)" mode for backend TLS. These options apply to the layer-7 HTTPS path and have no effect on Spectrum. They are not a knob you can turn to make orange-cloud work with mtg.
Anycast CDNs route the client to the nearest PoP. A user in Iran might land at a Frankfurt PoP that has good origin latency, or at a Dubai PoP that doesn't. Two consequences:
- Latency is not always better than a direct VPS in a nearby country. Sometimes it is worse.
- The censor sees connections going to "Cloudflare Frankfurt", not to your actual VPS country. This is usually what you want, but be aware that some censors block specific PoPs.
Cloudflare does not publish list prices for Enterprise or the Spectrum add-on; both are negotiated. AWS Global Accelerator's DT-Premium ($0.007–$0.105/GB) bites if your users route through expensive edges — an mtg instance carrying a few hundred users can move tens of GB/day, so back-of-the-envelope this before signing (AWS Global Accelerator pricing).
| Situation | Recommendation |
|---|---|
| Hobby / community proxy, ≤100 users, $5–20 VPS | Skip the CDN. Use SNI Router Setup. Cheaper, simpler, equally good against passive DPI. |
| Origin hosting provider's whole ASN is blocked | Try a different VPS provider first. CDN second. |
| Operating in a country that filters small ASNs but not Cloudflare | Cloudflare Spectrum on Enterprise (TCP add-on) or AWS Global Accelerator. |
| Already on AWS | AWS Global Accelerator → NLB → EC2 instance running mtg. |
| Already on GCP | External proxy NLB on TCP/443 → GCE instance. |
| Want it free | Free does not exist for L4-on-443 with hidden origin. The closest is a chain of free Wireguard hops, not a CDN. |
- Cloudflare Spectrum, Enterprise plan with TCP add-on, generic TCP on port 443 (Protocols per plan).
- AWS Global Accelerator → NLB → EC2 mtg. Standard L4 path (AWS Global Accelerator pricing).
- Self-rolled L4 SNI router on a Hetzner VPS. Not a CDN, but the closest "free" approximation; covered in SNI Router Setup.
- Cloudflare orange-cloud (layer-7 HTTP proxy). Breaks FakeTLS.
-
Cloudflare Tunnel (
cloudflared) for public clients. TCP routing is restricted to private/Access flows (Spectrum limitations). - Fastly, Gcore, BunnyCDN default products. HTTP/HTTPS only as self-serve SKUs; no documented L4-on-443 passthrough.
# 1. From a host that is NOT the origin, resolve the public hostname.
# You should see CDN anycast IPs, not your VPS IP.
dig +short proxy.example.com
# 2. Probe the public name with a random SNI:
openssl s_client -connect proxy.example.com:443 -servername random.test
# Expected: domain fronting via mtg returns the fronting site's cert.
# 3. Probe the origin IP directly (from outside, where the CDN allowlist applies):
openssl s_client -connect ORIGIN_IP:4443 -servername proxy.example.com
# Expected: connection refused / dropped (firewall blocks non-CDN sources).
# 4. From a normal client, share the tg://proxy?... URL using the public
# hostname and confirm the Telegram client connects.
# 5. Watch the mtg log — connections should arrive from the CDN's IP
# range, not from end-user IPs (unless you enabled PROXY protocol,
# in which case mtg will report the recovered client IP — see
# Monitoring and Diagnostics).If step 3 succeeds (you can talk to the origin from a random IP), the firewall is misconfigured and the CDN is decorative — fix that first.
See Monitoring and Diagnostics for what the mtg logs and metrics should look like under a CDN-fronted deployment.
- mtg + Cloudflare orange-cloud: does not work, ever.
- mtg + Cloudflare Spectrum (Enterprise + TCP add-on): works, Enterprise-tier pricing, contact sales.
- mtg + AWS Global Accelerator: works, requires AWS-resident origin, $0.025/hr fixed plus DT-Premium $0.007–$0.105/GB.
- mtg + Cloudflare Tunnel: does not work for public clients.
- mtg + Fastly / Gcore / BunnyCDN default: no self-serve L4-on-443 product.
- For most operators, the SNI router pattern on a regular VPS is a better answer than any CDN. Reach for a CDN only when the origin's IP/ASN is specifically the problem you are trying to solve.