Contents

SOCKS & HTTP

The everyday local inbounds your browser and system proxy talk to.

SOCKS5 and HTTP CONNECT are the doors between your applications and Xray — the ports your browser, curl, or the OS system proxy point at. They carry traffic in plaintext on localhost and offer zero resistance to censorship; their job is purely to accept connections so the router can send them out through a real tunnel (VLESS, Trojan, …).

One local port answers both SOCKS and HTTP

The socks inbound embeds a complete HTTP proxy server and picks the protocol per connection. NewServer constructs an http.Server alongside the SOCKS one, and Process reads exactly one byte before deciding where the connection goes:

proxy/socks/server.go proxy/socks/protocol.go
if firstbyte[0] != 5 && firstbyte[0] != 4 { // Check if it is Socks5/4/4a
	errors.LogDebug(ctx, "Not Socks request, try to parse as HTTP request")
	return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...)
}

A SOCKS5 hello starts with 0x05, SOCKS4/4a with 0x04 — and no HTTP method starts with either byte, so GET/CONNECT requests fall through to the HTTP handler with the consumed byte stitched back in front (ProcessWithFirstbyte).

{
	"listen": "127.0.0.1",
	"port": 1080,
	"protocol": "socks",
	"settings": { "auth": "password", "accounts": [{ "user": "u", "pass": "p" }], "udp": true }
}

The handshake accepts SOCKS4 and 4a too (handshake4), but only CONNECT — and SOCKS4 is rejected outright when password auth is on, because the protocol has no password field.

UDP associate splits control and data

A SOCKS5 UDP session is two connections: a TCP one that does nothing and a UDP one that does everything. After the client sends UDP ASSOCIATE, the server replies with the IP and port to send datagrams to, then parks the TCP connection — handleUDP literally copies its bytes into buf.DiscardBytes and waits for the client to hang up, because closing the TCP side ends the association.

proxy/socks/server.go
UDP associate: the TCP connection only marks the session alive; every datagram carries its own destination header

Every datagram is self-addressed: a 10-byte-ish header (RSV RSV FRAG ATYP ADDR PORT) precedes the payload, parsed by DecodeUDPPacket — fragmented packets (FRAG != 0) are dropped, not reassembled. Because each packet names its own destination, one association can talk to many remote hosts. handleUDPPayload attaches that per-packet destination to the payload itself (payload.UDP = &destination); the dispatch destination — what the session is keyed on — depends on cone behavior: with full-cone on, it is pinned to the first packet’s target and never changes (if !s.cone || dest == nil), and the outbound honors each payload’s own address; with cone off, the dispatch is re-targeted with every packet.

UDP authentication is only an IP allowlist

Username/password auth cannot protect UDP, so Xray falls back to filtering by source IP. UDP datagrams arrive on a separate socket with no session state, so there is nowhere to put credentials. The source is refreshingly honest about it:

proxy/socks/udpfilter.go
// In the sock implementation of * ray, UDP authentication is flawed and can be bypassed.
// ...
// We create a filter, add remote IP to the pool when it try to establish a UDP connection with auth.
// And drop UDP packets from unauthorized IP.

When auth is enabled, UDPFilter.Add records the client IP at UDP ASSOCIATE time and Check drops datagrams from any other IP. There is no expiry — the comment notes a timeout was deliberately skipped.

As outbounds, they chain you through an upstream proxy

The same two protocols work in reverse to hop through another proxy — a corporate gateway, an SSH -D tunnel, Tor. The SOCKS outbound performs the full client handshake (ClientHandshake), including UDP ASSOCIATE: it dials the relay address the server returns and wraps each packet with UDPWriter, so UDP survives the hop. The HTTP outbound only ever sends CONNECT and refuses UDP with an explicit error.

proxy/socks/client.go proxy/http/client.go
{
	"protocol": "socks",
	"settings": { "servers": [{ "address": "10.0.0.2", "port": 1080 }] }
}
Don't

Chain through an HTTP outbound when apps need UDP

http.Client.Process returns "UDP is not supported by HTTP outbound" — DNS, QUIC and games routed there simply fail.

Do

Use a SOCKS outbound (or route UDP elsewhere)

The SOCKS client negotiates UDP associate and relays datagrams; keep an HTTP upstream only for TCP routes.

The HTTP outbound has two tricks of its own. It always reads your app’s first payload before even dialing the upstream (link.Reader.ReadMultiBuffer() runs before setUpHTTPTunnel), so the opening bytes go out the moment the tunnel exists — which also means a protocol where the server speaks first will stall here waiting for data. And if the upstream proxy speaks TLS and negotiates h2, Xray caches the HTTP/2 connection per proxy address (cachedH2Conns) and multiplexes future CONNECTs as streams over it, shipping that first payload inside the CONNECT request body itself — no extra round trip.