Домашняя mesh-сеть на AmneziaWG
Мои роутеры стоят в разных местах: несколько хабов на арендованных серверах в разных странах и несколько площадок — дом, стойка хоумлаба и ещё пара мест. Все на OpenWrt, а связаны между собой AmneziaWG — форком WireGuard, пакеты которого не похожи на WireGuard. Эта заметка — про маршрутизацию поверх, а не про сами туннели.
Устройство
- Один туннель на линк. Каждый линк — отдельный интерфейс точка-точка ровно с одним пиром: хабы связаны в mesh-сеть, каждая площадка — с каждым хабом. Интерфейсов больше, зато каждый простой, со своими счётчиками, и перезапускается отдельно.
- IPv6 внутри туннелей. На каждом линке
/127и link-local адрес. У каждого роутера есть loopback-адрес, по которому до него достаёт остальная сеть. Loopback-адреса живут на dummy-интерфейсах Linux, которые штатный OpenWrt не умеет объявлять в/etc/config/network, поэтому я собрал для них небольшой протокол netifd — netifd-proto-dummy, и страницу для LuCI — luci-proto-dummy. - Всё в Ansible. Интерфейсы, ключи, зоны файрвола и маршрутизация
генерируются из инвентаря с помощью
community.openwrt.
Сами туннели — ключи, общие PSK, пиры, удаление лишнего — ведут модули
wg_interfaceиwg_peer, которые я написал как раз для этой сети (#281), а всё остальное идёт пакетными операциямиuci— см. заметку про ucode.
Маршрутизация
На каждом роутере один демон BIRD:
- OSPFv3 — underlay: находит пути между loopback-адресами через туннели, держит равноценные пути и даёт каждому направлению линка свою стоимость.
- BGP — overlay: переносит сами префиксы — сети площадок, адреса сервисов — между loopback-адресами, а next hop разрешается через OSPF.
- Без BFD. Для жёстких отказов хватает подкрученного dead interval OSPF в несколько секунд.
Две детали делают всё это удобным:
- Липкий обратный путь. Когда хабов несколько, ответ может попытаться уйти не через тот туннель, через который пришёл запрос, а stateful-файрволы такого не любят. vpn-sticky помечает каждое соединение входящим туннелем (conntrack mark в nftables), а ответы уходят обратно через таблицу маршрутизации этого туннеля. Правила nftables генерируются из UCI шаблоном на ucode.
- Anycast-сервисы. Хабы анонсируют общие адреса сервисов (например, DNS) в BGP, только пока проходит локальная проверка здоровья, так что клиенты всегда попадают на работающий.
Особенность netifd
Протоколу маршрутизации на link-local адресах нужен маршрут fe80::/64 на
каждом туннеле. netifd в OpenWrt добавляет его, только если у интерфейса
ненулевая метрика, а Linux держит один fe80::/64 на одно значение метрики —
поэтому туннели с одинаковой метрикой молча теряют link-local маршруты, кроме
последнего поднятого. Лекарство скучное: каждому туннелю своя метрика.
Что не взлетело
Отчасти хотелось автоматически замечать задушенные трансграничные линки и обходить их. Я попробовал два протокола, учитывающих качество каналов:
- Babel (сначала отдельный babeld, потом из FRR): соседи поднялись, RTT хорошо показывал медленные линки, но маршруты по сети так и не сошлись за время теста.
- BMX7 вообще не нашёл соседей. Он отключает интерфейсы без глобального
IPv6-адреса (
/127не считается), а обнаружение соседей у него начинается с multicast, который туннели семейства WireGuard не передают.
И у обоих есть проблема глубже: качество линка они считают по маленьким hello-пакетам. Задушенный линк часто идеально пропускает ping и hello, пока объёмный TCP еле ползёт, и метрика на hello считает его здоровым.
Все три пакета лежат в моём фиде для OpenWrt, my-openwrt-feed, вместе с остальным, что крутится на этих роутерах.
Дальше
Внешний контроллер, который по-настоящему измеряет каждый линк — пропускную способность, а не только доступность, — и передаёт результат в стоимости OSPF, с гистерезисом, чтобы маршруты не прыгали.