Overview

This tutorial will show you how to use Mongoose Library functions to implement a TCP-based server, in this case a SOCKS5 proxy server

We only implement a subset of the SOCKS5 TCP services, that is, this example allows your client of choice to exchange data with a desired TCP server through it, by connecting to a SOCKS5 interface without user authentication.

We'll have two connections:

  • The usual connection from the outside world, where we are a server
  • A new connection to the desired server, where we are a client

We'll then need two callback functions, one to handle traffic from the client, and the other to handle traffic from the destination server. All traffic will be forwarded in both directions after the SOCKS5 protocol handshake takes place.

Build and run

  • If you've not already done so, clone the Mongoose Library repo
    $ git clone https://github.com/cesanta/mongoose
    
  • Build the example, this will also start Mongoose:
    $ cd mongoose/examples/socks5-server
    $ make clean all
    
  • Start another terminal, and execute:
$ curl -x socks5h://localhost:1080 -k -X GET http://info.cern.ch:80

You'll see the very first web page at CERN

The last command instructed curl to connect to localhost at port 1080 and request a connection to the domain name info.cern.ch on port 80. Then, curl sends an HTTP GET request that gets forwarded to the other end, whose response is forwarded back to curl, that prints it.

How it works

Once a client is connected, the process progresses in three phases:

  1. Handshake, where the authentication takes place
  2. Request, where the client requests a remote host to connect to and the server tries to do that
  3. Exchange, where the connection to the desired host has taken place and traffic flows both ways through the proxy

Finally, the client or the remote server will disconnect.

  • We handle this process with a finite state machine, and store its current state into label, a buffer contained in our mg_connection structure. This structure is initialized to zero at connection creation and closure time, so our initial state will be defined as zero.

    enum {
      ...
      STATE_HANDSHAKE = 0,          // Connection state: in handshake
    
  • A server is initialized, listening at TCP port 1080, its event handler function will be function fn() with no arguments passed:

  • At the event handler function, fn(), we handle the following events:

    • MG_EV_READ: data from the client, processed depending on which phase we are in
    • MG_EV_CLOSE: connection closures

For data communication we mainly use these functions:

size_t mg_iobuf_del(struct mg_iobuf *io, size_t offset, size_t len);
int mg_send(struct mg_connection *c, const void *data, size_t size);
struct mg_connection *mg_connect(struct mg_mgr *mgr, const char *url,
                                 mg_event_handler_t fn, void *fn_data);

Since this is plain TCP, we leave data in the receive buffer until we have as many bytes as we need to handle the next operation; at that time, once that amount of data has been processed, we delete it from the receive buffer by calling the mg_iobuf_del() function, so the buffer will now hold only new, fresh and unprocessed data.

The function mg_send() will append size bytes at data to the output buffer, to be sent by the event manager later.

The function mg_connect() will create a new connection to the URL specified in parameter url, its event handler function is the one specified in parameter fn, with parameter fn_data passed as its argument.

Handshake phase

  • We check the client is asking for SOCKS version 5 and then traverse the suggested authorization methods searching for one we support. As this is an example, we currently just support using no authorization.

Remember that, since this is plain TCP, we need to check the data we've been passed in the MG_EV_READ event contains a whole client handshake message.

  • We reply, and if everything looks fine, we advance to the Request phase and so change state accordingly. Otherwise, if no available method is supported, the client must close the connection.

Request phase

  • Once enough data is available to process a request, we check it is a supported request. Currently only connect requests are supported, and we handle IPv4, IPv6, and domain name formats of indicating the desired server address. With that information, we build an URL and call mg_connect() to initiate the new connection, handled by the event handler function fn2(), and we pass a pointer to our current connection c as its argument, so this handler can be able to send to our client whatever comes from the server we are connecting to. Note that we also set c->fn_data to point to this new connection, in order for the event handler fn() to get that information and be able to send to the server whatever comes from the client.
  • Then we build a response, send it, remove the request from our input buffer, and move on to the Exchange phase.

Exchange phase

  • In the Exchange phase, data coming from the client is sent to the server, and data coming from the server is sent to the client. We do this with a single function: exchange(). While the destination end is valid, that is, c->fn_data is not NULL, it sends data to it and deletes that data from the receive buffer. Otherwise, it signals the incoming end to flush its buffers and close.
  • From the client connection, this function is called from within the event handler function fn() on an MG_EV_READ event when we are in the Exchange phase, as we've seen above:
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_READ) {
    ...
    if (c->label[0] == STATE_ESTABLISHED) exchange(c);
  • From the server connection, this function is called from within the event handler function fn2() on an MG_EV_READ event:

Connection closure

  • When either end detects a connection closure, it calls the function disband(), which will clean up interconnections and signal it is time to flush buffers and close.

If for some reason the connection to the server fails, we'll also get an MG_EV_CLOSE event. For more ellaborate error handling, check the corresponding tutorial.

Browse latest code