Understanding SOCKS5 in C: Greeting and CONNECT

SOCKS5 is an application-layer proxy protocol defined by RFC 1928. Unlike a basic socket server, it negotiates an authentication method, receives a command describing a destination, opens that connection on the client’s behalf, and then relays bytes in both directions.

This study builds on the socket lifecycle example and the TCP stream-framing article.

SOCKS5 and Shadowsocks connection path An application sends a local SOCKS5 request to a Shadowsocks client. The client sends an authenticated encrypted Shadowsocks connection across the network to a remote server, which opens an ordinary TCP connection to the destination. Application browser or tool Local Shadowsocks client Remote Shadowsocks server Destination web or other service SOCKS5 authenticated encrypted Shadowsocks protocol TCP SOCKS5 is the local interface; Shadowsocks is the network protocol.
How a local SOCKS5 interface can feed a Shadowsocks connection without the two protocols being the same thing.

The Conversation

  1. The client sends a greeting containing the SOCKS version and authentication methods it supports.
  2. The server chooses one method or rejects the client.
  3. The client sends a command such as CONNECT with a destination address and port.
  4. The server attempts the connection and returns a status reply.
  5. After success, the server relays the TCP stream until either side closes.

Client Greeting

A client that supports only “no authentication” sends three bytes:

05 01 00
│  │  └─ method 0x00: no authentication
│  └──── one method follows
└─────── SOCKS version 5

If the server accepts that method, it replies 05 00. A reply of 05 ff means no offered method is acceptable.

Read and Select a Method

#include <stddef.h>
#include <stdint.h>
#include <sys/socket.h>

int recv_exact(int fd, void *buffer, size_t length);
int send_all(int fd, const void *buffer, size_t length);

int negotiate_no_auth(int client_fd)
{
    uint8_t header[2];
    if (recv_exact(client_fd, header, sizeof(header)) != 1)
        return -1;

    if (header[0] != 0x05 || header[1] == 0)
        return -1;

    uint8_t methods[255];
    if (recv_exact(client_fd, methods, header[1]) != 1)
        return -1;

    uint8_t selected = 0xff;
    for (uint8_t i = 0; i < header[1]; ++i) {
        if (methods[i] == 0x00) {
            selected = 0x00;
            break;
        }
    }

    const uint8_t reply[2] = {0x05, selected};
    if (send_all(client_fd, reply, sizeof(reply)) < 0)
        return -1;

    return selected == 0x00 ? 0 : -1;
}

The recv_exact() and send_all() helpers are the same stream-safe loops used in the TCP framing article.

The CONNECT Request

+-----+-----+-------+------+----------+----------+
| VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+-----+-----+-------+------+----------+----------+
|  1  |  1  | X'00' |  1   | Variable |    2     |
+-----+-----+-------+------+----------+----------+
  • VER must be 0x05.
  • CMD is 0x01 for CONNECT.
  • RSV is reserved and must be zero.
  • ATYP is 0x01 for IPv4, 0x03 for a domain name, or 0x04 for IPv6.
  • DST.PORT is an unsigned 16-bit port in network byte order.

Parse an IPv4 CONNECT Destination

#include <arpa/inet.h>
#include <stdint.h>
#include <string.h>

struct socks5_ipv4_target {
    struct in_addr address;
    uint16_t port;
};

int read_ipv4_connect(int fd, struct socks5_ipv4_target *target)
{
    uint8_t request[10];
    if (recv_exact(fd, request, sizeof(request)) != 1)
        return -1;

    if (request[0] != 0x05 || request[1] != 0x01 ||
        request[2] != 0x00 || request[3] != 0x01)
        return -1;

    memcpy(&target->address.s_addr, &request[4], 4);

    uint16_t network_port;
    memcpy(&network_port, &request[8], 2);
    target->port = ntohs(network_port);

    return target->port == 0 ? -1 : 0;
}

This deliberately accepts only IPv4 CONNECT requests. Domain names and IPv6 have different lengths and must be parsed separately. Never cast arbitrary bytes to larger integer pointers: copying with memcpy() avoids alignment and aliasing problems.

Server Reply

The reply has the same address shape as the request. The second byte reports the result:

  • 0x00 — succeeded
  • 0x01 — general server failure
  • 0x02 — connection not allowed
  • 0x03 — network unreachable
  • 0x04 — host unreachable
  • 0x05 — connection refused
  • 0x07 — command not supported
  • 0x08 — address type not supported

On success, the address and port in the reply identify the local endpoint used for the outbound connection—not the destination copied from the request.

What a Complete Proxy Still Needs

  • Resolve and connect to allowed destinations without creating DNS or internal-network bypasses.
  • Reject unsupported commands and address types with the correct status.
  • Relay traffic in both directions while handling partial writes, half-closes, and backpressure.
  • Apply authentication or strict source-address controls.
  • Set connection and idle timeouts, descriptor limits, and maximum concurrent sessions.
  • Bind only to an intentional interface. Never expose an unauthenticated teaching proxy to the internet.

References