WebRTC Leaks Your Real Address Even Behind a Proxy. Here Is the Path It Takes.
You pointed the browser at a SOCKS5 exit, loaded an IP echo page, and it returned the country you paid for. Then a call widget, a support chat, or a conferencing SDK negotiated a session, and the other side logged an address from your home ISP. Nothing was misconfigured in the tunnel. A different subsystem opened its own route out.
What the browser is actually collecting
WebRTC needs a peer-to-peer path, so before any media flows it runs ICE: it enumerates every way another host could reach this machine and hands that list to the remote party. Three candidate types matter here.
Host candidates are the machine's own interface addresses. Modern browsers obfuscate them as random .local mDNS names, so a stranger no longer reads 192.168.x.x directly — but the obfuscation is cosmetic, not a privacy boundary.
Server-reflexive candidates are the ones that hurt. The browser sends a STUN binding request to a public server and learns the address that server saw. That is your real public IP, measured on the machine's own network path.
Relay candidates only exist when a TURN server was configured, which is the single case where the address belongs to infrastructure instead of you.
The mechanism is structural: STUN is a UDP conversation with a third party, started by the browser, independent of whatever TCP tunnel you aimed at the page you were reading.
Four surfaces, one of which is not covered
| Surface | What leaves | Transport | Covered by a SOCKS5 proxy? |
|---|---|---|---|
| Host candidate | Local interface address | mDNS, link-local | No — never enters the tunnel |
| srflx via STUN | Public IP of your real path | UDP to 3478 | Only with UDP ASSOCIATE |
| DNS resolution | Resolver address, query names | UDP/TCP 53 | Client-dependent |
| The HTTP request | The page you asked for | TCP | Yes, when scoped correctly |
Notice that three of the four rows are UDP or link-local, while the proxy you configured is usually a TCP-only CONNECT. The page loads through the tunnel; the media negotiation does not.
Why the tunnel does not catch it
RFC 1928 defines exactly three commands. Most client libraries implement only the first and then advertise themselves as "SOCKS5 support".
| CMD | Name | What it actually carries |
|---|---|---|
0x01 | CONNECT | One outbound TCP stream |
0x02 | BIND | Inbound TCP, rarely implemented |
0x03 | UDP ASSOCIATE | Datagrams through a relay port |
A CONNECT-only client has no vocabulary for a datagram. It cannot forward the STUN request, and it usually does not fail loudly — the application simply falls back to the local path that is still available. That fallback is the leak.
This is the same root cause that makes some game clients behave oddly behind a proxy, and it is why "the proxy works" and "UDP works" are two separate facts you have to verify separately.
Prove it in twenty lines
Before touching browser flags, ask the exit whether it will carry datagrams at all. The greeting is two bytes, and the reply code tells you everything.
import socket def probe_udp_associate(host, port, timeout=8): s = socket.create_connection((host, port), timeout) s.sendall(b"\x05\x01\x00") # ver 5, 1 method, no-auth ver, method = s.recv(2) if (ver, method) != (5, 0): return f"handshake refused: method={method:#04x}" s.sendall(b"\x05\x03\x00\x01" + b"\x00" * 6) # UDP ASSOCIATE, 0.0.0.0:0 ver, rep, _, atyp = s.recv(4) if rep != 0x00: return f"REP={rep:#04x} " + ("cmd unsupported" if rep == 0x07 else "refused") size = {0x01: 4, 0x04: 16}.get(atyp) or s.recv(1)[0] s.recv(size + 2) # relay address + port return "UDP ASSOCIATE accepted" REP=0x07 means command not supported, and it is the answer you should expect from a plain TCP proxy. Note that acceptance is necessary but not sufficient: the browser still has to be told to use that path.
Closing it, layer by layer
| Layer | Action | What it changes |
|---|---|---|
| Browser policy | Chromium WebRtcIPHandlingPolicy=disable_non_proxied_udp | Forbids UDP that bypasses the proxy |
| Browser policy | Firefox media.peerconnection.ice.proxy_only=true | Same intent, different key |
| Stack | Disable IPv6 on the egress interface | Removes a whole parallel address space |
| Profile | Use a fingerprint browser that scopes UDP per profile | Policy ships pre-set, survives restarts |
The Chromium value is the important one to read carefully: it does not force UDP through the proxy, it forbids UDP that would not go through it. Calls may fail as a result. For isolation work that is the correct trade — a broken call is cheaper than a logged home address.
Five mistakes that keep the leak alive
- Confirming only the HTTP path. An IP echo page proves
CONNECTworks; it says nothing about ICE. - Assuming a proxy extension covers WebRTC. Extensions that rewrite proxy settings do not touch the ICE policy, which lives elsewhere.
- Treating mDNS
.localcandidates as safe. They hide the LAN address, not the public one seen by a STUN server. - Blocking UDP 3478 only. Several clients retry the same STUN service over TCP/443, which your tunnel may well forward.
- Re-testing after an exit change with the old browser profile. A new IP with a stale policy state reads as a regression that is not one.
FAQ
Does UDP ASSOCIATE alone stop the leak?
No. It makes datagrams reachable through the exit, which is a prerequisite. The client must also be configured to use that path, otherwise it keeps choosing the direct one.
Do desktop apps and mobile SDKs leak the same way?
Yes, often more quietly. Conferencing and game SDKs frequently bundle their own STUN logic, and the operating-system proxy setting never reaches it.
Is a relay candidate a good sign?
It is the one candidate type that does not expose your path, because the address belongs to a TURN server. The cost is bandwidth and latency, since every packet takes a detour.
Does this matter for scraping and API clients?
Rarely. A pure HTTP client cannot gather ICE candidates. The risk appears when you render pages in a real browser profile, which is exactly what anti-detect setups do.
Where this sits in an egress stack
Candidate policy is a property of the client, not of the exit, so a well-reputed IP and a correct tunnel still leave this path open until the browser is configured. The WebRTC leak walkthrough on socks5ip.com.cn covers the per-browser detection and fix steps, and the IP check centre confirms what the exit looks like from outside before you go chasing client behaviour.
Audit one layer at a time, and write down which layer you changed. Most "the proxy leaks" reports turn out to be an unmodified browser policy, not a dishonest exit.