Skip to content
Go back

REALITY: Borrowing a TLS Handshake

By SumGuy 13 min read
REALITY: Borrowing a TLS Handshake
Contents

Every disguise before this one had the same flaw

Obfs4 disguises your traffic as random noise. Shadowsocks disguises it as nothing recognizable at all. Domain fronting tried to disguise your destination by lying about it in two different headers. All of these approaches share a fatal assumption: that “looks like nothing” or “looks unusual” is a safe place to hide.

It isn’t. A DPI box doesn’t need to read your payload to flag you. It just needs to notice that your traffic doesn’t look like the thing it’s pretending to be, and every disguise-based scheme eventually produces a byte pattern, a timing quirk, or a handshake shape that a well-funded censor can fingerprint. “Looks like nothing recognizable” is itself a signature once enough traffic on the wire matches it.

REALITY, part of the XTLS project inside Xray-core, takes a different bet. Instead of dressing your traffic up as something innocent, it terminates a real TLS 1.3 session with a real website’s real certificate, live, on the wire, at the moment a censor’s box looks at it. When a prober connects to your server without valid credentials, it doesn’t get a fake cert generated by your proxy software. It gets forwarded straight through to the actual site you configured as your decoy, and that site answers with its own, entirely legitimate certificate. The censor’s TLS fingerprinting tools see a perfectly normal handshake with a perfectly normal cert chain, because that’s exactly what happened. That’s a genuinely large win. It is not invisibility, and treating it as invisibility is where people get hurt. Here is how it actually works, and where it stops working.

The probe defense is the whole point

Active probing is how modern censorship infrastructure finds circumvention servers that passive traffic analysis misses. The censor’s system logs an IP that looks suspicious (maybe a spike in TLS connections, maybe it showed up in a leaked server list), then sends its own connection to port 443 and inspects what comes back. If the response is a self-signed cert, an unusual TLS library fingerprint, or literally anything that doesn’t match a well-known service, that IP gets blocked.

Every “generate your own cert and slap it on port 443” setup fails this test immediately, because the censor’s probe is a client with no valid key, and your server answers it directly with your own homemade certificate. That’s the tell.

REALITY handles this by never answering a bad probe with anything of its own:

port 443
|
+----------+----------+
| |
client presents prober or scanner
valid REALITY key (no valid key,
in ClientHello or expired one)
| |
v v
REALITY validates REALITY validation
auth, proxies the FAILS, connection is
connection through silently forwarded to
to the real proxy the configured dest
backend (the real target site)
| |
v v
proxied traffic real site's real TLS
flows normally cert comes back to the
prober, unmodified

The prober gets a real answer from a real site. There is no fake certificate to fingerprint, no proxy-shaped TLS library to catch, because for that connection, nothing proxy-related actually happened. It just got routed to the destination it would have reached anyway.

What’s actually sitting inside that ClientHello

This is the part worth understanding in detail, because it explains why the trick works instead of just trusting that it does.

The client generates an ephemeral X25519 keypair and runs ECDH against the server’s long-term public key to get a shared secret. From that secret, an auth key gets derived with HKDF-SHA256, using the first 20 bytes of the ClientHello’s own random field as salt and the literal string “REALITY” as the info parameter. The remaining 12 bytes of that random field get reused as the AES-GCM nonce. That auth key then encrypts the first 16 bytes of the TLS SessionID field with AES-GCM, at a fixed offset inside the ClientHello.

Those 16 bytes aren’t random padding. The encrypted payload carries the Xray version (the first three bytes), a zero byte, and then a Unix timestamp. A shortId value occupies the remaining bytes, letting one server distinguish between different users or client groups without needing separate ports or certs for each.

Authentication then runs in the other direction too, and it’s worth being precise about which side does what, because plenty of write-ups get it backwards. The server, having derived the same auth key, mints an ephemeral certificate and signs it. The client is the side that verifies: it computes an HMAC-SHA512 over the ephemeral cert’s Ed25519 public key using the auth key, and compares that against the signature the server sent. If it matches, the client knows it reached a genuine REALITY server rather than the decoy site or a man in the middle. The server signs, the client checks.

Modern REALITY also supports a hybrid X25519 plus ML-KEM-768 key exchange when the negotiated fingerprint allows it, folding a post-quantum key exchange into the same handshake without changing any of the visible shape of the traffic.

Here’s why none of this raises an eyebrow on a DPI box: a SessionID field carrying what looks like arbitrary bytes is completely ordinary. TLS session resumption tickets are opaque blobs by design, every browser sends them, and there’s no spec-mandated structure a middlebox can check them against. Sixteen encrypted bytes sitting where a session identifier normally sits look exactly like session resumption data, because structurally, that’s exactly the field they’re occupying. There’s nothing to flag unless you already know the derivation scheme and have the server’s private key, at which point you’re not doing passive DPI anymore, you’re doing a targeted investigation.

uTLS isn’t a nice-to-have, it’s load-bearing

None of the cryptography above matters if the ClientHello it’s embedded in doesn’t look like a real browser’s ClientHello. This is where uTLS comes in, and it’s not optional polish on top of REALITY. It’s a structural requirement.

Go’s standard TLS library produces a ClientHello with its own extension ordering, cipher suite list, and elliptic curve preferences, and that shape is recognizable on its own. A Go-shaped ClientHello arriving on port 443 pretending to be a browser is a mismatch a fingerprinting engine can catch regardless of how clever the REALITY payload inside it is. uTLS solves this by letting the client emit a byte-identical copy of a real browser’s ClientHello, extension order, GREASE values, cipher suite list and all, controlled by the fingerprint setting in your client config (Chrome is the common choice).

The point to internalize here: a technically perfect REALITY implementation running behind a Go-default TLS stack is still flagged, because the layer that carries all this clever session-ID cryptography is itself a distinguishing feature if it doesn’t match a real browser. Get the fingerprint wrong and you’ve built a beautiful lock on a door shaped like nothing in the house.

Picking a dest that doesn’t get you flagged anyway

Your dest and serverNames values are the site REALITY forwards failed probes to, and also the SNI your client will present on every legitimate connection. Choosing well matters more than most guides let on.

The target has to support TLS 1.3 and HTTP/2, because REALITY relies on the underlying protocol behavior lining up with what a modern browser expects. More importantly, it has to be actually reachable from inside the censor’s network, not just from your own connection when you’re testing. A site that’s already partially blocked or geo-restricted from the censor’s vantage point makes a useless decoy. Beyond that, it should be plausible as something the client would hold long, quiet connections to.

Here’s the part people skip: pointing hours of bulk proxy traffic at one enormous, obviously-popular domain, from a residential IP, day after day, is its own anomaly. Real users don’t sit on a single connection to a major site for eight hours a night. Vary your dest choices sensibly and think about what traffic pattern you’re actually producing, not just whether the cert looks legitimate in isolation.

A config walkthrough

Keys below are generated with the xray x25519 helper baked into Xray-core, which spits out a private/public keypair for exactly this purpose. Everything shown here is placeholder. Don’t reuse it.

Server inbound:

{
"inbounds": [
{
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{ "id": "11111111-2222-3333-4444-555555555555", "flow": "xtls-rprx-vision" }
],
"decryption": "none"
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"target": "example.com:443",
"serverNames": ["example.com"],
"privateKey": "PLACEHOLDER_SERVER_PRIVATE_KEY",
"shortIds": ["a1b2c3d4"]
}
}
}
]
}

Matching client outbound:

{
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "your.server.ip",
"port": 443,
"users": [
{ "id": "11111111-2222-3333-4444-555555555555", "flow": "xtls-rprx-vision" }
]
}
]
},
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "example.com",
"password": "PLACEHOLDER_SERVER_PUBLIC_KEY",
"shortId": "a1b2c3d4",
"fingerprint": "chrome"
}
}
}
]
}

Nothing fancy: the server holds the private key and forwards to target on failure, the client holds the matching public key, the shortId ties them together, and fingerprint tells uTLS which browser to impersonate.

One naming gotcha that will bite you when you follow older tutorials. Current Xray-core calls the server-side fallback field target and the client-side key field password. Almost every guide written before 2026 uses dest and publicKey instead. dest is still accepted as an alias for target, so old configs keep working there. publicKey was renamed to password specifically to stop people confusing it with a TLS certificate key, and the docs don’t promise the old spelling keeps working. If you copy a config off a blog post and it refuses to start, that mismatch is the first thing to check.

The firewall temptation, and why it backfires

There’s an instinct, once you’ve got a proxy server exposed to the entire internet, to also lock it down. Port knocking, single-packet authorization, a firewall rule that only opens 443 after seeing a secret sequence first. It feels responsible.

It’s also a direct contradiction of everything REALITY is built to do. The whole design goal is that port 443 answers every single probe convincingly, including probes that don’t have valid credentials, because those probes get quietly routed to a real site with a real cert. If you put port knocking in front of that listener, a censor’s scanner hitting port 443 gets nothing back at all. No handshake, no cert, no response, just a silently dropped packet.

A host that drops packets on port 443 is more suspicious to an automated scanner than one that serves a valid TLS certificate from a real company, not less. Silence on the port everything in the world runs HTTPS over is itself a signal worth investigating further. You’d be taking a mechanism specifically engineered to make failed probes boring, and replacing it with a mechanism that makes failed probes interesting. Skip it. Let the fallback do the job it was built for.

The honest limits

REALITY leaks no certificate of its own and carries no distinctive handshake signature that a passive DPI box can key on. That’s real and it’s a large improvement over anything that came before it. It is not a zero fingerprint, and nobody running this setup should tell themselves otherwise, because the failure mode of believing your own marketing is getting caught by something REALITY was never designed to stop.

The known detection technique is SNI fuzzing, sometimes called cert-switch probing. A censor connects with the real SNI first, records the response, then connects again with a small perturbation to that SNI and compares. A REALITY server frequently shows a small behavioral difference on the perturbed probe: different cipher suite ordering, a different session ticket length, or slightly different handshake latency, because that second connection is being delegated to the fallback path rather than served natively the way the first one was. This detection is imperfect, but it’s reliable enough to shift a verdict from clean to suspicious, which is often all a censor needs before applying other scrutiny.

Datacenter IP reputation sits entirely outside the protocol layer and REALITY does nothing about it. Exit nodes sitting on well-known cloud or VPS IP ranges get classified as datacenter addresses with high confidence by commercial detection stacks, independent of whatever’s happening at the TLS layer. Combine a flagged IP range with a cert-switch probe result and you’ve got a system calling your server “VPN likely” without ever touching your handshake cryptography. Perfect protocol mimicry does not fix a bad IP.

The clearest cautionary tale here is a real one. Xray-core issue #5230, opened in mid-October 2025 and since closed, documented a uTLS fingerprint leak that had been sitting live for roughly two years. The cause was the ECH GREASE placeholder, the fake Encrypted Client Hello extension a client sends when the target doesn’t support real ECH, whose entire purpose is preventing a firewall from telling real ECH apart from decoy ECH. That placeholder didn’t correctly match the cipher suites the client was actually advertising, and the mismatch made the underlying Chrome fingerprint passively identifiable at something like even odds per individual connection. That sounds survivable until you remember a proxy client opens huge numbers of connections, at which point even-odds-per-connection approaches certainty across a session. The issue report puts the exposure window at roughly December 2023 through October 2025. It later picked up a CVE, CVE-2026-27017, which scopes the damage by release rather than by date: utls v1.6.0 through v1.8.0 are affected, fixed upstream in a single commit and shipped in v1.8.1. The lesson isn’t “REALITY is broken.” It’s that the mimicry layer is only as strong as its least-maintained implementation detail, and a leak in that detail can sit unnoticed for years while everyone assumes the protocol is solid. Keep clients current. Don’t bet the whole architecture on one layer never having a bug.

And padding doesn’t fix volume. XTLS Vision pads early packets, XHTTP has its own padding options, and both genuinely help with packet-shape analysis. Neither one changes the fact that a flawless, fingerprint-matched handshake followed by a sustained multi-gigabyte transfer to a site you’d normally load and close in two seconds is still an odd-looking flow. Shape isn’t the only thing a censor can measure.

What this actually buys you

REALITY solves content inspection and active probing about as well as anything currently deployed solves them. That’s a real, load-bearing layer of a censorship-resistant setup, and it’s worth building on top of. What it does nothing about is where your traffic is physically going, which relay it hops through, and whether that path survives a state deciding to null-route entire foreign IP ranges wholesale. That’s a routing and geography problem, not a handshake problem, and it’s a different layer entirely. That’s next.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
wg-easy: WireGuard for Humans Who Hate Config Files
Next Post
Forgejo Actions: Self-Hosted GitHub-Style CI Without GitHub

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts