Какое то время тому назад захотелось мне попробовать реализовать прокси сервер для собственных нужд, да такой, который можно было бы в дальнейшем использовать, а также, чтобы размер его был минимален. Естественным вариантом для меня стала реализация с использованием ассемблера. Программка получилась небольшая, удобная и в дальнейшем я очень часто ей пользовался. А вот теперь, по прошествии лет, хотелось бы показать простейшую реализацию одного протокола, SOCKS4. Данный протокол был создан для того, чтобы клиенты, находящиеся в локальной сети за межсетевым экраном могли обращаться во внешнюю сеть. В то же время запросы клиентов в таком случае есть возможность контролировать :) Самым первым, что нужно, при реализации – прочитать документацию с описанием данного протокола, так как мы хотим, чтобы наш протокол понимался стандартными программами, без “подтачивания напильником”. Итак, документация :

Описание протокола SOCKS
SOCKS: A protocol for TCP proxy across firewalls

  Теперь, вооружившись описанием, приступим. Работа прокси сервера состоит в том, чтобы принять от клиента запрос в определенном формате, сформировать сокет и подключить его по адресу запрошенному клиентом, после чего обеспечить обмен данными между двумя сокетами, до их закрытия со стороны сервера, либо клиента. Приступим к реализации.



Макросы и структуры данных, используемые в программе

  Сформируем включаемый файл, includes.inc. В данный файл поместим стандартные, при написании Windows программ макросы + структуры для работы с SOCKS4. Здесь я не буду приводить все макросы, я приведу лишь описание и функционал, необходимый для решения основной задачи, все остальное вы найдете в приложенном файле с исходными кодами.
; SOCKS4 – Структура, используемая клиентом, при запросе соединения
; на указанный сервер(DSTIP)/порт(DSTPORT)
CONNECT_SOCK4 Struc
  VN    Db ?
  CD    Db ?
  DSTPORT         Dw ?
  DSTIP   Dd ?
  NULL    Db ?
CONNECT_SOCK4 Ends

; SOCKS4 - ответ прокси-сервера о соединении.
RESPONSE_SOCK4  Struc
  VN    Db ?
  CD    Db ?
  DSTPORT         Dw ?
  DSTIP   Dd ?
RESPONSE_SOCK4  Ends


  По большому счету, структуры CONNECT_SOCK4 и RESPONSE_SOCK4 ничем не отличаются, так как мы реализуем протокол без авторизации. Но я решил все таки оставить их отдельно, чтобы в дальнейшем можно было легко изменить их, для доработки. В самих структурах, в переменной VN – указывается версия протокола, в нашем случае, здесь всегда должно быть 4, в случае с SOCKS5 в данной переменной находится 5 (протокол в принципе похож). Переменная CD используется для возврата клиенту результата запроса прокси сервера к запрошенному клиентом адресу (90 – соединение успешно / 91 – соединение не удалось).
У нас в программе по факту три этапа.
 * Первый, инициализируем сокет, слушаем сокет на предмет наличия клиентских запросов, создаем поток обработки.
 * Второй этап – анализ запроса клиента, попытка создания и соединения сокета с запрошенным клиентом сервером.
 * И окончательный, третий этап – пересылка данных между сокетом клиента и сокетом созданным и соединенным нами с запрошенным адресом.

Реализация первого этапа, инициализация программы :

; Основная процедура, является стартовой для программы
WinMain Proc
  LOCAL ThreadId, hServSock :DWORD
  LOCAL hostname[256] :BYTE
  LOCAL _wsa    :WSADATA
  LOCAL _our    :sockaddr_in

  ; Запуск библиотеки работы с сокетами, мы используем функционал версии 1.1,
        ; запросим его как минимальный
  invoke  WSAStartup, 0101h, ADDR _wsa
  .if eax == 0
    ; Берем свой адрес, подготавливаем структуру, для инициализации серверного сокета
    invoke  gethostname, ADDR hostname, 256
    invoke  gethostbyname, ADDR hostname
    .if eax == 0
      invoke  inet_addr, ADDR hostname
    .else
      mov eax, [eax + 12]
      mov eax, [eax]
      mov eax, [eax]
    .endif
    mov _our.sin_addr, eax
    invoke  inet_ntoa, eax

    mov _our.sin_family, AF_INET
    mov _our.sin_addr.S_un.S_addr, INADDR_ANY
    xor eax, eax
    ; Вносим порт, на котором хотим слушать входящие сообщения
    mov ax, SOCKS_PORT
    invoke  htons, eax
    mov _our.sin_port, ax

    invoke  socket, AF_INET, SOCK_STREAM, 0
    .if eax != INVALID_SOCKET
      ; Сохраняем созданный серверный сокет
      mov hServSock, eax
      ; Привязываем серверный сокет к нашему адресу и необходимому порту
      invoke  bind, hServSock, ADDR _our, SIZEOF sockaddr_in
      .if eax != SOCKET_ERROR
      @@:
        ; Инициируем сокет на ожидание
        invoke  listen, hServSock, SOMAXCONN
        .repeat
          ; Пришел клиент, получаем сокет с пришедшим клиентом
          invoke  accept, hServSock, NULL, NULL
        .until eax != INVALID_SOCKET
        ; Создаем поток, в котором будет обрабатываться текущий клиент
        xchg    eax, ebx
        invoke   CreateThread, NULL, NULL, ADDR socketThread, ebx, NULL, ADDR ThreadId
        ; Уходим на ожидание клиентов
        jmp @B
      .endif
    .endif
    invoke  closesocket, hServSock
  .endif

  invoke  ExitProcess, 0
WinMain   Endp

  Это первая наша процедура, я постарался максимально прокомментировать код, чтобы вы могли разобраться, но если все же что-то не понятно – обращайтесь либо ко мне, либо к MSDN. В принципе весь код написан с использованием синтаксиса MASM и WinAPI. Итогом работы приведенной функции должен быть работающий сокет на одном из сетевых адресов вашей машины (локальный адрес, либо внешний адрес если у вас реальный IP) + по соединению клиента функция создает отдельный поток, используемый для работы с пришедшим клиентом. Теперь пойдем дальше ...

Второй этап, анализ запроса клиента

  На втором этапе все, что необходимо сделать, это принять структуру CONNECT_SOCK4, создать сокет, попытаться соединить его и отправить клиенту ответ. Реализация :

socketThread  Proc    sock:DWORD
    LOCAL lpMem,
      _csock,
      ThreadId,
      dAmount   :DWORD
      
    LOCAL Remote    :sockaddr_in
    LOCAL wrFds,
      rdFds   :fd_set
    LOCAL hResp   :RESPONSE_SOCK4
      
  ; Готовимся к чтению данных из сокета
  invoke  FdZero, ADDR rdFds
  invoke  FdSet, sock, ADDR rdFds
  invoke  select, NULL, ADDR rdFds, NULL, NULL, NULL
  ; Получаем размер ожидающих чтения данных
  invoke  ioctlsocket, sock, FIONREAD, ADDR dAmount

  ; Резервируем память под данные
  mov   lpMem, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount)
  ; Читаем данные запроса из сокета
  invoke  recv, sock, lpMem, dAmount, 0      ; Запрос пришел
  lea   edi, hResp
  mov   esi, lpMem
  
  ; В Esi лежит пользовательский запрос. Мы обрабатываем (здесь) только версию SOCKS4,
  ; SOCKS5 можно в принципе здесь же обработать, но это позже ...
  Assume  Esi : Ptr CONNECT_SOCK4
  Assume  Edi : Ptr RESPONSE_SOCK4
  .if [esi].VN == 4
    ; Реализация протокола СОКС 4
    .if [esi].CD == 1
      invoke  socket, AF_INET, SOCK_STREAM, 0
      .if eax != INVALID_SOCKET
        mov   _csock, eax
        ; Берем данные удаленного хоста, с которым хочет соединиться клиент
        mov   Remote.sin_family, AF_INET
        mov   ax, [esi].DSTPORT
        mov   Remote.sin_port, ax
        mov   eax, [esi].DSTIP
        mov   Remote.sin_addr, eax
        mov   cx, [esi].DSTPORT
        mov   edx, [esi].DSTIP
        ; В Edi лежит ответ пользоват��лю 
        mov   [edi].VN, 0
        mov   [edi].DSTPORT, cx
        mov   [edi].DSTIP, edx
        ; Пытаемся соединиться с удаленным сервером                                      
        invoke  connect, _csock, ADDR Remote, SIZEOF Remote
        .if !eax
          ; Готовим ответ, что мы соединились
          mov   [edi].CD, 90
          ; Отправляем клиенту ответ, содержащий результат попытки соединения
          invoke send, sock, ADDR hResp, SIZEOF RESPONSE_SOCK4, 0
          ; Формируем структуру с информацией о серверном и
                                        ; соединенном клиентском сокетах
          ; - под серверным здесь подразумеваю сокет соединенный с клиентом,
                                        ;   приславшим запрос
          ; - под клиентским подразумеваю сокет соединенный с сервером,
                                        ;   данные которого запросил клиент 
          mov ebx, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, SIZEOF THREAD_DATA)
          Assume  Ebx : Ptr THREAD_DATA
          mov   eax, _csock
          mov   [ebx].Server, eax
          mov   eax, sock
          mov   [ebx].Client, eax
          Assume  Ebx : Nothing
          ; Запускаем поток обработки сокетов (читающий из клиентского и
                                        ; передающий в серверный сокет)
          invoke  CreateThread, NULL, NULL, ADDR ClientSock, ebx, NULL, ADDR ThreadId
        .else
          ; Если соединение не получилось - закрываем клиентский сокет
          invoke  closesocket, _csock
          ; Говорим, что произошла ошибка соединения
          mov   [edi][RESPONSE_SOCK4.CD], 91
          ; Отправляем клиенту ответ, содержащий результат попытки соединения
          invoke send, sock, ADDR hResp, SIZEOF RESPONSE_SOCK4, 0
        .endif
      .endif
    .endif
  .endif
  Assume  Edi: Nothing
  Assume  Esi: Nothing
  ; Высвобождаем память, выделенную под запрос
  invoke  LocalFree, lpMem
  ret
socketThread  Endp

  Результат выполнения данной процедуры – соединенный сокет, а также созданный поток, реализующий обмен данными между двумя сокетами. Все просто. Стоит только уточнить, здесь используется несколько моментов по адресации внутри структур, которые введены в MASM для облегчения жизни программиста. Первый момент, макрос “Assume”.
  Строчка Assume Esi: Ptr CONNECT_SOCK4 говорит компилятору о том, что в данном регистре(Esi) находится адрес структуры CONNECT_SOCK4, что в дальнейшем упрощает обращение к переменным внутри данной структуры. Assume Esi:Nothing отменяет привязку. Чтобы лучше понять, возможно будет проще, если я укажу несколько вариантов адресации :
Assume Esi:Ptr CONNECT_SOCK4
mov  al, [esi].VN      ; Помещаем в AL значение байта из  переменной VN структуры
mov  al, [esi].CD      ; Помещение в AL переменной CD
mov  ax. [esi].DSTPORT ; Помещение в AX переменной DSTPORT
Assume Esi:Nothing

либо
mov   al, [esi][CONNEСT_SOCK4.VN]      ; Помещаем в AL значение байта из переменной VN
mov   al, [esi][CONNEСT_SOCK4.CD]      ; Помещение в AL переменной CD
mov   ax, [esi][CONNEСT_SOCK4.DSTPORT] ; Помещение в AX переменной DSTPORT

либо
mov  al, byte ptr [esi]   ; Помещение в AL переменной VN
mov  al, byte ptr [esi+1] ; Помещение в AL переменной CD
mov  ax, word ptr [esi+2] ; Помещение в AX переменной DSTPORT


  Я думаю, вам очевидно так же как и мне, что быстрее, удобней и наглядней использовать первый вариант. Хотя если необходимо обратиться к одной переменной структуры – имеет право на существование и второй вариант. Третий же вариант думаю лучше использовать в случаях, когда данные по адресу не структурированы. Но, как известно, на вкус и цвет, каждый сам себе тамбовский волк. Пользуйтесь тем методом, который вам удобней.
  Еще один момент, который стоит уточнить. Макрос Result. Данный макрос написан, чтобы можно было одной строкой вызвать функцию WinAPI и занести результат исполнения в регистр или память. Так строка :
mov lpMem, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount)

Выполняет сначала вызов такого вида :
invoke LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount

  а уже после выполнения данного вызова результат исполнения (Eax) заносит в переменную lpMem. В данном конкретном случае, будет выделена память, а в переменную будет записан адрес, по которому находится выделенный для нас участок.

Этап третий, передача данных

  Итак, выполнены два самых сложных этапа. Клиент пришел, мы соединили его с удаленным сервером и настал черед простейшей “обезьяньей” работы. Передачи данных между двумя сокетами. Сделаем это просто, по-быстрому :
; Поток читающий из клиентского и передающий в серверный сокет....
ClientSock  Proc Param:DWORD
    LOCAL sserver, sclient:DWORD
    LOCAL rdFds  :fd_set
    LOCAL dAmount, lpBuf: DWORD

    ; В Param у нас находится информация о сокетах сервера и клиента,
        ; переносим в локальные переменные
  mov   ebx, Param
  Assume  Ebx: Ptr  THREAD_DATA
  mov   eax, [ebx].Server
  mov   sserver, eax
  mov   eax, [ebx].Client
  mov   sclient, eax
  Assume  Ebx : Nothing

    ; Не забудем высвободить память
  invoke  LocalFree, Param
  
@@:
  invoke  FdZero, ADDR rdFds
  invoke  FdSet, sserver, ADDR rdFds
  invoke  FdSet, sclient, ADDR rdFds
  invoke  select, NULL, ADDR rdFds, NULL, NULL, NULL
  ; Проверяем, есть ли данные для чтения
  .if eax == SOCKET_ERROR || eax == 0
    ; Данных нет - выходим
    jmp  @F
  .endif
  
  ; Есть ли данные от сервера, которые нужно передать клиенту?
  invoke FdIsSet, sserver, ADDR rdFds
  .if eax
    ; Получаем размер ожидающих чтения данных
    invoke  ioctlsocket, sserver, FIONREAD, ADDR dAmount

    ; Резервируем память под данные
    mov   lpBuf, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount)

    invoke recv, sserver, lpBuf, dAmount, 0
    .if eax == SOCKET_ERROR || eax == 0
      jmp @F
    .endif
    invoke send, sclient, lpBuf, eax, 0

    invoke  LocalFree, lpBuf
  .endif

  ; Есть ли данные от клиента для отправки серверному сокету?
  invoke FdIsSet, sclient, ADDR rdFds
  .if eax
    ; Получаем размер ожидающих чтения данных
    invoke  ioctlsocket, sclient, FIONREAD, ADDR dAmount

    ; Резервируем память под данные
    mov   lpBuf, @Result(LocalAlloc, LMEM_FIXED or LMEM_ZEROINIT, dAmount)

    invoke recv, sclient, lpBuf, dAmount, 0
    .if eax == SOCKET_ERROR || eax == 0
      jmp @F
    .endif
    invoke send, sserver, lpBuf, eax, 0

    invoke  LocalFree, lpBuf
  .endif

  ; Идем на новый цикл
  jmp    @B
  
@@:
  ; Закрываем сокеты
  invoke  closesocket, sserver
  invoke  closesocket, sclient
  
  ; Выходим из потока
  invoke  ExitThread, 0
ClientSock  Endp

  Первоначально в данной процедуре производится инициализация внутренних переменных из переданной в поток структуры, чтобы их было удобнее использовать. Затем, в цикле производится проверка, есть ли данные на чтение из сокетов, затем двумя кусками кода (фактически копипаст, здесь я не заморачивался выносом функции и оптимизацией ибо так наглядней) делается чтение из одного сокета и отправка во второй.
  Все, ура! Компилируем и пробуем. В принципе самый хороший вариант – FireFox. В настройках подключения указываем, что нужно использовать прокси сервер SOCKS4. Указываем его адрес и порт, на котором он у нас находится. После этого, сохраняем настройки и наслаждаемся интернетом, прошедшим, через наш прокси, размером 3,5 кбайт ))) Да, уточню. Для компиляции жлательно наличие установленного пакета MASM32, компиляция производится пакетными файлами bldall.bat либо bldallc.bat(консольный вариант приложе��ия).

Исходные тексты приложения, как и несколько старых проектов на ассемблере можно найти тут:

Socks4