run-as-daemon.dev
← Writing
vpn · xray · debugging

REALITY: "handshake did not complete successfully" is almost never your keys

2026-07-20 · ~6 min

You generated a fresh keypair, the port is open, the shortId matches, the client is recognized — and the tunnel still dies. Here is the setting nobody checks, and why your two favourite tools will actively mislead you.

The symptom

A VLESS + REALITY inbound. Client connects, but every request through the tunnel returns a TLS error (curl exit 35, code=000). Server log, at info level, is blunt:

REALITY: processed invalid connection from x.x.x.x:...:
         handshake did not complete successfully

Note the wording. Not "server name mismatch". Not "invalid client". The client got past authentication — REALITY knows who it is — and then the handshake itself failed to complete.

The two tools that lie to you

1. openssl says everything is fine

$ openssl s_client -connect HOST:443 -servername www.microsoft.com
subject=CN=www.microsoft.com
Verify return code: 0 (ok)

Looks perfect — a valid certificate for your dest. It means nothing. An unauthenticated probe is exactly what REALITY proxies to the real destination; you are talking to Microsoft, not to your proxy. A green openssl handshake tells you the fallback works, not that the tunnel does.

2. tcpdump says the packets never arrive

You tcpdump the listen port during a client connection and see… nothing. So you conclude the provider is dropping traffic. Then you point the same test at a server you know is serving hundreds of GB to happy clients — and tcpdump shows nothing there either. A single hand-crafted probe packet is not a valid handshake; the kernel and the daemon drop it silently. Never conclude "the path is broken" from a lone nc packet. Only a real client or a byte counter tells the truth.

The actual cause: your dest

Once keys are verified and the client is recognized, "handshake did not complete" points at exactly one thing: the TLS handshake REALITY relays with your chosen dest / serverNames target does not finish cleanly from this host's network.

The most-copied dest on the internet — www.microsoft.com — is also one of the flakiest to relay. Its CDN behaviour varies by egress network and quietly stalls the borrowed handshake.

The fix is one line. Point dest and serverName at a boring, single-cert, TLS 1.3 + HTTP/2 site that completes fast and consistently:

"realitySettings": {
  "dest": "www.apple.com:443",
  "serverNames": ["www.apple.com"]
}

Same keys, same port, same client. It connected on the first try. From two different vantage points. The variable was never the crypto — it was the site REALITY was pretending to be.

A debugging order that actually converges

  1. Turn server log to info/debug and read the exact REALITY line. It classifies the failure for you: name mismatch vs. invalid client vs. handshake-incomplete. Each points somewhere different.
  2. Ignore openssl "success" and single-packet tcpdump — both are false signals here.
  3. If keys are recognized but the handshake won't complete → change dest to a reliable target and retest before touching anything else.
  4. Only then consider transport (TCP vs XHTTP), port, or MTU.

Hours of chasing "the provider blocks me / the port is intercepted / the keys are wrong" collapsed into a one-word config change. The lesson isn't about REALITY specifically — it's that the loudest hypotheses are the ones your tools can't actually confirm. Read the daemon's own log first.

Running VPN or mesh infrastructure that has to stay up under real conditions? That's the day job. ftops.space — turnkey, one accountable owner.