Разбор работы прокси-сервера (на примере моей дипломной работы)
С 1 марта 2024 года Роскомнадзор имеет право блокировать интернет-ресурсы в которых содержится информация по обходу блокировок. Автор никоим образом не призывает читателей к нарушению закона с использованием содержащейся в данной статье информации. Вся информация представлена в исключительно образовательных и научных целях
Введение
В колледже я хотел стать системным администратором и активно развивался во всём что было связано с сетями, системами и интернетом. В этом мне помогли небезызвестные книжки Таненбаума, Куроуза и море сайтов. Моя дипломная работа звучала как "SOCKS5 прокси-сервер на Си и сокетах". Спустя три года я возвращаюсь к ней, так как во-первых, это интересно, а во-вторых, как мне кажется, сервер получился академический и понятный, так что на его примере можно многое понять о работе сетей
К тому времени я уже года два хорошо шарил за теорию и накопил достаточно знаний чтобы сделать что-то самостоятельно, к тому же, сроки поджимали. Для начала, я заказал справку об учебе из деканата, отправил JetBrains и они любезно предоставили мне для работы студенческий доступ к CLion IDE, за что им благодарен. Решил писать на Linux так как, опять же, интересно и грех писать сервера на чем-то другом
Немного теории перед тем как начнём
Как многие знают, прокси-сервер - это посредник, который взаимодействует с окружающим миром за пользователя. Принимая удар на себя, он позволяет скрыть своё присутствие в сети. Случаются ситуации когда нельзя получить доступ к сайту из страны пользователя. В таком случае развертывают прокси-сервер в другой стране, к которому имеется доступ, и который имеет доступ к самому ресурсу. Поэтому полезность прокси прямо пропорциональна его географическому положению.
Прокси-серверов существует большое количество. Самые распространенные основаны на протоколах http и socks. Прокси, в отличие от VPN, не шифрует соединение. Однако сами протоколы прикладного уровня, которые прокси передаёт, конечно же могут шифровать данные. Максимум чего стоит ждать пользователю от прокси - анонимность, но не безопасность
Протоколы - это соглашения, по которым компьютеры общаются друг с другом. Сюда относятся IP, TCP, SMTP, FTP, HTTP, DNS, DHCP и так далее. Каждый работает на своём уровне. Существует практика описывать спецификацию протокола в документах RFC (request for comments). Их можно найти онлайн и прочитать, чтобы понять как работает тот или иной протокол
По состоянию на 21 год описание краеугольного протокола HTTPS представлено в кучи RFC, таких как RFC2818, RFC9110, RFC7230, RFC8615 и прочих, состоящих из сотен страниц. Перелопатить их все бедному студенту нереально. Мне же хотелось что то попроще и приземлённей. Альтернатива - SOCKS5, описан в RFC1928. Опа, всего 9 страниц! Погнали!
SOCKS5, несмотря на цифру 5 в конце, датируется 1996 годом и практически изжил себя на сегодняшний день. Сама концепция проксей отмирает и вытесняется VPN
Ну окей, спецификация есть, как работает понятно, как реализовать то? И на каком языке программирования?
Реализовать то можно на любом, но нам нужна скорость. Поэтому, a fortiori выбираем Си
Для справки, все высокопроизводительные системные продукты (начиная от ядра Linux и заканчивая софтом для микроконтроллеров) пишутся на Си. Этот язык считается прародителем всех остальных. Выбирая его мы обеспечиваем себе тесную связь с аппаратурой и нативность
В стандартной библиотеке Си нет поддержки сети, поэтому для работы будем использовать POSIX. Это интерфейс взаимодействия с ОС, который реализован в Linux. Большинство дистрибутивов лишь частично POSIX совместимы, в них есть не все заявленные функции, но нам хватит имеющихся. Инфу о функциях POSIX можно найти, например, на OpenNet, или в страницах man
Для взаимодействия с сетью, все операционные системы предоставляют программисту интерфейс сокетов - абстракции сетевого соединения, чтобы не конструировать пакеты вручную. Его описание довольно громоздко и будет излишнем для данной статьи. Стоит лишь сказать что его понимание - важнейшая вещь при работе с сетями. Это - низший уровень сетевого взаимодействия для программиста. Ниже только драйвера для сетевых карт
Далее будет описан протокол SOCKS5, на котором работает сервер. Для других протоколов процесс взаимодействия может отличаться
Протокол
Когда пользователь хочет установить соединение с каким-то сайтом через прокси-сервер, он соединяется с прокси по порту 1080 и производит аутентификацию. Отправляется несколько байт. Первые два - номер версии и количество методов аутентификации, которые поддерживает пользователь. Затем идут сами методы, например GSSAPI, Логин/Пароль и т.д.
Сама аутентификация - неисправимый рудимент, которую можно легко обойти, но тем не менее, она присутствует в протоколе и сейчас уже выполняет фиктивную роль
Прокси отвечает пользователю аналогичным сообщением с номером версии и выбранным методом. Благо все клиенты SOCKS5 зачастую работают без аутентификации, я прошил в сервере всегда отвечать клиенту "NO AUTHENTICATION REQUIRED".
На этом этапе "аутентификация" пройдена и мы готовы работать. Клиент формирует запрос к прокси, мол, подключись туда-то за меня, который выглядит следующим образом
Давайте разберем поподробнее что из себя представляют данные поля, всё таки это запрос, самая важная часть протокола. Какие же возможности нам открыты?
VER - номер версии протокола. У SOCKS5 версия всегда равняется 5
CMD - номер команды, иначе говоря, что сделать прокси
И тут я отвлекусь и расскажу кое-что интересное!
В официальном RFC SOCKS5 упомянуты три поддерживаемых команды: CONNECT, BIND и UDP ASSOCIATE. Первая позволяет нам указать куда подсоединиться, вторая - привязать айпи и самому принимать соединения, а третья добавляет поддержку UDP
Но на деле во всех реализациях что клиентов, и большинства серверов поддерживается только CONNECT! Почему? Мне бы знать! Почитав об этом в интернетах, оказывается, что так сложилось исторически
А также, превеликая реализация SSH в Linux'е также поддерживает только CONNECT
Собственно, обнаружил данные факты я когда пытался добавить поддержку BIND, но, видимо, бог увёл. Возвращаясь к запросу
RSV - зарезервированное поле, которое не нашло применения
ATYP - тип адреса, следующего далее. Здесь возможны знакомые IP версии 4, версии 6 и доменное имя
DST.ADDR и DST.PORT - адрес и номер порта, куда нужно соединиться
Вот, собственно, и весь запрос
Имея возможность указать номер порта в запросе мы можем проксировать не только трафик сайтов, а любой протокол прикладного уровня. Поддержка доменного имени в ATYP позволяет прокси самому обрабатывать DNS запросы
Разберем для чего это может пригодиться!
Компьютеры в сети имеют IP-адреса. Поэтому когда вы хотите зайти, например, на сайт vk.com, вашему компьютеру необходимо знать айпи сервера вконтакте. Но как ему перевести удобное для человека имя в адрес понятный компьютеру? Для этого компьютер пользователя формирует DNS-запрос к ближайшему DNS-серверу (обычно находящегося у интернет провайдера), который переводит доменное имя (vk.com) в ip-адрес и отправляет его вам. Затем, имея айпи вк на руках, запрос с вашего компа летит по адресу (например в спб) и оттуда к вам возвращается ваша страница. Но что если доступа к DNS нет, а к прокси есть? Пусть например DNS-сервер не содержит нужной записи, или его ответ не долетает до вас. В этом случае, в поле DST.ADDR, мы указываем доменное имя, разрешать которое теперь обязанность прокси и ближайшего к нему DNS'а
SOCKS5-Клиент Firefox имеет такую поддержку
Получив запрос, прокси-сервер соединяется с указанным компьютером, а затем отправляет пользователю ответ, о том, как всё прошло
Ответ выглядит подобно запросу. В одном из полей содержится код статуса:
- 00 - Успех
- 01 - Общая ошибка SOCKS сервера
- 02 - Соединение не разрешено набором правил (firewall)
- 03 - Сеть недоступна
- 04 - Узел недоступен
- 05 - В соединении отказано
- 06 - TTL (Time-to-live) истёк
- 07 - Команда не поддерживается
- 08 - Тип адреса не поддерживается
У меня, конечно же, не было возможности реализовать поддержку и обнаружение всех ошибок, но это и не нужно. Поэтому в случае ошибки пользователь получает 04
После ответа, все последующие данные, сквозным образом, прокси передаёт в обе стороны. Так и происходит общение
Когда работа заканчивается, пользователь просто гасит соединение с прокси, а прокси в свою очередь рвёт с хостом
Особенности реализации
На сервер накладывается множество обязанностей. Он должен быть быстрым, производительным, отказоустойчивым, целостным и безопасным. Это верно для любого сервера. К сожалению, на момент диплома, я не догадался тупо сгенерировать запросы к серверу утилитой, и тестировал работоспособность из нескольких браузерных вкладок
Главный цикл сервера выглядит так
Здесь, каждый квант времени проверяется, пришло ли что-нибудь с той стороны, если да, сразу перенаправляется на другую. Главная задача - не задерживаться, и как можно быстрее проталкивать любые данные
За отправку и принятие отвечают функции send_all и recv_all
Это обертки над системными вызовами send и recv соответственно. Как сказано в https://www.opennet.ru/man.shtml?topic=recv&category=2&russian=0, обе функции могут считать не весь заявленный объем буфера и вернуть управление раньше, поэтому обертки убеждаются что были переданы все данные
Чё там по настройкам?
Сервер запускается из командной строки, ему можно передать несколько флагов-опций которые определят его дальнейшую работу. Из того что успел реализовать:
- Поддержка IPv4 и IPv6, хотя последнее протестировано не было, так как дом.ру не выдаёт IP-адреса 6 версии (но компилятор в голове говорил, что работать будет)
- Логирование в CSV файл. Сохраняется дата, время, адрес клиента, порт клиента, команда, тип адреса, адрес хоста и порт хоста. Просто, удобно, можно читать в Excel'е
- Смена порта сервера (по умолчанию 1080)
- Можно задать лимит соединений (по умолчанию 1024)
- Можно изменить размер буфера в соединениях. Он не должен быть слишком большим, иначе на компьютере закончится память когда подключатся много клиентов, и не слишком маленьким, чтобы не тормозить на проталкивании данных. По умолчанию я выставил его в 65535 байт, хотя по хорошему здесь нужно выбирать оптимальный размер исходя из статистики
Первое время была рабочая поддержка файрволла и аутентификации через логин/пароль, но я не нашёл клиентов использующих её, а сам ориентировался на клиент в Firefox
Продакшн
В то время я запустил его на сервере DigitalOcean в Германии, получилось открыть ряд недоступных сайтов (например, rutracker), позже у меня закончились деньги на карте, а ещё позже РКН вырезал целый диапазон айпишников DigitalOcean. На этом история и закончилась :)
Итоги
Много что хотелось бы рассказать еще о теории, но думаю сделать это в отдельных статьях, если дойдут руки
Если так подумать, то сервер может работать и на мак оси, так как, в отличие от линукса, она полностью POSIX-совместимая. Кому интересно может собрать сам через CMake: https://github.com/spawn18/tightsocks