"It works in the browser but hangs in my script." If you have run a proxy pool you have seen this line, and the usual answer is a shrug. The browser does the handshake for you and hides every step; your script inherits the failure but not the diagnosis. Reading it at the byte level collapses that ambiguity — a generic timeout becomes one specific phase.
Four phases, strictly ordered
SOCKS5 is small. RFC 1928 defines exactly four exchanges, and each one depends on the previous reply. A stall is almost always "the client is waiting for a reply the server will never send, because the previous message was malformed or unacceptable."
-
Greeting (client → proxy):
VER,NMETHODS, then that manyMETHODbytes. This is the client listing the auth methods it supports. -
Method selection (proxy → client):
VER,METHOD. The proxy picks one, or0xFFfor "none acceptable." -
Request (client → proxy):
VER,CMD,RSV,ATYP,DST.ADDR,DST.PORT. This is the actual ask — usuallyCONNECT. -
Reply (proxy → client):
VER,REP,RSV,ATYP,BND.ADDR,BND.PORT.REP = 0x00means success; anything else is a structured error, not a crash.
The version byte is 0x05 in all four. If you ever see 0x04 in a reply, you are talking SOCKS4 to a SOCKS5 endpoint, or vice versa.
The byte layout, phase by phase
| Phase | Direction | Fields (in order) | Sizes |
|---|---|---|---|
| Greeting | client → proxy | VER, NMETHODS, METHODS[] | 1, 1, NMETHODS |
| Method selection | proxy → client | VER, METHOD | 1, 1 |
| Request | client → proxy | VER, CMD, RSV, ATYP, DST.ADDR, DST.PORT | 1, 1, 1, 1, var, 2 |
| Reply | proxy → client | VER, REP, RSV, ATYP, BND.ADDR, BND.PORT | 1, 1, 1, 1, var, 2 |
ATYP decides how long ADDR is: 0x01 = 4-byte IPv4, 0x04 = 16-byte IPv6, 0x03 = one length byte followed by a domain name. Ports are big-endian. Getting the address type wrong is the single most common reason a hand-rolled client desynchronises: the proxy reads your domain name as raw address bytes and the stream never lines up again.
CMD is usually 0x01 (CONNECT). 0x02 (BIND) and 0x03 (UDP ASSOCIATE) exist, but most proxy services only implement CONNECT — a 0x03 request against such a proxy returns REP = 0x07 (command not supported), which is a configuration fact, not a bug in your code.
Auth is what silently kills a connection
Two methods carry most of the traffic: 0x00 (no auth) and 0x02 (username/password, defined in RFC 1929). If your client offers only 0x02 and the proxy is white-listed by source IP instead, it replies METHOD = 0xFF and drops the connection — to your socket an empty read, not an error. That is why "authenticated" proxies work from one machine and not another: the source IP is the real credential.
The practical fix is to offer both methods in the greeting and branch on the reply. Never assume the method you want.
import socket, struct
def socks5_connect(proxy_host, proxy_port, dst, timeout=10):
s = socket.create_connection((proxy_host, proxy_port), timeout)
# 1. Greeting: offer no-auth AND user/pass
s.sendall(b"\x05\x02\x00\x02")
ver, method = s.recv(2)
if method == 0xFF:
raise OSError("proxy rejected all offered auth methods")
# 2. (If method == 0x02, do the RFC 1929 sub-negotiation here.)
# 3. CONNECT with a domain name target
host = dst[0].encode()
req = b"\x05\x01\x00\x03" + bytes([len(host)]) + host + struct.pack("!H", dst[1])
s.sendall(req)
head = s.recv(4) # VER REP RSV ATYP
rep = head[1]
if rep != 0x00:
raise OSError(f"CONNECT failed, REP=0x{rep:02x}")
# 4. Consume BND.ADDR + BND.PORT so the socket is positioned at payload
ln = {0x01: 4, 0x04: 16}.get(head[3]) or s.recv(1)[0]
s.recv(ln + 2)
return s # s is now a plain tunnel; write HTTP or TLS over it
Once REP = 0x00 and the bound address is drained, the socket behaves like a direct connection. There is no further framing — that is the whole point of SOCKS5, and the reason it tunnels protocols it knows nothing about.
Mapping REP codes to real problems
| REP | Meaning | Usual cause |
|---|---|---|
0x01 |
General failure | Proxy backend could not open the upstream socket |
0x02 |
Connection not allowed | Egress ACL / rule rejected the target |
0x03 |
Network unreachable | Exit host has no route to the target |
0x04 |
Host unreachable | Target DNS resolved but the host is down |
0x05 |
Connection refused | Target reachable, port closed |
0x06 |
TTL expired | Rare in practice |
0x07 |
Command not supported | You asked for BIND/UDP on a CONNECT-only proxy |
0x08 |
Address type not supported | You sent IPv6 to an IPv4-only endpoint |
The distinction that saves the most time is 0x02 versus 0x05. A 0x02 means your proxy is healthy but policy said no — a credential, geo, or allowlist problem. A 0x05 means your proxy is healthy and policy said yes, but the destination refused — a target-side problem. Collapsing both into "the proxy is broken" sends you debugging the wrong end of the wire.
FAQ
Why does curl work with the same proxy but my Python socket does not?
curl handles the greeting, the RFC 1929 sub-negotiation, and the reply parsing for you; your script must do all three. The usual divergence is offering the wrong auth method set, or sending ATYP=0x01 with a hostname.
The connection opens but every request returns an error page. Where do I look?
Past the handshake. A successful REP = 0x00 means the tunnel is fine, so the failure is inside the tunnelled protocol — TLS handshake, HTTP status, or the target blocking the exit's ASN. Check the exit's IP class next, not the SOCKS layer.
Should I use domain (ATYP=0x03) or IP (ATYP=0x01) targeting?
Prefer the domain. It lets the proxy resolve the name from its network, which keeps DNS consistent with the exit IP. Sending a pre-resolved IP is a common leak: the name was resolved locally, so the DNS answer may point at the wrong region.
How do I check hundreds of endpoints without hand-rolling each one?
Read the bulk-validation pattern — connect, greet, and classify by REP in parallel — which is covered in the batch proxy check guide.
The handshake is unglamorous, but it turns "the proxy is broken" into one fixable sentence. For the client-side setup above it — Windows, mobile, browser, and router-level configuration — see the SOCKS5 usage guide this distils. To verify an exit before you commit to it, use the IP check center. Both are maintained by socks5ip.com.cn; confirm current terms with each platform directly for purchase details.
Top comments (0)