Contents

Living in a whitelist (RKN / Russia)

When only a domestic allowlist of domains resolves — ride the CDNs that are still permitted.

Some networks don’t blocklist — they allowlist. Instead of enumerating what’s forbidden, the filter drops everything except a short domestic list of permitted domains and the CDNs that serve them. RosKomNadzor-style “whitelist mode” is the extreme version of this: during a lockdown, foreign IP ranges stop routing and TLS handshakes to unlisted SNIs are reset. Your usual tricks invert — the goal is no longer to hide where you’re going, it’s to be indistinguishable from a destination that’s already on the list.

Obfuscation loses; belonging wins

On an allowlist, the censor doesn’t need to recognise your traffic as a proxy — it only needs to not recognise your destination as permitted. A perfectly camouflaged tunnel to an unlisted host is dropped exactly as fast as an obvious one, because the decision happens on the SNI before a single payload byte is seen.

XHTTP behind a permitted CDN wins for the opposite reason: the SNI it presents is the CDN’s own allowed hostname. You aren’t borrowing a foreign identity — you’re a genuine TLS connection to a domain the filter already trusts, and your real destination is hidden inside ordinary-looking HTTP requests that the CDN forwards to your origin.

The allowlist decides on the SNI. A permitted CDN's SNI passes; a foreign REALITY borrow-site's SNI is dropped.
transport/internet/splithttp/dialer.go transport/internet/reality/config.go

Point the tunnel at a CDN that’s already on the list

The move is to terminate TLS at a CDN inside the whitelist and let it route to your origin by host and path. XHTTP’s requests are plain HTTPS GET/POST — a CDN forwards them like any other site’s traffic, so from the filter’s side you are talking to the CDN, full stop.

Three knobs carry the disguise, and all three exist in the transport config ( transport/internet/splithttp/config.proto):

  • host — the Host: header the CDN routes on. Set it to a hostname the CDN serves that is on the allowlist. In Dial, requestURL.Host is taken from Config.Host first, falling back to the TLS/REALITY serverName only if empty — so host lets the routed hostname differ from the SNI when a CDN needs that.
  • path — the URL prefix your origin’s inbound matches on. Pick something that looks like a real asset route on that CDN.
  • modepacket-up is the default whenever REALITY isn’t in use (Mode == "" || "auto" resolves to packet-up in Dial, switching to stream-one/stream-up only when a realityConfig — or REALITY plus downloadSettings — is present). It splits uploads into many short POSTs and pulls the downlink as one long GET; that shape survives CDN buffering.
{
	"protocol": "vless",
	"settings": { "vnext": [ /* … your uuid + origin … */ ] },
	"streamSettings": {
		"network": "xhttp",
		"security": "tls",
		"tlsSettings": {
			"serverName": "cdn-edge.example-allowed.ru",
			"fingerprint": "chrome"
		},
		"xhttpSettings": {
			"host": "cdn-edge.example-allowed.ru",
			"path": "/assets/v3",
			"mode": "packet-up",
			"xPaddingBytes": "100-1000",
			"xmux": {
				"maxConcurrency": "16-32",
				"maxConnections": 0,
				"hMaxRequestTimes": "600-900",
				"hKeepAlivePeriod": 0
			}
		}
	}
}

Every key above is real: host, path, mode, xPaddingBytes, and the xmux block (maxConcurrency, maxConnections, hMaxRequestTimes, hKeepAlivePeriod) all appear in SplitHTTPConfig / XmuxConfig. The hostname and path values are placeholders — substitute a CDN and route that are actually permitted in your jurisdiction; the source can’t tell you which those are.

Tune padding and xmux to the CDN, not to a magic number

A CDN in front of you changes how requests should be sized and reused — the defaults assume a bare origin. Two levers matter under a whitelist.

xPaddingBytes (a from-to range, applied via GetNormalizedXPaddingBytes) pads each request so your POST/GET lengths don’t form a constant fingerprint after the CDN re-chunks them. It’s a range, not a fixed value, precisely so the sizes vary.

xmux governs how many HTTP requests share one underlying connection and how often connections rotate. maxConcurrency caps sub-requests per connection; maxConnections caps total connections (0 means “no cap — let concurrency decide”); hMaxRequestTimes retires a connection after N requests. Under a CDN you usually want fewer, longer-lived connections so you look like a browser holding a keep-alive to an edge node, not a burst of fresh handshakes.

Don't

Reach a foreign site directly and obfuscate hard

REALITY to www.some-foreign-cdn.com, perfect uTLS, domain fronting tricks. The SNI is foreign, so the allowlist resets the handshake before any of it is evaluated. Effort spent on camouflage is wasted on a destination that was never going to pass.

Do

Ride a CDN whose hostname is already permitted

XHTTP with host/path pointing at an allowlisted CDN edge. The SNI is trusted, the requests look like asset fetches, and your origin is reached by CDN routing — not by anything the filter inspects.

Where this fits

The mechanics of XHTTP itself — modes, the split uplink/downlink, why it’s CDN-shaped — are covered in xhttp. The general CDN-fronting setup (choosing an edge, wiring the origin inbound, TLS at the edge) is in cdn. This page is the special case where the CDN isn’t just convenient cover but the only thing that routes at all — and where a foreign REALITY target, the reflex choice everywhere else, is the one thing guaranteed to fail.