Behind a CDN (Cloudflare)
Hide your origin behind a big CDN so blocking you means blocking the whole CDN.
Put a CDN in front of your origin and the only address a censor can see is the CDN’s — blocking you means blocking Cloudflare. The visible IP belongs to a shared edge fronting millions of ordinary sites, and under TLS the visible SNI is your CDN hostname, not your server. Host-based and IP-based blocking now has to take down the whole CDN to reach you, which most regulators are unwilling to do.
The CDN routes you by Host, so that is what you configure
Behind a CDN there is no direct socket to your origin — the CDN picks your backend from the request’s Host/SNI, so the hostname is the config. In XHTTP the dialer builds the request URL straight from your transport settings: the scheme is https whenever TLS is present, and the Host header comes from host (falling back to the TLS serverName).
if tlsConfig != nil || realityConfig != nil {
requestURL.Scheme = "https"
}
requestURL.Host = transportConfiguration.Host
if requestURL.Host == "" && tlsConfig != nil {
requestURL.Host = tlsConfig.ServerName
} So the practical mapping is: connect to the CDN edge, present your CDN hostname as SNI, and let the CDN’s own dashboard route that hostname to your origin. A real XHTTP-behind-CDN outbound:
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "proxy.example.com",
"port": 443,
"users": [{ "id": "…", "encryption": "none" }]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "tls",
"tlsSettings": {
"serverName": "proxy.example.com",
"alpn": ["h2", "http/1.1"]
},
"xhttpSettings": {
"host": "proxy.example.com",
"path": "/assets",
"mode": "packet-up",
"scMaxEachPostBytes": 1000000
}
}
} Every key here exists in the source: network, security, tlsSettings.serverName, tlsSettings.alpn, and the XHTTP host / path / mode / scMaxEachPostBytes. The path is what your CDN rule matches on to forward to the origin.
Pick a transport the CDN can actually carry
A CDN terminates TLS at its edge and re-originates a fresh HTTP request to your backend — so anything that needs the client’s raw bytes to survive end-to-end is dead on arrival. Raw TCP has no HTTP envelope for the CDN to route, and REALITY needs the client’s own TLS ClientHello to reach the origin untouched. Behind a terminating CDN neither is possible.
| Transport | Envelope | Survives Cloudflare | Why |
|---|---|---|---|
| XHTTP | ordinary HTTP | Yes (packet-up) | uploads are bounded POSTs, download is SSE |
| WebSocket | HTTP/1.1 Upgrade | Yes | CDNs proxy Upgrade: websocket natively |
| HTTPUpgrade | HTTP/1.1 Upgrade | Yes | same Upgrade dance, thinner than WS |
| gRPC | HTTP/2 + trailers | Partially | needs gRPC explicitly enabled at the CDN |
| Raw TCP | none | No | nothing for the CDN to route |
| REALITY | forged TLS | No | TLS terminated before it reaches origin |
Prefer packet-up, because the CDN buffers request bodies
Cloudflare does not stream a request body to your origin — it buffers the upload, then forwards it — so a transport that relies on one long-lived streaming POST stalls. XHTTP’s packet-up mode is built for exactly this: the upload is chopped into independent, bounded POST requests rather than one infinite body.
In packet-up each POST is capped by scMaxEachPostBytes (default 1000000, one megabyte), the dialer sends them as discrete requests, and the download is a single long-lived GET that the CDN happily streams back. stream-up instead sends the client’s data as one continuous POST body — precisely the shape a buffering CDN refuses to forward incrementally.
mode: stream-up behind Cloudflare
One continuous upload POST. The CDN buffers the whole body before it reaches the origin, so uploads stall and the tunnel wedges.
mode: packet-up behind Cloudflare
Uploads become a stream of bounded POSTs, each under scMaxEachPostBytes. The CDN forwards each one as an ordinary request, and downloads stream back over a single GET.
WebSocket and HTTPUpgrade sidestep the body question entirely — both force ALPN http/1.1 and ride a bidirectional Upgrade socket the CDN proxies natively, at the cost of a handshake round-trip. gRPC works too, but only if you turn on gRPC support at the CDN, which is why it is “partial”. See XHTTP and WebSocket for the transport-level mechanics.