С/C++ Миниатюрный сокс-сервер

Николай "GorluM" Андреев

(gorlum@real.xakep.ru)

Прокси. Это слово знакомо каждому интернетчику. Его знает любитель поругаться грязными словами в чатах, им пользуются мерзкие кардеры в поисках бесплатного стафа, и уж тем более проксю юзают всеми любимые скрипткидисы, им ведь тоже необходимо иметь анонимность в Сети. А иначе, какие же они "хакеры"? :)

На самом деле полезность прокси бывает разной. Например, у тебя в доме настроена локалка, а на одном из компьютеров есть выход в интернет. Чтобы все остальные компы тоже смогли выползать в инет, тебе придется выполнить одно из следующих действий: поставить NAT, что может сделать далеко не каждый, установить обычный роутер, но для этого понадобится выделить подсеть IP-адресов, и самый распространенный выход - установить проксю. Причем последний вариант - самый простой в реализации. Но ты можешь возразить, сказав, что не весь сетевой софт будет работать с проксей. Доля правды в этом есть, но фактически прокси поддерживает весь популярный софт: mIRC, ICQ, Opera, Internet Explorer. Вряд ли ты сможешь найти известное сетевое приложение без возможности использования прокси. Так что ты будешь устанавливать именно проксю, причем не какой-нибудь WinGate, а собственную реализацию. Да-да, собственную! Этот урок программирования на C/C++ посвящен написанию своего прокси-сервера.

В качестве протокола нашего сервера мы будем использовать socks4. Несложный в реализации и широко распространенный, он оптимален для нашей задачи. А задача наша - написать прозрачный (не анонимный) прокси-сервер, работающий с большинством сетевых приложений. Причем, весить он будет всего 2.5 Кб, что позволит использовать его в любых проектах.

Немного RFC

Идея программы проста до безобразия. Создаем порт-сервер и ждем подключений. При соединении принимаем некоторую информацию от клиента, содержащую версию протокола, ip и порт, на которые необходимо законнектиться. Инфу о пользователе и просьбы пройти аутентификацию мы просто пропускаем. Оно нам не нужно. Если вдруг понадобится, ты сам сможешь это реализовать, почитав RFC1928.

Итак, необходимую информацию мы записываем в структуру tag_SOCKS4_REQUEST:

typedef struct tag_SOCKS4_REQUEST{

unsigned char ucVersion;

unsigned char ucCommand;

WORD wDestPort;

DWORD dwDestIp;

} SOCKS4_REQUEST;

Заметь, IP принимается как DWORD (двойное слово, 4 байта). Чтобы использовать его в функции connect, нам придется преобразовать это значение в тип IN_ADDR. IN_ADDR - это те же 4 байта, только разделенные в структуре. Удобнее всего сделать преобразование с помощью функции memcpy (A,B,C), которая выполняет копирование куска памяти длиной C из A в B, где A - указатель "куда" копировать, а B - "откуда".

Игнорировать ненужную нам информацию, мы будем с помощью дополнительной функции. Она будет читать из порта по байтику до тех пор, пока не придет завершающий символ - '\0'. Вот эта функция:

void FlushRecvBufferUntil(SOCKET s, char condition){

int iReceiveRes; char cDummy;

do iReceiveRes = recv(s, &cDummy, sizeof(cDummy), 0);

while (iReceiveRes != SOCKET_ERROR && iReceiveRes != 0 && cDummy != condition);

}

Нулевой байт будет означать, что клиентский sock4-запрос окончен, и нашему проксику пора соединиться с адресом назначения dwDesIp. Если соединение прошло успешно, мы создаем канал данных между начальным и конечным пунктом. Если же соединение не удалось, то сервер просто убьет созданный ранее сокет при помощи WinSock-функции closesocket.

Пишем

Для работы sock4-сервера, помимо главного потока, создающегося из EntryPoint виндами (в нем мы слушаем порт), нам потребуется еще один поток. Через него будет производиться коннект и пересылка данных от отправителя к получателю.

Из статьи в ][ 12.02 "Пишем своего трояна" ты уже должен знать, как создавать сокет и слушать порт. На этот раз нам придется немного модифицировать механизм приема новых соединений. Функцию accept, ожидающую подключений на указанный порт и возвращающую сокет, мы должны будем запихнуть в бесконечный цикл. А в самом цикле при новом соединении создадим тред и передадим ему полученный сокет. Выглядит это так:

while (true) {

CreateThread(0,0,secthread,(LPVOID)accept(s, 0, 0), 0, &a);

}

где secthread - имя треда для обработки соединения, s - сокет, слушающий порт. Теперь, если кто-то приконнектится к открытому порту, сервер создаст нить (thread), в которую передаст переменную типа SOCKET для работы с новым соединением.

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

#define _Thread(x) unsigned long __stdcall x (LPVOID pParam)

И теперь, если необходимо создать тред, достаточно написать: _Thread (имя_нити). Но на этом способе облегчения кодинга я не остановился. Пользуясь Visual Studio .NET, я откопал немало очень интересных для си-кодера фишек. Вот одна из них: user keyword, по-русски - пользовательские ключевые слова. Они позволяют выделить в VS определенные слова каким-нибудь цветом (см. скрин).

 

Я ими активно пользуюсь и тебе советую. Если ты юзаешь свои макросы, то очень удобно сделать их выделяющимися в тексте программы. Для добавления ключевых слов достаточно создать файл usertype.dat в директории, где лежит твоя VS.NET, в Common7\IDE, и записать notepad'ом в него нужные ключевые слова по одному в строке. После этого перезагрузи VS.NET и радуйся красивому сорцу.

Но вернемся к нашим баранам. В созданной нити мы будем обрабатывать входящие соединения и при необходимости создавать новые коннекты с серверами, указанными в запросах. Вот как это происходит. Сначала принимаем один байт, в нем содержится версия требуемого сокс-протокола. Причем получим этот байт, не удаляя его из очереди сообщений. Для этого при вызове winsock-функции recv в четвертом параметре передадим значение MSG_PEEK. Теперь, при следующем чтении из сокета, мы сначала получим этот же байт и только потом все остальное. Это необходимо для отброса лишних байт при проверке версии. Итак, если версия равна четырем, то заполним структуру socks4Request типа SOCKS4_REQUEST из нашего сокета:

recv(s, (char*)&socks4Request, sizeof(socks4Request), 0);

Далее запустим функцию FlushRecvBufferUntil, с помощью которой пропускаем мимо ушей все лишнее, что хочет сообщить нам клиент. И только когда заголовок запроса к прокси полностью принят (пришел '\0'), мы попытаемся сконнектиться с dwDestIp (его получили из запроса).

При успешном соединении создаем бесконечный цикл, в котором будем совершать обмен данными в обоих сокетах: полученных accept`ом и connect`ом. С помощью функции select и пары макросов проверим, есть ли данные в сокетах. Для этого сначала создадим переменную типа fd_set, потом макросом FD_ZERO обнулим ее, а FD_SET`ом добавим к ней указатель на сокет, из которого будем читать данные. Теперь, вызвав функцию select и передав во втором параметре нашу переменную, мы можем воспользоваться макросом FD_ISSET. С его помощью мы узнаем состояние очереди чтения. И только когда она не пуста, читаем данные из одного сокета и пишем их в другой.

Поскольку мы хотим выполнять эти действия в одном цикле и в одном потоке, нам надо избавиться от одного нежелательного эффекта. Дело в том, что функция recv останавливает нить, из которой запущена, и ждет получения данных. Если в момент такой остановки придут данные в другой сокет, наша программа не сможет их перенаправить, так как будет занята. Как выяснилось, выйти из этого положения довольно просто. Нашим сокетам необходимо сообщить некоторый параметр, заставляющий recv и другие подобные функции выполняться в отдельном потоке. Вот как это выглядит в нашей программе:

u_long ulVal = 0;

ioctlsocket(s, FIONBIO, &ulVal);

Сокеты, не останавливающие нить до конца выполнения той или иной операции, называются неблокирующими.

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

Break

Вот и все. Как видишь, такая полезная программа очень проста в написании. Осталось только вставить пару строк для уменьшения размера exe'шника. Получившийся прокси прекрасно работает со всеми приложениями, которые я смог найти у себя на винте. ICQ сначала ругалась, но потом я отрубил работу DNS через firewall, и все заработало.

Если возникли какие-то вопросы, идеи или замечания - пиши. Постараюсь ответить.

Удачного компилирования.



Опубликовал admin
3 Фев, Вторник 2004г.



Программирование для чайников.