socks5 vs socks5h DNS leak, demonstrated

    One character decides who resolves your hostnames. Here is the socks5 vs socks5h DNS leak, shown live with curl and tcpdump, plus the fix per client.

    · 13 min read · Protocols

    Two proxy URLs. One character apart. Completely different packet paths. The socks5 vs socks5h DNS leak is the most common privacy hole in proxy configuration precisely because both spellings connect, both return 200, and nothing in your application tells you which one you got. Under socks5:// your machine resolves the hostname and hands an IP address to the proxy. Under socks5h:// your machine hands over the hostname and the proxy resolves it. The first spelling publishes a DNS query for every host you touch to your resolver, your ISP, and every hop in between.

    This article shows the query escaping, live, with commands you can run in two terminals.

    Who resolves the hostname

    SOCKS5, defined in RFC 1928, lets a client specify the destination in three ways: an IPv4 address, an IPv6 address, or a domain name. The address type is a single byte in the connect request. Everything below follows from which of those three bytes your client chooses to send.

    Client-side resolution — socks5://. Your client calls the system resolver for example.com, gets back 93.184.216.34, opens the SOCKS handshake, and sends the connect request with address type IPv4 and those four bytes. The proxy never learns the name. Your resolver, and anyone on the path to it, learns it in cleartext unless you have deployed DoH or DoT.

    Proxy-side resolution — socks5h://. Your client skips the local lookup entirely. It sends the connect request with address type "domain name", a length byte, and the ASCII hostname. The proxy resolves the name from its own network position and connects. Your resolver never sees the query.

    The h is for hostname. The naming convention is inherited from SOCKS4 and SOCKS4a, where the a variant added exactly this capability for the same reason.

    There is one lookup you cannot avoid. If your proxy address is itself a hostname — gw.anonedge.com, say — your client has to resolve that name locally to open the TCP connection to the proxy. That query appears in the capture under both schemes. It is expected, it is not the leak, and confusing it for the leak is the single most common way people mis-read the test below.

    Demonstrate the socks5 vs socks5h DNS leak

    Two terminals. Terminal 1 watches every DNS packet leaving the host. Terminal 2 runs the same request twice, once per scheme.

    Terminal 1, on Linux:

    sudo tcpdump -n -l -i any 'port 53'
    

    -n stops tcpdump resolving addresses itself, which would pollute your own capture. -l line-buffers so you see queries as they happen. -i any is Linux-only; on macOS name the interface instead, for example -i en0.

    Terminal 2, the leaking version:

    curl -v -x socks5://login:[email protected]:824 https://example.com/
    

    Terminal 2, the non-leaking version:

    curl -v -x socks5h://login:[email protected]:824 https://example.com/
    

    Port 824 is the SOCKS5 rotating port on the AnonEdge gateway. The full port and protocol list is in the docs.

    Under the first command, terminal 1 shows two names: your gateway, and your target.

    14:22:07.118374 IP 10.0.0.12.54113 > 10.0.0.1.53: 9182+ A? gw.anonedge.com. (33)
    14:22:07.140902 IP 10.0.0.1.53 > 10.0.0.12.54113: 9182 1/0/0 A 203.0.113.9 (49)
    14:22:07.201558 IP 10.0.0.12.61224 > 10.0.0.1.53: 44031+ A? example.com. (29)
    14:22:07.226117 IP 10.0.0.1.53 > 10.0.0.12.61224: 44031 1/0/0 A 93.184.216.34 (45)
    

    Under the second command, the target query is gone. Only the gateway lookup remains:

    14:23:41.664290 IP 10.0.0.12.49882 > 10.0.0.1.53: 15507+ A? gw.anonedge.com. (33)
    14:23:41.688771 IP 10.0.0.1.53 > 10.0.0.12.49882: 15507 1/0/0 A 203.0.113.9 (49)
    

    That difference is the whole article. One query for the target, or none.

    The addresses and timings above are illustrative formatting of tcpdump's DNS output, not a measurement — run the commands and read your own capture.

    Two things that will make you think the test failed when it did not. First, your stub resolver caches. Run sudo resolvectl flush-caches on systemd-resolved hosts, or use a hostname you have not queried recently, or the leaking run will show nothing because the answer was already local. Second, if your system uses DNS-over-HTTPS, the query leaves on port 443 and a port 53 filter never sees it. That does not mean it did not leak — it means it leaked to a different party over a different port.

    The same test without root

    You do not need tcpdump or root at all. curl reports which side resolved, in its own verbose output. The strings come straight from curl's SOCKS implementation, which logs SOCKS5 connect to %s:%u (locally resolved) when the client resolved and SOCKS5 connect to %s:%u (remotely resolved) when the proxy did (lib/socks.c).

    curl -v -x socks5://login:[email protected]:824 https://example.com/ 2>&1 \
      | grep -i 'SOCKS5 connect'
    
    * SOCKS5 connect to 93.184.216.34:443 (locally resolved)
    

    Swap the scheme and run it again:

    curl -v -x socks5h://login:[email protected]:824 https://example.com/ 2>&1 \
      | grep -i 'SOCKS5 connect'
    
    * SOCKS5 connect to example.com:443 (remotely resolved)
    

    Whichever side of the string appears — an IP literal or a hostname — is the answer. That one grep belongs in your job's pre-flight, not in a debugging session after the fact.

    A third option, if you want a permanent record rather than a live capture: point the machine at a resolver you control and read its log. With dnsmasq, add log-queries to /etc/dnsmasq.conf, restart it, and every query lands in the system journal with a timestamp and the requesting client. That gives you an audit trail across a whole job run rather than a single request, which is what you actually want when you are trying to prove a fleet of workers is clean.

    What leaks, and to whom

    The target does not learn anything from a client-side lookup. It sees a TCP connection from the proxy's exit address either way. The exposure is entirely on your side of the tunnel, and it has three distinct consequences.

    Your resolver gets a complete list of every host you touch. Not the URLs, not the payloads — TLS covers those — but the hostnames, in order, with timestamps, from your source address. For a scraping or research workload that list is a near-perfect description of what you are doing and who you are doing it to. If the resolver belongs to your ISP, a corporate network, or a hosting provider, it is a third party you did not intend to include.

    The path to the resolver sees it too. Classic DNS on port 53 is unauthenticated cleartext. Anything between your host and the resolver reads the query and can rewrite the answer. That is a correctness problem before it is a privacy problem: a rewritten answer sends your proxied connection to an address of somebody else's choosing.

    Resolution position stops matching exit position. This is the one that quietly ruins data rather than exposing it, and it gets its own section below.

    The libraries that get this wrong by default

    Almost every SOCKS5 client defaults to local resolution, because the SOCKS5 scheme name without the h means exactly that. The fix is one string in every case.

    Client Leaks (client resolves) Correct (proxy resolves) Source
    curl, command line -x socks5://… or --socks5 -x socks5h://… or --socks5-hostname everything curl: SOCKS
    Python requests + PySocks socks5://user:pass@host:port socks5h://user:pass@host:port requests: SOCKS
    Node socks-proxy-agent socks5:// sets lookup = true socks5h:// or bare socks:// leave lookup off package source
    Go golang.org/x/net/proxy only if you hand it an IP literal pass a hostname; the dialer sends AddrTypeFQDN x/net internal/socks

    Three notes on that table.

    Python requests needs the extra. SOCKS support is marked "new in version 2.10.0" and is not installed by default; you need python -m pip install 'requests[socks]'. The documentation is explicit about the scheme semantics: "Using the scheme socks5 causes the DNS resolution to happen on the client, rather than on the proxy server… If you want to resolve the domains on the proxy server, use socks5h as the scheme" (requests advanced usage).

    Node's socks-proxy-agent has an asymmetry worth knowing. Its URL parser sets lookup = true for socks4 and socks5, and leaves it false for socks4a, socks5h and the bare socks scheme. So socks:// — the shortest spelling, and the one most copied from READMEs — is the safe one, while the explicit socks5:// is the leaking one. That is the opposite of what most people assume.

    Go is the outlier, in your favor. proxy.SOCKS5 from golang.org/x/net/proxy builds the connect request by calling net.ParseIP on the host portion of the address. If that fails — that is, if you passed a hostname — it appends AddrTypeFQDN and the name, and the proxy resolves. Go only resolves locally if you resolved locally first and passed it an IP. There is no socks5h scheme to remember, and no leak to fix, as long as you keep hostnames as hostnames all the way down.

    Browsers deserve a warning rather than a table row. Firefox exposes network.proxy.socks_remote_dns in about:config; read the value on the profile you are actually running rather than assuming a default, because it has changed across releases. For headless automation, remember that Playwright documents optional username and password "for HTTP(S) proxy" and that SOCKS5 proxy authentication has been an open request against the project since 2021 (playwright#10567) — so on that stack the scheme question is often settled for you by the auth question. Playwright's SOCKS5 limitation has the per-context detail.

    Split-horizon and CDN consequences

    Here is the failure mode that costs money rather than privacy.

    A large fraction of the public internet answers DNS queries differently depending on where the query came from. CDNs do it deliberately: an authoritative resolver looks at the querying resolver's address, or at the EDNS Client Subnet option, and returns the edge node nearest to that location. Anycast does it structurally. Split-horizon corporate DNS does it by policy.

    Under socks5:// the query comes from you. Under socks5h:// it comes from the proxy.

    So a worker sitting in Frankfurt, routing through a Brazilian exit, on socks5://, resolves the target to a European edge node and then connects to that European node from a Brazilian address. You get the German edge's cached content, the German edge's language negotiation, the German edge's inventory — attributed to a Brazilian IP. The response is a 200. The data is wrong, and nothing in it says so.

    The same mismatch is a fingerprint. A connection arriving at a Brazilian-facing edge from a Brazilian IP that was steered there by a German resolver is internally inconsistent, and it is not hard to notice. If you are running geo-sensitive collection, socks5h:// is a correctness requirement, not a privacy nicety.

    If your job depends on geography at all — country, state, city, ZIP or ASN — resolve on the proxy side so that DNS and TCP agree about where you are. The targeting parameters that select the exit are worthless if the hostname was resolved from somewhere else.

    HTTP proxies do not have this problem

    Ask an HTTP proxy for an HTTPS URL and your client sends:

    CONNECT example.com:443 HTTP/1.1
    Host: example.com:443
    Proxy-Authorization: Basic bG9naW46cGFzc3dvcmQ=
    

    The hostname is in the request line. It has to be — the proxy cannot open the tunnel without it, and it cannot read the TLS SNI because the TLS handshake has not started yet. Proxy-side resolution is not an option in HTTP CONNECT; it is the mechanism. Plain http:// requests through a proxy carry the absolute URI in the request line for the same reason.

    That is a real argument for protocol choice, and it is rarely made. If you have no specific need for SOCKS5 — non-HTTP protocols, UDP association, a client that only speaks SOCKS — then HTTP CONNECT removes an entire class of misconfiguration by construction. On the AnonEdge gateway that is port 823 for HTTP and HTTPS, against port 824 for SOCKS5:

    # HTTP CONNECT, rotating. The proxy resolves, always.
    curl -x "http://login:[email protected]:823" https://api.ipify.org/
    
    # SOCKS5, rotating, proxy-side resolution. Note the h.
    curl -x "socks5h://login:[email protected]:824" https://api.ipify.org/
    

    Both ports rotate the exit per connection. Sticky sessions run on ports 10000 to 20000 and take either protocol; the scheme rule does not change there. Connecting to the gateway has the reference.

    Where SOCKS5 still earns its place: protocols that are not HTTP, clients that cannot be told to use CONNECT, and tooling that speaks SOCKS natively. In those cases you keep SOCKS5 and you spell it socks5h.

    A pre-flight checklist

    Four checks, none of which take longer than a second, before a job runs.

    1. Grep the scheme in the config that will actually be used. Not the README, not the .env.example — the rendered config, the container environment, the CI secret. socks5:// anywhere is a finding.
    2. Assert remote resolution once at startup. Run the curl verbose check above and fail the job if the output does not contain (remotely resolved). Two lines of shell, catches every regression.
    3. Check ALL_PROXY and the lowercase variants. An ALL_PROXY=socks5://… inherited from a base image or a shell profile silently overrides nothing visible and leaks everything. Print the effective environment, do not assume it — ALL_PROXY and scheme selection has the precedence rules per tool.
    4. Watch port 53 for one request. The tcpdump above, once, on a fresh cache. If your target hostname appears, stop and fix the scheme before you run 10,000 of them.

    Then confirm the exit is what you asked for, with an external echo rather than the target itself:

    curl -s -x "socks5h://login:[email protected]:824" https://api.ipify.org/
    

    If that returns an address in the wrong country, the problem is targeting, not DNS — start with the troubleshooting guide. If it returns the right address but your data looks like it came from somewhere else, the problem is almost always the missing h.

    Frequently asked questions

    Is socks5h a different protocol from socks5?

    No. Both speak SOCKS5 as defined in RFC 1928, on the same port, with the same handshake. The h is a client-side URL scheme convention, not a wire protocol change. It selects which address type byte your client puts in the connect request: an IP address it resolved itself, or a domain name for the proxy to resolve. The proxy sees a normal SOCKS5 request in both cases.

    Does a DNS leak expose my traffic contents?

    No. It exposes hostnames, not payloads. TLS still protects the body of every HTTPS request. What your resolver and the network path to it learn is the ordered list of hosts you connected to, with timestamps, tied to your source address. For most proxy workloads that list is sensitive on its own, because it describes the job even when it does not describe the data.

    Will socks5h slow my requests down?

    Resolution moves to the proxy, so a cold lookup costs the proxy's resolver round trip instead of yours, and your local cache no longer helps. In practice the difference is one lookup per new hostname per connection, and it is frequently offset because the proxy resolves from a network position closer to the edge node it will actually connect to. Measure it on your own workload rather than assuming either direction.

    How do I check whether my current setup leaks, without changing anything?

    Run the same request under curl -v and grep for SOCKS5 connect. If the line ends in (locally resolved) your client is resolving and you are leaking. If it ends in (remotely resolved) the proxy is resolving. This reads curl's own log strings, requires no root, no packet capture, and no change to your configuration.

    Do HTTP proxies leak DNS the same way?

    No. An HTTP proxy receives the hostname in the CONNECT request line or in the absolute URI of a plain request, so it must resolve the name itself. There is no client-resolution mode to get wrong. That is a legitimate reason to prefer HTTP CONNECT on port 823 over SOCKS5 on port 824 when you have no protocol requirement pushing you toward SOCKS.

    What about socks4a?

    Same idea, older protocol. SOCKS4 supports IPv4 destinations only, so SOCKS4a was added to let the client pass a hostname for the proxy to resolve. The naming pattern — plain version resolves locally, lettered variant resolves remotely — carried straight into socks5 and socks5h. If you are choosing today, choose SOCKS5, which handles IPv6 and authentication.

    Fix the string, then start routing

    The correction is one character in one config value, and the verification is one grep. Do both before your next job, not after the data comes back wrong.

    Start routing on gw.anonedge.com:824 with socks5h://, or take port 823 and let HTTP CONNECT settle the question for you. Check the per-GB rates on the pricing section first.