Yesterday I was verifying that the SOCKS test suite of Tholian Stealth is working and I realized that writing failsafe tests are not as easy as someone might think. So here's my write-up about the SOCKS protocol.
The problem with SOCKS in a nutshell is that it is not as well documented as someone might think.
Most people are even unaware of the role that a SOCKS proxy plays in between network connections and therefore don't know what exactly a SOCKS proxy can or cannot do in regards to blocking ads and malicious domains.
So I thought that a reference-class article for SOCKS4 and SOCKS5 would be nice, documenting its quirks and common pitfalls while implementing those protocols.
For the implementation we're going to build in this article,
we only need plain node.js and its
net
core stack. The
implementation will be peer-to-peer, which means it can be used
for both the client-side and server-side whereas both sides are
implemented in node.js for the sake of simplicity.
Introduction
First off, you have to know that the
SOCKS
protocol is specified as
RFC1928
and
RFC1929
and didn't change since
1996
so it's somewhat safe to assume that this is the final version
of the network protocol.
The SOCKS protocol in general and its role is pretty much what telephone operators did in the past. A client connects to the SOCKS proxy and requests to connect to a specific target. The proxy tries to connect to the target, and if it succeeds reaches through the connection to the client.
Client: Please let me connect to IP 1.2.3.4. Proxy: Trying to connect... please hold the line ... Proxy: Here's the connection handle, any further data will be dispatched through automatically.
Client: Please let me connect to IP 1.2.3.4. Proxy: Trying to connect... please hold the line ... Proxy: Sorry, the given target is not reachable. Please try again later. Proxy: *hangs off the phone* Beep Beep Beep.
As both
SOCKS4
and
SOCKS4a
were proprietary protocols, they didn't
have any RFC.
SOCKS5
is referring a lot the
SOCKS4
protocol and
its featureset, so reading the RFC is quite complicated if you don't
know what the older protocol versions actually did feature-wise.
Usually though, most clients and servers that claim to support SOCKS5
actually only support the
user
and
password
authentication, not
the
IPv6
or
DNS/UDP
related features that come with it.
This implementation will focus mostly on the
IPv4
and
IPv6
differences and will - for the sake of simplicity - only support
TCP
based connections.
The
SOCKS4
featureset
:
- connects to
IPv4
(TCP)
The
SOCKS4a
featureset
:
- connects to
IPv4
(TCP) - connects to
domain
(TCP) - authentication via
user
is broken in practice (no password)
The
SOCKS5
featureset
:
- connects to
IPv4
(TCP and UDP) - connects to
IPv6
(TCP and UDP) - connects to
domain
(TCP) - authentication via
user
andpassword
SOCKS Protocol and Network States
Describing SOCKS data frames can be kind of complicated, because the interpretation of a SOCKS frame is different depending on its network state. So I'm trying to document the different network states first, so that you know what kind of states on both sides are possible.
We are also going to completely ignore the
SOCKS authentication methods
,
because they are broken across every single Browser; and implemented
in specification violating non-secure non-encrypted manners across
every piece of source code I've come across.
SOCKS Version 4
The SOCKS Version 4 network flow looks like this :
- Client sends Connection Request Frame
- Server responds with a Status Frame
... and that's pretty much it. Super simple.
SOCKS Version 4 Connection Request Frame :
+---------+---------+----+----+----+----+----+----+----+----+....+----+ | VERSION | COMMAND | DSTPORT | DSTIP | USERID |NULL| +---------+---------+----+----+----+----+----+----+----+----+....+----+ 1 1 2 4 variable 1
VERSION
(1 byte) represents theSOCKS version
, which can be either of0x04
or0x05
.COMMAND
(1 byte) represents theSOCKS command
, which can be either of the Commands explained below.DSTPORT
(2 bytes) represents the destination port from1
to65535
.DSTIP
is anIPv4
(4 bytes) represents the destination IP, and encodes 4 digits as0x00
to0xFF
respectively.USERID
(variable byte length, followed by aNULL
terminator byte) represents an authentication mechanism for a user-login; but without a password.
SOCKS Version 4 Commands :
0x01
representsCONNECT
and is a TCP/IP connection request which lets the Server established the connection.0x02
representsBIND
and is a TCP/IP port binding to allow the Client to establish the connection themselves.
SOCKS Version 4 Connection Status Frame :
+---------+--------+----+----+----+----+----+----+ | VERSION | STATUS | DSTPORT | DSTIP | +---------+--------+----+----+----+----+----+----+ 1 1 2 4
VERSION
(1 byte) represents theSOCKS version
, which can be either of0x04
or0x05
.STATUS
(1 byte) represents theSOCKS status
, which can be either of the Status explained below.DSTPORT
(2 bytes) represents the destination port from1
to65535
.DSTIP
is anIPv4
(4 bytes) represents the destination IP, and encodes 4 digits as0x00
to0xFF
respectively.
SOCKS Version 4 Status Codes :
0x5A
Request granted.0x5B
Request rejected or failed.
SOCKS Version 4 Status Codes for Authentication Mechanism :
(I would not recommend to implement them)
0x5C
Request failed because Client'sidentd
is not reachable from server.0x5D
Request failed because Client'sidentd
could not confirm the User ID.
SOCKS Version 5
Important
:
The SOCKS Version 5 protocol is different in
its network flow and data frame structure; and it has a
reserved byte with a
0x00
value where the SOCKS Version 4
protocol would otherwise expect a non-NULL byte data.
Additionally, the order of
Destination Port
and
Destination Address
is different from the order specified in SOCKS Version 4.
The SOCKS Version 5 network flow looks like this :
- Client and Server authenticate via handshake mechanism.
- Server authenticates or responds with error message.
- Client requests to connect or bind to an IPv4, IPv6 or domain.
- Server responds with connection status.
SOCKS Version 5 Handshake Request Frame :
+---------+----+----+----+....+ | VERSION | NMETHODS| METHODS | +---------+----+----+----+....+ 1 2 variable
SOCKS Version 5 Handshake Response Frame :
+---------+----+----+ | VERSION | METHOD | +---------+----+----+ 1 1
VERSION
(1 byte) represents theSOCKS version
, which is0x05
.NMETHODS
(1 byte) represents the byte length of the following encodedMETHOD
s.METHODS
(variable byte length) represents the encoded SOCKS methods.
SOCKS Version 5 Handshake Methods :
0x00
represents No Authentication required.0x01
representsGSSAPI
which is a "secure" context as defined per RFC1961 .0x02
representsusername/password
plaintext authentication.0x80
to0xFE
are reserved for private methods, but are never used in practice.0xFF
represents No Acceptable methods (in a response).
SOCKS Version 5 Connection Request :
After the Client and Server have negotiated an Authentication Method, the Client sends a Connection Request Frame to the Server.
+---------+---------+-----+------+----+----+----+----+ | VERSION | COMMAND | RSV | ATYP | DSTADDR | DSTPORT | +---------+---------+-----+------+----+----+----+----+ 1 1 1 1 variable 2
VERSION
(1 byte) represents theSOCKS version
, which is0x05
.COMMAND
(1 byte) represents either of the Commands explained below.RSV
(1 byte) represents the reserved byte which has to be0x00
(to be able to differ it from a SOCKS Version 4 Connection Request).ATYP
(1 byte) represents the address type, which can be either of0x01
(IPv4 address),0x03
(domain name),0x04
(IPv6 address).DSTADDR
(variable) represents the destination address as an IPv4 (in 4 octets), the domain name (with a prefixed byte length), or an IPv6 (in 16 octets).DSTPORT
(2 bytes) represents the destination port from0x01
(1
) to0xFF
(65535
).
SOCKS Version 5 Commands :
0x01
representsCONNECT
and is a TCP/IP connection request which lets the Server established the connection and forward further packets.0x02
representsBIND
and is a TCP/IP port binding to allow the Client to establish the connection themselves.0x03
representsUDP ASSOCIATE
and is a UDP associate request which lets the Server establish the connection and forward further packets.
SOCKS Version 5 Connection Status Frame :
After the Client has sent the Connection Request frame, the Server responds with a Connection Status Frame.
+---------+-------+-----+------+----+----+----+----+ | VERSION | REPLY | RSV | ATYP | BNDADDR | BNDPORT | +---------+-------+-----+------+----+----+----+----+ 1 1 1 1 variable 2
VERSION
(1 byte) represents theSOCKS version
, which is0x05
.REPLY
(1 byte) represents the reply message to the requested command and is either of the Replies explained below.RSV
(1 byte) represents the reserved byte which has to be0x00
(to be able to differ it from a SOCKS Version 4 Connection Request).ATYP
(1 byte) represents the address type, which can be either of0x01
(IPv4 address),0x03
(domain name),0x04
(IPv6 address).BNDADDR
(variable) represents the server-bound destination address as an IPv4 (in 4 octets), the domain name (with a prefixed byte length), or an IPv6 (in 16 octets).BNDPORT
(2 bytes) represents the server-bound destination port from0x01
(1
) to0xFF
(65535
).
SOCKS Version 5 Replies :
0x00
Success0x01
General SOCKS Failure0x02
Connection Not Allowed0x03
Network Unreachable0x04
Host Unreachable0x05
Connection Refused0x06
TTL (Time To Live) Expired0x07
Command Not Supported0x08
Address Type Not Supported
SOCKS Client
The demo will be implemented using modern node.js.
The client has to be implemented with the
net.createConnection()
interface, as the data that we need to access is
raw TCP data
and our library needs to support binary encodings.
As node.js runs
libuv
in the background, which is of asynchronous
nature, we also have to make sure that everything gets send as soon
as possible by calling
socket.setNoDelay(true)
.
// client.mjs import net from 'net'; import { SOCKS } from './SOCKS.mjs'; // Chapter: SOCKS Client let client = new net.createConnection({ host: 'localhost', port: 1080 }, () => { let handshake = { headers: { 'auth': [ 'none' ] } }; // Chapter: Handshake Request console.log('Send Handshake Request', handshake); SOCKS.send(client, handshake); }); client.once('data', (response) => { client.allowHalfOpen = false; client.setTimeout(0); client.setNoDelay(true); client.setKeepAlive(true, 0); client.removeAllListeners('timeout'); client.removeAllListeners('data'); // Chapter: Handshake Response SOCKS.receive(client, response, (data) => { console.log('Receive Handshake Response', data); if (data.headers['@version'] === 0x05 && data.headers['auth'] === 'none') { // Chapter: Connection Status client.once('data', (status) => { SOCKS.receive(client, status, (data) => { console.log('Receive Connection Status', data); if (data.headers['@status'] === 'success') { console.log('Client would be ready to do a HTTP request to ' + data.payload.host + ':' + data.payload.port + ' now.'); // TODO: client.write(HTTP_REQUEST); client.destroy(); } else { console.error('Server responded with an Error: ' + data.headers['@status']); client.destroy(); } }); }); setTimeout(() => { let request = { headers: { '@method': 'connect' }, payload: { domain: 'cookie.engineer', port: 80 // HTTP } }; // Chapter: Connection Request console.log('Send Connection Request', request); SOCKS.send(client, request); }, 1000); } else { client.destroy(); } }); SOCKS.receive(response); }); client.on('error', () => {}); client.on('close', () => {}); client.on('timeout', () => client.destroy());
Handshake Request and Response
The initial Handshake Request that is done from the Client in order to
find out what kinds of
auth
methods the Server supports.
In the following example, we'll emulate a simple SOCKS Version 5 Client to explain the previous example of the Client in more detail :
// client-manual.mjs import net from 'net'; const HANDSHAKE_REQUEST = [ 0x05, // SOCKS version 0x02, // 2 Auth Methods are supported 0x00, // methods[0]: No Authentication Required 0x02 // methods[1]: Username/Password ]; net.connect({ host: '127.0.0.1', port: 1080 }, () => { socket.once('data', (response) => { console.log('Handshake Response Frame', response); console.log('Server\'s SOCKS version (should be 0x05):', response[0]); console.log('Server\'s selected Auth Method (should be 0x00):', response[1]); // Chapter: Connection Request }); socket.write(HANDSHAKE_REQUEST); });
Connection Request and Status
A SOCKS Server should be able to handle both
connect
and
bind
requests
for
IPv4
addresses, requested
domain
s, and
IPv6
addresses.
The Connection Request is sent by the Client after the Authentication Method was completed, which means that for _all_ Connection Requests there is an Handshake Request and Response Frame that preceeds it; as there are no reusable Sessions in SOCKS and sockets cannot be reused.
// client-manual.mjs import net from 'net'; const HANDSHAKE_REQUEST = [ 0x05, // SOCKS version 0x02, // 2 Auth Methods are supported 0x00, // methods[0]: No Authentication Required 0x02 // methods[1]: Username/Password ]; const CONNECTION_REQUEST = [ 0x05, // SOCKS version 0x01, // CONNECT Command 0x00, // Reserved (due to SOCKS4 compatibility) 0x01, // IPv4 Address Type 0x01, // 1.3.3.7 0x03, 0x03, 0x07, 80 // HTTP ]; let client = net.createConnection({ host: '127.0.0.1', port: 1080 }, () => { client.once('data', (response) => { console.log('Handshake Response Frame', response); if (response[0] === 0x05 && response[1] === 0x00) { client.once('data', (status) => { console.log('Connection Status Frame', status); }); client.write(CONNECTION_REQUEST); } }); client.write(HANDSHAKE_REQUEST); });
Receiving SOCKS Frames
The integration of a receiving method for our SOCKS library is quite easy.
// SOCKS.mjs SOCKS.receive = (socket, buffer, callback) => { buffer = buffer instanceof Buffer ? buffer : null; callback = callback instanceof Function ? callback : null; if (buffer !== null) { let data = decode(socket, buffer); if (data !== null) { if (callback !== null) { callback(data); } return data; } } else { if (callback !== null) { callback(null); } else { return null; } } };
Decoding Logic
The Decoding Logic is a bit more complex, as it has to respect the following states :
- Server : If
buffer.length
is greater than3
, it is aconnect
orbind
Connection Request. - Server : If
buffer.length
is3
, it is a Handshake Request with a single Authentication Method (which is what all Web Browsers use). - Server If a Client sends more than a single authentication method, we won't support it.
- Client : If
buffer.length
is2
, it is a Handshake Response. - Client : If
buffer.length
is greater than2
, it is a Connection Status Response.
// Chapter: Decoding Logic const decode = function(socket, buffer) { let chunk = { headers: {}, payload: null }; if (buffer[0] === 0x05) { chunk.headers['@version'] = 5; } else if (buffer[0] === 0x04) { chunk.headers['@version'] = 4; } let is_server = socket._is_server === true; if (is_server === true) { // TODO: Support multiple Authentication Methods (buffer.length >= 3) if (buffer.length === 3) { let length = buffer[1]; let methods = buffer.slice(2, 2 + length); if (methods.length === length) { chunk.headers['auth'] = Array.from(methods).map((v) => { if (v === 0x00) { return 'none'; } else if (v === 0x02) { return 'login'; } else if (v === 0xff) { return 'error'; } return null; }).filter((method) => method !== null); } } else if (buffer.length > 3) { let method = buffer[1]; if (method === 0x01) { chunk.headers['@method'] = 'connect'; } else if (method === 0x02) { chunk.headers['@method'] = 'bind'; } let payload = decode_payload(buffer.slice(3)); if (payload !== null) { chunk.payload = payload; } } } else { if (buffer.length === 2) { let auth = buffer[1]; if (auth === 0x00) { chunk.headers['auth'] = 'none'; } else if (auth === 0x02) { chunk.headers['auth'] = 'login'; } else if (auth === 0xff) { chunk.headers['auth'] = 'error'; } } else if (buffer.length > 2) { let reply = buffer[1]; if (reply === 0x00) { chunk.headers['@status'] = 'success'; } else if (reply === 0x01) { chunk.headers['@status'] = 'error'; } else if (reply === 0x02) { chunk.headers['@status'] = 'error-blocked'; } else if (reply === 0x03) { chunk.headers['@status'] = 'error-network'; } else if (reply === 0x04) { chunk.headers['@status'] = 'error-host'; } else if (reply === 0x05) { chunk.headers['@status'] = 'error-connection'; } else if (reply === 0x07) { chunk.headers['@status'] = 'error-method'; } if (buffer.length > 3) { let payload = decode_payload(buffer.slice(3)); if (payload !== null) { chunk.payload = payload; } } } } return chunk; }; const decode_payload = function(buffer) { let payload = null; let type = buffer[0]; if (type === 0x01) { // Chapter: IPv4 Payload // 0x01: IPv4 Payload } else if (type === 0x03) { // Chapter: Domain Payload // 0x03: Domain Payload } else if (type === 0x04) { // Chapter: IPv6 Payload // 0x04: IPv4 Payload } return payload; };
Decoding 0x01 : IPv4 Payload
The
IPv4 Payload
is sent to communicate that the Client wants to
connect
or
bind
to a specific IP and a specific Port.
The encoded format of an IPv4 Payload is :
4 bytes
for the IP.2 bytes
for the Port.
// SOCKS.mjs in decode_payload() if (type === 0x01) { // 0x01: IPv4 Payload let raw_host = buffer.slice(1, 5); let raw_port = buffer.slice(5, 7); if (raw_host.length === 4 && raw_port.length === 2) { let ip = [ raw_host[0], raw_host[1], raw_host[2], raw_host[3] ].join('.'); let port = (raw_port[0] << 8) + (raw_port[1] & 0xff); if (port > 0 && port < 65535) { payload = ip + ':' + port; } } }
Decoding 0x03 : Domain Payload
The
Domain Payload
is sent to communicate that the Client wants to
connect
or
bind
to a specific domain (that the Server or Proxy
has to resolve first) and a specific Port.
In this case, the Server has to respond with a valid IP. If the IP
response is
0.0.0.0
, the Client must handle this as an unsuccessful
Connection Request.
The encoded format of a Domain Payload is :
1 byte
for the length of the Domain.n bytes
for the Domain, encoded as autf8
string.2 bytes
for the Port.
// SOCKS.mjs in decode_payload() if (type === 0x03) { // 0x03: Domain Payload let length = buffer[1]; let raw_domain = buffer.slice(2, 2 + length); let raw_port = buffer.slice(2 + length, 2 + length + 2); if (raw_domain.length > 0 && raw_port.length === 2) { let domain = Buffer.from(raw_domain).toString('utf8'); let port = (raw_port[0] << 8) + (raw_port[1] & 0xff); if (domain.length > 0 && port > 0 && port < 65535) { payload = domain + ':' + port; } } }
Decoding 0x04 : IPv6 Payload
The
IPv6 Payload
is sent to communicate that the Client wants to
connect
or
bind
to a specific IP and a specific Port.
The encoded format of an IPv6 Payload is :
8 bytes
for the IP.2 bytes
for the Port.
// SOCKS.mjs in decode_payload() if (type === 0x04) { let raw_host = buffer.slice(1, 17); let raw_port = buffer.slice(17, 19); if (raw_host.length === 16 && raw_port.length === 2) { let ip = [ raw_host.slice( 0, 2).toString('hex'), raw_host.slice( 2, 4).toString('hex'), raw_host.slice( 4, 6).toString('hex'), raw_host.slice( 6, 8).toString('hex'), raw_host.slice( 8, 10).toString('hex'), raw_host.slice(10, 12).toString('hex'), raw_host.slice(12, 14).toString('hex'), raw_host.slice(14, 16).toString('hex') ].join(':'); let port = (raw_port[0] << 8) + (raw_port[1] & 0xff); if (port > 0 && port < 65535) { payload = '[' + ip + ']:' + port; } } }
Sending SOCKS Frames
The integration of a sending method for our SOCKS library is quite easy.
// SOCKS.mjs SOCKS.send = (socket, data) => { data = data instanceof Object ? data : { headers: {}, payload: null }; let buffer = encode(socket, data); if (buffer !== null) { socket.write(buffer); } };
Encoding Logic
The Encoding Logic is a bit more complex, as it has to respect the following states :
- Server : If
headers['@version']
is set to0x05
or0x04
, set the SOCKS protocol version. - Server : If
headers['auth']
is eithernone
orlogin
, transmit a Handshake Response Frame. - Server : If
headers['@status']
is set, transmit a Connection Status Frame. - Client : If
headers['auth']
is anArray
, transmit a Handshake Request Frame. - Client : If
headers['@method']
is set, transmit a Connection Request Frame.
// Chapter: Encoding Logic const encode = function(socket, data) { data = data instanceof Object ? data : { headers: {}, payload: null }; if ((data.headers instanceof Object) === false) { data.headers = {}; } let blob = [ 0x05 ]; if (data.headers['@version'] === 5) { blob[0] = 0x05; } else if (data.headers['@version'] === 4) { blob[0] = 0x04; } let is_server = socket._is_server === true; if (is_server === true) { if (typeof data.headers['auth'] === 'string') { if (data.headers['auth'] === 'none') { blob[1] = 0x00; } else if (data.headers['auth'] === 'login') { blob[1] = 0x02; } else { blob[1] = 0xff; } } else if (typeof data.headers['@status'] === 'string') { if (data.headers['@status'] === 'success') { blob[1] = 0x00; } else if (data.headers['@status'] === 'error-blocked') { blob[1] = 0x02; } else if (data.headers['@status'] === 'error-network') { blob[1] = 0x03; } else if (data.headers['@status'] === 'error-host') { blob[1] = 0x04; } else if (data.headers['@status'] === 'error-connection') { blob[1] = 0x05; } else if (data.headers['@status'] === 'error-method') { blob[1] = 0x07; } else if (data.headers['@status'] === 'error') { blob[1] = 0x01; } blob[2] = 0x00; let payload = encode_payload(data.payload); if (payload !== null) { payload.forEach((v) => { blob.push(v); }); } } } else { if ((data.headers['auth'] instanceof Array) === true) { let methods = data.headers['auth'].map((v) => { if (v === 'none') { return 0x00; } else if (v === 'login') { return 0x02; } return null; }).filter((method) => method !== null); let length = methods.length; blob[1] = length; methods.forEach((v) => { blob.push(v); }); } else if (data.headers['@method'] === 'connect') { blob[1] = 0x01; blob[2] = 0x00; let payload = encode_payload(data.payload); if (payload !== null) { payload.forEach((v) => { blob.push(v); }); } } } if (blob.length > 1) { return Buffer.from(blob); } return null; };
Encoding 0x01 Domain Payload, 0x04 : IPv6 Payload
The decoding logic has been explained already in the previous chapters,
so it should be quite easy for you to handle the encoding of the payloads
and possible to write the
encode_payload
method yourself.
The method should handle two different scenarios :
- Encode a
{host,port}
Payload Object. - Encode a
{domain,port}
Payload Object.
In any Error Case, a Payload representing the IPv4
0.0.0.0
and the Port
0
should be returned.
SOCKS Server
The SOCKS Server is responsible for a lot of things, as it has to handle the Handshake Requests and the Connection Requests without a Session.
This means that for the Server has support the following states :
- Handshake Request with the
none
Authentication Method. - Connection Request for the
IPv4
Payload. - Connection Request for the
Domain
Payload. - Connection Request for the
IPv6
Payload. - The Connection Request's
connect
Method. - Unsupported : Handshake Request with the
auth
Authentication Method is currently unsupported (as it's unsecure as already explained). - Unsupported : The Connection Request's
bind
Method. It seems to be not used in the wild by any Web Browser anyways.
// server.mjs import dns from 'dns'; import net from 'net'; import { SOCKS } from './SOCKS.mjs'; // Chapter: SOCKS Server let server = new net.Server({ allowHalfOpen: true, pauseOnConnect: true }); server.on('connection', (socket) => { socket._is_server = true; socket.allowHalfOpen = true; socket.setNoDelay(true); socket.setKeepAlive(true, 0); socket.once('data', (request) => { if (request[0] === 0x05) { SOCKS.receive(socket, request, (data) => { console.log('Receive Handshake Request', data); if ( data.headers['@version'] === 5 && data.headers['auth'].includes('none') ) { socket.removeAllListeners('data'); socket.once('data', (request) => { SOCKS.receive(socket, request, (data) => { console.log('Receive Connection Request', data); // Chapter: Connection Status if ( data.headers['@version'] === 5 && data.headers['@method'] === 'connect' ) { // Chapter: Handling Host Connections // Chapter: Handling Domain Connections } else { socket.end(); } }); }); setTimeout(() => { SOCKS.send(socket, { headers: { '@version': 5, 'auth': 'none' }, payload: null }); }, 0); } else { socket.end(); } }); } else { socket.end(); } }); socket.resume(); }); server.on('error', () => server.close()); server.on('close', () => (server = null)); server.listen(1080, null);
Connection Status
The Connection Status Frame is sent by the SOCKS Server to the Client after the Server connected to (or tried to connect to) the requested Host or Domain.
Handling Host Connections
A Connection Request for a Host is quite easy to create, as it requires only a simple TCP-based Network Tunnel. The Server creates a TCP Socket to the Target Host and dispatches it through to the Client. Further Data sent by the Client will then just be delegated to the Target Host.
// server.mjs // (somewhere down the network event flow) // (...) console.log('Receive Connection Request', data); // Chapter: Connection Status if ( data.headers['@version'] === 5 && data.headers['@method'] === 'connect' ) { let host = data.payload.host || null; let domain = data.payload.domain || null; let port = data.payload.port || null; if (host !== null && port !== null) { let tunnel = null; try { tunnel = net.connect({ host: host, port: port }, () => { socket.pipe(tunnel); tunnel.pipe(socket); let status = { headers: { '@version': 5, '@status': 'success' }, payload: { host: host, port: port } }; console.log('Send Connection Status', status); SOCKS.send(socket, status); }); } catch (err) { tunnel = null; } if (tunnel === null) { SOCKS.send(socket, { headers: { '@version': 5, '@status': 'error-network' }, payload: null }); } } else if (domain !== null && port !== null) { // Chapter: Handling Domain Connections } else { // (...) } }
Handling Domain Connections
A Connection Request for a Domain requires a
dns.resolve4()
or
dns.resolve6()
call
before, and on successfully resolved
A
or
AAAA
domain entries a connection to those
addresses.
In theory, failed connections to the specific addresses should gracefully try out all
other entries if there are multiple available; but in practice no SOCKS Proxy supports
that. I'll leave that up to the reader to implement it, as it requires a timeout loop
that gradually checks for network timeouts and failure scenarios of the
net.connect()
API in node.js, which is rather unclean to implement for demo purposes.
Afterwards, the same Network Flow happens as in the Handling Host Connections example, where the Tunnel is piped through to the Client and further Data received is delegated to the Target Host.
// server.mjs // (somewhere down the network event flow) // (...) console.log('Receive Connection Request', data); // Chapter: Connection Status if ( data.headers['@version'] === 5 && data.headers['@method'] === 'connect' ) { let host = data.payload.host || null; let domain = data.payload.domain || null; let port = data.payload.port || null; if (host !== null && port !== null) { // Chapter: Handling Host Connections } else if (domain !== null && port !== null) { // TODO: Integrate IPv6 support dns.resolve4(domain, (err, addresses) => { if (err === null) { // TODO: Integrate timeout loop for addresses if (addresses.length > 0) { let tunnel = null; try { tunnel = net.connect({ host: addresses[0], port: port }, () => { socket.pipe(tunnel); tunnel.pipe(socket); let status = { headers: { '@version': 5, '@status': 'success' }, payload: { host: addresses[0], port: port } }; console.log('Send Connection Status', status); SOCKS.send(socket, status); }); } catch (err) { tunnel = null; } if (tunnel === null) { SOCKS.send(socket, { headers: { '@version': 5, '@status': 'error-network' }, payload: null }); } } else { SOCKS.send(socket, { headers: { '@version': 5, '@status': 'error-network' }, payload: null }); } } else { SOCKS.send(socket, { headers: { '@version': 5, '@status': 'error-network' }, payload: null }); } }); } else { // (...) } }
Reference Implementation
That's it. Our implementation is now fully peer-to-peer
ready and supports the complete
SOCKS5
protocol.
In case you missed something in between the lines or made a mistake, there's a reference implementation available.