DEV Community

全网低价IP
全网低价IP

Posted on Originally published at socks5ip.com.cn

Debugging SOCKS5 at the Wire Level: An RFC 1928 Walkthrough

"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."

  1. Greeting (client → proxy): VER, NMETHODS, then that many METHOD bytes. This is the client listing the auth methods it supports.
  2. Method selection (proxy → client): VER, METHOD. The proxy picks one, or 0xFF for "none acceptable."
  3. Request (client → proxy): VER, CMD, RSV, ATYP, DST.ADDR, DST.PORT. This is the actual ask — usually CONNECT.
  4. Reply (proxy → client): VER, REP, RSV, ATYP, BND.ADDR, BND.PORT. REP = 0x00 means 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
Enter fullscreen mode Exit fullscreen mode

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)