← All posts Blog Guides

The exit IP is fine. Your browser is telling on you.

Guides
The exit IP is fine. Your browser is telling on you.

A ticket arrives with a screenshot of a leak-test page. There are two addresses on it: the proxy exit, and the customer’s home address, a few rows apart. The message says some version of your proxy is leaking my real IP.

It is a fair conclusion from the evidence on screen, and in every case we have traced it has been wrong about the mechanism. The proxy carried one thing — the TCP connection you asked it to carry — and it carried it correctly. The second address on that page arrived by a route the tunnel never touched, or it was never a network fact at all.

That distinction is worth more than the ticket. Fix the wrong layer and you will change providers, watch the same failures follow you, and conclude that residential proxies do not work. Here is how to find out which half is broken in about five minutes, what actually leaks, and the order to fix it in.

The test that settles it first

Do not start with a composite score page. Start by testing the tunnel on its own, with a client that has no browser attached to it.

curl -s -x "http://USER:PASS@PROXY_HOST:PORT" https://api.ipify.org; echo

Then load the same echo URL in the automated browser you are actually running, through the same proxy, and compare.

There are only three interesting outcomes.

  • curl shows the exit address, the browser shows the exit address. The tunnel is fine and so is the browser’s HTTP path. Anything else on a leak-test page is coming from somewhere other than HTTP.
  • curl shows the exit address, the browser shows your home address. The tunnel is fine. Your browser is not sending that request through it — a bypass list, a PAC file, a per-context setting that never applied, or an extension.
  • curl shows your home address. The proxy is not in the path at all. Check the scheme, the credentials, and whether your client silently dropped the proxy on a redirect.

Only the third case is a proxy question. The first is the one that generates the screenshots, and it is the one worth understanding properly.

What a proxy actually carries

An HTTP proxy connection is a CONNECT to a host and a port, after which the proxy moves bytes between two TCP sockets and reads none of them, because they are inside your TLS session. A SOCKS5 proxy does the same for TCP, and carries UDP only where the server implements UDP ASSOCIATE and the client asks for it, which browsers do not.

So the tunnel covers the TCP connections your client chose to route through it. It does not cover:

  • UDP that the operating system sends from its own socket
  • DNS queries your client resolved before it opened the connection
  • Any request your client decided was exempt
  • Anything that never crossed the network at all — your clock, your fonts, your GPU string, your installed languages

That last category is the one people miss. A proxy changes where your traffic appears to come from. Everything above the network layer is still yours to get right.

Leak source one: WebRTC

To open a peer connection, a browser gathers ICE candidates. It enumerates the machine’s local interface addresses directly, and it asks a STUN server over UDP what public address the packets appear to come from. Both of those happen outside the HTTP path, from the operating system’s own socket. A page can start that process without any camera, microphone or call — a data channel is enough — and read the candidates as they arrive.

Test it in the browser you are worried about:

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
pc.createDataChannel('probe');
pc.onicecandidate = e => e.candidate && console.log(e.candidate.candidate);
pc.createOffer().then(o => pc.setLocalDescription(o));

If your LAN address and your home public address appear in that console, you have found the screenshot. No request went through the proxy to produce them.

The fix is to constrain WebRTC rather than tear it out. Chromium’s WebRTC IP handling setting takes the value disable_non_proxied_udp, which stops the browser sending UDP the configured proxy cannot carry — it falls back to TCP through the proxy — and keeps local interface addresses out of the candidate list. It is applied as an enterprise policy, named WebRtcIPHandlingPolicy in the registry and the extension API and documented on current Chrome as WebRtcIPHandling; Chromium also carries a command-line switch for the same value, under a name that has changed between versions. In Firefox, media.peerconnection.ice.proxy_only forces ICE through the proxy, and media.peerconnection.enabled set to false removes the API entirely. Apply whichever you choose, then re-run the probe above rather than trusting a setting name copied from a forum.

Prefer the proxy-only setting to full removal. A consumer browser has a working WebRTC stack; one that throws on RTCPeerConnection is describing itself as something unusual before you have loaded a page.

Leak source two: DNS resolved outside the tunnel

This one does not put your address on a results page, so it survives leak tests and still causes trouble.

With an HTTP CONNECT proxy, you hand over a hostname and the proxy resolves it. With SOCKS5, whether the hostname or a pre-resolved address goes over the wire depends on which scheme you asked for:

# your machine resolves the hostname, then asks the proxy for that address
curl -x socks5://USER:PASS@PROXY_HOST:PORT https://example.com

# the proxy resolves the hostname
curl -x socks5h://USER:PASS@PROXY_HOST:PORT https://example.com

The same distinction reaches Python through urllib3, which is what requests uses for SOCKS, and whose documentation recommends the socks5h:// form so that resolution is done at the proxy rather than on your machine. Not every client accepts that scheme — some reject socks5h outright and settle resolution another way — so check what yours does instead of assuming the URL is portable.

With the local-resolution form, your resolver sees every hostname you visit, and the answer you get is the one your local network gets. Geo-routed hostnames then resolve to infrastructure near you and are fetched through an exit somewhere else entirely. That combination is visible to the target and looks nothing like a normal client. Resolve at the proxy unless you have a specific reason not to.

Leak source three: address family

An IPv6 address appearing in a result is not automatically evidence of a leak. If a target is reachable only over IPv6, something in the path has to speak IPv6 to reach it, and the address you see may belong to the network you are exiting through. Look at the prefix before drawing a conclusion: if it is not from your own connection, it is not your address.

The genuine failure here is narrower. A dual-stack host running a client that only routes IPv4 sockets through the proxy will reach AAAA-only endpoints natively, from your own address, while every IPv4 request looks perfect. Test it by asking an IPv6-only echo endpoint who is calling:

curl -s -x "http://USER:PASS@PROXY_HOST:PORT" https://api6.ipify.org; echo

If your own address comes back, your client is proxying one address family and not the other.

Leak source four: the things that are not addresses

This is what actually flags accounts, and it is where the effort belongs.

The clock and the languages. Intl.DateTimeFormat().resolvedOptions().timeZone, new Date().getTimezoneOffset(), navigator.languages and the Accept-Language header all describe your machine. An exit in Frankfurt with a browser reporting Europe/London and en-GB is a contradiction the site can check on page load, and it costs nothing to check.

The subtler version is worse. Many setups override the timezone on the main thread and forget worker threads, so the page can ask twice and get two answers:

// main thread
Intl.DateTimeFormat().resolvedOptions().timeZone;

// worker thread — must agree with the line above
const src = `postMessage(Intl.DateTimeFormat().resolvedOptions().timeZone)`;
const w = new Worker(URL.createObjectURL(new Blob([src])));
w.onmessage = e => console.log('worker:', e.data);

A machine whose main thread and worker disagree about what timezone it is in is not a real machine. That is a stronger signal than the original mismatch it was meant to hide.

navigator.webdriver. Under default automation launch flags this is true, and reading it is one property access. Check it in the console of the browser you actually ship, not in a clean one.

Canvas, WebGL and fonts. The renderer string, the driver, the list of installed fonts and how the machine rasterises text describe hardware and an operating system. A container with a software renderer and a short font list, claiming a User-Agent from a consumer Windows laptop, is describing two different computers.

The TLS handshake. The ClientHello — cipher suites, extensions, their order, ALPN, supported groups — is produced by your HTTP client’s TLS stack, not by the User-Agent string in your headers. Fingerprints derived from it (JA3, JA4) are computed before a single byte of your HTTP request is parsed. A Python client sending a Chrome User-Agent has a Python handshake, and the two do not agree. Changing proxies does not touch this. Using a client that reproduces a real browser’s handshake does.

What we can and cannot see from here

We can tell you whether a tunnel opened on your credentials, which client address it came from, which host and port it was for, and how many bytes moved. We cannot see inside your TLS session, because that is the point of it. We never see your WebRTC candidates, because those packets do not come near us. We cannot read your clock.

So when the question is is your proxy leaking my IP, the test has to run on your side. That is a statement about where the packets go, and the tests above are the ones we would run ourselves.

Fix order

  1. Stop the traffic that is not in the tunnel. WebRTC UDP, local DNS resolution, bypass lists, anything the browser exempts. This is the only category that puts your literal address on a page.
  2. Make the environment agree with the exit. Timezone, locale, Accept-Language — on the main thread and in workers, checked rather than assumed.
  3. Remove the automation tells. navigator.webdriver and the launch switches that set it.
  4. Make the handshake match the client you claim to be. Either drive a real browser, or use a client that reproduces a real browser’s TLS signature. A User-Agent string on its own is a claim, not a disguise.
  5. Then, and only then, question the address. By this point you have eliminated the parts a provider cannot fix for you.

A proxy is transport. It changes the address your TCP connections appear to come from, and nothing else. Anyone selling one as leak-proof is describing something they do not control, because the client is what leaks. Test the two halves separately and the ticket usually answers itself.

For the other side of this — what a target site sees when it looks at the exit address, and why reputation scores flag it — see why IP checkers flag residential proxies and the IP was never yours alone. For how these signals are weighed together rather than one at a time, the honest version of getting past anti-bot defences.

Start routing today. Spin up in 90 seconds.

Create an account and ship your first ProxyOmega request before your coffee's cold.

ProxyOmega ProxyOmega

90M+ ethically-sourced IPs across 200+ countries and 30,000+ cities. Residential, mobile, ISP and IPv6 proxies for scraping and AI agents.

GDPRCCPA
Product
Premium Unlimited Budget Unlimited Unlimited Residential Proxies Residential / ISP Mobile IPv6 Chrome Extension
Solutions
Web scraping AI agents Price monitoring SERP & SEO Integrations All use cases
Resources
Glossary Error codes Free tools Proxies by platform Locations
Company
About Blog Docs Reseller program Affiliate Contact Sign in
© 2026 ProxyOmega Ltd. All rights reserved.