Skip to content

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

mermaid
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) + BndAddress

Auth Negotiation

Client -> Server:
+-----+----------+----------+
| VER | NMETHODS | METHODS  |
| 1B  | 1B       | 1-255B   |
+-----+----------+----------+
  0x05

Server -> Client:
+-----+--------+
| VER | METHOD |
| 1B  | 1B     |
+-----+--------+
  0x05

Method values:

ValueMethod
0x00No authentication
0x02Username/Password
0xFFNo 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=ok

Source: proxy/socks/protocol.go:232-265

SOCKS5 Request

+-----+-----+------+----------+----------+------+
| VER | CMD | RSV  | ATYP     | DST.ADDR | PORT |
| 1B  | 1B  | 1B   | 1B       | variable | 2B   |
+-----+-----+------+----------+----------+------+
  0x05              0x00

Commands:

ValueCommandSupport
0x01TCP CONNECTYes
0x02TCP BINDNo (returns 0x07)
0x03UDP ASSOCIATEYes (if enabled)
0xF0Tor ResolveTreated as CONNECT
0xF1Tor Resolve PTRTreated 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       0x00

For 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   0x00

Source: proxy/socks/protocol.go:324-363

Fragment byte must be 0x00 -- fragmented UDP is not supported:

go
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:

go
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:

go
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:

  1. Client sends CONNECT with command 0x03
  2. Server responds with its UDP relay address/port
  3. Client sends UDP datagrams to that address
  4. Server receives on UDP network, decodes SOCKS5 UDP header, dispatches
  5. TCP connection stays open as a keepalive signal
go
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:

go
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:

go
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)

mermaid
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:

  1. Parses the full HTTP request using Go's http.ReadRequest()
  2. Extracts the destination from the Host header or URL
  3. Dispatches the request to the outbound
  4. Supports keep-alive for sequential requests on the same connection
go
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:

go
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:

go
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:

go
var cachedH2Conns map[net.Destination]h2Conn

Source: 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:

go
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 AllowTransparent mode for transparent HTTP proxying (no absolute URL required)
  • Removes hop-by-hop headers per HTTP spec
  • Sets User-Agent to 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:

  1. Reads the first payload from the link reader
  2. Establishes HTTP CONNECT tunnel to the upstream proxy
  3. Sends the first payload immediately after tunnel establishment
  4. Supports custom headers via config (with Go template support for Source/Target)
go
data := struct {
    Source net.Destination
    Target net.Destination
}{...}
tmpl.Execute(&buf, data)

Source: proxy/http/client.go:180-203

Implementation Notes

  1. SOCKS/HTTP auto-detection: The SOCKS inbound reads the first byte. If it is not 0x04 or 0x05, 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

  1. CanSpliceCopy behavior: Both SOCKS and HTTP set CanSpliceCopy = 2 initially. After the protocol handshake (for TCP CONNECT), it is upgraded to 1, enabling zero-copy on the raw TCP stream. If TLS is detected on the transport, it starts at 3.

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

  1. UDP is not supported in HTTP proxy: The HTTP outbound explicitly rejects UDP targets.

Source: proxy/http/client.go:80-82

  1. DispatchLink vs Dispatch: For TCP CONNECT and SOCKS TCP, the inbound uses dispatcher.DispatchLink() which blocks until the connection completes, instead of Dispatch() which returns a Link for manual copy. This simplifies the lifecycle management.

Source: proxy/socks/server.go:163-169

Technical analysis for re-implementation purposes.