REALITY and XTLS-Vision

Building a Proxy Server That Pretends to Be Someone Else

Contents

A while back I wrote about Hysteria 2 as a stealth tunnel for getting past DPI-heavy networks. It works well, but what do you do when the censor starts dropping any TLS handshake that doesn't use a whitelisted SNI?

Hysteria 2 sidesteps this because it's UDP/QUIC and doesn't care about SNI the same way. But if you're on a network where UDP is throttled or blocked, you need a TCP-based fallback. And that fallback has to survive a censor who has decided that only microsoft.com, apple.com, and a bunch other approved domains are allowed to terminate TLS handshakes.

This is where REALITY and XTLS-Vision becomes very useful. They're two pieces of technology from the Project X / Xray-core ecosystem, and together they let you run a proxy server that:

  • Needs no domain name, so no Let's Encrypt, no DNS records.
  • Appears to be a legitimate TLS reverse proxy for a real website by borrowing the handshake from a legitimate source.
  • Survives active probing. A censor connecting to your server sees a real target's valid TLS handshake.
  • Fixes the TLS-in-TLS problem that is fingerprintable.

This is a very clever reuse of the TLS handshake protocol.

Hysteria 2 and REALITY/XTLS-Vision solve different problems. Hysteria 2 is UDP/QUIC, optimised for lossy high-latency links, and depends on whether UDP gets through. REALITY is TCP, optimised for stealth against SNI whitelists and DPI.

SNI whitelisting

For years the usual censorship-resistant proxy stack was some variant of VMess or Trojan, within TLS, fronted by a real domain with a Let's Encrypt cert, behind Cloudflare or directly on 443.

The logic was that if you look like HTTPS, you blend in with HTTPS. This is security through obscurity though and now censors started doing two things:

  1. Active probing. The censor's box connects to your server on 443 and verifies TLS. Your server presents its cert for vpn.mydomain.xyz. The censor now has a strong signal that this IP is running a proxy, because it's unlikely that normal public service uses vpn.mydomain.xyz on a random VPS.
  2. SNI whitelisting: China Mobile was the first widely reported case, then Iran has rolled out similar, where ISPs simply reset any TLS handshake whose SNI isn't on an approved list. vpn.mydomain.xyz gets dropped the moment the ClientHello goes out.

This can only really be fixed by making your server indistinguishable from a server hosting an approved site. Which sounds impossible as you don't own microsoft.com, and you can't get a cert for it.

REALITY

When a client connects to your REALITY server, the server performs the TLS handshake by proxying it to a real approved website in real time. The ClientHello comes in with SNI microsoft.com, your server opens a TCP connection to the real Microsoft, forwards the ClientHello, gets Microsoft's genuine ServerHello and certificate chain back, then relays that back the client. From the censor's perspective, your server is a reverse proxy for Microsoft, presenting Microsoft's real cert signed by Microsoft's CA.

The REALITY client embeds an authenticator in the ClientHello, derived from an X25519 exchange with your server's public key and protected with AEAD using the shortId. Only a client holding the right public key and shortId can produce a valid one. When your server sees a valid authenticator, it swaps in its own cert inside the already-encrypted tunnel and a normal proxy session begins. When it's missing or wrong (which is what an active probe or random scanner will hit) the server just keeps being a standard proxy to Microsoft. The prober gets a valid Microsoft response with nothing interesting to report.

This approach has a ton of benefits:

  • No domain needed on your side. You're using Microsoft's SNI and cert. You don't own Microsoft, obviously, but you also don't need to own anything. No DNS, no ACME, no renewal cron, etc.
  • No cert of yours to leak, there is no vpn.yourdomain.xyz certificate in the Certificate Transparency logs to be scraped.
  • Attacks return a normal Microsoft handshake, making it active-probe resistant and SNI whitelist compliant, as you literally are using a whitelisted SNI, with a matching cert chain.

XTLS-Vision

While REALITY handles what your connection looks like from the outside. XTLS-Vision handles what you carry inside it.

When you proxy someone's browser traffic through a TLS tunnel, you end up with TLS-inside-TLS: the user's HTTPS session to their bank is itself TLS, and you're wrapping that TLS inside your proxy's TLS. This creates two issues that XTLS-Vision solves.

First, performance: you're encrypting already-encrypted data, wasted resources on both ends.

Second, and more importantly, detectability. Even through the outer encryption, TLS-in-TLS has characteristic packet-size and timing patterns that sufficiently determined DPI engines can identify. Obviously, you can't see the content, but you can guesstimate a shape, and a TLS session that's carrying another TLS session is a very strong proxy signal.

Vision does deep inspection of the decrypted inner traffic at the server. When it detects that the inner payload is itself TLS, it stops double-wrapping. The inner TLS already provides confidentiality end-to-end, so the outer layer switches to just shuffling the bytes through without re-encrypting. The TLS-in-TLS shape disappears and both sides save CPU resources. This can actually be up to 5-10x faster than double-wrapping it.

Vision is the successor to an earlier mechanism called plain XTLS, which had maintainability issues.

What we're building

A modern stack is VLESS + TCP + REALITY + Vision, inside Xray-core:

  • VLESS is a lightweight proxy protocol with no built-in encryption (because the outer layer handles it)
  • TCP is the protocol
  • REALITY as the TLS replacement that disguises what the connection is
  • Vision as the flow control that fixes TLS-in-TLS

You need:

  • A VPS outside the censored region with root access. Any £3–5/mo box works. For this post I'm using a Hetzner CX22 in Helsinki.
  • A client device to connect from. I'll show Linux (Xray CLI) and Android (v2rayNG) configs.

Step 1: Install Xray-core on the server

The official install script is the path of least resistance:

bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install

This drops the xray binary at /usr/local/bin/xray, auto-installs a systemd service xray.service, and puts the config at /usr/local/etc/xray/config.json. If you'd rather not run an install script, but configure it manually, get the matching release from the Xray-core releases page, verify the checksum, and write your own systemd unit.

Check it's there:

xray version

Step 2: Generate keys and a shortId

REALITY authentication uses an X25519 keypair and a shortId as a second factor. The keypair generator is built into Xray:

xray x25519

You'll get something that looks like this:

Private key: kJ8h...redacted...X4=
Public key:  p9Qk...redacted...A0=

Save both. The private key goes on the server, the public on every client.

Generate a shortId. Anything hex, 0–16 chars; I use 8 bytes of random hex:

openssl rand -hex 8
3a7f9c1e4b2d8e06

And generate UUID for the VLESS client identity:

xray uuid
# -> e2b6f1a0-4c8d-4e29-9b3f-7a1d2c5e8f90

Step 3: Pick a steal-from target

Your dest has to pass a few tests first.

Requirements:

  • TLS 1.3 support. REALITY negotiates TLS 1.3; anything older and the handshake won't match.
  • X25519 key exchange. Should be the default on any modern site, but worth verifying.
  • HTTP/2 support (ALPN h2). Otherwise your ALPN advertisement won't match what the real site returns.
  • No CDNs! More specifically, avoid targets whose CDN POPs are obviously distant from your VPS region. Pointing a Finnish VPS at www.apple.com is fine because Apple is everywhere; pointing it at a domain that's only served from a US-east Cloudflare POP will generate mismatched TLS session ticket hints and latency fingerprints.
  • Not something the censor would block wholesale. Picking www.microsoft.com is safer because blocking Microsoft breaks half the corporate internet.

Verify a candidate:

openssl s_client -connect www.microsoft.com:443 -tls1_3 -alpn h2 </dev/null 2>/dev/null \
  | grep -E "Protocol|Cipher|ALPN"

You want to see TLSv1.3, a modern cipher, and ALPN protocol: h2. If you don't get h2 back, pick something else.

Good candidates I've tested: www.microsoft.com, addons.mozilla.org, dl.google.com, www.icloud.com, updates.cdn-apple.com. Avoid www.google.com and anything behind Cloudflare.

For this walkthrough I'm using www.microsoft.com.

Step 4: Server config

Here's the full /usr/local/etc/xray/config.json:

{
  "log": {
    "loglevel": "warning",
    "access": "none"
  },
  "inbounds": [
    {
      "port": 443,
      "protocol": "vless",
      "settings": {
        "clients": [
          {
            "id": "e2b6f1a0-4c8d-4e29-9b3f-7a1d2c5e8f90",
            "flow": "xtls-rprx-vision"
          }
        ],
        "decryption": "none"
      },
      "streamSettings": {
        "network": "tcp",
        "security": "reality",
        "realitySettings": {
          "show": false,
          "dest": "www.microsoft.com:443",
          "xver": 0,
          "serverNames": ["www.microsoft.com"],
          "privateKey": "..redacte",
          "shortIds": ["3a7f9c1e4b2d8e06"]
        }
      }
    }
  ],
  "outbounds": [
    { "protocol": "freedom", "tag": "direct" },
    { "protocol": "blackhole", "tag": "block" }
  ]
}

Explaining the fields:

  • log.access: "none" : Xray logs client IPs to an access log by default. On a proxy designed for anti-censorship that's a terrible default. Turn it off.
  • port: 443 : Has to be 443. Any other port defeats the disguise, Microsoft doesn't answer TLS on 8443.
  • decryption: "none" : VLESS has no built-in encryption layer because REALITY and Vision handle it. This is not the same as saying turning off encryption.
  • flow: "xtls-rprx-vision" : Enables the Vision flow control described above. Without this you get REALITY but not the TLS-in-TLS fix.
  • dest: "www.microsoft.com:443":Where unauthorised handshakes get transparently proxied. Must match one of serverNames.
  • serverNames : Which SNI values your server will accept from real clients. You can list multiple (like www.microsoft.com and learn.microsoft.com) and clients can pick one per connection, which is useful for blending traffic.
  • privateKey : From xray x25519. Guard this like an SSH private key, anyone holding it can impersonate your server.
  • shortIds : List of accepted shortIds. You can add more later to support more clients without rotating the keypair.

Enable and start:

systemctl enable --now xray
systemctl status xray

Check if it's listening:

ss -tlnp | grep :443
# LISTEN 0 4096 *:443 *:* users:(("xray",pid=12345,fd=12))

If you have ufw or nftables enabled, open 443/tcp.

Step 5: The port-forwarder issue

Here's the problem with the naive config above: if someone else sets up a REALITY client pointing at your IP with SNI www.microsoft.com and their own keypair, your server will forward their traffic to the real Microsoft, meaning you are acting as a free Microsoft proxy for them.

Two defences:

Option A: Pick a dest that nobody would want as a free proxy. A small fan site, a random university homepage, anything with zero commercial or circumvention value.

Option B: Use dokodemo-door with routing rules to drop any unauthorised SNI. The Xray-examples repo has a VLESS-XHTTP-Reality/minimal-steal_others variant that shows the pattern. If you do this, your server simply refuses to forward to Microsoft for anyone who doesn't hold your keys and unauthorised connections are dropped.

Step 6: Build a client URI

Every major Xray client (v2rayN, v2rayNG, Nekoray, Shadowrocket, FairVPN) accepts a VLESS URI. The format is:

vless://UUID@HOST:PORT?security=reality&sni=SNI&pbk=PUBKEY&sid=SHORTID&fp=chrome&flow=xtls-rprx-vision&type=tcp#LABEL

Filled in with our values:

vless://[email protected]:443?security=reality&sni=www.microsoft.com&pbk=p9Qk...A0&sid=3a7f9c1e4b2d8e06&fp=chrome&flow=xtls-rprx-vision&type=tcp#hel1-reality

The fp=chrome parameter tells the client to mimic Chrome's TLS fingerprint via uTLS. You can also use firefox, safari, ios, android, or edge. Pick one that matches what's plausible on your client device.

On Android, paste this into v2rayNG via "Import config from clipboard." On Linux, create an equivalent JSON config into Xray and point at it.

Step 7: Verify

At this point you'd want to confirm three things: that the disguise works, that the tunnel actually carries traffic, and that Vision is earning its keep.

7a. The disguise

From a completely unrelated machine (your laptop on home wifi, not the client device), run:

curl -v --resolve www.microsoft.com:443:203.0.113.42 https://www.microsoft.com/

Replace the IP with your VPS IP. You should get back a successful TLS handshake with the actual Microsoft homepage HTML.

7b. The tunnel

On the client, configure the system to route through the proxy (v2rayNG has a VPN mode; on Linux you can use tun2socks or set the SOCKS proxy Xray exposes locally). Then:

curl https://ifconfig.me
# -> 203.0.113.42

That should be your VPS IP if you're going through the tunnel.

7c. Vision

Run a throughput test with Vision on, then again with "flow": "" (empty string, no Vision) on both ends.

Flow Throughput (down) Server CPU
None 68 Mbps \~85% one core
xtls-rprx-vision 472 Mbps \~12% one core

With Vision, the inner TLS isn't re-encrypted and it should significantly increase throughput and reduce CPU usage.

Notes

You can add a new client without rotating keys, just append to clients[] in the inbound with a new UUID, and optionally a new shortId if you want to track which device is which. Reload with systemctl reload xray. If you need to revoke a device, remove its UUID from the list.

Fail2ban doesn't apply because there's nothing to ban. Unauthenticated probers just see Microsoft.

For logging, I set access: "none" in the config above. If you want some operational visibility, "access": "/var/log/xray/access.log" logs accepted connections with client IPs.

While Reality protects against passive DPI, SNI whitelists, and active probes that stop at the handshake, it does not protect against an adversary with out-of-band intelligence like traffic volume analysis, timing correlation, knowing your VPS IP from some other source (ISP IP is a very important factor), or simply IP-blocking the entire hosting provider's range. It requires you to pick and maintain a viable dest. If Microsoft tomorrow changed their ALPN to only h3 and dropped h2, your design would break.

In most of the world, running a personal proxy is fine, but in some jurisdictions it's not. I'm not a lawyer and this post isn't legal advice. You need to know the rules where you live and where your VPS lives.

Thanks to the Project X team for building the thing, and to RPRX for the original REALITY design.