Автоматический обход блокировок
- воскресенье, 2 июля 2023 г. в 00:00:15
Описание работы программы для автоматического обхода блокировок в интернете, код программы лежит на репозитории Antiblock.
Приблизительно в мае 2022 года был заблокирован один из доменов YouTube (yt3.ggpht.com), через который происходит выгрузка превью и логотипов каналов. Если до этого блокировки меня особо не утруждали, то YouTube без картинок, для активного пользователя, это тяжело. Далее были предприняты несколько разных подходов к оптимальному обходу блокировки этого домена.
Весной 2022 года стало актуально создавать собственные VPN на основе мощностей арендных VPS. Я тоже создал свой VPN WireGuard. Почитать как это сделать самому можно например в статье.
Пропускание всего трафика с телефона или планшета через VPN приводило к быстрой разрядке батареи. И на некоторые сайты не заходило через VPN, например Госуслуги или Avito. Поэтому было решено придумать более гибкий и адаптивный метод, а именно маршрутизацию сразу на роутере. Но для маршрутизации на роутере необходимо иметь роутер с поддержкой WireGuard, а значит необходим роутер, на который можно установить OpenWrt. Мой предыдущий роутер (RT-AC57U V3) не имел такой возможности, а так как я хотел достаточно мощнее устройство для экспериментов и настройке сетевого диска, то выбор пал на Beelink U59 Pro. К нему была куплена пара мощных антенн и другой Wi-Fi модуль (Mediatek MT7921K). Подробнее о сборке и настройке я расскажу в следующих статьях. Таким образом у меня появился x86 роутер с возможностью компилировать и запускать на нем код. И была предпринята следующая попытка.
Второй попыткой настроить обход блокировок было внести в таблицу маршрутизации все заблокированные IP-адреса, выложенные на сайте antifilter.download. В списке allyouneed.lst находится около десяти тысяч IP-адресов и подсетей, что достаточно легко помещается в таблицу маршрутизации роутера. Для автоматического внесения адресов был написал Bash скрипт.
ip r | grep VPN | cut -d ' ' -f1 | xargs -I{} ip route del {}
curl https://antifilter.download/list/allyouneed.lst > ip.txt
cat ip.txt | xargs -I{} ip route add {} dev VPN
Если добавить этот скрипт в автозапуск Cron, то таблица маршрутизации будет иметь актуальный список IP-адресов и подсетей.
Скрипт работал, но быстро обнаружились его изъяны. IP-адреса доменов входящие в CDN очень часто меняются, поэтому их нет в списке allyouneed.lst. А домен, ради которого всё затевалось (yt3.ggpht.com), как раз тоже входит в CDN Google. Поэтому от этого подхода пришлось отказаться, но он может кому-то пригодится из-за простой настройки и отсутствию необходимости компилировать код под своё устройство.
"Чтобы бороться с DPI надо думать, как DPI".
Почитав ранее статьи про BPF, я подумал реализовать простой DPI на основе BPF, которая будет маршрутизировать трафик в зависимости от SNI TLS Handshake. Это возможно было сделать как напрямую маршрутизируя пакеты через BPF, так и выставляя флаг nf_conntrack, а далее маршрутизировать через nftables. Но тут снова сыграл свою роль самый важный для меня домен (yt3.ggpht.com), общение с ним идет не через TLS 1.3, а через QUIC, а значит и nf_conntrack не будет толком работать, да и пакеты зашифрованные, хоть и известным ключом, и для анализа придется их детектировать и расшифровывать. Для обычно не продуктивных роутеров это будет очень тяжелая нагрузка. От этого подхода тоже было решено отказаться.
Осознав плюсы и минусы предыдущих попыток, было решено маршрутизировать в зависимости от DNS пакетов. Была написана программа прокси DNS запросов, которая автоматически добавляет IP-адреса заблокированных доменов в таблицу маршрутизации. И удаляет при истечении времени жизни IP-адреса. Программа выложена на репозитории.
Кратко опишу работу программы:
Для быстрой проверки входит ли домен в список заблокированных доменов, необходимо добавить заблокированные домены в хеш-таблицу. Перепробовав разные хеш-таблицы для Си, ни одна из них не имела нужные характеристики. Была написана библиотека для хеш-таблицы, подробнее о ней будет рассказано в следующей статье. Для экономии памяти заблокированные домены хранятся не как массив указателей на нуль-терминированные строки, а как длинный массив лежащих подряд нуль-терминированных строк. Экономия памяти, потому что malloc на каждую строку занимал бы служебную информацию. А в хеш-таблице можно хранить смещение начала строки от начала массива, тем самым хеш-таблица состоит из четырехбайтных int, а не восьмибайтных pointer. Список заблокированных доменов автоматически обновляется каждые 12 часов в отдельном потоке. Домены скачиваются с сайта. Код описан в файле urls_read.c.
При поступлении DNS запроса от клиента, id запроса заменяется на внутренний, чтобы не было совпадения id номеров с разных клиентов. Старый id, IP-адрес, порт, время прихода пакета и хэш домена запоминаются в массив в поле с номером нового id. При увеличении нагрузки до предельной, если ответы на запросы не успевают приходить, то мы сможем начать отбрасывать пакеты от клиента на этапе поиска нового внутреннего id.
При поступлении ответа от DNS сервера, по пришедшему id смотрим поле с этим номером в массиве из предыдущего абзаца. Проверяем не истекло ли время возврата пакета, проверяем совпадение хэш домена с сохранённым, если всё хорошо, то помещаем пришедший пакет в кольцевой буфер для обработки другим потоком. Кольцевой буфер необходим для постоянства в использовании памяти. Код обработки запросов описан в файле net_data.c.
Бывает много разных типов DNS ответов, но нас интересуют два варианта типа “A” и “CNAME”. Тип “A” делает прямое соответствие между доменом и IP-адресом. Тип “CNAME” делает соответствие между данным доменом и новым доменом. Если встречаем заблокированный домен с ответом типа “A”, то добавляем IP-адрес в таблицу маршрутизации, и добавляем IP-адрес в хэш таблицу с ключом IP-адрес, а значением временем истечения жизни IP-адреса. Если встречаем заблокированный домен с ответом типа “CNAME”, то добавляем домен в список временно заблокированных, а так же добавляем домен с хэш таблицу с ключом домен, а значением временем истечения жизни домена. Код описан в файле dns_ans.c.
Отдельный поток раз в минуту проверяет истекшие IP-адреса и домены и удаляет их. Код описан в файле ttl_check.c.
Тестирование проводилось на отдельно написанной программе, которая эмулирует большое количество DNS запросов. Тестировалось на миллионе самых популярных доменов по версии CloudFlare. Моя программа выдерживала 100 000 запросов в минуту. А так же тестирование проводилось с использованием AddressSanitizer и MemorySanitizer, никаких проблем выявлено не было. При работе программа потребляет очень мало оперативной памяти, примерно 12 MB, причем 8 MB это объем заблокированных доменов, тем самым подходит даже самым простым роутерам.
Например я хочу зайти на RuTracker.
Программа детектировала DNS запрос с заблокированным доменом "RuTracker" и добавила два IP адреса "172.67.187.38" и "104.21.72.173" в таблицу маршрутизации и запомнила что в 15:44:53 необходимо удалить их, так как их актуальность истечет.
Моя программа пишет лог действий в формате CSV:
15:40:30,add ip,rutracker.org,172.67.187.38,15:44:53
15:40:30,add ip,rutracker.org,104.21.72.173,15:44:53
Таблица маршрутизации:
root@OpenWrt:~# ip r | grep VPN
104.21.72.173 via 192.168.6.37 dev VPN
172.67.187.38 via 192.168.6.37 dev VPN
Мы видим внесенные IP адреса, но уже 15:45:19 IP адреса удаляются.
Лог программы:
15:45:19,del ip,,172.67.187.38,15:44:53
15:45:19,del ip,,104.21.72.173,15:44:53
Новая таблица маршрутизации:
root@OpenWrt:~# ip r | grep VPN
Таким образом мы динамически узнаем нынешний IP адрес домена и добавляем именно актуальный IP адрес, тем самым имеем возможность использовать домены лежащие на CDN.