Two handshakes · one connection
Every HTTPS connection carries two handshakes. The TCP handshake is answered by whatever connected to us; through a proxy, that is the proxy. The TLS handshake cannot be answered by a proxy – it has to reach the real browser. We time both on our own socket and divide. No JavaScript is needed for the measurement itself.
First match wins, top to bottom. The row that decided your connection is highlighted.
The TCP clock. Linux keeps a running estimate of the round trip time of every TCP
connection, and we read its minimum straight from the socket (TCP_INFO). It measures the
distance to whatever completed the three-way handshake. On a direct connection that is your machine. Through
an HTTP or SOCKS proxy, a residential proxy network or a TLS-terminating gateway it is the proxy – and
proxy exits tend to sit in well-connected places, a few milliseconds from our servers.
The TLS clock. Our engine terminates TLS itself, with nothing in front of it, so it sees the
moment your ClientHello arrives and the moment the handshake completes. A proxy relays these
bytes but cannot answer them: it does not hold a certificate for our name, and a browser would refuse one it
forged. So the TLS handshake always round-trips to the real browser, however many hops sit in between.
Divide, don't measure. On a direct connection both clocks reach the same machine, so the TLS handshake takes about one round trip plus a little cryptography: a ratio near 1–2×. Behind a relay the TCP clock stops at the proxy while the TLS clock continues to the browser, and the ratio climbs to 3× and far beyond. Because it is a ratio, your distance from us and the jitter on the path cancel out; only an extra terminating hop does not. A ratio above 3.0 raises an eyebrow, above 3.5 it makes a claim – and the two handshakes must differ by at least 20 ms, so that on a very short path the browser's crypto time alone cannot look like a relay.
A slow client is not a proxy. An overloaded phone stalls between our reply and its
Finished, which inflates the TLS clock exactly like a relay would. What tells them apart is the
wait before the ClientHello: a proxy completes our TCP handshake first, confirms the tunnel
to the browser, and only then does the ClientHello travel back through the chain – one hidden
round trip, 94–455 ms in live traffic. A stalled direct client sends its ClientHello
within 0–16 ms of connecting. Some proxy networks ship the ClientHello together with
the connect request and show no wait, but their exits sit in datacenters, where no browser lives. A high
ratio is therefore called relayed only with a real wait or a hosting exit; otherwise it stays
suspicious.
Explain before you accuse. Zscaler, Cisco Umbrella, iCloud Private Relay and similar gateways terminate TLS by design and score 20–40×. Their addresses carry an ipapi.is egress-service flag, which is checked before any threshold: they are reported as a gateway, not counted as a proxy.
What it cannot see. A full-tunnel VPN carries the browser's own TCP stack, so both handshakes reach the same machine and the ratio stays near 1 – VPNs are the job of the latency and VPN tests, not this one. A resumed TLS session needs fewer round trips and lowers the ratio, which only makes the test more conservative. When WebRTC is available, the UDP egress test answers first and this test is the fallback.
Credit. This test implements the method published by Justin at proxy.kyc.red: comparing the TLS handshake time against the kernel's TCP RTT of the same connection. His write-up includes the reasoning and measured ratios across sixty residential proxy exits. Read it at https://proxy.kyc.red/.