Portgen — обходим фильтрацию портов
Привет, GT!
Не растекаясь мыслями по деревьям, приступим к делу. Для обеспечения себя быстрым и нецензурируемым интернетом я уже давно использую стандартную схему: OpenVPN и самый простой VPS за рубежом. В качестве транспортного протокола используется UDP.
Проблема
В один «прекрасный» момент я обнаружил, что VPN отвалился и больше не поднимается. Не буду описывать долгое исследование проблемы — скажу сразу итог: помогло изменение номера порта. Помогло ненадолго: через пару-тройку дней туннель оборвался снова и снова был восстановлен сменой порта.
Гипотеза
На фаерволе провайдера статистика моего трафика выглядит примерно так: 100% или чуть меньше ходит по единственному протоколу UDP, через единственный порт и на единственный IP. Таким образом, если представить это себе в виде графика, он будет разительно отличаться от такового у среднестатистического пользователя, где будет множество «пиков» графика на разные протоколы, порты (TCP 80, 443, 993…) и IP-адреса.
Похоже, эвристика их фаервола реагирует именно на это и блокирует соединение, по которому проходит доля трафика, большая некоего порога. При этом, такой фильтр по определению будет срабатывать с некоторым запаздыванием, поскольку статистика — наука, любящая большие числа.
Задача
Нужно разработать решение, которое будет менять номер порта VPN с заданным интервалом, заведомо меньшим интервала пересчёта статистики у фаервола. Порт должен меняться в непредсказуемой манере, чтобы исключить работу фильтра на опережение. Также нужно иметь возможность запустить смену порта вручную на разные форс-мажорные случаи. Смена по времени должна работать даже при полной потере управления сервером.
Алгоритм
В общем виде формула генерации выглядит таким образом:
port = hash(shared_secret + round(time) + ondemand_key) % (port_max - port_min + 1) + port_min
- hash () — хеш-функция
- shared_secret — ключ, обеспечивающий непредсказуемость следующего результата без его знания. Единственная секретная часть системы.
- round (time) — некоторая функция загрубления от текущего времени по UTC. Степень загрубления определяет интервал смены портов, например отбрасывание секунд и минут — интервал 1 час, отбрасывание ещё и часов с сохранением метки AM/PM — 12 часов, и так далее.
- ondemand_key — ключ, подгружаемый с надёжного стороннего сервера для того, чтобы была возможность вручную сменить порт, изменив этот ключ.
- port_min и port_max — диапазон используемых портов, минимальный и максимальный соответственно.
Для того, чтобы система быстро реагировала на изменение ondemand-ключа, я запускаю скрипт по планировщику с маленьким интервалом в несколько минут. При этом надо предусмотреть проверку на то, изменились ли параметры на самом деле, чтобы при отрицательном результате скрипт сразу бы завершился.
Посмотрим на выражение выше, которое к моменту проверки уже вычислено. Если не изменилось ни значение hash (), ни границы диапазона — значит, изменений не было. Для этого возьмём хеш ещё раз:
cache = hash(hash(...) + port_min + port_max)
После этого прочитаем старое значение cache из специального файла, сравним, и если они различаются — обновим файл и продолжим выполнение скрипта; если одинаковые — выход.
Далее — дело техники: подставим значение порта в шаблон правила для iptables (этих шаблонов два — для клиентской и серверной роли скрипта), добавим правило в таблицу, прочтём предыдущее правило из другого файла и, если оно существует, удалим его из iptables, обновим файл текущим правилом.
Теоретически, в этот момент соединение уже должно работать на новом порту, однако на практике оно почему-то продолжает использовать старый до перезапуска демона. Буду благодарен, если кто-нибудь объяснит причину такой багофичи, но пока я просто отдаю из скрипта команду на перезапуск. Перебой связи при этом — несколько секунд и неудобства не причиняет.
Система перезапуска по требованию
В качестве хранилища ondemand-ключа очень удобно использовать публичную VCS (у меня Гитхаб). В принципе, подошёл бы любой бесплатный хостинг, однако VCS имеет важное преимущество в хранении истории правок. Может произойти такая ситуация, когда один из концов туннеля обновил свой ключ, а второй по какой-либо причине не может достучаться до репозитория и работает по старому ключу в кэше. В этом случае можно попробовать откатить коммит: если падение связи, которое заставило админа дёрнуть ondemand, вызвано не блокированием порта, а например временным сбоем у аплинка, возврат ключа вскоре поднимет туннель. На практике подобные ситуации пару раз бывали.
Скрипт, который вытягивает ключ, не представляет собой ничего особенного — обыкновенный wget, проверка его кода возврата и кэширование в файл — поэтому описывать его смысла нет, код всё скажет сам за себя.
Скрипт управления ondemand-ключами — более объёмная штука, хотя по сути тоже прост — это обвязка для упрощения операций над репозиторием ключей. Позволяет выполнять в одну команду операции создания, удаления и смены ключей.
Заключение
Считаю, что инструмент для решения поставленной задачи удался, и успешно работает уже больше года. Более того, получившуюся утилиту можно считать универсальным оружием против подобного типа фильтрации, поскольку:
- помимо OpenVPN скрипты можно использовать для любого сетевого сервиса
- в качестве криптографического аппарата можно использовать любую хеш-функцию, что даёт задаток на возможную в будущем компрометацию сегодняшних алгоритмов
- один сервер может обслуживать большое количество клиентов с уникальными ключами — достаточно лишь раскидать экземпляры portgen в индивидуальные папки
Ссылки
github.com/Raegdan/portgen — сама программа portgen
github.com/Raegdan/portgen-ondemand — программа управления ondemand-ключами и мои ключи заодно. Сделано это было исключительно для удобства. Ключи ondemand несекретные, поэтому если кому-то зачем-то вдруг нужен кусочек моего /dev/urandom — мне не жалко ;)
Примечание
В принципе, не могу не согласиться с таким аргументом, и программный код — действительно больше профиль Хабра. Однако, во-первых, этот проект напрямую относится к борьбе против цензурирования Интернета — тематике, которая была явным образом отселена сюда. Во-вторых, моя единственная до сих пор статья была написана до разделения и осталась на Хабре, что вызывает известные ограничения для моей учётки на GT. Прошу отнестись с пониманием.