Какое то время тому назад захотелось мне попробовать реализовать прокси сервер для собственных нужд, да такой, который можно было бы в дальнейшем использовать, а также, чтобы размер его был минимален. Естественным вариантом для меня стала реализация с использованием ассемблера. Программка получилась небольшая, удобная и в дальнейшем я очень часто ей пользовался. А вот теперь, по прошествии лет, хотелось бы показать простейшую реализацию одного протокола, SOCKS4. Данный протокол был создан для того, чтобы клиенты, находящиеся в локальной сети за межсетевым экраном могли обращаться во внешнюю сеть. В то же время запросы клиентов в таком случае есть возможность контролировать :) Самым первым, что нужно, при реализации – прочитать документацию с описанием данного протокола, так как мы хотим, чтобы наш протокол понимался стандартными программами, без “подтачивания напильником”. Итак, документация :
Описание протокола SOCKS
SOCKS: A protocol for TCP proxy across firewalls
Теперь, вооружившись описанием, приступим. Работа прокси сервера состоит в том, чтобы принять от клиента запрос в определенном формате, сформировать сокет и подключить его по адресу запрошенному клиентом, после чего обеспечить обмен данными между двумя сокетами, до их закрытия со стороны сервера, либо клиента. Приступим к реализации.
Макросы и структуры данных, используемые в программе
Сформируем включаемый файл, includes.inc. В данный файл поместим стандартные, при написании Windows программ макросы + структуры для работы с SOCKS4. Здесь я не буду приводить все макросы, я приведу лишь описание и функционал, необходимый для решения основной задачи, все остальное вы найдете в приложенном файле с исходными кодами.
По большому счету, структуры CONNECT_SOCK4 и RESPONSE_SOCK4 ничем не отличаются, так как мы реализуем протокол без авторизации. Но я решил все таки оставить их отдельно, чтобы в дальнейшем можно было легко изменить их, для доработки. В самих структурах, в переменной VN – указывается версия протокола, в нашем случае, здесь всегда должно быть 4, в случае с SOCKS5 в данной переменной находится 5 (протокол в принципе похож). Переменная CD используется для возврата клиенту результата запроса прокси сервера к запрошенному клиентом адресу (90 – соединение успешно / 91 – соединение не удалось).
У нас в программе по факту три этапа.
* Первый, инициализируем сокет, слушаем сокет на предмет наличия клиентских запросов, создаем поток обработки.
* Второй этап – анализ запроса клиента, попытка создания и соединения сокета с запрошенным клиентом сервером.
* И окончательный, третий этап – пересылка данных между сокетом клиента и сокетом созданным и соединенным нами с запрошенным адресом.
Реализация первого этапа, инициализация программы :
Это первая наша процедура, я постарался максимально прокомментировать код, чтобы вы могли разобраться, но если все же что-то не понятно – обращайтесь либо ко мне, либо к MSDN. В принципе весь код написан с использованием синтаксиса MASM и WinAPI. Итогом работы приведенной функции должен быть работающий сокет на одном из сетевых адресов вашей машины (локальный адрес, либо внешний адрес если у вас реальный IP) + по соединению клиента функция создает отдельный поток, используемый для работы с пришедшим клиентом. Теперь пойдем дальше ...
Второй этап, анализ запроса клиента
На втором этапе все, что необходимо сделать, это принять структуру CONNECT_SOCK4, создать сокет, попытаться соединить его и отправить клиенту ответ. Реализация :
Результат выполнения данной процедуры – соединенный сокет, а также созданный поток, реализующий обмен данными между двумя сокетами. Все просто. Стоит только уточнить, здесь используется несколько моментов по адресации внутри структур, которые введены в MASM для облегчения жизни программиста. Первый момент, макрос “Assume”.
Строчка Assume Esi: Ptr CONNECT_SOCK4 говорит компилятору о том, что в данном регистре(Esi) находится адрес структуры CONNECT_SOCK4, что в дальнейшем упрощает обращение к переменным внутри данной структуры. Assume Esi:Nothing отменяет привязку. Чтобы лучше понять, возможно будет проще, если я укажу несколько вариантов адресации :
либо
либо
Я думаю, вам очевидно так же как и мне, что быстрее, удобней и наглядней использовать первый вариант. Хотя если необходимо обратиться к одной переменной структуры – имеет право на существование и второй вариант. Третий же вариант думаю лучше использовать в случаях, когда данные по адресу не структурированы. Но, как известно, на вкус и цвет, каждый сам себе тамбовский волк. Пользуйтесь тем методом, который вам удобней.
Еще один момент, который стоит уточнить. Макрос Result. Данный макрос написан, чтобы можно было одной строкой вызвать функцию WinAPI и занести результат исполнения в регистр или память. Так строка :
Выполняет сначала вызов такого вида :
а уже после выполнения данного вызова результат исполнения (Eax) заносит в переменную lpMem. В данном конкретном случае, будет выделена память, а в переменную будет записан адрес, по которому находится выделенный для нас участок.
Этап третий, передача данных
Итак, выполнены два самых сложных этапа. Клиент пришел, мы соединили его с удаленным сервером и настал черед простейшей “обезьяньей” работы. Передачи данных между двумя сокетами. Сделаем это просто, по-быстрому :
Первоначально в данной процедуре производится инициализация внутренних переменных из переданной в поток структуры, чтобы их было удобнее использовать. Затем, в цикле производится проверка, есть ли данные на чтение из сокетов, затем двумя кусками кода (фактически копипаст, здесь я не заморачивался выносом функции и оптимизацией ибо так наглядней) делается чтение из одного сокета и отправка во второй.
Все, ура! Компилируем и пробуем. В принципе самый хороший вариант – FireFox. В настройках подключения указываем, что нужно использовать прокси сервер SOCKS4. Указываем его адрес и порт, на котором он у нас находится. После этого, сохраняем настройки и наслаждаемся интернетом, прошедшим, через наш прокси, размером 3,5 кбайт ))) Да, уточню. Для компиляции жлательно наличие установленного пакета MASM32, компиляция производится пакетными файлами bldall.bat либо bldallc.bat(консольный вариант приложе��ия).
Исходные тексты приложения, как и несколько старых проектов на ассемблере можно найти тут:
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
