On this page:
«socks-connect-body»
2.1.1 Initial exchange
«send-version-identifier»
«authentication-methods»
«send-supported-authentication-methods»
«receive-version-and-execute-method»
2.1.2 Authentication methods
«method-none»
«method-username-password»
«receive-username-password-status»
2.1.3 Connect to destination
«request-connect»
«make-addr»
«send-addr»
«send-domain»
«send-destination-address»
«receive-connect»
«read-server-bindings»

2.1 SOCKS connection process🔗ℹ

  1. Tell the SOCKS server which authentication methods we allow.

  2. Server replies with its chosen authentication method.

  3. Do authentication according to that method.

  4. Tell the SOCKS server which destination host and port to connect to.

  5. Server attempts remote connection and replies with connection status.

  6. If connection was successful, the TCP ports act exactly as if they were directly connected to the destination. (No more in-band SOCKS messages are possible.)

(define-values (username password)
  (if (pair? username-password)
      (values (car username-password) (cdr username-password))
      (values #f #f)))
«authentication-methods»
(define-values (in out) (tcp-connect socks-host socks-port))
(parameterize ([current-input-port in] [current-output-port out])
  «send-version-identifier»
  «receive-version-and-execute-method»
  «request-connect»
  «receive-connect»
  (values in out))

The SOCKS5 protocol is described in RFC 1928, SOCKS Protocol Version 5, which I will quote from throughout this document.

2.1.1 Initial exchange🔗ℹ

Initial request

When a TCP-based client wishes to establish a connection to an object, it must open a TCP connection to the appropriate SOCKS port on the SOCKS server system. The SOCKS service is conventionally located on TCP port 1080. If the connection request succeeds, the client enters a negotiation for the authentication method to be used, authenticates with the chosen method, then sends a relay request. The SOCKS server evaluates the request, and either establishes the appropriate connection or denies it.

The client connects to the server, and sends a version identifier/method selection message:

+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 |    1     | 1 to 255 |
+----+----------+----------+

The version number is 5 for SOCKS5.

The "methods" fields describe authentication methods. The client sends all supported authentication method implementations, and the server chooses which one shall be used (or 0xFF for no choice).

Here are the authentication methods that I will implement:

(define methods (ann (make-hasheq) (Mutable-HashTable Integer (-> Void))))
(hash-set! methods 0 (λ () «method-none»))
(when (and username password)
  (hash-set! methods 2 (λ () «method-username-password»)))

Send the number of supported authentication methods, then the ID of each method.

(send (bytes (hash-count methods)))
(for ([(id fn) (in-hash methods)])
  (send (bytes id)))

Initial response

From my list of supported authentication methods, the server chooses which authentication method shall be used.

The server selects from one of the methods given in METHODS, and sends a METHOD selection message:

+----+--------+
|VER | METHOD | +----+--------+ | 1  | 1 |
+----+--------+

After I receive this data, I verify the version field, then hand over to the server’s chosen authentication method to negiotiate authentication.

(define bs (expect 2))
(define ver (bytes-ref bs 0))
(unless (eq? ver 5)
  (error 'receive-version-and-execute-method "server responded with unexpected version #~a" ver))
(define method-id (bytes-ref bs 1))
(define method (hash-ref methods method-id))
(unless method
  (error 'receive-version-and-execute-method "server said to use unsupported method #~a" method))
(method)

2.1.2 Authentication methods🔗ℹ

None

For this method, nothing happens. The server is already happy and there is no need to send authentication details. There is no additional data exchange.

Username/password

This method is described in RFC 1929, Username/Password Authentication for SOCKS V5:

Once the SOCKS V5 server has started, and the client has selected the Username/Password Authentication protocol, the Username/Password subnegotiation begins. This begins with the client producing a Username/Password request:

+----+------+----------+------+----------+
|VER | ULEN | UNAME | PLEN | PASSWD |
+----+------+----------+------+----------+
| 1 |  1   | 1 to 255 |  1   | 1 to 255 |
+----+------+----------+------+----------+

As a limitation of the SOCKS5 protocol, the username and password will be transmitted through the network in cleartext.

(send (bytes 1))
(send (bytes (bytes-length username)))
(send username)
(send (bytes (bytes-length password)))
(send password)
(flush-output)
«receive-username-password-status»

The server verifies the supplied UNAME and PASSWD, and sends the following response:

+----+--------+
|VER | STATUS | +----+--------+ | 1  | 1 |
+----+--------+

Status 0 means success. The RFC has a chart of what the other numbers mean, if you were interested.

(let* ([bs (expect 2)]
       [ver (bytes-ref bs 0)]
       [status (bytes-ref bs 1)])
  (unless (eq? ver 1)
    (error 'receive-username-password-status "server responded with unexpected version #~a" ver))
  (unless (eq? status 0)
    (error 'receive-username-password-status "server rejected username/password combination (status #~a)" ver)))

2.1.3 Connect to destination🔗ℹ

Connect request

Once the method-dependent subnegotiation has completed, the client sends the request details. If the negotiated method includes encapsulation for purposes of integrity checking and/or confidentiality, these requests MUST be encapsulated in the method- dependent encapsulation.

The SOCKS request is formed as follows:

+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | +----+-----+-------+------+----------+----------+ | 1  | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
  • VER: Protocol version=5

  • CMD: Connect=1, Bind=2, UDP Associate=3

  • RSV: Reserved=0

  • ATYP: Address type: IPv4=1, IPv6=4, Fully-qualified domain name=3

  • DST-ADDR: Destination address

  • PORT: Destination port in network octet order

For CMD, I will only implement Connect in this implementation. Connect is used in all circumstances, whereas Bind is only used with protocols that establish server-to-client connections like FTP, and UDP Associate is only used with UDP.

(send (bytes 5 1 0))
«send-destination-address»
(send (integer->integer-bytes dest-port 2 #f #t))
(flush-output)

The destination address can be one of the following:

To make the end-user interface easier, my program will receive every type as a string (or bytes), and branch based on how the string is formatted. I will use the net/ip module to help.

(define addr (with-handlers ([exn:fail:contract? (λ (_) #f)])
               (make-ip-address dest-host)))

Then, if it is an IP address, send it:

[(ip-address? addr) (send (bytes (if (eq? (ip-address-version addr) 4) 1 4)))
                    (send (ip-address->bytes addr))]

Otherwise, it’s a domain name, so send that:

[else (send (bytes 3 (string-length dest-host)))
      (send dest-host)]

Putting the previous chunks together:

Connect response

The server evaluates the request, and returns a reply formed as follows:

+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +----+-----+-------+------+----------+----------+ | 1  | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+

The complete connection is successful if Rep=0.

(let* ([bs (expect 4)]
       [ver (bytes-ref bs 0)]
       [rep (bytes-ref bs 1)]
       [addr-type (bytes-ref bs 3)])
  (unless (eq? ver 5)
    (error 'receive-connect "server responded with unexpected version #~a" ver))
  (unless (eq? rep 0)
    (error 'receive-connect "server rejected connection for reason #~a" rep))
  «read-server-bindings»)

The server says which destination host and port it connected to, but I don’t care, so I just read those bytes and throw them into the void.

(void 'server-binding-address
      (case addr-type
        [(1) (expect 4)]
        [(4) (expect 16)]
        [(3) (let ([bs (expect 1)])
               (expect (bytes-ref bs 0)))]))
(void 'server-binding-port
      (integer-bytes->integer (expect 2) #f #t))

After this point, the connection is completed. The TCP ports act exactly as if they were directly connected to the destination, but in reality, the SOCKS server will be forwarding all data through the ports in both directions.

The SOCKS protocol does not support any more in-band messages, which means I can hand off the ports to the next protocol layer like HTTP without having to worry about escape codes or control being passed back to the SOCKS layer.