Building a SOCKS Proxy with Node.js (Part 2)

Let’s start by importing net, building a server that listens on a specific port, and then creating some event listeners.

‘use strict’;
const net = require(‘net’);
//create the server object
let server = net.createServer();
//listen to a port. If we don’t pass it a configuration object, the //port chosen will be random. For our purposes, this port choice is //arbitrary.
server.listen({port: 5000},function() {
console.log('server listening to %j', server.address());
});
//listen for connection event. Let’s print the connection object so //we can see what it is.
server.on(‘connection’, function(conn){
console.log(conn);
});

Run this with Node, and then send some data to it. I’m going to use a Telnet client (Telnet being yet another application layer protocol) to send some information to this server. If you’re running OSX/macOS, you probably have a default Telnet client as part of your system software. In your command line just type which telnet to verify that it lives at /usr/bin/telnet. You can run this from the command line to set up a Telnet connection (run in a separate terminal tab so as not to interrupt the Node process):

telnet localhost 5000

This combination of IP address (localhost being a stand-in for the common loopback127.0.0.1 ) and port (5000) represent the network socket address. When you submit this in your Telnet terminal tab, you should immediately see an object like this displayed in your Node terminal:

Socket {
_connecting: false,
_hadError: false,
_handle:
TCP {
_externalStream: {},
fd: 18,
reading: true,
owner: [Circular],
onread: [Function: onread],
onconnection: null,
writeQueueSize: 0 },
_parent: null,
_host: null,
_readableState:
//...and so on

This is a Node net.Socket object, which is a duplex Stream object. We are essentially looking at an abstraction of the connection between the client and our server. Let’s examine it a little closer, and see what interesting details are contained. First, lets remove the private methods and fields (as denoted by properties beginning with an underscore _) and see what we have left.

Socket {
readable: true,
writable: true,
domain: null,
finish: [Function: onSocketFinish],
allowHalfOpen: false,
destroyed: false,
bytesRead: 0,
server:
Server {
domain: null,
allowHalfOpen: false,
pauseOnConnect: false,
} }
We can see that the socket object is both readable and writable, still exists, has some sort of finish callback, has not read any bytes read yet, and is associated with a Server object. Domain is null. allowHalfOpen is an interesting property. A half open TCP connection is one where either the host or client has removed the socket, but the other has not been notified.

TCP

The socket is the interface over which we receive TCP segments. In the previous entry, we mentioned that application layer protocols are encapsulated within TCP segments. Just like HTTP requests and responses consist of headers followed by data, TCP segments also have headers that contain directions about what to do with it’s data. While the headers of an HTTP request might contain authentication information, file types, domains, and path information, the headers of a TCP segment specify lower level concerns. Source Port and Destination Port determine where the segment is from and where it is going. The Sequence Number indicates where this particular TCP segment relates to others in a sequence (TCP segments can essentially be linked together to transport larger payloads than can fit in a single segment). The Acknowledgment Number is the sequence number of the next sequence expected. There are also various 1-bit flags (off/on values) that affect the behavior and interpretation of the segment(SYN, ACL, FIN, URG, etc.)

An illustration of the byte placement that the protocol follows, so that TCP segments can be consistently interpreted by information/communication systems. Source: wikipedia.org

When we get a TCP segment over a socket within net, it’s important to remember that the segment has already been unwrapped. The header has already been removed, and we are left with the payload. Let’s take a look at this in the code.

Data

We’re going to make one slight adjustment to the code from earlier. We will attach an event listener to the connection (the socket object) and log the data to our terminal.

‘use strict’;
const net = require(‘net’);
//create the server object
let server = net.createServer();
//listen to a port. If we don’t pass it a configuration object, the //port chosen will be random.
server.listen({
port: 5000
},function() {
console.log(‘server listening to %j’, server.address());
});
//listen for connection event. Let’s print the connection object so //we can see what it is.
server.on(‘connection’, function(conn){
//listen for data event on the socket, and print it out
conn.on(`data`,function(data){
console.log(data)
})
});

Let’s run this again, and hit it with Telnet. Since we removed the logging of the socket object on the connection event, we will not immediately log anything upon the initial Telnet connection. However, once we connect to Telnet, we can type simple text messages, and send them by hitting enter. Let’s send the message hey via Telnet. We should see something like this:

<Buffer 68 65 79 0d 0a>

A buffer is essentially an array of bytes. But these bytes encode the actual message. Let’s add a line to decode these bytes into a form that we can read.
Add this line before conn.on(data...:

conn.setEncoding(‘utf8’);

Now let’s restart the server, connect via Telnet and send hey again.

hey

Now we see that telnet is sending the text encoded into bytes and contained within the TCP segment. Let’s try something different to see a similar result; type localhost:5000 into your web browser. You should see something like this:

GET / HTTP/1.1
Host: localhost:5000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8

This is the HTTP request as transmitted from your browser. HTTP is a text based protocol, compared to TCP’s byte sequences. All that Node’s http library is really doing is parsing and interpreting this information, so that we can trigger, assemble, and send appropriate responses.

Before we move on, let’s make on more adjustment to the data event listener:

conn.on(‘data’,function(data){
console.log(data);
conn.write(data);
})

Let’s say hey to this one more time via Telnet, but this time we should actually see a response in Telnet itself. We sent a TCP segment in response, that has been unwrapped and displayed.

Call and Response

In part 3, we will start to look at the SOCKS protocol, and how to interpret it using Node and net.

Like what you read? Give Patrick Ackerman a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.