You pointed a client at a SOCKS5 exit, checked the address, and it geolocates to the country you paid for. Then the site still serves you the home-language version, and a login from an unfamiliar city shows up in the account history two days later.
The tunnel is not broken. SOCKS5 is a connect verb over a byte pipe — it carries exactly the traffic you hand it, from the process you configured. Name resolution is a separate conversation, and it habitually happens outside that pipe.
One byte decides where the lookup happens
RFC 1928 defines a single request type that matters here. The client asks the proxy to open a TCP connection to a destination, and the destination is encoded in the ATYP field:
-
ATYP=0x01— a raw IPv4 address. The client already resolved the name itself, so the DNS query left through the machine's own resolver before the tunnel was ever involved. -
ATYP=0x03— a domain name. The client hands over the hostname and the proxy resolves it on the exit side. -
ATYP=0x04— an IPv6 address, with the same consequence as0x01.
So "SOCKS5 is configured" covers two different behaviours, decided by one byte set by a library flag most people never read.
Who resolves the name, by client
| Client and setting | ATYP sent | Resolver actually used | Leaks |
|---|---|---|---|
curl --socks5 |
0x01 | local stub resolver | yes |
curl --socks5-hostname |
0x03 | proxy egress | no |
PySocks, rdns=False (default) |
0x01 | local stub resolver | yes |
PySocks, rdns=True
|
0x03 | proxy egress | no |
Chromium --proxy-server=socks5://
|
0x03 | proxy egress | only via DoH or bypass |
| OS-wide TUN on a router | n/a | whatever the host emits | depends on port 53 |
The two curl flags look interchangeable and are not. A scraper moved from --socks5-hostname to --socks5 keeps working perfectly while quietly publishing every hostname it visits.
Three escape paths that survive a working tunnel
The client resolves locally. This is the common case; most HTTP libraries default to it. Nothing about the exit looks wrong afterwards — the IP and the ASN are correct — but a resolver upstream logged every domain.
UDP port 53 never enters the tunnel. A SOCKS5 CONNECT is TCP-only. Plain DNS over UDP is simply not carried, so an app-level proxy setting leaves the system resolver untouched. UDP ASSOCIATE exists for this, but support on the proxy side is uneven and most clients never request it.
Split tunneling on a network-level tunnel. L2TP with default-gateway routing disabled, or a TUN interface with a bypass list, sends the resolver query out of the physical interface while the browser still shows the remote address. The tunnel is up; one route is wrong.
Symptom to cause to fix
| What you observe | What it means | What actually fixes it |
|---|---|---|
| Exit IP correct, site language still local | names resolved locally (ATYP=0x01) |
switch the client to remote resolution |
| Proxy on, unrelated apps still resolve | app-level scoping only | drop or redirect outbound 53 at the gateway |
| Tunnel up, leak test shows home resolver | split tunneling | restore default route, push DNS to the tunnel |
| Fixed by restarting the browser only | resolver or browser cache | flush, then retest with a fresh hostname |
The fourth row is worth sitting with. A test run right after a fix can measure the state before the fix.
Proving it with 20 lines
This performs the handshake by hand and sends ATYP=0x03, which delegates resolution to the exit. Run it against a hostname that only the exit can resolve, then block outbound port 53 and run it again.
import socket, struct
def socks5_connect_domain(proxy, pport, host, dport):
s = socket.create_connection((proxy, pport), timeout=8)
s.sendall(b"\x05\x01\x00") # VER, NMETHODS, NO-AUTH
if s.recv(2) != b"\x05\x00":
raise OSError("method negotiation failed")
name = host.encode("idna")
req = b"\x05\x01\x00\x03" + bytes([len(name)]) + name + struct.pack(">H", dport)
s.sendall(req) # ATYP=0x03: exit resolves
rep = s.recv(4)
if len(rep) < 4 or rep[1] != 0:
raise OSError("SOCKS5 reply code %s" % (rep[1] if len(rep) > 1 else "short"))
return s
If the second run still connects while socket.getaddrinfo() for the same name fails, your names are being resolved inside the tunnel — that is the behaviour you want, and it is now measured rather than assumed.
Mistakes that keep the leak alive
- Treating a browser proxy extension as an operating-system setting. It is not; every other process keeps its own path out.
- Copying
--socks5instead of--socks5-hostnameinto a scraper template, then wondering why the exit logs look clean and the target still localises you. - Enabling DNS over HTTPS to "solve" leaks while DoH is pinned to a provider outside the tunnel. The query moved; it did not move into the pipe.
- Believing the leak-test page, when that page was itself loaded through the proxy. Test from the machine's own stack with port 53 blocked.
- Ignoring negative caching and TTLs, then re-testing inside the same minute and reading a stale answer as a regression.
FAQ
Does SOCKS5 carry DNS at all?
Yes, through ATYP=0x03 for TCP destinations, and through UDP ASSOCIATE if the proxy implements it. Neither is automatic — the client has to choose it, which is why library defaults matter more than the proxy vendor.
Is DNS over HTTPS a fix?
It relocates the resolver, and whether that relocation is inside the tunnel depends entirely on where DoH is configured. Configured inside the proxy scope it helps; configured on the device with a bypass list it is another exit path.
Port 53 is blocked but names still resolve. Why?
Something is answering without a network query: a stub resolver cache, a browser's own resolver, or a DoH endpoint over 443. Flush, use a hostname never queried before, and repeat.
Does this affect API clients and scrapers, not just browsers?
It affects them more. Browsers usually delegate hostnames correctly; Python, Go and Node clients often resolve locally by default, and their traffic never looks different from the outside.
Where this sits in a proxy stack
Resolution policy is a property of the client, not of the exit, so a clean IP reputation and a correct tunnel still leave one path open until the client is configured to delegate. The DNS leak detection walkthrough on socks5ip.com.cn covers the per-platform fixes, and the IP check centre confirms what the exit actually looks like to the outside before you go chasing resolver behaviour.
Measure the path, then fix one layer at a time. A tunnel that carries the names is worth more than a larger pool of addresses.
Top comments (0)