Socks5 – A .NET Core implementation from scratch

A few days ago I needed a way to connect to a server using a Socks5 proxy but couldn’t find an up-to-date implementation for .NET Core, so I decided to give it a go myself.

The implementation is pretty straightforward and easy, I got inspired from starksoft-aspen and followed the official RFC. All the code in this blog post is available in this Gist.

What is Socks

Socks is an internet protocol used to exchanges packets between a client and a server using a proxy in the middle. It also provides authentication methods. Socks works on the 5th layer (session) of the OSI model that controls communication between computers.

About Socks5

There are a few versions of Socks and, Socks5 is, obviously, an improvement of the previous version 4, which adds more authentication methods and support for IPv6 and UDP.
Here are the authentication methods that Socks5 supports:

  • No authentication
  • GSSAPI
  • Username/password
  • Methods assigned by IANA
  • Methods reserved for private use

Implementation

Socks5 Implementation - Sequence Diagram

Socks5 Implementation - Sequence Diagram

Constants & Options

Here are the constants (never-changing values) that we’ll using during the implementation:

public class Socks5Constants
{

    public const byte Reserved = 0x00;
    public const byte AuthNumberOfAuthMethodsSupported = 2;
    public const byte AuthMethodNoAuthenticationRequired = 0x00;
    public const byte AuthMethodGssapi = 0x01;
    public const byte AuthMethodUsernamePassword = 0x02;
    public const byte AuthMethodIanaAssignedRangeBegin = 0x03;
    public const byte AuthMethodIanaAssignedRangeEnd = 0x7f;
    public const byte AuthMethodReservedRangeBegin = 0x80;
    public const byte AuthMethodReservedRangeEnd = 0xfe;
    public const byte AuthMethodReplyNoAcceptableMethods = 0xff;
    public const byte CmdConnect = 0x01;
    public const byte CmdBind = 0x02;
    public const byte CmdUdpAssociate = 0x03;
    public const byte CmdReplySucceeded = 0x00;
    public const byte CmdReplyGeneralSocksServerFailure = 0x01;
    public const byte CmdReplyConnectionNotAllowedByRuleset = 0x02;
    public const byte CmdReplyNetworkUnreachable = 0x03;
    public const byte CmdReplyHostUnreachable = 0x04;
    public const byte CmdReplyConnectionRefused = 0x05;
    public const byte CmdReplyTtlExpired = 0x06;
    public const byte CmdReplyCommandNotSupported = 0x07;
    public const byte CmdReplyAddressTypeNotSupported = 0x08;
    public const byte AddrtypeIpv4 = 0x01;
    public const byte AddrtypeDomainName = 0x03;
    public const byte AddrtypeIpv6 = 0x04;

}

And here are the options a user needs to provide in order to use the implementation:

public class Socks5Options
{

    public string ProxyHost { get; }
    public int ProxyPort { get; }
    public string DestinationHost { get; }
    public int DestinationPort { get; }
    public AuthType? Auth { get; }
    public (string Username, string Password) Credentials { get; }

    public Socks5Options(string proxyHost, int proxyPort, string destHost, int destPort)
    {
        ProxyHost = proxyHost;
        ProxyPort = proxyPort;
        DestinationHost = destHost;
        DestinationPort = destPort;
        Auth = AuthType.None;
    }

    public Socks5Options(string proxyHost, string destHost, int destPort) : this(proxyHost, 1080, destHost, destPort) { }

    public Socks5Options(string proxyHost, int proxyPort, string destHost, int destPort, string username,
        string password) : this(proxyHost, proxyPort, destHost, destPort)
    {
        Auth = AuthType.UsernamePassword;
        Credentials = (username, password);
    }

    public Socks5Options(string proxyHost, string destHost, int destPort, string username, string password) :
        this(proxyHost, 1080, destHost, destPort, username, password)
    { }

}

public enum AuthType
{
    None,
    UsernamePassword
}

I only implemented two authentication methods: NoAuthentication and UsernamePassword, but you can easily add the others.

Selecting an authentication method

Firstly, after the client successfully connects to the proxy server, it needs to send the authentication methods it handles. I’ll be sending the two methods previously shown.

/*
+----+----------+----------+
| VER | NMETHODS | METHODS |
+----+----------+----------+
| 1  | 1        | 1 to 255 |
+----+----------+----------+
*/
var buffer = new byte[4] {
    5,
    2,
    Socks5Constants.AuthMethodNoAuthenticationRequired, Socks5Constants.AuthMethodUsernamePassword
};
await socket.SendAsync(buffer, SocketFlags.None);

The proxy server responds with the chosen method (if any):

/*
+-----+--------+
| VER | METHOD |
+-----+--------+
| 1   | 1      |
+-----+--------+
*/
var response = new byte[2];
var read = await socket.ReceiveAsync(response, SocketFlags.None);
if (read != 2)
    throw new SocksocketException($"Failed to select an authentication method, the server sent {read} bytes.");

if (response[1] == Socks5Constants.AuthMethodReplyNoAcceptableMethods)
{
    socket.Close();
    throw new SocksocketException("The proxy destination does not accept the supported proxy client authentication methods.");
}

if (response[1] == Socks5Constants.AuthMethodUsernamePassword && options.Auth == AuthType.None)
{
    socket.Close();
    throw new SocksocketException("The proxy destination requires a username and password for authentication.");
}

Username/Password authentication

Secondly, we check if the server chose the Username/Password method (0x02) and if so, we send him the authentication credentials:

/*
+-----+------+----------+------+----------+
| VER | ULEN | UNAME    | PLEN | PASSWD   |
+----+-------+----------+------+----------+
| 1  | 1     | 1 to 255 | 1    | 1 to 255 |
+----+-------+----------+------+----------+
*/
var buffer = ConstructAuthBuffer(options.Credentials.Username, options.Credentials.Password);
await socket.SendAsync(buffer, SocketFlags.None);

The ConstructAuthBuffer, as you can understand from its name, constructs a byte[] buffer that contains the command.

The proxy server then responds with either a success or a fail:

/*
+----+--------+
|VER | STATUS |
+----+--------+
| 1  |   1    |
+----+--------+
*/
var response = new byte[2];
var read = await socket.ReceiveAsync(response, SocketFlags.None);
if (read != 2)
    throw new SocksocketException($"Failed to perform authentication, the server sent {read} bytes.");

if (response[1] != 0)
{
    socket.Close();
    throw new SocksocketException("Proxy authentication failed.");
}

Connecting to the remote server

Lastly, we need to tell the proxy server to connect to the remote server:

/*
+-----+-----+-------+------+----------+----------+
| VER | CMD | RSV   | ATYP | DST.ADDR | DST.PORT |
+--- -+-----+-------+------+----------+----------+
| 1   | 1   | X'00' | 1    | Variable | 2        |
+-----+-----+-------+------+----------+----------+
*/

var addressType = GetDestAddressType(options.DestinationHost);
var destAddr = GetDestAddressBytes(addressType, options.DestinationHost);
var destPort = GetDestPortBytes(options.DestinationPort);

var buffer = new byte[6 + options.DestinationHost.Length];
buffer[0] = 5;
buffer[1] = Socks5Constants.CmdConnect;
buffer[2] = Socks5Constants.Reserved;
buffer[3] = addressType;
destAddr.CopyTo(buffer, 4);
destPort.CopyTo(buffer, 4 + destAddr.Length);

await socket.SendAsync(buffer, SocketFlags.None);

The proxy server responds with, again, either a success or a fail with some other information:

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

var response = new byte[255];
await socket.ReceiveAsync(response, SocketFlags.None);

if (response[1] != Socks5Constants.CmdReplySucceeded)
    HandleProxyCommandError(response, options.DestinationHost, options.DestinationPort);

The HandleProxyCommandError method throws a comprehensive error for the user to know exactly what went wrong.

We can now use the socket like we always do, all packets will be forwarded to the remote server by the proxy server. There will be a latency depending on the proxy server.

Conclusion

As you can see, implementing the Socks5 internet protocol is fairly easy and understandable. The implementation is small and well documented in the RFCs.
The protocol can be used to connect to servers using a VPN that provides Socks5 proxies, for whatever reason you may need.

Once again, the code of this implementation is available in this Gist and is ready to use!

Zanid Haytam Written by:

Zanid Haytam is an enthusiastic programmer that enjoys coding, reading code, hunting bugs and writing blog posts.

comments powered by Disqus