Автоматический обход блокировок
Описание работы программы для автоматического обхода блокировок в интернете, код программы лежит на репозитории antiblock.
Приблизительно в мае 2022 года был заблокирован один из доменов YouTube (yt3.ggpht.com), через который происходит выгрузка превью и логотипов каналов. Если до этого блокировки меня особо не утруждали, то YouTube без картинок, для активного пользователя, это тяжело. Далее были предпринятые несколько разных подходов к оптимальному обходу блокировки этого домена.
Пример отсутствия превью
Попытка номер один — Свой собственный VPN
Весной 2022 года стало актуально создавать собственные VPN на основе мощностей арендных VPS. Я тоже создал свой VPN WireGuard. Почитать как это сделать самому можно например в статье.
Пропускание всего трафика с телефона или планшета через VPN приводило к быстрой разрядке батареи. И на некоторые сайты не заходило через VPN, например Госуслуги или Avito. Поэтому было решено придумать более гибкий и адаптивный метод, а именно маршрутизацию сразу на роутере. Но для маршрутизации на роутере необходимо иметь роутер с поддержкой WireGuard, а значит необходим роутер, на который можно установить OpenWrt. Мой предыдущий роутер (RT-AC57U V3) не имел такой возможности, а так как я хотел достаточно мощнее устройство для экспериментов и настройке сетевого диска, то выбор пал на Beelink U59 Pro. К нему была куплена пара мощных антенн и другой Wi-Fi модуль (Mediatek MT7921K). Подробнее о сборке и настройке я расскажу в следующих статьях. Таким образом у меня появился X86 роутер с возможностью компилировать и запускать на нем код. И была предпринята следующая попытка.
Beelink U59 Pro с антеннами
Попытка номер два — Таблица маршрутизации
Второй попыткой настроить обход блокировок было внести в таблицу маршрутизации все заблокированные 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. Поэтому от этого подхода пришлось отказаться, но он может кому-то пригодится в связи с простой настройки и отсутствию необходимости компилировать код под свой устройство.
Попытка номер три — BPF DPI
«Чтобы бороться с 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, никаких проблем выявлено не было.