Contents

VLESS-REALITY: the gold standard

The baseline everyone recommends — and exactly why it is hard to block.

Every “which protocol should I use in 2026” thread ends the same way: VLESS + XTLS-Vision + REALITY over raw TCP. Not because it is new or clever, but because it removes the three things a censor can actually pin an attack on — a certificate you had to obtain, a fake domain you had to name, and the TLS-in-TLS shape that gives tunnels away. This page is the copy-paste baseline and the reasons each knob is where it is.

You never obtain, host, or fingerprint a certificate

REALITY borrows a real site’s certificate at handshake time instead of shipping one you control. With plain TLS you need a domain, a cert from a CA, and that domain has to look plausible — every one of those is a fingerprint a censor can enumerate. REALITY skips all of it: you point dest at a real TLS 1.3 site, list its name in serverNames, and your server relays the genuine handshake to it for anyone who can’t prove they’re a real client.

transport/internet/reality/config.go transport/internet/reality/reality.go

The authentication rides inside the ClientHello’s SessionId: the client derives a shared key from your publicKey via X25519, and stamps an AEAD-sealed tag into the 32-byte session id (reality.go, UClient). A real browser hitting the borrowed site would never carry that tag — so the server can tell your client apart from the whole internet without ever emitting a byte that looks non-standard.

Authenticated clients get the tunnel; everyone else — including active probers — is transparently proxied to the borrowed site and gets its real certificate.

The server config is short because there is nothing fake to describe

A working REALITY inbound is four transport keys plus one user. Every key below exists in REALITYConfig / the VLESS inbound; there is no cert path, no fake serverName you invented, no ALPN games.

{
	"listen": "0.0.0.0",
	"port": 443,
	"protocol": "vless",
	"settings": {
		"clients": [
			{
				"id": "b831381d-6324-4d53-ad4f-8cda48b30811",
				"flow": "xtls-rprx-vision"
			}
		],
		"decryption": "none"
	},
	"streamSettings": {
		"network": "raw",
		"security": "reality",
		"realitySettings": {
			"dest": "www.microsoft.com:443",
			"serverNames": ["www.microsoft.com"],
			"privateKey": "<x25519-private-key-base64url>",
			"shortIds": ["", "0123456789abcdef"]
		}
	}
}
proxy/vless/inbound/inbound.go

decryption is "none" on purpose — VLESS carries no transport crypto of its own, REALITY is the encryption layer. In fact Build() refuses to load the inbound if you leave it out. privateKey is the X25519 secret whose public half your clients carry; shortIds is an allow-list of up-to-16-hex-char ids (the empty string matches clients configured without one). Note the constraint enforced in Build(): REALITY only rides RAW (network: "raw"), XHTTP, or gRPC — not WebSocket, not mKCP.

The client must impersonate a real browser, byte for byte

fingerprint is not cosmetic — REALITY refuses to handshake with anything that can’t do TLS 1.3. The client uses uTLS to forge a named browser’s exact ClientHello: leaving fingerprint empty falls back to Chrome, unsafe and hellogolang are rejected at config load, and UClient errors out if the chosen fingerprint can’t produce a TLS 1.3 key share (reality.go). Match serverName to one of the server’s serverNames, carry the matching publicKey and one shortId:

{
	"protocol": "vless",
	"settings": {
		"vnext": [
			{
				"address": "203.0.113.10",
				"port": 443,
				"users": [
					{
						"id": "b831381d-6324-4d53-ad4f-8cda48b30811",
						"flow": "xtls-rprx-vision",
						"encryption": "none"
					}
				]
			}
		]
	},
	"streamSettings": {
		"network": "raw",
		"security": "reality",
		"realitySettings": {
			"serverName": "www.microsoft.com",
			"fingerprint": "chrome",
			"publicKey": "<x25519-public-key-base64url>",
			"shortId": "0123456789abcdef",
			"spiderX": "/"
		}
	}
}

Vision earns the flow field by killing TLS-in-TLS

flow: "xtls-rprx-vision" is what makes this beat “just wrap VLESS in TLS”. A naive tunnel puts a TLS record stream inside another TLS record stream, and the nested record boundaries are a statistically obvious signature. Vision detects the inner handshake and switches to direct copy afterwards, so the payload rides the outer connection without nested records. The inbound enforces the pairing: a TCP client that sends an empty flow against a user configured for xtls-rprx-vision is rejected with “the pure TLS proxy has certain TLS in TLS characters” (inbound.go).

proxy/vless/vless.go proxy/vless/encoding/encoding.go
Don't

VLESS + TLS, no flow

You get a real cert, but the tunnel is TLS-in-TLS. xtls-rprx-vision also refuses UDP, and the server will reject a mismatched empty flow outright.

Do

VLESS + REALITY + Vision, matched both ends

flow set to xtls-rprx-vision on the inbound client and the outbound user, network: "raw" on both ends. Vision goes direct after the inner handshake, REALITY borrows the cert, and there is nothing to fingerprint.

See protocols/vless for the wire framing and protocols/vision for exactly how the splice decides when the inner handshake is done.

What the censor actually sees

A TLS 1.3 connection to a popular site, with that site’s genuine certificate. The SNI is real. The cert chain validates. An active prober that connects gets transparently proxied to the borrowed site and receives its real response — so there is no “honeypot” tell to distinguish your server from a reverse proxy in front of that domain. That indistinguishability, not the crypto, is why this stack is the default: you are not hiding a tunnel, you are being a real site until proven otherwise.