Skip to content

Cloudflare and CDN Deployment

Alexey Dolotov edited this page Apr 25, 2026 · 1 revision

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.


Why operators reach for a CDN

Three motivations, roughly in order of how often they come up:

  1. 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.
  2. Borrow shared reputation. A connection to 1.1.1.1 or 104.16.0.0/13 looks far less like "random VPS in Helsinki" than a Hetzner IP does. Censors that score by ASN/geo will rate it lower.
  3. Free DDoS scrubbing and anycast routing. The CDN absorbs L3/L4 floods so the origin VPS doesn't get knocked offline.

Why it is not a silver bullet

  • A CDN does not fix the SNI/IP-mismatch problem. See Surviving Active Probing. If the secret encodes www.vk.com and the public IP is Cloudflare anycast, the censor's DNS for www.vk.com still 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.


"CDN" is two products in a trenchcoat

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.


Cloudflare specifically

Cloudflare Proxy ("orange cloud", default)

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.

Cloudflare Spectrum (layer-4 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:

  1. Add the domain to a Cloudflare account on the Enterprise plan with the Spectrum add-on enabled.
  2. Open the domain → Spectrum tab → Create an Application.
  3. Choose TCP, edge port 443, origin = your VPS IP and origin port (e.g. 4443) (Get started).
  4. 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-listener config option (see below).
  5. 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.

Cloudflare Tunnel (cloudflared)

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.

Cloudflare Workers / TCP sockets

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.


Other CDNs and L4 anycast services

AWS Global Accelerator

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.

Google Cloud — External Network Load Balancer

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.

What was cut from this section

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.

"GRE tunnel + L4 proxy" providers

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.


Origin configuration when fronted by a CDN

Three things matter.

1. mtg listens on an internal port

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 = true

For domain fronting, mtg has a separate [domain-fronting].proxy-protocol toggle for outbound PROXY headers to the fronting backend. These two settings are independent.

2. Firewall: only accept connections from CDN IP ranges

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:

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.

3. Don't leak the origin

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.


Caveats

PROXY protocol mismatches

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.

TLS-on-TLS is not happening

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.

Geolocation routing

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.

Cost surprises

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).


Decision table

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.

Verified vs theoretical

Verified working

Does not work, despite frequent suggestions

  • 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.

Validating a CDN-fronted setup

# 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.


Summary

  • 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.

Clone this wiki locally