Не знаю, заметил ли кто, но в России образца 2025 года довольно непросто живётся без PBR (Policy-Based Routing). То Goland нужно как-то обновить, то у ChatGPT очередные галлюцинации почитать, а тыкать туда-сюда VPN на каждом из домашних устройств — занятие не из интересных. И если в простых случаях (например, сроутить весь трафик от телевизора, кроме сетей Яндекса, куда надо) всё понятно, то когда речь заходит о точечных ресурсах, всё начинает быть гораздо веселее за счёт прогрессирующей глобализации и массового использования CDN.
Конечно, человечество десятки лет назад придумало связку dns + ipset, но по моим ощущениям чем дальше, тем хуже это работает. IP у одних и тех же ресурсов всё чаще пересекаются (привет CDN), отладка усложняется, да и вообще как-то слишком уж скучно звучит. А наши руки не для скуки — наши руки для изобретения очередной хераборы!
Планчик
Итак, у нас имеется горстка доменов, которые надо как-то сроутить, и сильное желание не использовать ipset. Логично было бы сделать что-то с DNS, отдавая фейковые адреса для нужных доменов, которые потом как-то сроутить и красиво оттранслировать в настоящие адреса. Вам это тоже звучит знакомо? Вот и мне звучит ровно как NAT64:
На схемке два важных компонента:
- DNS64 — специальный DNS-сервер, который умеет синтезировать IPv6-адреса с заданным префиксом при запросе
AAAA
записи у домена, который ею не обладает. Так, дляgithub.com
он вернёт64:ff9b::8c52:7904
, имея на руках140.82.121.4
- NAT64 — технология трансляции пакета с IPv6-адресом в пакет с IPv4. Стандартно живёт за префиксом
64:ff9b::/96
, но в домашних условиях можете делать как чувствуете — я разрешил
Для IPv6 есть ещё горстка занятных технологий в духе 464XLAT (CLAT + PLAT), но нам это пока не особо интересно. Не думаю, что кто-то всерьёз решился переехать на IPv6-only сеть дома, так что продолжают жить с dual-stack.
А вот что нам действительно интересно, так это то, что DNS64+NAT64 идеально вписывается в концепцию хераборы. Смотрите сами:
- пишем DNS-сервер, который:
- прячет от клиента
A
-записи для нужных нам доменов - для них же синтезирует адреса, смотрящие в NAT64, при запросе
AAAA
записи
- прячет от клиента
- настраиваем NAT64, который оттранслирует выданные нами IPv6-адреса в IPv4 с нужным нам адресом источника
- дальше можем роутить эти пакетики как душе угодно
- звучит обнадёживающе
Но прежде чем приступать к реализации, важно сделать оговорку, что без глобальной IPv6-адресации тут делать нечего. Так уж вышло, что IPv6-инфраструктура, будучи сломанной чуть более чем полностью на старте, всячески пессимизируется некоторыми клиентами до тех пор, пока они не убедятся, что IPv6 подаёт признаки жизни. Например, macOS обычно даже не будет пытаться резолвить AAAA
записи, пока не поймёт (сама или после хорошего пендаля), что это вообще стоит делать. Иными словами, вы можете реализовать всю схему, используя только ULA (Unique Local Address), но без GUA (Global Unicast Address) со своим префиксом будете страдать с любым устройством не на линуксе.
Подготовка
Как я и говорил ранее, без честного IPv6 DNS64 заведётся, но с клиентами придётся пострадать. А страдать мы, как известно, не любим. Поэтому, прежде чем начинать, перевожу свой Iskratel Innbox G84 от МГТС в режим моста (см. подсказки на 4PDA) и получаю заветный IPv6-адрес + IPv6 Prefix Delegation:
Не скажу, что сильно рад оставлять Innbox G84 воткнутым в сеть вообще, но замена ещё в пути. А пока в роли роутера у меня выступает NanoPi R5S с OpenWrt 24.10 на борту. Поэтому все действия ниже буду показывать именно на нём.
NAT64
Пару вводных:
- дома у меня используется ULA-префикс
fd42:dead:cafe::/48
- NAT64 будет использовать префикс
fd42:dead:cafe:64:1::/96
, потому что мне так больше нравится - трафик я буду роутить в wireguard-туннель
ext
с локальным IP10.8.3.6/32
Погнали! Добавляю таблицу маршрутизации ext
в файл /etc/iproute2/rt_tables
:
300 ext
Настраиваю правило маршрутизации для неё в файле /etc/config/network
:
config rule
option src '10.8.3.6/32'
option lookup 'ext'
config route
option interface 'ext'
option target '0.0.0.0'
option netmask '0.0.0.0'
option table 'ext'
Ставлю jool для NAT64-трансляции:
# opkg install jool-tools-netfilter kmod-jool-netfilter
И правлю pool6/pool4 на нужные мне значения в конфиге /etc/jool/jool-nat64.conf.json
:
{
"comment": {
"description": "Sample full NAT64 configuration for EXT iface.",
"last update": "2025-06-14"
},
"instance": "ext-nat64",
"framework": "netfilter",
"global": {
"manually-enabled": true,
"pool6": "fd42:dead:cafe:64:1::/96",
"lowest-ipv6-mtu": 1280,
"logging-debug": false,
"zeroize-traffic-class": false,
"override-tos": false,
"tos": 0,
"mtu-plateaus": [
65535,
32000,
17914,
8166,
4352,
2002,
1492,
1006,
508,
296,
68
],
"address-dependent-filtering": false,
"drop-externally-initiated-tcp": false,
"drop-icmpv6-info": false,
"source-icmpv6-errors-better": true,
"f-args": 11,
"handle-rst-during-fin-rcv": false,
"tcp-est-timeout": "2:00:00",
"tcp-trans-timeout": "0:04:00",
"udp-timeout": "0:05:00",
"icmp-timeout": "0:01:00",
"logging-bib": false,
"logging-session": false,
"maximum-simultaneous-opens": 10,
"ss-enabled": false,
"ss-flush-asap": true,
"ss-flush-deadline": 2000,
"ss-capacity": 512,
"ss-max-payload": 1452
},
"pool4": [
{
"protocol": "TCP",
"prefix": "10.8.3.6/32"
},
{
"protocol": "UDP",
"prefix": "10.8.3.6/32"
},
{
"protocol": "ICMP",
"prefix": "10.8.3.6/32"
}
]
}
Включаю использование NAT64 в Jool и рестартую сервис:
# uci set jool.general.enabled="1"
# uci set jool.nat64.enabled="1"
# uci commit jool
# service jool restart
Проверяю:
% ping6 fd42:dead:cafe:64:1::1.1.1.1
PING6(56=40+8+8 bytes) fd42:dead:cafe:0:104f:8f49:d9a4:295c --> fd42:dead:cafe:64:1:0:101:101
16 bytes from fd42:dead:cafe:64:1:0:101:101, icmp_seq=0 hlim=56 time=48.700 ms
16 bytes from fd42:dead:cafe:64:1:0:101:101, icmp_seq=1 hlim=56 time=58.458 ms
Работает! Осталось убедиться, что при проходе через NAT64 и напрямую получаем разный внешний IP:
% host wtfismyip.com
wtfismyip.com has address 65.108.75.112
wtfismyip.com has IPv6 address 2a01:4f9:6b:4b55::acab:f001
wtfismyip.com mail is handled by 10 wtfismyip-com.mail.protection.outlook.com.
% curl -g 'http://65.108.75.112/text'
109.252.59.219
% curl -g 'http://[fd42:dead:cafe:64:1::65.108.75.112]/text'
193.160.101.66
Всё как и планировал — напрямую выходим из Москвы, через NAT64 — из Финки.
DNS64
NAT64 работает — теперь нужен DNS-сервер, который синтезирует правильные IPv6-адреса. Такой DNS несложно написать самому, но для MVP я решил воспользоваться CoreDNS, который из коробки поддерживает dns64 плагин. Остается только добавить недостающий сторонний плагин finalize и кодогенерацию. Так и появился sly64, который в текущей итерации не делает ничего замысловатого:
Принимает на вход произвольный YAML с данными, например:
ext: - "chatgpt.com" - "wtfismyip.com"
К нему добавляется шаблон с желаемым конфигом:
.:53 { errors forward . 1.1.1.1 1.0.0.1 } {{range $domain := .ext}} {{$domain}}:53 { template IN A { rcode NOERROR } forward . 127.0.0.1:5554 } {{end}} .:5554 { bind 127.0.0.1 errors # resolves CNAMEs to their IP address finalize # Apply DNS64 synthesis to queries routed here dns64 { translate_all allow_ipv4 prefix fd42:dead:cafe:64:1::/96 } # Forward the original domain names forward . 1.1.1.1 1.0.0.1 }
Применяет шаблон к данным и получает финальный конфиг:
.:53 { errors forward . 1.1.1.1 1.0.0.1 } chatgpt.com:53 { template IN A { rcode NOERROR } forward . 127.0.0.1:5554 } wtfismyip.com:53 { template IN A { rcode NOERROR } forward . 127.0.0.1:5554 } .:5554 { bind 127.0.0.1 errors # resolves CNAMEs to their IP address finalize # Apply DNS64 synthesis to queries routed here dns64 { translate_all allow_ipv4 prefix fd42:dead:cafe:64:1::/96 } # Forward the original domain names forward . 1.1.1.1 1.0.0.1 }
Осталось собрать Docker-образ: https://github.com/buglloc/sly64/pkgs/container/sly64
Закинуть его на роутер:
Убрать штатный dnsmasq с 53-го порта, дабы не мешал — и вуаля:
% host github.com 10.0.0.1
Using domain server:
Name: 10.0.0.1
Address: 10.0.0.1#53
Aliases:
github.com has address 140.82.121.3
github.com mail is handled by 1 aspmx.l.google.com.
github.com mail is handled by 10 alt3.aspmx.l.google.com.
github.com mail is handled by 10 alt4.aspmx.l.google.com.
github.com mail is handled by 5 alt1.aspmx.l.google.com.
github.com mail is handled by 5 alt2.aspmx.l.google.com.
% host wtfismyip.com 10.0.0.1
Using domain server:
Name: 10.0.0.1
Address: 10.0.0.1#53
Aliases:
wtfismyip.com has IPv6 address fd42:dead:cafe:64:1:0:416c:4b70
wtfismyip.com mail is handled by 10 wtfismyip-com.mail.protection.outlook.com.
% curl wtfismyip.com
193.160.101.66
Дальше можно крутить настойки CoreDNS по вкусу — будь то кеширование или блокировка рекламы. А так хорошо бы ещё научить его ходить в разные апстримы (или через разные интерфейсы), чтобы не возиться с любителями route53 и прочими почитателями гео штук. К счастью, нынешняя конструкция никак не ограничивает полет фантазии :)
Заключение
Пока не знаю, насколько устойчивой получилась эта конструкция (сильно переживаю за МГТС), тут только время покажет. Но вот прямо сейчас она мне нравится — простая и забавная. Я такое люблю :) Единственный нюанс: в текущей конфигурации jool
может работать только с одним NAT64-инстансом. Поэтому, если понадобится больше одного маршрута, придётся играться с net ns (см. Multiple NAT64s in a Single Machine). Ну да это не беда, если понадобится — поиграю.
Ну а если всё пойдёт хорошо, то стоит написать отдельный DNS-сервер или плагин для CoreDNS, чтобы не возиться с кодгеном и упростить пайплайн обработки запросов. Так что, если всё сложится, ещё услышимся ^^
А пока у меня всё. Всем кота! (づ˶•༝•˶)づ♡