There’s a WebRTC trick that anti-fraud people have leaned on for years, and it goes like this. Open an RTCPeerConnection, let it gather ICE candidates, and read the server-reflexive one — the srflx. That’s the public address a STUN server told the browser it’s coming from. The whole point is that it sidesteps a proxy — and it works for one reason: residential proxies carry TCP, not UDP. SOCKS5’s UDP-associate is barely implemented in the wild, and the bandwidth-sharing SDKs behind these pools don’t relay UDP at all. So the page loads over the proxy’s TCP exit, but WebRTC’s UDP has no path through the proxy — it goes straight out the host’s real interface instead, and the srflx leaks the true IP hiding behind the disguise. For about a year, that was a great tell.
Then the anti-detect browsers caught up — and they beat it with the same trap a lot of client-side detection walks into.
The leak check reads a value the adversary writes
The check reads the candidate the browser reports. So the browser reports a candidate that passes. The blunt ones kill the srflx outright. The careful ones forge it: they announce a reflexive address whose IP is the proxy’s own exit, so your leak check lines up the WebRTC address against the proxy address, sees them agree, concludes there’s no leak, and waves the session through.
Nothing was bypassed. The browser was asked what its reflexive address is, and it answered with a number it made up. That’s the structural problem, and it isn’t specific to WebRTC: anything the browser computes and hands you is a claim, and the claim belongs to whoever controls the browser. Read the navigator object, the canvas hash, the ICE candidates — you’re reading fields the adversary can write. On their machine, running their build, every answer is theirs to author.
Read your own socket, not their answer
Here’s the shift that fixes it. Don’t read the candidate the browser reports. Be the STUN server it talks to, and check whether the packet actually arrived.
The mechanics are small. A request hits /init and gets back a request_id and a dedicated UDP port on a STUN server I control — one port per session. The page runs its ordinary ICE gathering against stun:<host>:<that-port>. If the browser genuinely does WebRTC, a STUN binding request lands on that port and I write down two things: that it arrived, and the source address I saw it come from. Then /consume returns that ground truth, atomically and once.
Why a port per session, rather than matching on the STUN transaction ID like a normal STUN exchange? Because WebRTC generates that transaction ID itself and never exposes it to JavaScript — the page can’t tell me which packet was “its.” The only thing the page can steer at me and I can tie back to the session is the port I handed it. So I correlate by port.
And that’s the entire idea. The srflx candidate is a claim the browser authors. Whether a UDP packet reached my socket is an observation I author. A browser can announce any reflexive address it likes; it cannot make a packet appear on my server that it never sent. Claim versus ground truth — and I only trust the half I can see for myself.
Test your own browser
The button below runs the probe against the live server and shows what it saw — whether the reflexive address your browser announced is backed by a packet that actually reached me.
Four outcomes
Cross two facts — did the browser report a srflx, and did a packet reach me — and every probe lands in one of four cells.
browser reports srflx | packet reached me | verdict | how to read it |
|---|---|---|---|
| yes | yes | Genuine | a real round-trip; the reflexive address is one it actually got |
| yes | no | Forged | it announced a public address it never sent a packet for |
| no | yes | Suppressed | a packet arrived but no candidate surfaced — unusual, worth a look |
| no | no | Blocked | WebRTC UDP is off — privacy setting, firewall, or NAT |
The one that earns the whole exercise is Forged. On its default settings, a mainstream anti-detect browser I tested reported a server-reflexive candidate of 203.0.113.145 — a clean, plausible public IP (I’ve put a documentation-range address here; the real one it named turned out to be a stranger’s residential line) — and my server received nothing on the port. The address was fabricated whole; there was never a packet. The candidate even carried a secondary tell, raddr 0.0.0.0 rport 0 — though that one’s shaky, because a genuine Chrome hiding its local IPs behind mDNS zeroes the raddr exactly the same way. Either way it’s something the browser wrote. The proof is the empty socket.
Where it runs out
Now the part most write-ups skip, and the reason I’ll call this a signal and not a verdict.
Only Forged is strong. “A browser announced a public address it never sent a packet for” has almost no honest explanation — real reflexive candidates come from real round-trips — so when the probe says Forged, believe it. Everything else is soft, and Blocked is the trap. No srflx and no packet is exactly what a privacy-conscious human produces: Firefox with resistFingerprinting, a WebRTC-blocking extension, a corporate firewall that drops UDP. “No packet” is not “lying.” It’s the resting state of a great many careful, ordinary browsers. Grade Blocked as guilt and you punish the most privacy-aware people on your site.
Here’s a concrete way to get it wrong. I first tested against a STUN server on 127.0.0.1. Chrome cheerfully STUNned loopback and came back Genuine; Firefox refused to gather a reflexive candidate for a loopback address at all and came back Blocked. Same browsers, same probe, opposite verdicts — produced by the test rig, not the browser. If a localhost quirk can flip the result, so can a NAT. (The widget above talks to the real public server, which is the only setup whose verdicts mean anything.)
And the false negatives cut deeper than the false positives. An anti-detect browser doesn’t have to forge WebRTC — it can simply switch it off and land in the same benign Blocked as the privacy user. The probe pushes the adversary from a forge I can catch into a silence I can’t. Worse still is the tier that doesn’t evade at all: a genuine Chrome driven by Puppeteer on a real residential exit does a real STUN round-trip and comes back Genuine, correctly, because it is a real browser doing real WebRTC. It’s the same wall the TLS layer hits — the handshake can say this is Chrome but never this is a human’s Chrome. WebRTC names the forger and cannot see the driver. Genuine means not forging; it does not mean human.
So why bother
Because no single signal was ever going to answer is there a person here — and the value of this one isn’t its verdict, it’s how much it moves the odds when you lay it beside the others.
Blocked is ambiguous on its own. It stops being ambiguous the instant you ask what else is true of the session. A privacy-minded human who blocks WebRTC is on their own connection, with an ordinary browser’s TLS handshake and an ordinary consumer OS underneath. A session that blocks WebRTC and rides a residential proxy exit and carries an anti-detect TLS fingerprint and whose TCP/IP stack says a datacenter Linux box terminated the connection is not exercising a privacy setting. The residential-proxy corner is the sharpest, and it’s the UDP fact from the start of this post that sharpens it: the proxy carries TCP and not UDP, so a rig behind one has no honest answer to give the probe — its real UDP path doesn’t exit where its TCP path does. Suppress WebRTC and it’s Blocked; forge the candidate and it’s Forged; let it run and it leaks the real host straight past the disguise. The genuine resident whose line is being borrowed has none of this trouble — their own UDP just works. Each of those facts is weak on its own. The joint distribution is not — there’s no innocent population sitting in that corner.
That’s the same move as reputation itself: not what a thing looks like, but the weight of everything true about it at once. The TLS layer names the cheap attackers and flags the shape; the TCP/IP layer catches the OS the proxy is really running under; the active probe turns one of the browser’s claims into a fact I observed rather than a fact it reported. Any one of them is a hunch. Stacked, they’re a decision.
The honest version
So: a strong signal, not a truth serum. It catches a specific, common evasion — the anti-detect browser that forges its WebRTC leak — cleanly and cheaply, and it does it by refusing to read the browser’s answer and checking its own instead. It does not catch the browser that stays quiet, and it cannot see the real browser being driven by a script. Nothing at this layer can.
It’s one probe in a stack, and it earns its place by being the honest kind: it only claims what it can prove, and all it can prove is that a packet either arrived or it didn’t. The widget above is that, live and whole. If it calls you Genuine, that’s your browser telling the truth about WebRTC — no more than that, and no less.