Proxy DNS leaks: how a proxied collection stack still exposes its real network path

A proxied scraper can look correctly routed on every metric you check and still leak. The TCP connection goes out through the exit node, the response comes back through the exit node, and the target sees the proxy’s IP the whole time. But before any of that happens, something on the collection host had to turn a hostname into an IP address, and that step doesn’t automatically travel through the proxy just because the rest of the request does. When it doesn’t, the operator’s real network path gets exposed to whatever DNS resolver handled the lookup, even though the actual HTTP traffic never touched it.

This is what people mean by a DNS leak in a proxy context, and it’s a separate failure mode from a proxy going down or getting blocked. The proxy can be working perfectly and the leak still happens, because resolution and connection are two different steps in the stack, handled by different pieces of code, and they don’t inherit the same routing by default.

What’s actually leaking

DNS resolution is a client asking a resolver “what’s the IP for this hostname,” and the resolver answering. If that question gets asked from the collection host directly, over the host’s normal internet connection, then whoever operates that resolver, whether it’s the host’s ISP, a public resolver like a DoH provider, or a resolver baked into a container’s default network, sees the real source IP and ASN of the machine doing the crawling, along with the hostname it’s about to visit.

None of that is visible to the target site. The target still only sees the proxy’s exit IP. The leak is upstream of the target, at the resolver. But it’s still a leak, because it defeats the point of routing the collection through a proxy pool in the first place: the goal was to make the crawling traffic look like it originated somewhere else, and DNS resolution quietly reveals where it actually originated.

SOCKS5 has two resolution modes, and they look almost identical

This is the most common place the leak shows up, because the difference is one letter and most people don’t notice it. SOCKS5 supports sending either a raw IP address or a domain name as the destination address type. If a client sends the domain name, the proxy server resolves it. If the client resolves it first and sends an IP, the client’s own resolver does the work.

curl and most SOCKS5 libraries expose this as two separate proxy schemes. socks5:// resolves the hostname locally, on the machine running curl, before ever talking to the proxy. socks5h:// passes the hostname through and lets the proxy server do the resolution. Same proxy, same exit IP, completely different DNS path. Writing socks5://proxyhost:1080 instead of socks5h://proxyhost:1080 is a one-character typo that produces a fully working scraper with a fully leaking resolver.

Python’s requests library, when backed by PySocks, has the same distinction. So does proxychains, which calls this the “remote DNS” setting. It’s worth actually checking which one a given tool defaults to rather than assuming, because defaults vary and some libraries silently fall back to local resolution if the remote lookup fails, which means a leak can appear intermittently under specific error conditions rather than consistently.

HTTP and HTTPS proxies are usually fine, with some exceptions

Forward HTTP proxies work differently and are generally safer on this specific point. For plain HTTP, the client sends an absolute-form request line, GET http://example.com/path HTTP/1.1, straight to the proxy, and the proxy is the one that resolves the hostname. For HTTPS, the client sends CONNECT example.com:443 to the proxy and the proxy resolves and opens the tunnel. In both cases the client never needs to know the target’s IP address at all.

Where this breaks down is in code that resolves ahead of the actual request for some other reason. Health check logic that pings a hostname before deciding to route through it. Certificate pinning implementations that resolve to fetch or validate against a known IP. Connection pooling libraries that pre-resolve to key their pool by IP instead of hostname. Retry logic that catches a connection failure and falls back to a “direct” resolve-and-connect path without going through the configured proxy at all. None of these are exotic; they show up in production HTTP clients regularly, and they resolve outside the proxy without the operator ever setting a socks5:// scheme wrong, because the leak isn’t in the proxy configuration, it’s in a secondary code path that doesn’t check whether a proxy is configured before it runs.

Containers add another resolver in the chain

Running collection workers in containers adds a hop that’s easy to overlook. Docker’s default bridge network ships an embedded DNS server at 127.0.0.11 inside each container, which forwards queries it can’t answer to whatever resolver the host is configured to use, usually from /etc/resolv.conf on the host or a systemd-resolved stub. If the application inside the container honors HTTP_PROXY and HTTPS_PROXY environment variables for its HTTP traffic but has no equivalent DNS configuration, the actual request goes through the proxy while the resolution goes through the container’s embedded resolver, out to the host, out to whatever upstream DNS the host uses, over the host’s real network interface.

This gets worse if the host, or the resolver library inside the container, is configured to use DNS over HTTPS. A DoH query to a public resolver is itself an HTTPS connection, and unless something explicitly routes that connection through the proxy too, it goes out directly, over the real interface, to the DoH provider, who now has a direct log entry connecting the operator’s real IP to the hostname about to be scraped.

What this costs, beyond getting blocked

A DNS leak doesn’t usually get a scraper detected by the target site directly, since the target never sees the leaked query. The cost is different. It hands metadata about the operation, which hostnames are being crawled and how often, to a third party the operator doesn’t control: an ISP, a public DoH provider, or whatever resolver happens to be configured on the box. For anyone running a proxy pool specifically to keep collection infrastructure separate from their own identity, that’s the exact thing the proxy pool was supposed to prevent.

It also creates a stable fingerprint across an otherwise rotating setup. If exit IPs rotate through a proxy pool but every session resolves DNS from the same origin ASN, that origin is a constant sitting underneath a variable, and it’s the kind of constant that correlates sessions together even when the visible IP changes on every request.

How to actually check for it

The reliable way to check is a packet capture on the collection host while the scraper runs, filtering for UDP port 53, TCP port 853 for DNS over TLS, and TCP 443 traffic to known DoH resolvers. Anything showing up on those filters, other than traffic addressed to the proxy itself, is a leak. This is more trustworthy than reading library documentation, because the actual behavior can drift between versions and between code paths in ways the docs don’t cover.

curl -v against a real target through the proxy is a fast manual check. With a working socks5h:// proxy, curl’s verbose output shows it connecting to the proxy host and never resolving the target itself. With socks5://, the log line shows curl resolving the target’s hostname to an IP before it ever talks to the proxy, which is visible right there in the trace.

Third-party DNS leak test services, the kind that hand back a unique subdomain and log which resolvers query it, work well for this too, as long as the request that triggers the lookup goes through the exact same code path and network configuration as the real scraper, not a browser tab opened separately on the same machine. Testing the browser instead of the actual worker process is a common way to get a clean result that doesn’t reflect what the pipeline actually does.

A short defensive checklist

Use socks5h:// rather than socks5:// for any SOCKS proxy, or the equivalent “remote DNS” flag for whatever tool is in use, and confirm it with a packet capture rather than trusting the config. For HTTP and HTTPS proxies, verify with a capture that the client is genuinely sending CONNECT or absolute-form requests rather than falling back to a direct resolve-and-connect path under retry or pooling logic. In containerized setups, either point the container’s resolver explicitly at something routed through the same proxy path, or run DNS resolution through a tool that tunnels it over the proxy alongside the HTTP traffic, rather than leaving it on the container’s default embedded resolver. For browser automation, disable DNS prefetching in the automation profile, since prefetch can trigger a lookup ahead of the navigation logic that’s supposed to be proxy-aware, and keep in mind that WebRTC is a separate leak vector in real browser sessions worth checking independently, since most proxies only carry TCP and WebRTC’s ICE gathering can use UDP that never touches the proxy at all. Re-verify after any change to the proxy library version, the container base image, or the automation framework, since this is exactly the kind of behavior that changes silently between releases.

None of this is a one-time fix. It’s a check that belongs in the same rotation as proxy health checks, because a resolver leak and a dead proxy fail in opposite directions: one makes the scraper stop working where you’d notice immediately, and the other keeps it working while quietly exposing the thing the proxy was there to hide.

Data Research Tools writes about scraping infrastructure, proxy pools, and the operational details that don’t show up in vendor marketing. Read more on the home page.

Get new guides and videos first — join the Telegram channel.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *