REALITY: "handshake did not complete successfully" is almost never your keys
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
- Turn server log to
info/debugand read the exact REALITY line. It classifies the failure for you: name mismatch vs. invalid client vs. handshake-incomplete. Each points somewhere different. - Ignore
openssl"success" and single-packettcpdump— both are false signals here. - If keys are recognized but the handshake won't complete → change
destto a reliable target and retest before touching anything else. - 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.