Маршрутизация по DNS на OpenWrt

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Написать данную статью меня побудили следующие обстоятельства:

  • Обновление ОС на своём роутере до OpenWrt 23.05, сломавшее мой предыдущий setup, где я делал роутинг по GeoIP.

  • Многочисленные вопросы знакомых и в дискуссиях в постах на Хабре

  • Статья на Хабре, по которой я стал делать и понял, что так делать не надо.

Постановка задачи

И так, у вас есть настроенный на роутере VPN. Это может быть VPN во внешний мир из РФ, а может быть и VPN из внешнего мира в РФ для использования российских сервисов, отгородившихся от внешнего мира (например, ГИС ЖКХ). И вот у вас теперь задача: на одни сайты заходить через VPN, а на другие через маршрутизацию на шлюз вашего провайдера, так как в эпоху сегментации Интернета парадигма "всё в VPN" не работает. При этом вы можете захотеть не только делить трафик на российский и зарубежный, но и добавлять исключения в эти правила. Например, сайт drive2.ru требует привязывать номер телефона для российских пользователей, пользующихся перепиской в личных сообщениях, что он определяет по IP: добавляя такие исключения, можно избавиться от лишнего сбора данных о себе.

Какие могут быть пути решения? В своё время автор настроил маршрутизацию по GeoIP, однако это решение имело массу недостатков: сложность обновления баз, сложность управления исключениями из правил и в целом достаточно сложный setup, который нельзя было целиком провернуть из графического интерфейса OpenWrt (luci). Можно делать делать, наверное, ещё как-то, но не цель статьи сделать подробный обзор таких методов.

Сначала в этой статье мы рассмотрим то, как это настроить, во второй половине разберёмся с тем, как это работает под капотом.

Требования

Итак, речь в данной статье применима для маршрутизаторов, на которых стоит OpenWrt 23.05. Это довольно сильное требование, так механизмы, через которое всё это осуществляется, сильно эволюционировали в предыдущих два релиза (21-я версия была ещё на iptables, 22-я получилась каким-то переходным этапом между iptables и nftables, а 23-я наконец мигрировала на nftables полностью). Поэтому описанное совершенно точно не будет работать на более старых версиях.

От читателя также требуются базовые умения работать с OpenWrt (установка пакетов, настройка ssh-доступа и т.п.). Кроме того требуется, чтобы читатель уже разобрался с настройкой VPN на своём роутере и настроил его. Выбору и настройке VPN на маршрутизаторе вообще и на OpenWrt в частности посвящено множество статей, в том числе и на Хабре.

Возможно, что в каком-то виде этот гайд подойдёт для юзеров Keenetic'ов, потому что KeeneticOS это в сущности OpenWrt, но так как у меня нет под рукой гаджета, то не могу проверить.

Настройка

Для начала нам потребуется установить пакеты dnsmasq-full и luci-app-pbr. Установка dnsmasq-full сопряжено с тем, что перед этим требуется удалить dnsmasq (иначе будет конфликт). Если удалять dnsmasq через web-интерфейс, то есть риск оставить роутер без работающего DNS, что сделает невозможным установку dnsmasq-full. Поэтому такую замену пакета рекомендуется делать через ssh в соответствии с данным рецептом. У автора прокатило сделать всё через web-интерфейс, но так делать можно только на свой страх и риск. После установки dnsmasq-full и luci-app-pbr роутер стоит перезагрузить, и зайти по новой в web-интерфейс. Появится новое меню Services, а если оно у вас уже было, то появится новый пункт меню Policy Routing:

Собственно, выбираем Policy Routing и попадаем в раздел настройки маршрутизации по политикам. Первое, что необходимо сделать - это на вкладке Basic Configuration настроить опцию "Use resolver set support for domains" в значение "Dnsmasq nft set":

После этого настраиваем собственно сами политики, прописывая в remote addresses непосредственно доменные имена или их суффиксы:

В политиках нельзя указывать регулярные выражения или wildcard'ы. Но соответствие находится по суффиксу. То есть первая политика на домен "ru" будет подходить для всех доменов, оканчивающихся на ".ru" (точку в правила не нужно включать!!!). При этом будет применяться там политика, у которой самое длинное совпадение по доменному имени. Поэтому в указанном примере, домены "*.drive2.ru" будут переопределять первое правило для всех доменов "ru". Обратите внимание на интерфейсы - автор настраивает роутинг для маршрутизатора, находящегося за границей РФ. Поэтому российский трафик заворачивается в VPN (интерфейс wg0 - это WireGuard туннель в РФ), а исключения идут через wan-интерфейс.

ВАЖНО: если ваш туннель настроен так, что адрес VPN-сервера указан в виде доменного имени, а не IP-адреса, и это имя попадает в правило, заворачивающее трафик в туннель, то нужно добавить доменное имя вашего VPN-сервера в исключения. Иначе есть риск, что ядро будет пытаться завернуть трафик туннеля в туннель и всё умрёт.

После настройки всего этого нужно нажать кнопочку Save & Apply, а после этого собственно включить "политическую" маршрутизацию в верху страницы (на скриншоте она уже уже включена):

Вот собственно и вся настройка! Открыв ya.ru и введя запрос "мой ip", я могу убедиться, что Яндекс считает, что я зашел через РФ. Как видно, кроме трюка с установкой пакета dnsmasq-full всё остальное делается исключительно через web-интерфейс и доступно практически домохозяйке.

Гораздо интереснее пытливому читателю будет узнать то, как это работает под капотом и что стоит за этой кажущейся простотой, на самостоятельную настройку которой можно было бы угробить несколько часов, а то и дней (если изучать вопрос с нуля). Этому будет посвящен остаток статьи.

Заглядываем под капот

Как это работает под капотом? Сначала поймём проблематику. Маршрутизация выполняется на роутере ядром ОС (коим является Linux) по правилам, которые содержат только IP-адреса и адреса подсетей. Резолвинг же DNS-имён в IP-адреса происхоит в пространстве пользователя я является довольно сложным, многостадийным процессом, переносить который в ядро не стоит по целому ряду причин. Кроме того, мы не только хотим маршрутизировать по доменному имени, но и создавать агрегирующие правила, которые включают в себя, например, всю зону RU, то есть нужен ещё механизм матчинга - определения того, попадает ли домен в то или иное агрегирующее правило. Таким образом, нужен некоторый клей, который будет склеивать матчинг и резолвинг DNS-имён с маршрутизацией по IP-адресам. И вот тут наступает проблема как это сделать.

Я не буду делать в данной статье обзор различных методов решения этой проблемы, а опишу именно то, как это реализовано в OpenWrt 23.05. И так, начнём с основного. OpenWrt 23.05 перешел полностью на использование nftables вместо старого и привычного механизма iptables, который используется для различных манипуляций с трафиком (NAT, Firewall, policy based routing и так далее).

Nftables позволяет завести множества (set), в которых хранятся различные однотипные данные, использующиеся в работе правил (rules). Для осуществления нашей задачи нам необходимо создать одно или несколько множеств IP-адресов, куда будут попадать IP-адреса прорезолвленных доменов, которые мы хотим маршрутизировать особенным образом. Кто будет наполнять эти множества и как? Очень просто: собственно dnsmasq (в версии, которая собрана со всеми включенными опциями dnsmasq-full) умеет принимать в качестве конфигурации правила матчинга доменов и имена nft set'ов, в которые нужно добавлять IP-адреса прорезолвленных доменов, которые попадают в те или иные правила.

# ps | grep dnsmasq
11519 root      2912 S    {dnsmasq} /sbin/ujail -t 5 -n dnsmasq -u -l -r /bin/ubus -r /etc/TZ -r /etc/dnsmasq.conf -r /etc/ethers -r /etc/group -r /etc/hosts -r /etc/passwd -w /tmp/dhcp.leases -r /tmp/dnsmasq.d -r /tmp/hosts -r /tmp/r
11521 dnsmasq   5128 S    /usr/sbin/dnsmasq -C /var/etc/dnsmasq.conf.cfg01411c -k -x /var/run/dnsmasq/dnsmasq.cfg01411c.pid
18988 root      1376 S    grep dnsmasq

Видно, что процесс dnsmasq запущен с указанием того, что конфигурацию нужно взять в файле /var/etc/dnsmasq.conf.cfg01411c. Заглянем в него:

# auto-generated config file from /etc/config/dhcp
conf-file=/etc/dnsmasq.conf
dhcp-authoritative
domain-needed
localise-queries
read-ethers
enable-ubus=dnsmasq
expand-hosts
bind-dynamic
local-service
edns-packet-max=1232
domain=home.arpa
local=/home.arpa/
except-interface=pppoe-wan
except-interface=wg0
addn-hosts=/tmp/hosts
dhcp-leasefile=/tmp/dhcp.leases
resolv-file=/tmp/resolv.conf.d/resolv.conf.auto
stop-dns-rebind
rebind-localhost-ok
dhcp-broadcast=tag:needs-broadcast
conf-dir=/tmp/dnsmasq.d
user=dnsmasq
group=dnsmasq


dhcp-ignore-names=tag:dhcp_bogus_hostname
conf-file=/usr/share/dnsmasq/dhcpbogushostname.conf

bogus-priv
conf-file=/usr/share/dnsmasq/rfc6761.conf
dhcp-range=set:lan,192.168.1.100,192.168.1.249,255.255.255.0,12h
no-dhcp-interface=pppoe-wan

Вроде бы нет никакой информации о том, что мы рассматриваем в данной статье, но в 22-й строчке видим conf-dir=/tmp/dnsmasq.d - то есть директиву включить в конфиг все файлы найденные в директории /tmp/dnsmasq.d. А что у нас в той директории? А там только один файл с именем pbr - собственно этот файл и создаётся пакетом pbr (policy based routing), который устанавливается по зависимостям при установке пакета luci-app-pbr. Заглянем в него:

nftset=/ru/4#inet#fw4#pbr_wg0_4_dst_ip_cfg046ff5 # russia
nftset=/drive2.ru/4#inet#fw4#pbr_wan_4_dst_ip_cfg056ff5 # russia_except

Как видно тут содержаться все наши политики, настроенные в статье в количестве двух. Из этого следует, что адрес доменов, попадающие в зону ".ru" после резолвинга добавляются в множество pbr_wg0_4_dst_ip_cfg046ff5, а наш домен-исключение drive2.ru будет добавлен в множество pbr_wan_4_dst_ip_cfg056ff5. Проверим, что там есть. Зайдем по ssh на роутер и дадим команду nft list sets.

table inet fw4 {
        set pbr_wg0_4_dst_ip_cfg046ff5 {
                type ipv4_addr
                flags interval
                counter
                auto-merge
                comment "russia"
                elements = { 5.61.23.66 counter packets 0 bytes 0, 5.101.37.37 counter packets 150 bytes 12811,
                             223.121.15.28 counter packets 0 bytes 0 }
        }
        set pbr_wan_4_dst_ip_cfg056ff5 {
                type ipv4_addr
                flags interval
                counter
                auto-merge
                comment "russia_except"
                elements = { 91.215.43.178 counter packets 52 bytes 3120 }
        }
}

Отлично, резолвер уже что-то напихал в этим множества. Но что теперь делать с этой красотой? Нужные какие-то правила, которые будут этим множества использовать. Давайте посмотрим, дав команду nft list ruleset. Я отрезал нерелевантные части правил и получил:

table inet fw4 {
        chain mangle_prerouting {
                type filter hook prerouting priority mangle; policy accept;
                jump pbr_prerouting comment "Jump into pbr prerouting chain"
        }

        chain pbr_prerouting {
                ip daddr @pbr_wg0_4_dst_ip_cfg046ff5 goto pbr_mark_0x020000 comment "russia"
                ip daddr @pbr_wan_4_dst_ip_cfg056ff5 goto pbr_mark_0x010000 comment "russia_except"
        }

        chain pbr_mark_0x010000 {
                counter packets 52 bytes 3120 meta mark set meta mark & 0xff01ffff | 0x00010000
                return
        }

        chain pbr_mark_0x020000 {
                counter packets 99038 bytes 18734081 meta mark set meta mark & 0xff02ffff | 0x00020000
                return
        }
}

В соответствии с цепочкой правил pbr_prerouting ядро заглядывает в наши множества и, если ip-адрес попадает в одно из них, происходит переход к цепочке правил pbr_mark_0x010000 или pbr_mark_0x020000, а в этих цепочках правил пакет маркируется меткой 0x00010000 или 0x00020000 соответственно. Метка - это число, которое присваивается пакету и сопровождает его внутри ядра Linux до тех пор, пока он не отправится в сеть. На основе этой метки ядро может принимать дальнейшие решения о том, что с этим пакетом делать. Одно решений, которое может принять ядро - это таблица маршрутизации, по которой данный пакет надо маршрутизировать. Мы можем посмотреть набор таких правил, по которым принимаются решения командой ip rule:

0:      from all lookup local
30000:  from all fwmark 0x10000/0xff0000 lookup pbr_wan
30001:  from all fwmark 0x20000/0xff0000 lookup pbr_wg0
32766:  from all lookup main
32767:  from all lookup default

А вот и наши метки на строках 2 и 3. Если у пакета есть метка 0x10000 с маской 0xff0000, то для его маршрутизации используется таблица маршрутизации pbr_wan, а если 0x20000 с маской 0xff0000, то таблица маршрутизации pbr_wg0. В остальных случаях используется таблица маршрутизации по умолчанию. Тут нужно дать, наверное, пояснение, ибо многие не в курсе того, что кроме дефолтной таблицы маршрутизации могут существовать альтернативные. Они используются в правилах политики есть директива lookup с указанием альтернативной таблицы маршрутизации. Таким образом, у нас может быть несколько default route (0.0.0.0/0) - один в основной таблице и в альтернативных таблица. Давайте же посмотрим, на содержание этих таблиц pbr_wan и pbr_wg0, командой ip route list table <имя таблицы>:

# ip route list table pbr_wan
default via 172.16.0.1 dev pppoe-wan 
192.168.1.0/24 dev br-lan proto kernel scope link src 192.168.1.1 
# ip route list table pbr_wg0
default via 192.168.7.2 dev wg0 
192.168.1.0/24 dev br-lan proto kernel scope link src 192.168.1.1 

Тут всё просто. В одном случае весь трафик улетает через шлюз провайдера (для доменов-исключений), во втором - через VPN туннель. В обоих случаях исключение добавляется для локальной сети, которая не маршрутизируется никуда.

Ну вот собственно и всё. Подытожим то, как осуществляется эта кухня:

  1. Резолвер (dnsmasq) по сконфигурированным правилам добавляет прорезолвленные IP-адреса в множества nftables.

  2. Правила nftables проверяют попадает ли адрес назначения того или иного IP-пакета в одно из множеств и, если попадает, присваивает пакету определенную метку.

  3. Политика маршрутизации трафика в зависимости от наличия у пакета той или иной метки выбирает таблицу маршрутизации, которая будет использоваться для маршрутизации пакета.

Возможные проблемы, с которыми вы столкнетесь

Тут нужно также рассказать о том, какие косяки могут вас поджидать.

Косяк номер один. Теоретически можно себе представить, что на одном и том же IP-адресе будут жить два web-сервера. На одном из них будет сайт, который требует, чтобы вы входили на него через VPN, а на другом будет жить сайт, который требует, чтобы вы входили без VPN. Это невозможно разрулить данной схемой, так как маршрутизация выполняется только по IP-адреса сайта.

Косяк номер два. Несмотря на то, что nftables поддерживают множества, в которых элементы добавляются с определенным временем жизни, которое можно сбрасывать, если какое-то правило нашло этот элемент в множестве, Policy routing в OpenWrt этого не использует. Попав в множество единожды, адрес остается там до перезагрузки. Это может привести, во-первых, к раздуванию размера таблицы со временем. Во-вторых, может случится так, что сайт переедет на другой IP-адрес, а старый IP-адрес останется в множестве. В принципе это не очень страшно, кроме того случая, если на этом IP-адресе не появится другой сайт, на который вам нужно ходить без VPN'а. Это, конечно, крайне маловероятно, но нужно это иметь ввиду.

Косяк номер три. Как правило, любой сайт ссылается на другие сайты (скрипты, изображения, статический контент и прочее). Может получиться так, что вы завернете весь трафик на этот сайт, например, в VPN, а внутри HTML-страницы будут ходить на другие домены, которые не будут заворачиваться в VPN. Это может приводить к спецэффектам, которые потребуется расследовать. Тут на помощь может прийти какой-нибудь режим разработчика в браузере. Хуже, если этим будем заниматься какой-нибудь апп на смартфоне. Тогда придётся включать логирование DNS-запросов и вылавливать проблему.

Источник: https://habr.com/ru/articles/785032/


Интересные статьи

Интересные статьи

В данной статье рассмотрим как:Настроить обход блокировок с помощью Trojan TCP, tun2socks и bird2 (BGP) на устройствах с 8 Мб ПЗУ и 64 ОЗУ (8/64)Настроить существующее подключение к OpenVPN серверу, к...
HomeProxy или luci-app-homeproxy это часть проекта ImmortalWRT, графическая надстройка для sing-box позволяющая настроить подключение к shadowsocks, xray, vless, vmess, trojan. В данной статье будет р...
Приветствую, дорогой читатель!Меня зовут Евгений, я занимаюсь развитием сетевой инфраструктуры в Домклик. Сегодняшняя статья будет охватывать только применение динамической маршрутизации на основе FRR...
«Open Source» и «децентрализация» – это два основных шильдика, обеспечивающих хорошее первое впечатление о проекте. Нередко популярные проекты лукавят на эти темы, либо в...
Сегодня мы публикуем вторую часть перевода материала, который посвящён разработке крупномасштабных React-приложений. Здесь речь пойдёт об управлении состоянием приложений, о маршрутизации и о раз...