Тема довольно изъезжена, знаю. К примеру, есть отличная чланак, но там рассматривается только IP-часть блоклиста. Мы же добавим еще и домены.
В связи с тем, что суды и РКН блокируют всё направо и налево, а провайдеры усиленно пытаются не попасть под штрафы, выписанные «Ревизорро» — сопутствующие потери от блокировок довольно велики. Да и среди «правомерно» заблокированных сайтов много полезных (привет, rutracker)
Я живу вне юрисдикции РКН, но на родине остались родители, родственники и друзья. Так что было решено придумать легкий для далеких от ИТ личностей способ обхода блокировок, желательно вовсе без их участия.
В этой заметке я не буду расписывать базовые сетевые вещи по шагам, а опишу общие принципы как можно реализовать эту схему. Так что знания как работает сеть вообще и в Linux в частности — must have.
Врсте брава
Для начала освежим в памяти что же блокируется.
В выгружаемом XML от РКН несколько типов блокировок:
IP
Домаин
УРЛ адреса
Мы их сведем для простоты к двум: IP и домен, а из блокировок по URL будем просто вытаскивать домен (точнее за нас это уже сделали).
Хорошие люди из Роскомсвобода реализовали прекрасный АПИ за, через который можно получать то, что нам нужно:
Для этого нам нужен какой-нибудь маленький зарубежный VPS, желательно с безлимитным траффиком — таких много по 3-5 баксов. Брать нужно в ближнем зарубежье чтобы пинг был не сильно большой, но опять-таки учитывать, что интернет и география не всегда совпадают. А так как никакого SLA за 5 баксов нет — лучше взять 2+ штуки у разных провайдеров для отказоустойчивости.
Далее нам необходимо настроить зашифрованный туннель от клиентского роутера до VPS. Я использую Wireguard как самый быстрый и простой в настройке т.к. клиентские роутеры у меня тоже на базе Linux (APU2 или что-то на OpenWRT). В случае каких-нибудь Mikrotik/Cisco можно использовать доступные на них протоколы вроде OpenVPN и GRE-over-IPSEC.
Идентификация и перенаправление интересующего траффика
Можно, конечно, завернуть вообще весь интернет-траффик через зарубеж. Но, скорее всего, от этого сильно пострадает скорость работы с локальным контентом. Плюс требования к полосе пропускания на VPS будут сильно выше.
Поэтому нам нужно будет каким-то образом выделять траффик к заблокированным сайтам и выборочно его направлять в туннель. Даже если туда попадёт какая-то часть «лишнего» траффика, это всё равно гораздо лучше, чем гонять всё через тоннель.
Для управления траффиком мы будем использовать протокол BGP и анонсировать маршруты до необходимых сетей с нашего VPS на клиентов. В качестве BGP-демона возьмём BIRD, как один из наиболее функциональных и удобных.
IP
С блокировками по IP всё понятно: просто анонсируем все заблокированные IP с VPS. Проблема в том, что подсетей в списке, который отдает API, около 600 тысяч, и подавляющее большинство из них — это хосты /32. Такое количество маршрутов может смутить слабые клиентские роутеры.
Поэтому было решено при обработке списка суммировать до сети /24 если в ней 2 и более хоста. Таким образом количество маршрутов сократилось до ~100 тысяч. Скрипт для этого будет дальше.
domeni
Тут сложнее и способов есть несколько. Например, можно поставить прозрачный Squid на каждом клиентском роутере и делать там перехват HTTP и подглядывание в TLS-хендшейк с целью получения запрашиваемого URL в первом случае и домена из SNI во втором.
Но из-за всяких новомодных TLS1.3+eSNI анализ HTTPS с каждым днем становится всё менее реальным. Да и инфраструктура со стороны клиента усложняется — придется использовать как минимум OpenWRT.
Поэтому я решил пойти по пути перехвата ответов на DNS-запросы. Тут тоже над головой начинает витать всякий DNS-over-TLS/HTTPS, но эту часть мы можем (пока что) контролировать на клиенте — либо отключить, либо использовать свой сервер для DoT/DoH.
Как перехватывать DNS?
Тут тоже может быть несколько подходов.
Перехват DNS-траффика через PCAP или NFLOG
Оба эти способа перехвата реализованы в утилите sidmat. Но она давно не поддерживается и функционал очень примитивен, так что к ней нужно всё равно нужно писать обвязку.
Анализ логов DNS-сервера
К сожалению, известные мне рекурсоры не умеют логгировать ответы, а только запросы. В принципе это логично, так как в отличии от запросов ответы имеют сложную структуру и писать их в текстовой форме трудновато.
DNSTap
К счастью, многие из них уже поддерживает DNSTap для этих целей.
Что такое DNSTap?
Это клиент-серверный протокол, основанный на Protocol Buffers и Frame Streams для передачи с DNS-сервера на некий коллектор структурированных DNS-запросов и ответов. По сути DNS-сервер передает метаданные запросов и ответов (тип сообщения,IP клиента/сервера и так далее) плюс полные DNS-сообщения в том (бинарном) виде в котором он работает с ними по сети.
Важно понимать, что в парадигме DNSTap DNS-сервер выступает в роли клиента, а коллектор — в роли сервера. То есть DNS-сервер подключается к коллектору, а не наоборот.
На сегодняшний день DNSTap поддерживается во всех популярных DNS-серверах. Но, например, BIND во многих дистрибутивах (вроде Ubuntu LTS) часто собран почему-то без его поддержки. Так что не будем заморачиваться пересборкой, а возьмём более легкий и быстрый рекурсор — Unbound.
Чем ловить DNSTap?
Ту је некиколичество CLI-утилит для работы с потоком DNSTap-событий, но для решения нашей задачи они подходят плохо. Поэтому я решил изобрести свой велосипед, который будет делать всё что необходимо: dnstap-bgp
Алгоритам рада:
При запуске загружает из текстового файла список доменов, инвертирует их (habr.com -> com.habr), исключает битые строки, дубликаты и поддомены (т.е. если в списке есть habr.com и www.habr.com — будет загружен только первый) и строит префиксное дерево для быстрого поиска по этому списку
Выступая в роли DNSTap-сервера ждет подключения от DNS-сервера. В принципе он поддерживает как UNIX- так и TCP-сокеты, но известные мне DNS-сервера умеют только в UNIX-сокеты
Поступающие DNSTap-пакеты десериализуются сначала в структуру Protobuf, а затем само бинарное DNS-сообщение, находящееся в одном из Protobuf-полей, парсится до уровня записей DNS RR
Проверяется есть ли запрашиваемый хост (или его родительский домен) в загруженном списке, если нет — ответ игнорируется
Из ответа выбираются только A/AAAA/CNAME RR и из них вытаскиваются соответствующие IPv4/IPv6 адреса
IP-адреса кешируются с настраиваемым TTL и анонсируются во все сконфигурированные BGP-пиры
При получении ответа, указывающего на уже закешированный IP — его TTL обновляется
После истечения TTL запись удаляется из кеша и из BGP-анонсов
Дополнительный функционал:
Перечитывание списка доменов по SIGHUP
Синхронизация кеша с другими экземплярами dnstap-bgp через HTTP/JSON
Дублирование кеша на диске (в базе BoltDB) для восстановление его содержимого после перезапуска
Поддержка переключения в иной network namespace (зачем это нужно будет описано ниже)
ИПв6 подршка
Ограничења:
IDN домены пока не поддерживаются
Мало настроек BGP
Я собрал RPM и DEB пакеты для удобной установки. Должны работать на всех относительно свежих OS с systemd, т.к. зависимостей у них никаких нет.
Схема
Итак, приступим к сборке всех компонентов воедино. В результате у нас должна получиться примерно такая сетевая топология:
Логика работы, думаю, понятна из диаграммы:
У клиента настроен наш сервер в качестве DNS, причем DNS запросы тоже должны ходить по VPN. Это нужно для того чтобы провайдер не мог использовать перехват DNS для блокировки.
Клиент при открытии сайта посылает DNS-запрос вида «а какие IP у xxx.org»
невезан резолвит xxx.org (или берет из кеша) и отправляет ответ клиенту «у xxx.org такие-то IP», параллельно дублируя его через DNSTap
dnstap-bgp анонсирует эти адреса в БИРД по BGP в том случае если домен есть в списке заблокированных
БИРД анонсирует маршрут до этих IP с next-hop self клиентскому роутеру
Последующие пакеты от клиента к этим IP идут уже через туннель
На сервере для маршрутов к заблокированным сайтам у меня внутри BIRD используется отдельная таблица и с ОС она никак не пересекается.
В этой схеме есть недостаток: первый SYN пакет от клиента, скорее всего, успеет уйти через отечественного провайдера т.к. маршрут анонсируется не мгновенно. И тут возможны варианты в зависимости от того как провайдер делает блокировку. Если он просто дропает траффик, то проблем нет. А если он редиректит его на какой-то DPI, то (теоретически) возможны спецэффекты.
Также возможны чудеса с несоблюдением клиентами DNS TTL, что может привести к тому что клиент будет юзать какие-то устаревшие записи из своего протухшего кеша вместо того чтобы спросить Unbound.
На практике у меня ни первое ни второе не вызывало проблем, but your mileage may vary.
Подешавање сервера
Для удобства раскатывания я написал роль для Ansible. Она может настраивать как сервера, так и клиенты на базе Linux (рассчитано на deb-based дистрибутивы). Все настройки достаточно очевидны и задаются в inventory.yml. Эта роль вырезана из моего большого плейбука, поэтому может содержать ошибки — повуци захтеве welcome 🙂
Пройдёмся по основным компонентам.
БГП
При запуске двух BGP-демонов на одном хосте возникает фундаментальная проблема: BIRD никак не хочет поднимать BGP-пиринг с локалхостом (или с любым локальным интерфейсом). От слова совсем. Гугление и чтение mailing-lists не помогло, там утверждают что это by design. Возможно есть какой-то способ, но я его не нашёл.
Можно попробовать другой BGP-демон, но мне нравится BIRD и он используется везде у меня, не хочется плодить сущности.
Поэтому я спрятал dnstap-bgp внутрь network namespace, которое связано с корневым через veth интерфейс: это как труба, концы которой торчат в разных namespace. На каждый из этих концов мы вешаем приватные p2p IP-адреса, которые за пределы хоста не выходят, поэтому могут быть любыми. Это тот же механизм который используется для доступа к процессам внутри любимого всеми Docker и других контейнеров.
Для этого был написан скрипта и в dnstap-bgp был добавлен уже описанный выше функционал перетаскивания себя за волосы в другой namespace. Из-за этого его необходимо запускать под root либо выдать бинарнику CAP_SYS_ADMIN через команду setcap.
Пример скрипта для создания namespace
#!/bin/bash
NS="dtap"
IP="/sbin/ip"
IPNS="$IP netns exec $NS $IP"
IF_R="veth-$NS-r"
IF_NS="veth-$NS-ns"
IP_R="192.168.149.1"
IP_NS="192.168.149.2"
/bin/systemctl stop dnstap-bgp || true
$IP netns del $NS > /dev/null 2>&1
$IP netns add $NS
$IP link add $IF_R type veth peer name $IF_NS
$IP link set $IF_NS netns $NS
$IP addr add $IP_R remote $IP_NS dev $IF_R
$IP link set $IF_R up
$IPNS addr add $IP_NS remote $IP_R dev $IF_NS
$IPNS link set $IF_NS up
/bin/systemctl start dnstap-bgp
router id 192.168.1.1;
table rkn;
# Clients
protocol bgp bgp_client1 {
table rkn;
local as 65000;
neighbor 192.168.1.2 as 65000;
direct;
bfd on;
next hop self;
graceful restart;
graceful restart time 60;
export all;
import none;
}
# DNSTap-BGP
protocol bgp bgp_dnstap {
table rkn;
local as 65000;
neighbor 192.168.149.2 as 65000;
direct;
passive on;
rr client;
import all;
export none;
}
# Static routes list
protocol static static_rkn {
table rkn;
include "rkn_routes.list";
import all;
export none;
}
rkn_routes.list
route 3.226.79.85/32 via "ens3";
route 18.236.189.0/24 via "ens3";
route 3.224.21.0/24 via "ens3";
...
ДНС
По умолчанию в Ubuntu бинарник Unbound зажат AppArmor-профилем, который запрещает ему коннектиться ко всяким там DNSTap-сокетам. Можно либо удалить нафиг этот профиль, либо отключить его:
Это, наверное, надо добавить в плейбук. Идеально, конечно, поправить профиль и выдать нужные права, но мне было лень.
unbound.conf
server:
chroot: ""
port: 53
interface: 0.0.0.0
root-hints: "/var/lib/unbound/named.root"
auto-trust-anchor-file: "/var/lib/unbound/root.key"
access-control: 192.168.0.0/16 allow
remote-control:
control-enable: yes
control-use-cert: no
dnstap:
dnstap-enable: yes
dnstap-socket-path: "/tmp/dnstap.sock"
dnstap-send-identity: no
dnstap-send-version: no
dnstap-log-client-response-messages: yes
Скачивание и обработка списков
Скрипт для скачивания и обработки списка IP-адресов
Он скачивает список, суммаризует до префикса pfx. У dont_add и dont_summarize можно сказать IP и сети, которые нужно пропустить или не суммаризовать. Мне это было нужно т.к. подсеть моего VPS оказалась в блоклисте 🙂
Самое смешное что API РосКомСвободы блокирует запросы с дефолтным юзер-агентом Питона. Видать скрипт-кидди достали. Поэтому меняем его на Огнелиса.
Пока что он работает только с IPv4 т.к. доля IPv6 невелика, но это будет легко исправить. Разве что придется использовать еще и bird6.
rkn.py
#!/usr/bin/python3
import json, urllib.request, ipaddress as ipa
url = 'https://api.reserve-rbl.ru/api/v2/ips/json'
pfx = '24'
dont_summarize = {
# ipa.IPv4Network('1.1.1.0/24'),
}
dont_add = {
# ipa.IPv4Address('1.1.1.1'),
}
req = urllib.request.Request(
url,
data=None,
headers={
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36'
}
)
f = urllib.request.urlopen(req)
ips = json.loads(f.read().decode('utf-8'))
prefix32 = ipa.IPv4Address('255.255.255.255')
r = {}
for i in ips:
ip = ipa.ip_network(i)
if not isinstance(ip, ipa.IPv4Network):
continue
addr = ip.network_address
if addr in dont_add:
continue
m = ip.netmask
if m != prefix32:
r[m] = [addr, 1]
continue
sn = ipa.IPv4Network(str(addr) + '/' + pfx, strict=False)
if sn in dont_summarize:
tgt = addr
else:
tgt = sn
if not sn in r:
r[tgt] = [addr, 1]
else:
r[tgt][1] += 1
o = []
for n, v in r.items():
if v[1] == 1:
o.append(str(v[0]) + '/32')
else:
o.append(n)
for k in o:
print(k)
Скрипт для обновления
Он у меня запускается по крону раз в сутки, может стоит дергать раз в 4 часа т.к. это, по-моему, период обновления который РКН требует от провайдеров. Плюс, есть какие-то еще суперсрочные блокировки у них, которые может и быстрее прилетают.
Делает следующее:
Запускает первый скрипт и обновляет список маршрутов (rkn_routes.list) для BIRD
Релоадит BIRD
Обновляет и подчищает список доменов для dnstap-bgp
Релоадит dnstap-bgp
rkn_update.sh
#!/bin/bash
ROUTES="/etc/bird/rkn_routes.list"
DOMAINS="/var/cache/rkn_domains.txt"
# Get & summarize routes
/opt/rkn.py | sed 's/(.*)/route 1 via "ens3";/' > $ROUTES.new
if [ $? -ne 0 ]; then
rm -f $ROUTES.new
echo "Unable to download RKN routes"
exit 1
fi
if [ -e $ROUTES ]; then
mv $ROUTES $ROUTES.old
fi
mv $ROUTES.new $ROUTES
/bin/systemctl try-reload-or-restart bird
# Get domains
curl -s https://api.reserve-rbl.ru/api/v2/domains/json -o - | jq -r '.[]' | sed 's/^*.//' | sort | uniq > $DOMAINS.new
if [ $? -ne 0 ]; then
rm -f $DOMAINS.new
echo "Unable to download RKN domains"
exit 1
fi
if [ -e $DOMAINS ]; then
mv $DOMAINS $DOMAINS.old
fi
mv $DOMAINS.new $DOMAINS
/bin/systemctl try-reload-or-restart dnstap-bgp
Они были написаны не особо задумываясь, поэтому если видите что можно улучшить — дерзайте.
Подешавање клијента
Тут я приведу примеры для Linux-роутеров, но в случае Mikrotik/Cisco это должно быть еще проще.
Для начала настраиваем BIRD:
bird.conf
router id 192.168.1.2;
table rkn;
protocol device {
scan time 10;
};
# Servers
protocol bgp bgp_server1 {
table rkn;
local as 65000;
neighbor 192.168.1.1 as 65000;
direct;
bfd on;
next hop self;
graceful restart;
graceful restart time 60;
rr client;
export none;
import all;
}
protocol kernel {
table rkn;
kernel table 222;
scan time 10;
export all;
import none;
}
Таким образом мы будем синхронизировать маршруты, полученные из BGP, с таблицей маршрутизации ядра за номером 222.
После этого достаточно попросить ядро глядеть в эту табличку перед тем как заглядывать в дефолтную:
# ip rule add from all pref 256 lookup 222
# ip rule
0: from all lookup local
256: from all lookup 222
32766: from all lookup main
32767: from all lookup default
Всё, осталось настроить DHCP на роутере на раздачу туннельного IP-адреса сервера в качестве DNS и схема готова.
Ограничења
При текущем алгоритме формирования и обработки списка доменов в него попадает, в том числе, youtube.com и его CDNы.
А это приводит к тому что все видео будут ехать через VPN, что может забить весь канал. Возможно стоит составить некий список популярных доменов-исключений, которые блокировать у РКН пока что кишка тонка. И пропускать их при парсинге.
Закључак
Описанный способ позволяет обходить практические любые блокировки, которые реализуют провайдеры на данный момент.
У основи, dnstap-bgp можно использовать для любых других целей где необходим некий уровень управления траффиком на основе доменного имени. Только нужно учитывать что в наше время на одном и том же IP-адресе может висеть тысяча сайтов (за каким-нибудь Cloudflare, например), так что этот способ имеет довольно низкую точность.
Но для нужд обхода блокировок этого вполне достаточно.