Disclaimer: Целью статьи является изучение протокола SOCKS4. Представленная реализация является исключительно учебной, не использует аутентификацию клиентов, шифрование или маскировку трафика, вследствие чего не может быть использована для обхода каких-либо блокировок.

Краткое описание протокола

Протокол SOCKS4 предназначен для проксирования TCP соединений.

Клиент устанавливает соединение с прокси и присылает один запрос с указанием команды и её параметров; прокси отвечает одним или двумя ответными сообщениями, и, если удалось выполнить команду, начинает пересылать данные между клиентом и целью.

Запрос клиента имеет вид: VERSION (1 байт), COMMAND (1 байт), PORT (2 байта), IPv4 ADDRESS (4 байта), USER_ID (нуль-терминированная строка).

Расширение протокола SOCKS4a позволяет клиенту делегировать прокси разрешение доменных имён; то есть, в конце запроса может присутствовать необязательный параметр DOMAIN (нуль-терминированная строка).

Поддерживаются две команды:

CONNECT - клиент соединяется с прокси и говорит, куда подключиться; прокси соединяется с целью, подтверждает клиенту успех соединения и начинает пересылать данные между клиентом и целью.

BIND - клиент соединяется с прокси; прокси готовится к приёму входящего соединения и сообщает клиенту адрес и порт для подключения; клиент передаёт эту информацию цели; цель подключается к прокси; прокси подтверждает клиенту успешное соединение и начинает пересылать данные между клиентом и целью.

Ответ прокси имеет вид: VERSION (1 байт), STATUS (1 байт), PORT (2 байта), IPv4 ADDRESS (4 байта).

Подробные описания протокола SOCKS4 и расширения SOCKS4a:

https://www.openssh.com/txt/socks4.protocol

https://www.openssh.com/txt/socks4a.protocol

Скучная часть реализации

Импортируем необходимые библиотеки:

import sys, os, argparse, traceback, logging
import enum, struct, socket, socketserver, select

Напишем стартовый код приложения, разбирающий аргументы командной строки (адрес и порт, по которым прокси будет принимать соединения клиентов, уровень логгирования, необязательное имя лог-файла) и настраивающий логгирование:

def parse_args():
    parser = argparse.ArgumentParser(description = 'Simple SOCKS4 server')
    
    parser.add_argument('--log-level', action = 'store', type = str, 
        dest = 'log_level', default = 'DEBUG', help = 'Log level', 
        choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
    parser.add_argument('--log-path', action = 'store', type = str, 
        dest = 'log_path', default = None, help = 'Log file path')
    parser.add_argument('--host', action = 'store', type = str, 
        dest = 'host', default = '127.0.0.1', help = 'Server IP or hostname')
    parser.add_argument('--port', action = 'store', type = int, 
        dest = 'port', default = 1080, help = 'Port to listen')
    
    return parser.parse_args()

if __name__ == '__main__':
    try:
        args = parse_args()
        
        logging.basicConfig(format = '%(asctime)s [%(levelname)s]: %(message)s',
            level = logging.getLevelName(args.log_level.upper()), filename = args.log_path)
        logging.info(f'Starting server at {args.host}:{args.port}')
        
        # TODO
    except KeyboardInterrupt as e:
        logging.info('Shutting down')
        sys.exit(0)
    except Exception as e:
        logging.error(f'{str(e)}\n{traceback.format_exc()}')
        sys.exit(1)

Основой прокси выступит стандартная библиотека socketserver, а именно, класс ThreadingTCPServer. Он будет принимать входящие соединения от клиентов и запускать наш обработчик в отдельных потоках. Добавим заглушку обработчика клиентских соединений:

class SOCKS4Handler(socketserver.StreamRequestHandler):
    def handle(self):
        logging.info('Connection accepted: %s:%s' % self.client_address)
        try:
            # TODO
            pass
        finally:
            logging.info('Connection terminated: %s:%s' % self.client_address)
            self.server.close_request(self.request)

В стартовый код, после инициализации логгирования, добавим запуск сервера с нашим обработчиком:

        with socketserver.ThreadingTCPServer((args.host, args.port), SOCKS4Handler) as server:
            server.serve_forever() # interrupt with Ctrl+C

Определим пару вспомогательных констант - таймаут для сокетов (в секундах) и кол-во байт, вычитываемых прокси (в рабочем режиме, когда трафик перенаправляется между клиентом и целью) из сокета за один раз:

SOCKET_TIMEOUT_SEC = 120
STREAM_BUFFER_SIZE = 4096

Нескучная часть реализации

В соответствии с полным описанием протокола определим наборы констант - версии запроса и ответа, коды команд, статусы ответов (в данной реализации будут использоваться только первые два):

class SOCKS4_VER(enum.IntEnum):
    REQUEST = 0x04
    REPLY = 0x00

class SOCKS4_CMD(enum.IntEnum):
    # Establish a TCP/IP stream connection
    CONNECT = 0x01
    # Establish a TCP/IP port binding
    BIND = 0x02

class SOCKS4_REPLY(enum.IntEnum):
    # Request granted
    GRANTED = 0x5A
    # Request rejected or failed
    FAILED_OR_REJECTED = 0x5B
    # Request failed because client is not running identd (or not reachable from server)
    FAILED_NO_IDENTD = 0x5C
    # Request failed because client's identd could not confirm the user ID in the request
    FAILED_BAD_IDENTD = 0x5D

socket.recv(bufsize [, flags]) может вернуть меньше байт, чем запрошено. Чтобы упростить код, разбирающий запрос клиента, напишем вспомогательные функции - вычитывающую строго n байт из сокета (или выбрасывающую исключение), и вычитывающую нуль-терминированную строку:

def recvall(sock, n, /):
    data, count = [], 0
    while count < n:
        packet = sock.recv(n - count)
        if not packet:
            raise EOFError(f'Read {count} bytes from socket, expected {n} bytes')
        data.append(packet)
        count += len(packet)
    return b''.join(data)

def recv_null_terminated_string(sock, /):
    data = []
    while True:
        char = recvall(sock, 1)
        if char == b'\x00':
            return b''.join(data).decode('utf-8')
        data.append(char)

Разбирать запрос клиента будем так: вычитываем часть запроса с фиксированной длиной (8 байт - VERSION, COMMAND, PORT, IPv4 ADDRESS), вычитываем обязательную часть запроса с переменной длиной (нуль-терминированная строка USER_ID). Если ADDRESS имеет вид 0.0.0.x (причём x != 0) - то имеем дело с SOCKS4a запросом; в таком случае вычитываем необязательную часть запроса с переменной длиной (нуль-терминированная строка DOMAIN). В случае заведомо ошибочных ситуаций (недопустимые VERSION, COMMAND, пустой DOMAIN для SOCKS4a запроса) будем кидать исключение. Добавим метод в обработчик запроса SOCKS4Handler:

    def read_socks4_request(self):
        # fixed-length: VER(1), CMD(1), DSTPORT(2), DSTADDR(4)
        header = recvall(self.connection, 8)
        ver, cmd, dst_port = struct.unpack('!BBH', header[0 : 4])
        dst_addr = socket.inet_ntop(socket.AF_INET, header[4 : 8])
        
        if ver != SOCKS4_VER.REQUEST:
            raise SOCKS4ProtocolError(f'Unknown version: {ver}')
        if (cmd != SOCKS4_CMD.CONNECT) and (cmd != SOCKS4_CMD.BIND):
            raise SOCKS4ProtocolError(f'Unknown command: {cmd}')
        
        # variable-length: USERID, DOMAIN (optional)
        user_id = recv_null_terminated_string(self.connection)
        
        dst_domain = None # SOCKS 4A
        if (header[4 : 7] == b'\x00\x00\x00') and (header[7 : 8] != b'\x00'):
            dst_domain = recv_null_terminated_string(self.connection)
            if len(dst_domain) == 0:
                raise SOCKS4ProtocolError(f'Empty domain field in SOCKS 4A request: {dst_addr}')
        
        return (ver, cmd, dst_port, dst_addr, user_id, dst_domain)

Также добавим исключение, выбрасываемое в read_socks4_request():

class SOCKS4ProtocolError(Exception):
    pass

Ответ клиенту всегда имеет фиксированную длину 8 байт. Значения полей PORT и IPv4 ADDRESS не имеют смысла, если статус ответа отличен от SOCKS4_REPLY.GRANTED, т.е. можно задать им значения по умолчанию. Добавим в обработчик SOCKS4Handler метод для отправки ответов клиенту:

    def write_socks4_reply(self, status, addr = '0.0.0.0', port = 0, /):
        logging.debug(f'Reply: {status};{addr};{port}')
        reply = struct.pack('!BBH', SOCKS4_VER.REPLY, status, port)
        reply += socket.inet_pton(socket.AF_INET, addr)
        self.connection.sendall(reply)

Также добавим вспомогательный метод для установки таймаута и флага keep alive для сокетов:

    def tune_socket_options(self, sock, /):
        sock.settimeout(SOCKET_TIMEOUT_SEC)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

В блок try метода handle() добавим основу будущего прокси - приём запроса клиента, разрешение доменного имени (если пришел SOCKS4a запрос), ветвление в зависимости от команды:

            self.tune_socket_options(self.connection)
            
            (ver, cmd, dst_port, dst_addr, user_id, dst_domain) = self.read_socks4_request()
            logging.debug(f'Request: {ver};{cmd};{dst_port};{dst_addr};{user_id};{dst_domain}')
            
            if dst_domain:
                dst_addr = socket.gethostbyname(dst_domain)
                logging.debug(f'Resolved {dst_domain} into {dst_addr}')
            
            if cmd == SOCKS4_CMD.CONNECT:
                pass # TODO
                
            elif cmd == SOCKS4_CMD.BIND:
                pass # TODO

Этот код может кидать ряд исключений - EOFError (из функции recvall()), socket.gaierror (из socket.gethostbyname()), SOCKS4ProtocolError (из метода read_socks4_request()) и т.д. В зависимости от исключения можно просто закрыть соединение, или (в большинстве случаев) нужно отправить клиенту ответ со статусом SOCKS4_REPLY.FAILED_OR_REJECTED, однако если прокси вышел в рабочий режим (перекидывает трафик между клиентом и целью) - отправку ответов следует запретить. Добавим перед блоком try метода handle() флажок, разрешающий отправку ответов:

        reply_on_fail = True

Блок try метода handle() дополним блоками except:

        except EOFError as e:
            logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        except socket.gaierror:
            logging.warning(f'Unable to resolve domain: {dst_domain}')
            self.write_socks4_reply(SOCKS4_REPLY.FAILED_OR_REJECTED)
        except Exception as e:
            logging.warning(f'{str(e)}\n{traceback.format_exc()}')
            if reply_on_fail:
                self.write_socks4_reply(SOCKS4_REPLY.FAILED_OR_REJECTED)

В обработчик SOCKS4Handler добавим заглушку, которая в дальнейшем будет пересылать трафик между клиентом и целью:

    def stream_tcp(self, socket_a, socket_b, /):
        # TODO
        pass

К этой заглушке мы ещё вернёмся, а пока что закончим работу над методом handle() добавив обработку SOCKS4_CMD.CONNECT и SOCKS4_CMD.BIND.

Обработка команды CONNECT сводится к

  • созданию сокета

  • попытке соединения с целью (адрес и порт цели получены из запроса клиента) и отправке ответа со статусом SOCKS4_REPLY.GRANTED

  • после чего прокси начинает пересылать трафик между парой сокетов.

Если соединение не получится установить - обработчик исключений отправит клиенту ответ со статусом SOCKS4_REPLY.FAILED_OR_REJECTED.

            if cmd == SOCKS4_CMD.CONNECT:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as dst_socket:
                    self.tune_socket_options(dst_socket)
                    dst_socket.connect((dst_addr, dst_port))
                    (bind_addr, bind_port) = dst_socket.getsockname()
                    
                    reply_on_fail = False
                    self.write_socks4_reply(SOCKS4_REPLY.GRANTED, bind_addr, bind_port)
                    self.stream_tcp(self.connection, dst_socket)

Обработка команды BIND (ну, насколько я понял) сводится к

  • созданию сокета, слушающего случайный порт

  • отправке адреса и порта (на которых прокси ожидает соединения) в ответе со статусом SOCKS4_REPLY.GRANTED клиенту

  • приеме входящего соединения от цели

  • проверке соответствия адреса цели адресу, указанному в запросе клиента

  • ещё одном ответе клиенту со статусом SOCKS4_REPLY.GRANTED, адресом и портом соединившейся с прокси цели

  • после чего прокси начинает пересылать трафик между парой сокетов.

Если что-то пойдет не так - обработчик исключений отправит клиенту ответ со статусом SOCKS4_REPLY.FAILED_OR_REJECTED.

            elif cmd == SOCKS4_CMD.BIND:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as bind_socket:
                    self.tune_socket_options(bind_socket)
                    
                    bind_addr = socket.gethostbyname(socket.gethostname())
                    bind_socket.bind((bind_addr, 0)) # any port
                    bind_socket.listen(1)
                    (bind_addr, bind_port) = bind_socket.getsockname()
                    
                    self.write_socks4_reply(SOCKS4_REPLY.GRANTED, bind_addr, bind_port)
                    (peer_socket, (peer_addr, peer_port)) = bind_socket.accept()
                    with peer_socket:
                        self.tune_socket_options(peer_socket)
                        
                        if dst_addr != peer_addr:
                            raise SOCKS4ProtocolError(f'Got connection from {peer_addr}; expected from {dst_addr}')
                        
                        reply_on_fail = False
                        self.write_socks4_reply(SOCKS4_REPLY.GRANTED, peer_addr, peer_port)
                        self.stream_tcp(self.connection, peer_socket)

На этом основная часть прокси, непосредственно касающаяся протокола SOCKS4, закончена.

Пересылка трафика между клиентом и целью

Что нам нужно сделать концептуально в методе stream_tcp()?

  • Узнать, есть ли данные для чтения в каком либо из сокетов.

  • Прочитать данные, записать их в буферы.

  • Отправить данные из буферов в соответствующие сокеты.

  • Удалить отправленную часть данных из буферов.

  • Если при чтении или отправке данных произошло исключение - завершить цикл пересылки данных.

  • Попытаться отправить остатки данных из буферов, игнорируя исключения.

Добавим в SOCKS4Handler метод, формирующий описание пары сокетов, которое будет записываться в лог:

    def get_stream_desc(self, socket_a, socket_b, /):
        addr_a, port_a = socket_a.getpeername()
        addr_b, port_b = socket_b.getpeername()
        return f'{addr_a}:{port_a} <--> {addr_b}:{port_b}'

Напишем вспомогательные функции для безопасного приема и отправки данных в сокеты:

  • safe_recv() принимает сокет, буфер с данными и флаг завершения цикла пересылки; возвращает буфер, дополненный новыми данными и потенциально измененный флаг завершения цикла пересылки

  • safe_send() принимает сокет, буфер с данными и флаг завершения цикла пересылки; возвращает буфер, из которого удалены отправленные данные и потенциально измененный флаг завершения цикла пересылки

  • safe_sendfinal() принимает сокет и буфер, данные из которого надо попробовать отправить, игнорируя ошибки

def safe_recv(sock, buf, done, /):
    try:
        packet = sock.recv(STREAM_BUFFER_SIZE)
        if len(packet) > 0:
            buf += packet
        else:
            done = True
    except Exception as e:
        logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        done = True
    return buf, done

def safe_send(sock, buf, done, /):
    try:
        bytes_sent = sock.send(buf)
        buf = buf[bytes_sent : ]
    except Exception as e:
        logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        done = True
    return buf, done

def safe_sendfinal(sock, buf):
    try:
        sock.sendall(buf)
    except:
        pass

С использованием этих функций метод stream_tcp() обработчика SOCKS4Handler будет иметь вид:

    def stream_tcp(self, socket_a, socket_b, /):
        stream_info = self.get_stream_desc(socket_a, socket_b)
        logging.debug(f'Starting stream: {stream_info}')
        
        sockets_list = [socket_a, socket_b]
        buf_a2b, buf_b2a = b'', b''
        done = False
        
        while not done:
            read_ready, write_ready, _ = select.select(sockets_list, sockets_list, [], 0.5)
            
            if socket_a in read_ready:
                buf_a2b, done = safe_recv(socket_a, buf_a2b, done)
            if socket_b in read_ready:
                buf_b2a, done = safe_recv(socket_b, buf_b2a, done)
            
            if socket_a in write_ready:
                buf_b2a, done = safe_send(socket_a, buf_b2a, done)
            if socket_b in write_ready:
                buf_a2b, done = safe_send(socket_b, buf_a2b, done)
        
        logging.debug(f'Finalizing stream: {stream_info}')
        safe_sendfinal(socket_a, buf_b2a)
        safe_sendfinal(socket_b, buf_a2b)
        
        logging.debug(f'Stopped stream: {stream_info}')

Это не самая лучшая реализация, но даже в таком виде uTorrent смог пропихнуть через прокси поток данных в 0.5 Gbps (с чудовищной загрузкой CPU).

Как протестировать?

Проще всего проверить команду CONNECT:

curl -v --socks4 127.0.0.1:1080 -U userid:ignored ya.ru
curl -v --socks4a 127.0.0.1:1080 -U userid:ignored ya.ru

В логах прокси увидим информацию о приеме/разрыве соединений, разобранный запрос клиента и ответ прокси, и проч.:

2024-08-15 22:49:21,216 [INFO]: Starting server at 127.0.0.1:1080
2024-08-15 22:49:42,229 [INFO]: Connection accepted: 127.0.0.1:58551
2024-08-15 22:49:42,270 [DEBUG]: Request: 4;1;443;77.88.55.242;username;None
2024-08-15 22:49:42,280 [DEBUG]: Reply: 90;192.168.10.101;58554
2024-08-15 22:49:42,280 [DEBUG]: Starting stream: 127.0.0.1:58551 <--> 77.88.55.242:443
2024-08-15 22:49:42,630 [DEBUG]: Finalizing stream: 127.0.0.1:58551 <--> 77.88.55.242:443
2024-08-15 22:49:42,630 [DEBUG]: Stopped stream: 127.0.0.1:58551 <--> 77.88.55.242:443
2024-08-15 22:49:42,630 [INFO]: Connection terminated: 127.0.0.1:58551
2024-08-15 22:50:01,330 [INFO]: Connection accepted: 127.0.0.1:58558
2024-08-15 22:50:01,332 [DEBUG]: Request: 4;1;443;0.0.0.1;username;ya.ru
2024-08-15 22:50:01,339 [DEBUG]: Resolved ya.ru into 77.88.55.242
2024-08-15 22:50:01,350 [DEBUG]: Reply: 90;192.168.10.101;58559
2024-08-15 22:50:01,350 [DEBUG]: Starting stream: 127.0.0.1:58558 <--> 77.88.55.242:443
2024-08-15 22:50:01,466 [DEBUG]: Finalizing stream: 127.0.0.1:58558 <--> 77.88.55.242:443
2024-08-15 22:50:01,466 [DEBUG]: Stopped stream: 127.0.0.1:58558 <--> 77.88.55.242:443
2024-08-15 22:50:01,466 [INFO]: Connection terminated: 127.0.0.1:58558
(Ctrl+C)
2024-08-15 22:50:24,247 [INFO]: Shutting down
Полный исходный код прокси

###################################################################################################

import sys, os, argparse, traceback, logging
import enum, struct, socket, socketserver, select

###################################################################################################

SOCKET_TIMEOUT_SEC = 120
STREAM_BUFFER_SIZE = 4096

###################################################################################################

# SOCKS 4 / 4A (TCP)
# https://www.openssh.com/txt/socks4.protocol
# https://www.openssh.com/txt/socks4a.protocol

class SOCKS4_VER(enum.IntEnum):
    REQUEST = 0x04
    REPLY = 0x00

class SOCKS4_CMD(enum.IntEnum):
    # Establish a TCP/IP stream connection
    CONNECT = 0x01
    # Establish a TCP/IP port binding
    BIND = 0x02

class SOCKS4_REPLY(enum.IntEnum):
    # Request granted
    GRANTED = 0x5A
    # Request rejected or failed
    FAILED_OR_REJECTED = 0x5B
    # Request failed because client is not running identd (or not reachable from server)
    FAILED_NO_IDENTD = 0x5C
    # Request failed because client's identd could not confirm the user ID in the request
    FAILED_BAD_IDENTD = 0x5D

###################################################################################################

def recvall(sock, n, /):
    data, count = [], 0
    while count < n:
        packet = sock.recv(n - count)
        if not packet:
            raise EOFError(f'Read {count} bytes from socket, expected {n} bytes')
        data.append(packet)
        count += len(packet)
    return b''.join(data)

def recv_null_terminated_string(sock, /):
    data = []
    while True:
        char = recvall(sock, 1)
        if char == b'\x00':
            return b''.join(data).decode('utf-8')
        data.append(char)

###################################################################################################

def safe_recv(sock, buf, done, /):
    try:
        packet = sock.recv(STREAM_BUFFER_SIZE)
        if len(packet) > 0:
            buf += packet
        else:
            done = True
    except Exception as e:
        logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        done = True
    return buf, done

def safe_send(sock, buf, done, /):
    try:
        bytes_sent = sock.send(buf)
        buf = buf[bytes_sent : ]
    except Exception as e:
        logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        done = True
    return buf, done

def safe_sendfinal(sock, buf):
    try:
        sock.sendall(buf)
    except:
        pass

###################################################################################################

class SOCKS4ProtocolError(Exception):
    pass

class SOCKS4Handler(socketserver.StreamRequestHandler):
    def tune_socket_options(self, sock, /):
        sock.settimeout(SOCKET_TIMEOUT_SEC)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    
    def read_socks4_request(self):
        # fixed-length: VER(1), CMD(1), DSTPORT(2), DSTADDR(4)
        header = recvall(self.connection, 8)
        ver, cmd, dst_port = struct.unpack('!BBH', header[0 : 4])
        dst_addr = socket.inet_ntop(socket.AF_INET, header[4 : 8])
        
        if ver != SOCKS4_VER.REQUEST:
            raise SOCKS4ProtocolError(f'Unknown version: {ver}')
        if (cmd != SOCKS4_CMD.CONNECT) and (cmd != SOCKS4_CMD.BIND):
            raise SOCKS4ProtocolError(f'Unknown command: {cmd}')
        
        # variable-length: USERID, DOMAIN (optional)
        user_id = recv_null_terminated_string(self.connection)
        
        dst_domain = None # SOCKS 4A
        if (header[4 : 7] == b'\x00\x00\x00') and (header[7 : 8] != b'\x00'):
            dst_domain = recv_null_terminated_string(self.connection)
            if len(dst_domain) == 0:
                raise SOCKS4ProtocolError(f'Empty domain field in SOCKS 4A request: {dst_addr}')
        
        return (ver, cmd, dst_port, dst_addr, user_id, dst_domain)
    
    def write_socks4_reply(self, status, addr = '0.0.0.0', port = 0, /):
        logging.debug(f'Reply: {status};{addr};{port}')
        reply = struct.pack('!BBH', SOCKS4_VER.REPLY, status, port)
        reply += socket.inet_pton(socket.AF_INET, addr)
        self.connection.sendall(reply)
    
    def get_stream_desc(self, socket_a, socket_b, /):
        addr_a, port_a = socket_a.getpeername()
        addr_b, port_b = socket_b.getpeername()
        return f'{addr_a}:{port_a} <--> {addr_b}:{port_b}'
    
    def stream_tcp(self, socket_a, socket_b, /):
        stream_info = self.get_stream_desc(socket_a, socket_b)
        logging.debug(f'Starting stream: {stream_info}')
        
        sockets_list = [socket_a, socket_b]
        buf_a2b, buf_b2a = b'', b''
        done = False
        
        while not done:
            read_ready, write_ready, _ = select.select(sockets_list, sockets_list, [], 0.5)
            
            if socket_a in read_ready:
                buf_a2b, done = safe_recv(socket_a, buf_a2b, done)
            if socket_b in read_ready:
                buf_b2a, done = safe_recv(socket_b, buf_b2a, done)
            
            if socket_a in write_ready:
                buf_b2a, done = safe_send(socket_a, buf_b2a, done)
            if socket_b in write_ready:
                buf_a2b, done = safe_send(socket_b, buf_a2b, done)
        
        logging.debug(f'Finalizing stream: {stream_info}')
        safe_sendfinal(socket_a, buf_b2a)
        safe_sendfinal(socket_b, buf_a2b)
        
        logging.debug(f'Stopped stream: {stream_info}')
    
    def handle(self):
        logging.info('Connection accepted: %s:%s' % self.client_address)
        
        reply_on_fail = True
        try:
            self.tune_socket_options(self.connection)
            
            (ver, cmd, dst_port, dst_addr, user_id, dst_domain) = self.read_socks4_request()
            logging.debug(f'Request: {ver};{cmd};{dst_port};{dst_addr};{user_id};{dst_domain}')
            
            if dst_domain:
                dst_addr = socket.gethostbyname(dst_domain)
                logging.debug(f'Resolved {dst_domain} into {dst_addr}')
            
            if cmd == SOCKS4_CMD.CONNECT:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as dst_socket:
                    self.tune_socket_options(dst_socket)
                    dst_socket.connect((dst_addr, dst_port))
                    (bind_addr, bind_port) = dst_socket.getsockname()
                    
                    reply_on_fail = False
                    self.write_socks4_reply(SOCKS4_REPLY.GRANTED, bind_addr, bind_port)
                    self.stream_tcp(self.connection, dst_socket)
                
            elif cmd == SOCKS4_CMD.BIND:
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as bind_socket:
                    self.tune_socket_options(bind_socket)
                    
                    bind_addr = socket.gethostbyname(socket.gethostname())
                    bind_socket.bind((bind_addr, 0)) # any port
                    bind_socket.listen(1)
                    (bind_addr, bind_port) = bind_socket.getsockname()
                    
                    self.write_socks4_reply(SOCKS4_REPLY.GRANTED, bind_addr, bind_port)
                    (peer_socket, (peer_addr, peer_port)) = bind_socket.accept()
                    with peer_socket:
                        self.tune_socket_options(peer_socket)
                        
                        if dst_addr != peer_addr:
                            raise SOCKS4ProtocolError(f'Got connection from {peer_addr}; expected from {dst_addr}')
                        
                        reply_on_fail = False
                        self.write_socks4_reply(SOCKS4_REPLY.GRANTED, peer_addr, peer_port)
                        self.stream_tcp(self.connection, peer_socket)
            
        except EOFError as e:
            logging.warning(f'{str(e)}\n{traceback.format_exc()}')
        except socket.gaierror:
            logging.warning(f'Unable to resolve domain: {dst_domain}')
            self.write_socks4_reply(SOCKS4_REPLY.FAILED_OR_REJECTED)
        except Exception as e:
            logging.warning(f'{str(e)}\n{traceback.format_exc()}')
            if reply_on_fail:
                self.write_socks4_reply(SOCKS4_REPLY.FAILED_OR_REJECTED)
        finally:
            logging.info('Connection terminated: %s:%s' % self.client_address)
            self.server.close_request(self.request)

###################################################################################################

def parse_args():
    parser = argparse.ArgumentParser(description = 'Simple SOCKS4 server')
    
    parser.add_argument('--log-level', action = 'store', type = str, 
        dest = 'log_level', default = 'DEBUG', help = 'Log level', 
        choices = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
    parser.add_argument('--log-path', action = 'store', type = str, 
        dest = 'log_path', default = None, help = 'Log file path')
    parser.add_argument('--host', action = 'store', type = str, 
        dest = 'host', default = '127.0.0.1', help = 'Server IP or hostname')
    parser.add_argument('--port', action = 'store', type = int, 
        dest = 'port', default = 1080, help = 'Port to listen')
    
    return parser.parse_args()

if __name__ == '__main__':
    try:
        args = parse_args()
        
        logging.basicConfig(format = '%(asctime)s [%(levelname)s]: %(message)s',
            level = logging.getLevelName(args.log_level.upper()), filename = args.log_path)
        logging.info(f'Starting server at {args.host}:{args.port}')
        
        with socketserver.ThreadingTCPServer((args.host, args.port), SOCKS4Handler) as server:
            server.serve_forever() # interrupt with Ctrl+C
    except KeyboardInterrupt as e:
        logging.info('Shutting down')
        sys.exit(0)
    except Exception as e:
        logging.error(f'{str(e)}\n{traceback.format_exc()}')
        sys.exit(1)

###################################################################################################

Комментарии (1)


  1. wesp1nz
    16.08.2024 01:41
    +1

    Интересно посмотреть на производительность. Хочется больше тестов!