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.
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.
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.
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.
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.
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.
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.
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.
Four checks, none of which take longer than a second, before a job runs.
.env.example — the rendered config, the container
environment, the CI secret. socks5:// anywhere is a finding.(remotely resolved).
Two lines of shell, catches every regression.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.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.
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.
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.
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.
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.
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.
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.
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.