You set the proxy, the TLS padlock shows, the page loads. As far as most people check, everything works. But a proxy only carries the traffic you send through it. If your system resolves the hostname before connecting through the proxy, that DNS query walked out on its own path: plaintext on UDP port 53, visible to the Wi-Fi access point, the ISP, and whoever operates the resolver. The target website never sees it, your proxy never sees it, and nothing in your application logs records that it happened.
That is the quiet failure mode of proxy setups, and it usually comes down to a one-character difference in how you spell the proxy URL. We did not want to assert this from documentation, so we captured the packets.
How we tested
Every claim in this article comes from packet captures we ran on a clean server. The method:
- Start
tcpdumpwatching port 53 (DNS), port 853 (DNS-over-TLS), and UDP port 443 (QUIC). - Request a page on a unique random hostname we minted for that single run, for example
tpmmf8ba6h7wl.dt.simplyproxies.com. A unique name defeats every DNS cache, so if the name gets resolved anywhere, a query for exactly that string must leave the machine. - Check the capture for the unique name. If it appears in a port-53 packet, the setup resolved DNS locally, outside the proxy, no matter what the proxy encrypted afterwards.
This is the same idea commercial DNS-leak-test sites use, but self-hosted on our side of the glass: nothing depends on a third party's widget, and the raw evidence is a text file.
Environment of record, all runs on the same box:
| Component | Version |
|---|---|
| OS | Ubuntu 26.04.1 LTS |
| curl | 8.18.0 |
| tcpdump | 4.99.6 |
| Browser | Chromium headless shell 156.0.8078.4 (Playwright) |
| System resolver | systemd-resolved, forwarding to the provider resolver |
We used a real authenticated UK mobile proxy service for the proxy legs (Simply Proxies), because the whole point is testing the configurations people actually run, not a lab stub. The capture filter deliberately excluded the proxy ports, so no credentials appear anywhere in the evidence.
Setup 1: no proxy, the control
The baseline. Fetch the page with plain curl and watch port 53:
sudo tcpdump -ni any -l 'port 53'
curl -s https://tpmmf8ba6h7wl.dt.simplyproxies.com/
Captured, verbatim:
127.0.0.1.50317 > 127.0.0.53.53: 10332+ [1au] A? tpmmf8ba6h7wl.dt.simplyproxies.com. (63)
127.0.0.1.50317 > 127.0.0.53.53: 41633+ [1au] AAAA? tpmmf8ba6h7wl.dt.simplyproxies.com. (63)
141.95.51.235.44567 > 213.186.33.99.53: 28517+ [1au] A? tpmmf8ba6h7wl.dt.simplyproxies.com. (63)
Two hops, both plaintext: the application asks the local resolver stub, which forwards the identical question to the upstream resolver. Anyone on that path sees the full hostname, twice, for A and AAAA records. This is what "leaking" looks like in a capture, and it is the baseline every proxy setup has to beat.
Setup 2: an HTTPS proxy with CONNECT
An HTTPS proxy (TLS-wrapped HTTP proxy) works differently: the client opens TLS to the proxy itself and sends a CONNECT hostname:port request inside that TLS. The hostname travels to the proxy encrypted, and the proxy does the resolving at its end.
curl -s -x "https://USER:PASS@proxy.simplyproxies.com:6889" https://tpmmf8ba6h7wl.dt.simplyproxies.com/
Capture result: zero matches for the unique name on port 53. No local query ever left the machine. The hostname existed only inside the encrypted tunnel to the proxy. Whatever resolver your proxy operator uses sees the name; your local network does not.
Setup 3: SOCKS5 with local resolution, the socks5:// trap
Here is the one that surprises people. These two URLs look interchangeable:
curl --proxy socks5://USER:PASS@proxy.simplyproxies.com:6890 https://...
curl --proxy socks5h://USER:PASS@proxy.simplyproxies.com:6890 https://...
They are not. The h stands for hostname, and per the curl man page the difference is exactly the thing we are testing: plain socks5:// means "use the specified SOCKS5 proxy - but resolve the hostname locally", while socks5h:// means "let the proxy resolve the hostname".
Run the socks5:// variant and the capture lights up with the same plaintext queries as the no-proxy control:
127.0.0.1.58198 > 127.0.0.53.53: 48310+ [1au] A? tu3f5mzdfkx2t.dt.simplyproxies.com. (63)
141.95.51.235.xxxxx > 213.186.33.99.53: xxxxx+ [1au] A? tu3f5mzdfkx2t.dt.simplyproxies.com. (63)
The proxy is doing its job for the connection itself, but the hostname already leaked before the first SOCKS byte was sent. If your tool builds socks5:// URLs by default, every hostname you ever touch is announced on your local network path.
It gets worse. On our test box the same request did not just leak, it failed outright:
* Trying 51.255.200.116:6890...
* cannot complete SOCKS5 connection to www.google.com. (5)
Reply code 5 is connection refused per RFC 1928. Why? The test server has IPv6, the local resolver answered with an IPv6 address first, and that address family does not survive this particular proxy path. Forcing IPv4 with curl -4 made the identical request succeed (HTTP 200). So socks5:// on this setup means: leaked hostname and a connection that works only when the local resolver happens to hand back an address your proxy chain can carry. Two failure modes for the price of one typo.
Setup 4: SOCKS5 with proxy resolution, socks5h://
Same proxy, same port, one letter changed:
curl -s --proxy "socks5h://USER:PASS@proxy.simplyproxies.com:6890" https://www.google.com/
200
Capture result: zero matches for the unique name on port 53. And this is not a curl trick: SOCKS5 has supported exactly this mode since 1996. The RFC 1928 request format defines three address types, including ATYP 0x03: DOMAINNAME, which places a fully-qualified domain name directly in the CONNECT request. That is the mechanism socks5h uses: the client hands the name to the proxy, the proxy resolves it at its end, and no local query exists.
The trade-off is worth knowing: with remote resolution, names that only your local resolver can answer (internal hostnames, split-horizon DNS) will stop resolving. For public scraping and automation targets that never matters; for a laptop doing internal work it might.
Browsers: Chromium
Browsers are where proxy settings get fiddly, so we ran the same capture against headless Chromium 156.
Chromium with no proxy (control): the unique hostname appeared in port-53 traffic 41 times during a single navigation (Chromium resolves a lot: the main document plus prefetch and speculative lookups). Everything plaintext, exactly like the curl control.
Chromium pointed at our HTTPS proxy: zero local queries for the hostname. The browser handed the name to the proxy layer untouched. The leak-free behaviour is right, but we hit a different wall: page loads failed with net::ERR_PROXY_AUTH_UNSUPPORTED. Chromium's network error list defines that code (-115) as: "The proxy requested authentication (for tunnel establishment) with an unsupported method." In practice, this Chromium could not authenticate to a TLS-wrapped HTTPS proxy at all. The DNS verdict still holds, the name went to the proxy layer even though the tunnel was refused, but it is a reminder that browser proxy support is a patchwork: credentials that curl takes for granted can simply not be implementable in your browser.
Chromium pointed at an authenticated SOCKS5 proxy: it failed outright with net::ERR_SOCKS_CONNECTION_FAILED, and this one is by design. Chromium's SOCKS5 client source hard-codes its method-selection greeting to a single offer:
static constexpr std::array<uint8_t, 3> kSOCKS5GreetWriteData{
0x05, 0x01, 0x00}; // no authentication
Username/password (RFC 1929, method 0x02) is never offered, and if the server selects any other method the handshake fails on the spot. Chromium has no SOCKS5 authentication code to disable; it was never written. Our test relay requires a username and password, so the browser was dead at the greeting, and the same would happen against any authenticated SOCKS5 endpoint.
Two useful corollaries from the same source file: Chromium always sends the destination as a domain name (kEndPointDomain, the ATYP 0x03 path), meaning browser SOCKS5 is remote-resolution by design, so the socks5:// trap is a command-line and library footgun, not a browser one; and a browser that needs to pass proxy authentication has exactly zero luck with SOCKS5 in Chromium, while an HTTPS proxy fails later with the unsupported-method error above. Device-wide tunneling (next section) sidesteps the whole matrix.
The QUIC observation
While the captures were running we also watched UDP port 443 (QUIC/HTTP3). The pattern was clean:
| Setup | UDP/443 packets seen |
|---|---|
| Chromium, no proxy, loading google.com | 28 |
| Chromium via HTTPS proxy, same page | 0 |
| curl via HTTPS proxy | 0 |
| curl via socks5h | 0 |
When a proxy is configured, no QUIC egress happened at all: the traffic fell back to TCP inside the tunnel. For DNS-leak purposes this is good news (QUIC is one less side channel), and it matches how the stack treats proxied UDP in general. We observed this on one box with one browser; treat it as a data point, not a specification.
What we did not test
Honesty about scope, because DNS leaks have more doors than a proxy can close:
- WebRTC can expose addresses to pages through ICE/STUN regardless of proxy settings, because it negotiates its own paths. Our headless browser cannot exercise it meaningfully; check a WebRTC-specific test in your real browser.
- DoH and DoT (DNS over HTTPS/TLS): our test box used plain systemd-resolved forwarding. A machine configured with encrypted DNS would not show port-53 plaintext, but the queries would still leave the machine to a resolver of its own choosing, which is its own trust decision.
- Proxy-unaware applications that ignore system proxy settings entirely. Device-wide tunneling (routing at the network layer, as with tun2socks) is the fix for those, and it is a different setup than any tested here.
How to verify your own setup
Two minutes, no third-party site:
# Terminal 1: watch DNS (sudo or root)
sudo tcpdump -ni any -l 'port 53'
# Terminal 2: fetch a one-off name under any domain you like
curl -s "https://test$RANDOM$RANDOM.yourdomain.com/" || true
If the unique name shows up in the tcpdump output, that setup resolved locally. Repeat with your real proxy configuration and your real browser. If you use our SOCKS5 endpoint, the connection strings the dashboard builds are already socks5h:// for exactly this reason; if you hand-roll URLs, keep the h.
Summary
- A proxy only tunnels what you send through it. Local hostname resolution walks out separately, in plaintext, and nothing in the proxied connection can hide it.
- With
socks5://(local resolution) we captured the full plaintext query path, identical to running no proxy at all. Withsocks5h://(proxy resolution) the local query never existed. - The HTTPS/CONNECT setups resolved nothing locally in every run.
socks5://also broke outright when the local resolver handed back an address the proxy path could not carry;socks5h://loaded the same page fine.- Chromium sent hostnames to the proxy layer when configured with a proxy, but could not authenticate to our TLS-wrapped HTTPS proxy at all (
net::ERR_PROXY_AUTH_UNSUPPORTED), and its SOCKS5 support has no authentication code whatsoever (hard-coded no-auth greeting), which failed our authenticated relay at the greeting. Its SOCKS5 always sends the hostname to the proxy, so thesocks5://trap does not apply to it. - One letter (
socks5vssocks5h) was the difference between a leak and no leak. Check your own stack the same way we did: a tcpdump filter on port 53 and a hostname nobody has ever resolved before.
Last verified 2026-10-27: all captures and command outputs in this article are from runs on that date (Ubuntu 26.04.1, curl 8.18.0, tcpdump 4.99.6, Chromium headless 156.0.8078.4).
Try It With Real UK Mobile IPs
Run the examples in this guide through genuine UK 4G/5G exits. 500 MB free, no card required.
Start Free Trial