Contents

REALITY

Borrow a real website's TLS certificate so even active probes see a legitimate site.

Every other TLS-based transport has the same fatal tell: somewhere sits a certificate you had to obtain, and a domain a censor can enumerate. Get a self-signed cert and TLS fingerprinting flags it; get a Let’s Encrypt cert for vpn.example.com and the SNI is now on a blocklist. REALITY removes the certificate from your side of the wire entirely — it borrows the TLS handshake of a real, popular website that you do not control, so a prober who pokes at your server is transparently handed the real site.

The server owns no certificate — it re-signs a dummy one per handshake

REALITY never presents a cert you configured; it generates one self-signed ed25519 certificate at process start and, for each authenticated handshake, overwrites its signature with an HMAC only real clients can recompute. There is no PEM file to fingerprint and no CA chain to validate.

The client’s proof is smuggled into a place every TLS handshake already has: the 32-byte Session ID. Xray overwrites it with a version stamp, a Unix timestamp, and the user’s shortId, then AES-GCM-seals the whole thing with a key HKDF-derived from an x25519 shared secret — so on the wire it looks like an ordinary random session ID.

hello.SessionId[0] = core.Version_x
hello.SessionId[1] = core.Version_y
hello.SessionId[2] = core.Version_z
hello.SessionId[3] = 0 // reserved
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
copy(hello.SessionId[8:], config.ShortId)
// ... AuthKey = X25519(ephemeral, config.PublicKey), then HKDF "REALITY"
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)
copy(hello.Raw[39:], hello.SessionId) // the fixed location of `Session ID`

The server, holding the matching PrivateKey, recomputes the same AuthKey, decrypts the Session ID, and checks the timestamp is within MaxTimeDiff (when set) and the shortId is registered. If it all checks out, the server sends its dummy ed25519 leaf certificate — after stamping the last 64 bytes, the signature field, with HMAC-SHA512(AuthKey, pubkey) (h.Sum(cert[:len(cert)-64])). The client verifies the server by recomputing that HMAC — not by trusting a CA.

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

An active prober gets the real website, byte for byte

When authentication fails — a censor’s probe, a timestamp outside MaxTimeDiff, a wrong shortId — the server does not error or reset; it splices the connection to the real dest and steps out of the way. The prober completes a genuine TLS handshake with, say, www.microsoft.com and sees Microsoft’s real, CA-valid certificate.

Valid clients get a forged temp cert and a tunnel; probers get transparently forwarded to the real dest and see its genuine cert.

The server-side handshake makes this a race the prober can’t win. Before reading a single byte from the visitor, REALITY already dialed the real Dest and wrapped the socket in a MirrorConn. It reads the ClientHello, and only if the SNI is in ServerNames and the embedded x25519 auth validates does it set hs.c.conn = conn and hijack the handshake. Otherwise:

if hs.c.conn != conn {
	// forwarded SNI: hs.clientHello.serverName
	_, err := io.Copy(target, NewRatelimitedConn(underlying, &config.LimitFallbackUpload))
	// ... bidirectional splice to the real dest
}

Because the ClientHello was mirrored to the real site as it arrived, the visitor is now in a real TLS session with a real server. There is no fake domain, no self-signed cert, and no timing anomaly to detect.

A rejected client behaves like a browser, not a scanner

When the client detects it reached the real site instead of a REALITY server, it doesn’t just hang up — it keeps the connection open and crawls the site like a browser. An abrupt drop right after a failed handshake would itself be a fingerprint.

If uConn.Verified is false, Xray logs received real certificate (potential MITM or redirection) and launches a “spider” over HTTP/2 against the real ServerName in a background goroutine: it starts at SpiderX, follows href="..." links it scrapes from responses, adds random padding cookies and Referer headers, and paces itself with random delays and concurrency drawn from SpiderY. UClient itself sleeps a random delay, then returns REALITY: processed invalid connection while the crawl continues — and the spider deliberately never closes the connection.

Don't

Reuse one dest for a whole fleet of servers

Every server borrowing www.microsoft.com with the same key makes a cluster of hosts that all “are” Microsoft. Correlated SNIs across many IPs is exactly the pattern large-scale fingerprinting looks for.

Do

Match dest to something you'd plausibly reach from here

Pick a dest that’s ordinary traffic for your users’ region and reachable from the server, give each user a distinct shortId, and let the server’s forwarding sell the cover story.

A minimal client stream config borrowing Microsoft’s TLS looks like this:

{
	"security": "reality",
	"realitySettings": {
		"serverName": "www.microsoft.com",
		"fingerprint": "chrome",
		"publicKey": "<server x25519 public key>",
		"shortId": "0123abcd",
		"spiderX": "/"
	}
}

The fingerprint is not cosmetic: REALITY runs on uTLS and needs a fingerprint that emits a TLS 1.3 ClientHello, or the handshake can’t carry an x25519 key share and aborts with does not support TLS 1.3. Everything else — the borrowed cert, the invisible auth, the forwarded probe — follows from that one real-looking hello.