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

Compared to SOCKS4, SOCKS5 has the following additional capabilities:

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

HTTP proxy is even more limited. It is exclusively used for HTTP(S) traffic.

Note: none of the proxy technologies is encrypted by default and most are not even capable of encryption. Proxying a connection is a way to evade censorship and evade geofencing (block by source IP).

  • Why not just using a private VPN instead of the proposed private Proxy?
    • Because, as long as encryption is not required, a SOCKS5 proxy has a much smaller overhead than a VPN and a single-core VM or CT is capable of top up its network interfaces with minimum latency. Using a proxy is much easier to associate a single application (e.g. a secondary browser) to use it instead of the whole operating system or network-based routing.

Note: my Internet speed was the bottleneck during the speed test.

It is worthy to mention that multiple sources on the internet offer frequently updated lists of open proxy servers [Link].

Proxies in general can be used as cache for static web content to reduce data traffic in metered or impaired links [Link], but is is not the scope of this post.

The following codes make possible to hop (proxy) on another server to gain access to the internet 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 mail.go and add the following content in it:

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: the syscall was used to increase the limit of open file descriptors (connections) allowed by the system to the user. It could also be accomplished via console using ulimit -n 65536.


PYTHON – This example requires 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):
        pice_of_data = self.connection.recv(1)
        if len(pice_of_data) == 0:
            return
        version = ord(pice_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')

        # change the following flag to True to do not check credentials
        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 code, that can be dropped on an existent web server can provide a hop for single

<?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 to be installed:

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

Utilisation (the file size will be limited to the PHP allowed memory size on the server configuration):

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