ThisCalculator

Everyday

Go Proxy: HTTP CONNECT and SOCKS5 Handlers

Updated · Standard formula, shown below

Go Proxy: HTTP CONNECT and SOCKS5 Handlers

This article explains how to build a Go proxy that handles both HTTP CONNECT (for HTTPS traffic) and SOCKS5 (for general TCP traffic) protocols. You’ll learn the core handler logic, the differences between the two protocols, and how to implement them in a single server.

Background: Why Two Protocols?

HTTP CONNECT and SOCKS5 are two of the most common proxy protocols, each designed for different use cases. HTTP CONNECT is a method within the HTTP protocol that tells a proxy server to establish a tunnel to a target host. It’s widely used by browsers and HTTP clients to proxy HTTPS traffic, because the proxy only sees the initial CONNECT request and then blindly forwards encrypted bytes. SOCKS5, on the other hand, is a standalone protocol that operates at a lower level. It can handle any TCP (and with extensions, UDP) traffic, not just HTTP. It’s often used by SSH tunnels, instant messaging apps, and other non-HTTP applications. Understanding both is essential for building a versatile proxy that serves a broad range of clients.

How HTTP CONNECT Works

In HTTP CONNECT, the client sends a request line like CONNECT example.com:443 HTTP/1.1 followed by headers. The proxy must respond with 200 Connection established if it can connect to the target. After that, the proxy simply copies bytes between the client’s connection and the target’s connection, acting as a transparent pipe. Here’s a minimal Go handler:

func handleConnect(w http.ResponseWriter, r *http.Request) {
    destConn, err := net.Dial("tcp", r.Host)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    hijacker, ok := w.(http.Hijacker)
    if !ok {
        http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
        return
    }
    clientConn, _, err := hijacker.Hijack()
    if err != nil {
        return
    }
    clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
    go transfer(destConn, clientConn)
    go transfer(clientConn, destConn)
}

The key is using http.Hijacker to take over the underlying TCP connection from the HTTP server. The transfer function simply does io.Copy in both directions. This works for HTTPS, WebSockets, and any other TCP-based protocol that starts with an HTTP CONNECT request.

How SOCKS5 Works

SOCKS5 starts with a handshake. The client sends a greeting: version (0x05), number of auth methods, and the methods. A typical response from the server is 0x05 0x00 (no authentication). Then the client sends a request: version, command (1 for CONNECT), reserved byte, address type (1 for IPv4, 3 for domain, 4 for IPv6), address, and port. The server replies with a success message and then relays data. Here’s a condensed Go implementation:

func handleSocks5(client net.Conn) {
    defer client.Close()
    // Read greeting
    buf := make([]byte, 2)
    io.ReadFull(client, buf)
    if buf[0] != 0x05 { return }
    nMethods := int(buf[1])
    methods := make([]byte, nMethods)
    io.ReadFull(client, methods)
    client.Write([]byte{0x05, 0x00}) // no auth

    // Read request
    header := make([]byte, 4)
    io.ReadFull(client, header)
    if header[0] != 0x05 || header[1] != 0x01 { return }
    var addr string
    switch header[3] {
    case 0x01: // IPv4
        ip := make([]byte, 4)
        io.ReadFull(client, ip)
        addr = net.IP(ip).String()
    case 0x03: // Domain
        lenBuf := make([]byte, 1)
        io.ReadFull(client, lenBuf)
        domain := make([]byte, lenBuf[0])
        io.ReadFull(client, domain)
        addr = string(domain)
    case 0x04: // IPv6
        ip := make([]byte, 16)
        io.ReadFull(client, ip)
        addr = net.IP(ip).String()
    }
    portBuf := make([]byte, 2)
    io.ReadFull(client, portBuf)
    port := binary.BigEndian.Uint16(portBuf)

    target, err := net.Dial("tcp", net.JoinHostPort(addr, strconv.Itoa(int(port))))
    if err != nil {
        client.Write([]byte{0x05, 0x05, 0x00, 0x01, 0,0,0,0, 0,0})
        return
    }
    client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0}) // success
    go transfer(target, client)
    go transfer(client, target)
}

This handles IPv4, IPv6, and domain names. The response format is fixed: version, reply code, reserved, address type, address (4 bytes for IPv4), and port (2 bytes). For simplicity, the example uses all zeros for the bound address, which is acceptable for most clients.

Combining Both in One Server

To build a proxy that supports both protocols, you need to listen on a single TCP port and inspect the first byte to decide which handler to use. If the first byte is 0x05, it’s SOCKS5; otherwise, it’s HTTP (which starts with a method like GET, POST, or CONNECT). Here’s a simple approach:

func main() {
    listener, _ := net.Listen("tcp", ":8080")
    for {
        conn, _ := listener.Accept()
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    buf := make([]byte, 1)
    conn.Read(buf)
    if buf[0] == 0x05 {
        // Need to prepend the byte back
        conn = &prependConn{conn, buf}
        handleSocks5(conn)
    } else {
        // For HTTP, you can either read the full request or use a buffered reader
        // and pass it to an http.Server with a custom ConnContext.
        // Simpler: read the request line to see if it's CONNECT.
        // But for simplicity, you can use a bufio.Reader and pass it to a custom handler.
    }
}

For HTTP, you can use a bufio.Reader to read the request, then decide if it’s a CONNECT or a regular HTTP request. If it’s CONNECT, use the hijack method; otherwise, you can forward the request as a regular HTTP proxy (which is more complex). A simpler alternative is to run two separate listeners on different ports, but the single-port approach is more elegant.

Common Problems and How to Solve Them

  • Hijacking issues: When you hijack an HTTP connection, the http.Server no longer manages it. You must manually close both connections and handle timeouts.
  • Half-closed connections: In TCP, one side can close its write end while still reading. Use CloseWrite() on the underlying TCPConn when one direction finishes, not just Close().
  • Buffer sizes: For high throughput, use a larger buffer (e.g., 32KB) in io.Copy or use io.CopyBuffer.
  • Authentication: For SOCKS5, you may need to support username/password (method 0x02). For HTTP CONNECT, you can use standard HTTP authentication headers.
  • Timeouts: Set read/write deadlines to prevent idle connections from hanging forever.

Choosing the Right Approach

When building a proxy, consider your primary use case. If you’re building a forward proxy for web browsing, HTTP CONNECT is sufficient and simpler because it integrates with the HTTP ecosystem. If you need to proxy arbitrary TCP traffic (e.g., for a custom protocol, SSH, or a game), SOCKS5 is more appropriate. Many production proxies like Squid support both, but for a lightweight custom tool, you might choose one. Also consider whether you need UDP support (SOCKS5 has UDP ASSOCIATE, but it’s more complex) and whether you need authentication.

Safety and Security Considerations

Running a proxy exposes your server to the internet. Always implement authentication if your proxy is not for personal use only. Never log full URLs or payloads, as that could leak sensitive data. Be aware that an open proxy can be abused for malicious activities, so restrict access by IP or use strong credentials. Also, be careful with DNS rebinding attacks: always validate the hostname in CONNECT requests to prevent requests to internal IPs. Use net.SplitHostPort and check the IP isn’t loopback or private unless you intend to allow it.

Frequently Asked Questions

Can I use the same port for HTTP and SOCKS5?

Yes, you can inspect the first byte to distinguish them. SOCKS5 always starts with 0x05, while HTTP starts with a method letter (e.g., ‘C’ for CONNECT, ‘G’ for GET). This is a common practice in multi-protocol proxies.

How do I handle HTTPS traffic with HTTP CONNECT?

HTTPS uses CONNECT to establish a tunnel. After the 200 response, the proxy just relays encrypted bytes. You don’t need to decrypt or inspect the traffic; it’s a pure TCP pipe.

What’s the difference between a forward and reverse proxy?

A forward proxy sits between clients and the internet, forwarding requests on behalf of clients (like the one described here). A reverse proxy sits in front of servers, forwarding requests to backend servers (like Nginx or HAProxy). This article focuses on forward proxies.

Do I need to support UDP for SOCKS5?

Only if your clients need UDP (e.g., DNS over proxy or some games). The UDP ASSOCIATE command is more complex and requires a separate UDP socket. For most TCP-based use cases, you can ignore it.

How do I test my proxy?

For HTTP CONNECT, you can use curl -x http://localhost:8080 https://example.com. For SOCKS5, use curl --socks5-hostname localhost:8080 https://example.com. You can also use nc to test raw TCP connections.

Can I add authentication to both protocols?

Yes. For HTTP CONNECT, you can check the Proxy-Authorization header. For SOCKS5, implement the username/password method (0x02) and verify credentials. Both are straightforward to add.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.