SOCKS5 and HTTP Proxy
Xray-core implements SOCKS4/4a/5 and HTTP/HTTPS proxy protocols as both inbound (local proxy server) and outbound (upstream proxy client) handlers. The SOCKS inbound can also automatically detect and forward HTTP requests to the built-in HTTP handler.
SOCKS5 Protocol
Overview
- Direction: Inbound + Outbound
- Transport: TCP + UDP
- Authentication: None or Username/Password (RFC 1929)
- Commands: CONNECT (TCP), UDP ASSOCIATE
- Standards: RFC 1928 (SOCKS5), RFC 1929 (Username/Password Auth)
Wire Format
SOCKS5 Handshake
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Version(1B) + NMethods(1B) + Methods(NB)
S->>C: Version(1B) + ChosenMethod(1B)
alt Username/Password Auth
C->>S: AuthVer(1B) + ULen(1B) + User + PLen(1B) + Pass
S->>C: AuthVer(1B) + Status(1B)
end
C->>S: Version(1B) + Cmd(1B) + Rsv(1B) + Address
S->>C: Version(1B) + Rep(1B) + Rsv(1B) + BndAddressAuth Negotiation
Client -> Server:
+-----+----------+----------+
| VER | NMETHODS | METHODS |
| 1B | 1B | 1-255B |
+-----+----------+----------+
0x05
Server -> Client:
+-----+--------+
| VER | METHOD |
| 1B | 1B |
+-----+--------+
0x05Method values:
| Value | Method |
|---|---|
0x00 | No authentication |
0x02 | Username/Password |
0xFF | No acceptable methods |
Source: proxy/socks/protocol.go:27-33
Username/Password Authentication (RFC 1929)
Client -> Server:
+-----+------+----------+------+----------+
| VER | ULEN | UNAME | PLEN | PASSWD |
| 1B | 1B | 1-255B | 1B | 1-255B |
+-----+------+----------+------+----------+
0x01
Server -> Client:
+-----+--------+
| VER | STATUS |
| 1B | 1B |
+-----+--------+
0x01 0x00=okSource: proxy/socks/protocol.go:232-265
SOCKS5 Request
+-----+-----+------+----------+----------+------+
| VER | CMD | RSV | ATYP | DST.ADDR | PORT |
| 1B | 1B | 1B | 1B | variable | 2B |
+-----+-----+------+----------+----------+------+
0x05 0x00Commands:
| Value | Command | Support |
|---|---|---|
0x01 | TCP CONNECT | Yes |
0x02 | TCP BIND | No (returns 0x07) |
0x03 | UDP ASSOCIATE | Yes (if enabled) |
0xF0 | Tor Resolve | Treated as CONNECT |
0xF1 | Tor Resolve PTR | Treated as CONNECT |
Source: proxy/socks/protocol.go:15-22, proxy/socks/protocol.go:164-180
SOCKS5 Response
+-----+-----+------+----------+----------+------+
| VER | REP | RSV | ATYP | BND.ADDR | PORT |
| 1B | 1B | 1B | 1B | variable | 2B |
+-----+-----+------+----------+----------+------+
0x05 0x00For UDP ASSOCIATE, BND.ADDR and BND.PORT indicate the UDP relay endpoint.
Source: proxy/socks/protocol.go:300-310
SOCKS5 UDP Datagram
+------+------+------+----------+----------+------+---------+
| RSV | RSV | FRAG | ATYP | DST.ADDR | PORT | DATA |
| 1B | 1B | 1B | 1B | variable | 2B | ... |
+------+------+------+----------+----------+------+---------+
0x00 0x00Source: proxy/socks/protocol.go:324-363
Fragment byte must be 0x00 -- fragmented UDP is not supported:
if packet.Byte(2) != 0 /* fragments */ {
return nil, errors.New("discarding fragmented payload.")
}Source: proxy/socks/protocol.go:334-336
SOCKS4/4a Support
The inbound also handles SOCKS4/4a based on the version byte:
switch version {
case socks4Version: // 0x04
return s.handshake4(cmd, reader, writer)
case socks5Version: // 0x05
return s.handshake5(cmd, reader, writer)
}Source: proxy/socks/protocol.go:222-230
SOCKS4a extends SOCKS4 with domain name support: if the IP starts with 0x00, a domain name follows after the user ID null terminator.
Source: proxy/socks/protocol.go:72-78
Inbound (Server)
File: proxy/socks/server.go
Auto-detection: The server reads the first byte to distinguish SOCKS from HTTP:
if firstbyte[0] != 5 && firstbyte[0] != 4 {
// Not SOCKS, try HTTP
return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...)
}Source: proxy/socks/server.go:92-95
UDP Associate flow:
- Client sends CONNECT with command 0x03
- Server responds with its UDP relay address/port
- Client sends UDP datagrams to that address
- Server receives on UDP network, decodes SOCKS5 UDP header, dispatches
- TCP connection stays open as a keepalive signal
func (*Server) handleUDP(c io.Reader) error {
// Wait until client closes the TCP connection
return common.Error2(io.Copy(buf.DiscardBytes, c))
}Source: proxy/socks/server.go:182-186
UDP Filter: When authentication is enabled, only clients who have established a TCP UDP ASSOCIATE connection are allowed to send UDP packets:
if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) {
return nil
}Source: proxy/socks/server.go:189-192
Outbound (Client)
File: proxy/socks/client.go
The client performs the full SOCKS5 handshake with the upstream server:
func ClientHandshake(request *RequestHeader, reader io.Reader, writer io.Writer) (*RequestHeader, error) {
// 1. Send auth method selection
// 2. Read server's chosen method
// 3. If password auth, send credentials
// 4. Send CONNECT or UDP ASSOCIATE request
// 5. Read response
}Source: proxy/socks/protocol.go:421-515
For UDP, the client opens a separate UDP connection to the address returned by the SOCKS5 UDP ASSOCIATE response.
Source: proxy/socks/client.go:146-163
HTTP Proxy Protocol
Overview
- Direction: Inbound + Outbound
- Transport: TCP, UNIX socket
- Authentication: HTTP Basic Auth (Proxy-Authorization header)
- Methods: CONNECT (tunnel), Plain HTTP (proxy)
- UDP: Not supported
HTTP CONNECT (Tunneling)
sequenceDiagram
participant C as Client
participant P as Proxy
participant S as Server
C->>P: CONNECT host:port HTTP/1.1
P->>S: TCP connect to host:port
S->>P: Connected
P->>C: HTTP/1.1 200 Connection established
C->>P: [raw bytes]
P->>S: [raw bytes]
S->>P: [raw bytes]
P->>C: [raw bytes]Source: proxy/http/server.go:176-202
Plain HTTP Proxying
For non-CONNECT requests, the server:
- Parses the full HTTP request using Go's
http.ReadRequest() - Extracts the destination from the
Hostheader or URL - Dispatches the request to the outbound
- Supports keep-alive for sequential requests on the same connection
keepAlive := strings.TrimSpace(strings.ToLower(
request.Header.Get("Proxy-Connection"))) == "keep-alive"Source: proxy/http/server.go:163
Authentication
Basic authentication is checked via the Proxy-Authorization header:
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
cs := string(c)
s := strings.IndexByte(cs, ':')
return cs[:s], cs[s+1:], true
}Source: proxy/http/server.go:63-78
Failed auth returns 407 Proxy Authentication Required with a Proxy-Authenticate: Basic realm="proxy" header.
Source: proxy/http/server.go:128
HTTP/2 Support (Outbound)
The HTTP outbound client supports both HTTP/1.1 and HTTP/2 when connecting through an upstream HTTP proxy:
switch nextProto {
case "", "http/1.1":
return connectHTTP1(rawConn)
case "h2":
t := http2.Transport{}
h2clientConn, err := t.NewClientConn(rawConn)
return connectHTTP2(rawConn, h2clientConn)
}Source: proxy/http/client.go:320-332
H2 connections are cached per destination for multiplexing:
var cachedH2Conns map[net.Destination]h2ConnSource: proxy/http/client.go:47-48
1xx Response Handling
The server properly handles HTTP 1xx intermediate responses (e.g., 100 Continue) by forwarding them to the client before reading the actual response:
if strings.HasPrefix(status, "1") {
// Read until \r\n\r\n, forward to client
writer.Write(ResponseHeader1xx)
}
return http.ReadResponse(r, req)Source: proxy/http/server.go:316-344
Inbound (Server)
File: proxy/http/server.go
Key behaviors:
- Supports
AllowTransparentmode for transparent HTTP proxying (no absolute URL required) - Removes hop-by-hop headers per HTTP spec
- Sets
User-Agentto empty string if not provided (prevents Go default) - Handles keep-alive connections with loop (
goto Start)
Outbound (Client)
File: proxy/http/client.go
The client:
- Reads the first payload from the link reader
- Establishes HTTP CONNECT tunnel to the upstream proxy
- Sends the first payload immediately after tunnel establishment
- Supports custom headers via config (with Go template support for
Source/Target)
data := struct {
Source net.Destination
Target net.Destination
}{...}
tmpl.Execute(&buf, data)Source: proxy/http/client.go:180-203
Implementation Notes
- SOCKS/HTTP auto-detection: The SOCKS inbound reads the first byte. If it is not
0x04or0x05, it forwards the connection to an embedded HTTP server. This allows a single port to serve both SOCKS5 and HTTP proxy.
Source: proxy/socks/server.go:92-95
CanSpliceCopy behavior: Both SOCKS and HTTP set
CanSpliceCopy = 2initially. After the protocol handshake (for TCP CONNECT), it is upgraded to1, enabling zero-copy on the raw TCP stream. If TLS is detected on the transport, it starts at3.SOCKS4 auth rejection: SOCKS4 does not support password authentication. If the server requires auth, SOCKS4 connections are immediately rejected.
Source: proxy/socks/protocol.go:50-53
- UDP is not supported in HTTP proxy: The HTTP outbound explicitly rejects UDP targets.
Source: proxy/http/client.go:80-82
- DispatchLink vs Dispatch: For TCP CONNECT and SOCKS TCP, the inbound uses
dispatcher.DispatchLink()which blocks until the connection completes, instead ofDispatch()which returns aLinkfor manual copy. This simplifies the lifecycle management.
Source: proxy/socks/server.go:163-169