SOCKS5 is a widely used proxy protocol for facilitating secure and efficient communication between clients and servers.

Compared to SOCKS4, SOCKS5 adds the following capabilities:

  • Authentication
  • IPv6
  • Domain name resolution (DNS)
  • UDP association

HTTP proxies are even more limited — they handle HTTP(S) traffic exclusively.

Note: proxy technologies are not encrypted by default, and most are not capable of encryption at all. Proxying a connection is primarily used to evade censorship or bypass geofencing (IP-based blocking).

  • Why use a private proxy instead of a private VPN?
    • When encryption is not required, a SOCKS5 proxy has much lower overhead than a VPN. A single-core VM or container can saturate its network interfaces with minimal latency. A proxy is also easier to scope to a single application (e.g. a secondary browser) rather than routing all traffic from the entire OS or network.

Note: my internet connection was the bottleneck during the speed test.

It is worth noting that multiple sources online offer frequently updated lists of open proxy servers [Link].

Proxies can also be used to cache static web content and reduce traffic on metered or impaired links [Link], though that is outside the scope of this post.

The following examples show how to proxy through another server to access the internet while hiding the real source of the request.


GOLANG — A high-performance proxy written in Go using a third-party module [Link]:

Create a new file called main.go and add the following content:

package main

import (
        "fmt"
        "syscall"
        "github.com/things-go/go-socks5"
)

func main() {
        if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &syscall.Rlimit{Max: 65536, Cur: 65536}); err != nil {
                fmt.Println("Error setting rlimit:", err)
        }

        server := socks5.NewServer()

        if err := server.ListenAndServe("tcp", ":1080"); err != nil {
                panic(err)
        }
}
go run main.go

Note: syscall is used here to raise the open file descriptor (connection) limit for the process. The same result can be achieved from the console with ulimit -n 65536.


PYTHON — This example requires a username and password for authentication. It is a fork of the code available at [Link].

import select
import socket
import struct
from socketserver import ForkingTCPServer, TCPServer, StreamRequestHandler

class ThreadingTCPServer(ForkingTCPServer, TCPServer):
    pass


class SocksProxy(StreamRequestHandler):
    username = 'username'
    password = 'password'

    def handle(self):
        # greeting header - read and unpack 2 bytes from a client
        header = self.connection.recv(2)
        version, nmethods = struct.unpack("!BB", header)

        # socks 5
        assert version == 5
        assert nmethods >= 0

        # get available methods
        methods = self.get_available_methods(nmethods)

        # accept only USERNAME/PASSWORD auth
        if 2 not in set(methods):
            # close connection
            self.server.close_request(self.request)
            return

        # send welcome message
        self.connection.sendall(struct.pack("!BB", 5, 2))

        if not self.verify_credentials():
            return

        # parsing the request
        version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))
        assert version == 5

        if address_type == 1:  # IPv4
            address = socket.inet_ntoa(self.connection.recv(4))
        elif address_type == 3:  # DNS
            try:
                domain_length = self.connection.recv(1)[0]
                address = self.connection.recv(domain_length)
                address = socket.gethostbyname(address)
            except:
                address = '0.0.0.0'

        port = struct.unpack('!H', self.connection.recv(2))[0]

        # replying to the request
        try:
            if cmd == 1:
                remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                remote.connect((address, port))
                bind_address = remote.getsockname()
            else:
                self.server.close_request(self.request)

            addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]
            port = bind_address[1]
            reply = struct.pack("!BBBBIH", 5, 0, 0, 1,
                                addr, port)

        except Exception as err:
            reply = self.generate_failed_reply(address_type, 5)

        self.connection.sendall(reply)

        # establish data exchange
        if reply[1] == 0 and cmd == 1:
            self.exchange_loop(self.connection, remote)

        self.server.close_request(self.request)

    def get_available_methods(self, n):
        methods = []
        for i in range(n):
            methods.append(ord(self.connection.recv(1)))
        return methods

    def verify_credentials(self):
        piece_of_data = self.connection.recv(1)
        if len(piece_of_data) == 0:
            return
        version = ord(piece_of_data)
        assert version == 1

        username_len = ord(self.connection.recv(1))
        username = self.connection.recv(username_len).decode('utf-8')

        password_len = ord(self.connection.recv(1))
        password = self.connection.recv(password_len).decode('utf-8')

        # set the following flag to True to skip credential checks
        accept_any_creds = False
        if (username == self.username and password == self.password) or accept_any_creds:
            response = struct.pack("!BB", version, 0)
            self.connection.sendall(response)
            return True

        response = struct.pack("!BB", version, 0xFF)
        self.connection.sendall(response)
        self.server.close_request(self.request)
        return False

    def generate_failed_reply(self, address_type, error_number):
        return struct.pack("!BBBBIH", 5, error_number, 0, address_type, 0, 0)

    def exchange_loop(self, client, remote):

        while True:

            try:
                # wait until client or remote is available for read
                r, w, e = select.select([client, remote], [], [])

                if client in r:
                    data = client.recv(4096)
                    if remote.send(data) <= 0:
                        break

                if remote in r:
                    data = remote.recv(4096)
                    if client.send(data) <= 0:
                        break
            except KeyboardInterrupt:
                self.server.close_request(self.request)
                exit()
            except Exception as err:
                print(err)

if __name__ == '__main__':
    with ThreadingTCPServer(('0.0.0.0', 1080), SocksProxy) as server:
        try:
            server.serve_forever()
        except:
            pass

BONUS

The following PHP script can be dropped onto an existing web server to provide a simple single-request proxy:

<?php
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
  $url = $_GET['url'];

  $ch = curl_init();
  curl_setopt($ch, CURLOPT_URL, $url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HEADER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, getallheaders());

  $response = curl_exec($ch);
  $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
  $header = substr($response, 0, $header_size);
  $body = substr($response, $header_size);

  curl_close($ch);
  foreach (explode("\r\n", $header) as $header_line) {
    header($header_line);
  }

  echo $body;
} else {
  die('Invalid request method');
}
?>

It requires the PHP cURL module:

sudo apt-get install php-curl -y
sudo systemctl restart apache2

Usage (file size is limited by the PHP memory limit set in the server configuration):

curl "http://200.200.200.200/path/proxy.php?url=https://7-zip.org/a/7z2201-x64.exe" > 7zip.exe