Как мы провели IPv4 через WireGuard поверх IPv6 и потеряли обратный маршрут
Началось всё с неприятно знакомой картины: клиент WireGuard подключён, handshake свежий, счётчики RX/TX растут, а IPv4-интернет не работает. Сам туннель между маршрутизаторами тоже жив, OSPF-соседство в состоянии Full, удалённый роутер спокойно выходит в интернет. На первый взгляд сломаться уже нечему. На практике пакет умудрялся пройти почти весь путь и потеряться буквально на последнем повороте.
Зачем здесь вообще IPv6 underlay
Нам понадобился дополнительный удалённый egress: часть IPv4-трафика должна была выходить через отдельный RouterOS. Между площадками уже был нормальный публичный IPv6, поэтому транспорт WireGuard подняли именно поверх него. Внутри туннеля при этом осталась обычная IPv4-сеть.
Схема получилась такой:
клиент WireGuard → Linux-хост портала → центральный RouterOS → WireGuard поверх публичного IPv6 → удалённый RouterOS → IPv4-интернет.
IPv6 здесь не заменяет клиентский IPv4 и не участвует в пользовательской адресации. Он только несёт UDP-пакеты WireGuard между двумя маршрутизаторами. Это удобное разделение: внешний транспорт живёт своей жизнью, а внутреннюю IPv4-маршрутизацию можно менять независимо.
При underlay MTU 1500 мы оставили WireGuard MTU 1420. Расчёт скучный, но полезный: IPv6-заголовок занимает 40 байт, UDP ещё 8, служебная часть WireGuard data packet — 32. Итого 80 байт накладных расходов, поэтому 1500 − 80 = 1420.
Как должен был идти трафик
На центральном роутере появилась отдельная FIB-таблица с default route через туннельный адрес удалённого RouterOS. Трафик Linux-хоста портала маркировался и отправлялся в эту таблицу. На удалённой стороне были обратный маршрут к сети портала, разрешение forward из туннеля в ISP и обычный masquerade на внешнем интерфейсе.
Мы отдельно проверили транспорт:
WireGuard handshake обновлялся с обеих сторон;
внутренние адреса туннеля пинговались в обоих направлениях;
OSPF поднялся поверх WireGuard и установил ожидаемые маршруты;
удалённый RouterOS сам успешно ходил во внешний IPv4;
центральный роутер выбирал нужную policy table.
То есть сам IPv6 underlay и межроутерный WireGuard работали. Но клиентский трафик всё равно не возвращался.
Почему первые подозрения оказались неверными
Сначала мы сравнили рабочий egress через другой роутер и новый egress. Отличались rp-filter, форма правила masquerade и набор forward-правил. Эти отличия выглядели подозрительно, но после приведения настроек к рабочему шаблону симптом остался.
Это был хороший сигнал перестать гадать по конфигам и посмотреть, где реально находится последний живой пакет.
Два tcpdump и один route get
На Linux-хосте портала одновременно запустили захват на интерфейсе WireGuard и на LAN-интерфейсе.
На выходе всё выглядело правильно: пакет клиента приходил из WG-пула, пересылался в LAN и получал SNAT в адрес самого хоста. На LAN-интерфейсе были видны и запросы к внешнему адресу, и ответы обратно. Значит, удалённый RouterOS, NAT и интернет-маршрут свою работу уже сделали.
Но на wg-интерфейсе были только исходящие запросы клиента. Ответы доходили до Linux-хоста и дальше в туннель не попадали. Счётчик forward в направлении LAN → WireGuard тоже не рос.
Решающим оказался обычный поиск маршрута для реального ответного пакета. Ядро выбирало основной default route хоста, а не wg-интерфейс. Причина была простой: policy rule существовало только для пакетов от клиентского пула. Ответ из интернета приходит к клиентскому пулу, поэтому правило по source на него не распространяется.
Точечное исправление
В отдельной таблице уже был link route клиентской сети через wg-интерфейс. Не хватало симметричного правила выбора этой таблицы по адресу назначения:
ip rule add to <WG_CLIENT_POOL> lookup <VPN_TABLE> ip rule add from <WG_CLIENT_POOL> lookup <VPN_TABLE>
Первое правило возвращает ответы в WireGuard, второе сохраняет прежний исходящий policy routing. После добавления destination rule тот же route lookup выбрал wg-интерфейс. В захвате появились echo reply к адресу клиента, обратный forward-счётчик начал расти, а реальный клиент получил интернет через удалённый egress.
Что из этого стоит запомнить
Свежий WireGuard handshake доказывает только работоспособность транспорта. OSPF Full доказывает соседство и обмен маршрутами между роутерами. Masquerade доказывает, что пакет можно выпустить наружу. Ни один из этих фактов не гарантирует, что Linux выберет правильную таблицу для ответного трафика после обратного NAT.
Для таких схем полезнее не перебирать настройки вслепую, а строить короткую цепочку доказательств: вход в wg, выход из LAN, возврат на LAN, решение route lookup, возврат в wg. Как только каждый переход подтверждён захватом или счётчиком, «мистическая проблема туннеля» обычно превращается в одно конкретное правило.
Больше технических разборов публикую на сайте.
