WebRTC IP leakage: how RTCPeerConnection exposes the real address behind a proxy
You have routed the browser through a clean residential proxy. The page sees the proxy’s exit IP, the HTTP headers are consistent, the TLS handshake matches a real Chrome. Then a few lines of JavaScript create an RTCPeerConnection, point it at a public STUN server, and read back the connectivity candidates the browser gathers. One of those candidates carries the machine’s actual public IP, the one assigned by the real ISP, not the proxy. The proxy never saw the packet. It went out over UDP on the default interface, straight past the tunnel, and came back with the truth.
That is the WebRTC IP leak, and it has been the same shape since Daniel Roesler published a one-page demo of it in January 2015. The interesting part is not that it exists. It is that the fix browsers shipped four years later solved the local-network half of the problem cleanly and left the public-IP half almost entirely intact, because that half is not a bug. It is WebRTC working as designed. This post walks the ICE machinery that produces the leak, the candidate strings a script actually parses, the multicast-DNS mitigation Chrome turned on by default in 2019, the ways that mitigation has been worked around, and how anti-fraud and anti-bot systems turned the leftover signal into a coherence check that a proxy alone cannot pass.
A roadmap
We start with why WebRTC has to learn your IP addresses in the first place, which means a short tour of ICE, STUN, and the four candidate types defined in RFC 8445. Then the exact API call sequence a page uses to gather candidates without ever placing a call, and the candidate-line grammar it reads them out of. From there, the two halves of the leak: the local LAN address and the real public address, why a VPN stops one and not the other, and what RFC 8828 says browsers are allowed to do about it. Then the mDNS mitigation, its .local hostnames, and the prflx-candidate bypass that punched through it in 2021. Finally the part that matters for detection: how a leaked candidate becomes a five-way consistency check, and why the contradiction, not the IP, is the signal.
Why WebRTC needs your IP at all
WebRTC moves audio, video, and arbitrary data peer to peer. Two browsers, ideally, talk straight to each other rather than relaying every packet through a server, because relaying media is expensive and adds latency. To talk directly, each side has to tell the other where it can be reached. That is harder than it sounds, because almost every device on the internet sits behind a NAT, often several, and the address a machine knows about itself (192.168.1.42) is not the address the rest of the world sees.
The protocol that sorts this out is ICE, Interactive Connectivity Establishment, specified in RFC 8445 (July 2018, which obsoleted the older RFC 5245). ICE has each endpoint gather a list of candidates, possible transport addresses where it might be reachable, then trades that list with the peer and runs connectivity checks across every pairing until one works. The candidates are the leak surface, so they are worth naming precisely.
RFC 8445 defines four candidate types. A host candidate is an address bound directly to a local interface, physical or virtual, including the virtual interface a VPN client installs. A server-reflexive candidate (srflx in the candidate line) is the address a STUN server reports seeing when the endpoint sends it a packet, which after one or more NAT hops is the public IP and port. A peer-reflexive candidate (prflx) is one discovered during the connectivity checks themselves, when a packet arrives from an address nobody had listed yet. A relayed candidate (relay) is an address on a TURN server that will forward traffic when a direct path cannot be found. The first two are where almost all of the privacy damage happens.
The STUN mechanism is the heart of it. STUN, Session Traversal Utilities for NAT, lets an endpoint discover the public side of its NAT by asking a server “what address did this packet appear to come from?” The server reads the source IP and port off the received packet and copies them into the response, in an attribute called XOR-MAPPED-ADDRESS. There is no magic and no exploit. The packet physically traversed the NAT, so its source address is, by definition, the public one. The browser learns its own external IP because a third party told it.
Gathering candidates without making a call
Nothing about this requires a real WebRTC session. A page never has to ask for the camera, never has to find a peer, never has to ring anyone. It only has to start the gathering process and then read what comes back, and the gathering starts the moment the connection has something to negotiate.
The canonical sequence, the one Roesler’s 2015 demo used and the one almost every leak test still uses, is short. Construct an RTCPeerConnection with a STUN server in the iceServers config. Call createDataChannel("") on it, which gives the connection a media stream to negotiate and therefore a reason to gather candidates. Call createOffer(), then setLocalDescription() on the result, which kicks off ICE. From there the candidates arrive asynchronously on the onicecandidate event, one RTCIceCandidate at a time, and the same strings also accumulate in pc.localDescription.sdp. A script reads the candidate field off each event and pulls the IP out with a regex over the dotted-quad and colon-hex forms. That is the entire technique.
Two properties of this matter for anyone trying to block it. First, the STUN traffic does not go through XMLHttpRequest or fetch, so it never appears in the network panel of the developer tools and is invisible to request-blocking extensions that hook those APIs. The packets are UDP datagrams emitted by the browser’s media stack, below the layer most blockers can see. Second, the values come back fast and silently, with no permission prompt, because gathering connectivity candidates is not treated as a sensitive operation the way getUserMedia is. The 2015 disclosure made exactly this point, and it is still true.
A candidate line, the thing the regex runs over, follows the grammar from RFC 5245 and its successors. A server-reflexive line looks like candidate:842163049 1 udp 1677729535 203.0.113.7 54596 typ srflx raddr 0.0.0.0 rport 0 generation 0. The fields after typ tell you which kind it is. The address right before typ is the candidate’s own transport address, and for an srflx candidate that address is your public IP. The collector does not need to understand ICE. It needs to find typ srflx and read the number in front of it.
The two halves of the leak
The leak is really two leaks, and they behave differently behind a proxy. Keeping them separate is the only way the rest of the picture makes sense.
The first is the local address leak. Host candidates carry the IP bound to a local interface, which on a home network is something like 192.168.1.42 or 10.0.0.5, and on a corporate network can be a routable internal address that maps the company’s subnet structure. This does not identify you on the public internet, but it is a strong cross-site correlator and a way to probe a LAN. Two visits that report the same private IP and same local subnet are very likely the same machine, and a script that enumerates host candidates can map which internal hosts respond. This is the half browsers eventually moved to fix.
The second is the public address leak, and it is the one that breaks proxies. The server-reflexive candidate is, by construction, whatever public IP the STUN packet went out on. If WebRTC’s UDP traffic does not follow the same path as the browser’s HTTP traffic, the srflx candidate carries the real ISP-assigned address while every HTTP request carries the proxy’s. A plain HTTP or SOCKS proxy only proxies TCP HTTP traffic. The browser’s media stack opens its own UDP sockets on the default route and talks to the STUN server directly. The proxy is simply not in that path. A VPN that captures all traffic at the network layer does cover the UDP, so a properly configured full-tunnel VPN closes the public-IP leak; a browser-level proxy or a split-tunnel VPN does not. This distinction is the source of nearly every “but I was using a VPN” surprise. The defense has to sit at the layer the UDP actually traverses.
RFC 8828, “WebRTC IP Address Handling Requirements” (Uberti and Shieh, January 2021), is the document that tells implementers what they are allowed to expose. It defines four modes. Mode 1 enumerates addresses on all interfaces and discloses the most, gated behind explicit consent. Mode 2, the default when no consent is given, uses the kernel’s default route and reveals the private addresses associated with it. Mode 3, “default route only,” withholds the private host addresses and offers only what STUN and TURN discover. Mode 4 forces WebRTC media through the same proxy as the HTTP traffic, falling back to TCP if the proxy cannot do UDP. The spec recommends Mode 2 as the default, which is precisely why an out-of-the-box browser still hands a server-reflexive candidate to any page that asks. Mode 3 and Mode 4 exist for environments that care, and they are not on by default. The privacy posture most people run is the one that leaks.
For operators, the practical tell from RFC 8828 is the one it spells out for applications: if no host candidates appear at all, the browser is in Mode 3 or Mode 4. The absence of host candidates is itself a fingerprint, and a slightly unusual one, because the overwhelming majority of real browsers are in Mode 2 and do produce them.
The mDNS mitigation and its hostnames
The local-address half got a real fix, and it is a clever one. Rather than refusing to gather host candidates, which would break peer-to-peer connectivity on a lot of real networks, browsers replace the private IP in the host candidate with a randomly generated name and resolve that name only over the local network.
The mechanism is “Using Multicast DNS to protect privacy when exposing ICE candidates,” an IETF draft by Youenn Fablet (Apple), Jeroen de Borst (Google), Justin Uberti, and Qingsi Wang (Google). When the browser gathers a host candidate it deems unsafe to expose, it mints a fresh name, registers it on the local link with multicast DNS, and puts the name into the candidate in place of the IP. The draft requires that name to be a version-4 UUID followed by .local, so a candidate line carries something like 1f4712db-ea17-4bcf-a596-105139dfd8bf.local where the dotted quad used to be. The page sees a random hostname. The real peer, on the same network segment, can resolve it through mDNS and recover the address to make the connection; a remote web app cannot, because mDNS is link-local and the UUID is meaningless off the LAN.
The rollout history is precise. The experiment landed in Chrome Canary M73 in early 2019 behind the flag --enable-webrtc-hide-local-ips-with-mdns, surfaced in chrome://flags as “Anonymize local IPs exported by WebRTC.” Chrome 76 turned it on by default in August 2019, a change Qingsi Wang announced on the discuss-webrtc list, noting roughly a two percent relative reduction in connection rate as the cost. The same announcement spelled out a deliberate exception that still holds: the obfuscation does not apply to a page that already has getUserMedia permission. If the page can use your camera or microphone, the host candidates come back as real IPs again, on the reasoning that you have already granted it a higher level of trust. Safari and the Chromium-derived browsers (Edge, Opera, Brave) followed, and mDNS host obfuscation is now the default across the major engines.
What it does not touch is the server-reflexive candidate. mDNS hides the LAN address; the public IP that STUN reflects back is still a real, routable address that the page can read directly. The mitigation was scoped to the local-network privacy problem, and on that problem it works. The proxy-busting half was left where RFC 8828’s Mode 2 default leaves it, in the open.
Working around the mitigation
Any obfuscation that ships a value the page can still read invites someone to recover the original, and host candidates did. The cleanest published example is “prflxion,” disclosed by Amir Pirogovsky of Peer5 and reported to Google on 10 June 2021. It got around mDNS without disabling it, by abusing how the browser handled IPv4 traffic on an IPv6 socket.
The mechanics are worth stating because they show what a real bypass looks like, which is nothing like the leak itself. WebRTC creates a peer-reflexive candidate when a connectivity check arrives from an address that was not in the original candidate list, and the function that sanitized candidates failed to apply mDNS treatment to prflx candidates. Separately, when an IPv4 packet reaches a socket bound as IPv6 (AF_INET6), the kernel maps the IPv4 address into the IPv4-mapped IPv6 form ::ffff:192.168.x.x, which still encodes the original private address. The exploit set up two local peer connections, swapped an IPv6 candidate’s mDNS hostname onto an IPv4 candidate while keeping the IPv6 port, forced IPv4 traffic through the IPv6 socket to trigger the 4-in-6 mapping, and read the resulting unsanitized prflx candidate out of getStats(). The mapped address gave back the real LAN IP. Chromium patched it within days, by 15 June 2021. The point of recounting it is not the recipe; it is that the host-candidate hiding is a sanitization layer, and sanitization layers leak through edge cases the original designers did not enumerate. The same is true of the iframe and third-party-context rules: the draft says browsers should not register mDNS names in a cross-origin context, which has its own knock-on effects on what a deeply embedded frame can and cannot see.
There is a broader lesson here that connects WebRTC to the rest of the fingerprinting surface. A defense that returns a transformed value rather than no value is always a candidate for inversion. Canvas randomization has the same problem, and so does the approach anti-detect browsers take when they spoof rather than suppress. The leak that returns nothing is safe; the leak that returns a scrambled something is a puzzle waiting for a solver. This is the recurring theme across the navigator object surface and the screen and device-pixel-ratio tells: suppression holds, substitution is fragile.
How anti-fraud and anti-bot systems use it
For a privacy researcher the leak is about tracking. For an anti-bot vendor it is about contradiction. The two uses share a mechanism and diverge sharply in what they do with the result.
The public-IP leak is most valuable to a detector not as an identifier but as a second opinion on where the client really is. A bot operator presents a US residential proxy. The HTTP layer says United States. The TLS handshake can be made to say a real browser. The headers can be made consistent. Then a WebRTC server-reflexive candidate comes back carrying an IP in a datacenter range in another country, and the story falls apart, not because the datacenter IP is inherently bad, but because it disagrees with everything else the client claimed. The proxy could be flawless and the verdict would be the same. The contradiction is the signal.
This is why WebRTC sits inside what some scraping-tooling vendors describe as a five-vector coherence check: the country of the proxy exit IP, the timezone the browser reports through Intl.DateTimeFormat().resolvedOptions().timeZone, the Accept-Language header, the WebRTC-revealed network, and the location of the DNS resolver that looked up the page’s domain. Each of those is a guess at “where is this client,” and a real client makes all five agree without effort because they all derive from one true location. A proxied client has to forge five things that were never meant to be forged together. WebRTC is the one of the five that an HTTP-layer proxy cannot reach at all, which makes it the most expensive to fake correctly and therefore the most informative when it disagrees. The timezone and Intl cross-check is the natural companion to this; the two together catch a proxy that lines up the IP geography but forgets the clock, or vice versa.
There is a quieter signal underneath the geography. The mere shape of the candidate set is informative. A genuine consumer Chrome on a home network produces an srflx candidate plus mDNS-obfuscated host candidates. A headless automation rig in a datacenter, or a browser configured to suppress WebRTC, produces something different: no candidates at all, only relay candidates, host candidates that are raw IPs when they should be .local, or an srflx address that matches the HTTP-layer IP exactly when a real NAT’d home connection would show two different addresses. Each of those is a deviation from the modal browser, and modern detection runs on deviation. An anti-detect browser that spoofs the candidate has to spoof the whole plausible set, in the right format, consistent with the rest of the fingerprintable runtime, which is a much harder problem than returning one fake string.
The exact way each commercial vendor scores the WebRTC signal is not publicly documented, and it would be dishonest to claim otherwise; what is documented is that the candidate-level data is collected client-side and folded into a server-side decision, the same architecture that the residential-proxy and ASN reputation checks ride on. The WebRTC candidate is one input into that, valuable precisely because it is hard to align with the others when the others are forged.
Where it stands in 2026
The leak has aged into two stable states that reflect what browsers decided to fix and what they decided to leave. The local-network half is largely closed by default. mDNS obfuscation is the standard behavior in Chrome, Edge, Brave, Opera, and Safari, host candidates come back as .local UUIDs to ordinary pages, and the published bypasses against that obfuscation were edge-case socket tricks that got patched within days of disclosure. If your threat model is a tracker enumerating your LAN, the modern browser already handles it, with the one carve-out that a page holding camera or microphone permission gets the raw addresses back.
The public-IP half is still open, and it is open on purpose. A default browser runs RFC 8828 Mode 2, gathers a server-reflexive candidate from a public STUN server, and hands the real external IP to any page that creates a peer connection. Closing it requires Mode 3 or Mode 4, an extension, a browser like Brave that restricts the candidate set, or a full-tunnel VPN that owns the UDP path. None of those is the default, and the people most surprised by the leak are the ones who proxied their HTTP and assumed the rest followed. It did not. The server-reflexive candidate took the default route the whole time, and the STUN server told it the truth. For anyone routing a browser through a proxy and hoping a site cannot tell, the WebRTC candidate is the line in the connection that never learned to lie, and the detectors know exactly where to read it.
Sources & further reading
- Keränen, Holmberg, Rosenberg (2018), RFC 8445: Interactive Connectivity Establishment (ICE) — defines the host, server-reflexive, peer-reflexive, and relayed candidate types and the STUN-based gathering process.
- Uberti, Shieh (2021), RFC 8828: WebRTC IP Address Handling Requirements — the four IP-handling modes and the Mode 2 default that leaves the public IP exposed.
- Fablet, de Borst, Uberti, Wang (2021), Using Multicast DNS to protect privacy when exposing ICE candidates — the IETF draft specifying UUID
.localhostnames for host candidates. - Qingsi Wang (2019), PSA: Private IP addresses exposed by WebRTC changing to mDNS hostnames — the Chrome 76 default-on announcement and the getUserMedia exception.
- Jeroen de Borst (2019), PSA: WebRTC host candidate obfuscation experiment in M73 — the original experiment and the
--enable-webrtc-hide-local-ips-with-mdnsflag. - Daniel Roesler (2015), webrtc-ips demo and source — the original one-page leak demo and the createDataChannel/createOffer/onicecandidate sequence it used.
- Amir Pirogovsky / Peer5 (2021), Prflxion: a WebRTC IP leak — the IPv4-in-IPv6 prflx-candidate bypass of mDNS, reported and patched in June 2021.
- Al-Fannah (2017), One Leak Will Sink A Ship: WebRTC IP Address Leaks — a measurement study of WebRTC IP leakage across browsers and its tracking implications.
- MDN (2024), RTCIceCandidate — the JavaScript surface that exposes candidate
address,type, andrelatedAddressto a page. - Scrapfly (2024), WebRTC Proxy Leak Detection — describes surfacing host, srflx, and relay candidates to detect a datacenter IP behind a residential proxy.
Further reading
Device fingerprinting in anti-bot stacks: FingerprintJS, the entropy budget, and stability
How device fingerprinting works in anti-bot stacks, traced through FingerprintJS open-source and Pro: the signal set, the entropy budget that makes a visitor ID unique, why client-side hashes drift, and how it differs from bot detection.
·23 min readHow anti-bot vendors detect residential proxies and ASN reputation scoring
Traces how anti-bot systems classify an IP at the network layer: ASN reputation, datacenter-versus-residential-versus-mobile labelling, IP-quality scoring, known-proxy feeds, and why even a clean home IP still leaks.
·22 min readResidential vs datacenter vs mobile proxies: detection, cost, and use cases
A vendor-neutral comparison of the three proxy types: how each is sourced, how each gets detected at the ASN and reputation layer, what a gigabyte actually costs, and which job each one fits.
·19 min read