АйТи Фреш
Главная / Статьи / Сети
Сети

Включили IPv6 forwarding — и сервер остался без маршрута по умолчанию

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~26 мин чтения
Включили IPv6 forwarding — и сервер остался без маршрута по умолчанию
Иллюстрация к статье «Включили IPv6 forwarding — и сервер остался без маршрута по умолчанию».

Ситуация, которую я разбирал у клиентов уже раз десять: на сервере поднимают Docker с IPv6, libvirt-сеть, WireGuard или NAT66 — и через минуту машина перестаёт ходить в интернет по IPv6. Адрес на интерфейсе пока есть, соседи пингуются, маршрута по умолчанию нет — а через какое-то время пропадёт и сам SLAAC-адрес. В sysctl честно стоит accept_ra=1, а RA от роутера ядро молча выбрасывает. Ниже — что именно делает ядро (со ссылками на исходники 6.12), почему единица здесь означает «нет», в каком порядке крутить ручки и как закрепить конфиг так, чтобы его не переписал systemd-networkd или NetworkManager.

Симптом: IPv4 живой, IPv6 отвалился ровно в момент деплоя

Выглядит это всегда одинаково. Вечер, на шлюзе или на хосте с контейнерами что-то включили: "ipv6": true в daemon.json, туннель WireGuard по инструкции из интернета, тестовый NAT66, libvirt-сеть. По SSH машина доступна, по IPv4 всё зелёное. А мониторинг начинает сыпать: не резолвится AAAA, не отвечает apt по v6, не собирается образ, потому что registry разрешается в IPv6 и трафик уходит в никуда. Первое, что говорит админ: «сеть у провайдера моргнула». Не моргнула.

Диагностика занимает тридцать секунд и почти всегда даёт одну и ту же картину: глобальный адрес на интерфейсе на месте, link-local на месте, соседи по /64 пингуются, а ip -6 route show default возвращает пустоту. Отсюда и обманчивое ощущение «адрес же есть, значит IPv6 настроен». Адрес есть, потому что он был получен по SLAAC раньше и живёт по своему valid_lft — сутки, месяц, сколько там прислал роутер. Маршрут по умолчанию живёт по другому таймеру и был удалён из таблицы явно, ядром, в момент вашей команды. А вот адрес доживает своё: обновлять его больше нечем, потому что новые RA ядро не принимает. Когда истечёт preferred_lft, адрес станет deprecated, когда истечёт valid_lft — исчезнет. У провайдеров с короткими lifetime это случается через несколько часов, и тогда авария «второй волны» выглядит уже как полное отсутствие IPv6 на интерфейсе, хотя к этому моменту все давно забыли про вчерашний деплой.

Второй маркер — sysctl. Смотрим два значения на аплинке, и они выглядят абсолютно «правильно»: accept_ra=1 и forwarding=1. Дальше происходит главная ошибка разбора: единицу читают как «RA разрешены, безусловно». Это не так. В IPv6 accept_ra — не булев флаг, а три состояния, и значение 1 означает «принимать RA, только если forwarding выключен». Вы включили forwarding — вы выключили приём RA.

Не перезагружайте сервер «чтобы починилось». Ребут обычно возвращает связность (sysctl сбрасывается, forwarding ещё не применён), причина уходит из вида и вернётся при следующем деплое — уже ночью и уже без вас.

Что на самом деле делает ядро: три куска кода из 6.12

Вся логика приёма Router Advertisement в Linux сводится к одной inline-функции. Она читает per-interface значения и решает, разбирать входящий RA или выкинуть. Смотрим include/net/ipv6.h ядра 6.12:

static inline bool ipv6_accept_ra(const struct inet6_dev *idev)
{
	s32 accept_ra = READ_ONCE(idev->cnf.accept_ra);

	/* If forwarding is enabled, RA are not accepted unless the special
	 * hybrid mode (accept_ra=2) is enabled.
	 */
	return READ_ONCE(idev->cnf.forwarding) ? accept_ra == 2 :
		accept_ra;
}

То есть при forwarding=1 функция возвращает истину исключительно при accept_ra==2. Единица превращается в ноль. Эта же функция вызывается в трёх местах: в ndisc_router_discovery() при приёме RA (и в логе с ND_PRINTK можно увидеть «did not accept ra for dev»), в таймере отправки Router Solicitation и при попытке дёрнуть RS через rtnetlink — там ядро прямо отвечает «Router advertisement is disabled on device». Это второй важный момент: RS ядро тоже перестаёт слать. В документации это сказано буквально: «If and only if the functional setting is to accept Router Advertisements, Router Solicitations will be transmitted». Сервер не спросит роутер заново — он будет молча ждать периодического RA и так же молча его выбрасывать. Само не починится никогда.

И третий кусок — почему маршрут исчезает мгновенно, а не «протухает». В addrconf_fixup_forwarding() после записи нового значения стоит if (newf) rt6_purge_dflt_routers(net);, а внутри чистки — вот такое условие (net/ipv6/route.c):

if (rt->fib6_flags & (RTF_DEFAULT | RTF_ADDRCONF) &&
    (!idev || idev->cnf.accept_ra != 2) &&
    fib6_info_hold_safe(rt)) {
	rcu_read_unlock();
	ip6_del_rt(net, rt, false);

Читается однозначно: включение forwarding сносит все маршруты по умолчанию, полученные автоконфигурацией (флаги RTF_DEFAULT|RTF_ADDRCONF, в выводе ip -6 route это proto ra), кроме тех, что живут на интерфейсе с accept_ra==2. Статический дефолт, который вы прописали руками (proto static / proto boot), чистка не трогает — поэтому на серверах со статикой проблемы и не возникает. И отдельный подарок: purge вызывается независимо от того, писали вы в all.forwarding или в eth0.forwarding — чистятся все таблицы маршрутизации в netns, а не только маршруты этого интерфейса.

Отдельно про SLAAC. Параметр autoconf в документации ядра не имеет самостоятельного дефолта: его функциональное значение включено, если включён accept_ra_pinfo, а тот, в свою очередь, включён, только если функционально включён accept_ra. Получается цепочка forwarding=1 → accept_ra=1 фактически выключен → accept_ra_pinfo выключен → autoconf выключен. Поэтому выставлять autoconf=1 вручную бесполезно: Prefix Information из RA до этой проверки не доходит, так как весь RA отброшен раньше. Лечится не autoconf, а accept_ra.

И ещё одна деталь из того же кода, которую полезно помнить: запись в net.ipv6.conf.default.forwarding чистку маршрутов не запускает — функция возвращается раньше. Purge срабатывает при записи единицы в all.forwarding или в forwarding конкретного интерфейса. Поэтому рецепт «поставлю в default, а живые интерфейсы не трону» формально не ломает маршрут прямо сейчас, но первый же новый интерфейс или ребут с этим значением приведут к той же картине.

Запись в net.ipv6.conf.all.forwarding не просто «включает флаг all» — она копирует значение в default и проходит по всем существующим устройствам (addrconf_forward_change). Один sysctl меняет поведение каждого интерфейса машины разом.
Включили IPv6 forwarding — и сервер остался без маршрута по умолчанию — схема
Схема к статье. Открыть схему в полном размере

accept_ra=2, порядок команд и ловушка с «all»

Лечится это одной строкой, но со строго определённым порядком. Правильно так: сначала accept_ra=2 на аплинке, потом forwarding. Тогда в момент purge условие idev->cnf.accept_ra != 2 не выполнится, и существующий RA-маршрут переживёт включение форвардинга — связность не мигнёт вообще. Если сделать наоборот, маршрут удалится, и вернётся он только со следующим RA: у radvd и большинства провайдерских железок MaxRtrAdvInterval по умолчанию 600 секунд, так что до десяти минут вы сидите без IPv6, даже сделав всё «в итоге правильно».

# порядок важен: сначала разрешаем RA при форвардинге, потом форвардинг
sysctl -w net.ipv6.conf.eth0.accept_ra=2
sysctl -w net.ipv6.conf.all.forwarding=1

# проверяем, что дефолт на месте и пришёл именно из RA
ip -6 route show default
# default via fe80::1 dev eth0 proto ra metric 1024 expires 1797sec pref medium

Теперь ловушка, на которой я лично потерял часа полтора несколько лет назад и с тех пор проверяю первым делом. net.ipv6.conf.all.accept_ra=2 не работает. В таблице sysctl ядра accept_ra объявлен с обычным proc_dointvec — никакой особой обработки «all», в отличие от forwarding или disable_ipv6, у него нет. А ipv6_accept_ra() читает только idev->cnf, то есть значение конкретного интерфейса. Записанная в all двойка не попадает никуда: она меняет псевдоустройство, которое при разборе RA не читается. Ветка default тоже не спасёт на живой машине — она работает как шаблон для интерфейсов, которые появятся позже. Ставить нужно по имени интерфейса. Именно поэтому половина рецептов из интернета «не помогает», хотя в sysctl -a двойка гордо стоит.

Заодно про соседние ручки, чтобы не крутить лишнего. accept_ra_defrtr, accept_ra_pinfo и accept_ra_rtr_pref не имеют собственного дефолта в привычном смысле: их функциональное значение включено, если accept_ra включён, и выключено, если выключен. То есть при accept_ra=1+forwarding=1 они все фактически в нуле, и выставлять accept_ra_defrtr=1 отдельно, не тронув accept_ra, бессмысленно — RA до этой проверки просто не доживает. А вот когда приём RA разрешён, accept_ra_defrtr=0 — законный способ сказать «префиксы бери, дефолт не бери», это иногда нужно на машинах с несколькими аплинками.

Если форвардинг включает libvirt, вы, скорее всего, увидите проблему ещё до аварии. При старте IPv6-сети libvirt сам читает /proc/net/ipv6_route, находит маршруты с флагом RTF_ADDRCONF и проверяет accept_ra их интерфейса. При значении 1 сеть не поднимается с ошибкой «Check the host setup: interface … has kernel autoconfigured IPv6 routes and enabling forwarding without accept_ra set to 2 will cause the kernel to flush them, breaking networking». Это ровно та же логика, что описана выше, только libvirt честно отказывается ломать хост. Docker такой проверки не делает: демон просто включает форвардинг, и маршрут исчезает молча.

Есть и второй путь, без RA вовсе: статический маршрут по умолчанию. Статику purge не трогает, и хост с форвардингом живёт спокойно при accept_ra=0. Адрес в этом случае тоже задаётся статически (или приходит по DHCPv6 в режиме stateful). Важно помнить, что DHCPv6 шлюз не выдаёт: опции default router в протоколе нет, маршрут по умолчанию в IPv6 приходит только из RA или из конфига. Поэтому «переведу аплинк на DHCPv6-клиент и забуду про RA» без статического дефолта не работает.

# вариант со статикой: RA на аплинке не слушаем вообще
sysctl -w net.ipv6.conf.eth0.accept_ra=0
ip -6 addr add 2001:db8:10::2/64 dev eth0
ip -6 route add default via 2001:db8:10::1 dev eth0
sysctl -w net.ipv6.conf.all.forwarding=1
Проверяйте настройку там, где её читает ядро: `cat /proc/sys/net/ipv6/conf/eth0/accept_ra`. Значение в all — декорация.
Порядок действий: accept_ra=2, порядок команд и ловушка с «all» — схема
Порядок действий: accept_ra=2, порядок команд и ловушка с «all». Открыть схему в полном размере

Разбор из практики: цветочная база, 16 рабочих мест, Docker на шлюзе

Условный клиент — цветочная база «Букет-Опт», 16 рабочих мест: склад-холодильник, отдел продаж, бухгалтерия и курьеры. Канал с честным IPv6 от провайдера: /64 на стык приходит по SLAAC, дефолт — из RA, никакой статики. Периметровый шлюз — небольшой сервер на Debian 12, ядро из bookworm, на нём же живут пара служебных контейнеров: сборщик метрик с датчиков температуры в холодильных камерах и прокси-кэш для обновлений. В четверг вечером подрядчик, настраивавший интеграцию с интернет-витриной (не мы), включил в daemon.json IPv6 для контейнерной сети, перезапустил dockerd и ушёл. Демон при поднятии IPv6-сети выставил net.ipv6.conf.all.forwarding=1 — как и положено роутеру для контейнеров. В этот же миг ядро вычистило default via fe80::… proto ra и перестало принимать RA на аплинке, потому что accept_ra там был штатной единицей.

Утром — обращение: «сайты открываются через раз, заказы от оптовиков в 1С грузятся по минуте, почта то есть, то нет» — а в пятницу утром у цветочной базы пик отгрузок. Классика двойного стека: приложения делают Happy Eyeballs, часть запросов уходит по AAAA, упирается в отсутствующий маршрут, ждёт таймаут и переключается на IPv4. Пользователь видит не «нет интернета», а «всё тормозит». Мониторинг молчал: ICMP-проверки шли по IPv4, а за наличием IPv6-маршрута никто не следил. Шестнадцать человек, из которых половина на телефонах с заказчиками, полдня работали с «тормозящим интернетом». Диагностика заняла минут пять — ip -6 route show default пусто, sysctl net.ipv6.conf.eth0.accept_ra = 1, net.ipv6.conf.all.forwarding = 1, дата изменения — вчера 19:40, в journal рядом рестарт dockerd. Дальше уже дело техники.

Чинили в два шага. Сначала аварийно вернули связность: выставили accept_ra=2 на аплинке и, чтобы не ждать следующего RA, руками прописали временный дефолт через link-local шлюза. Через полторы минуты пришёл очередной RA, маршрут переехал в proto ra с нормальным expires, временный сняли. Потом закрепили: отдельный файл в /etc/sysctl.d с явным именем интерфейса, на внутренних мостах (br0 и docker0) accept_ra=0, чтобы шлюз случайно не начал слушать RA из локалки, и мониторинг на сам факт наличия IPv6-дефолта. С тех пор — тишина, хотя dockerd перезапускался ещё десяток раз.

# /etc/sysctl.d/60-ipv6-router.conf
# аплинк: принимаем RA даже в режиме маршрутизатора
net.ipv6.conf.eth0.accept_ra = 2
net.ipv6.conf.eth0.accept_ra_defrtr = 1
net.ipv6.conf.eth0.accept_ra_min_lft = 60
net.ipv6.conf.eth0.accept_ra_from_local = 0

# внутренние интерфейсы RA не слушают
# (docker0 появляется позже systemd-sysctl — для него дублируем
#  значение в default и проверяем после старта dockerd)
net.ipv6.conf.default.accept_ra = 0
net.ipv6.conf.br0.accept_ra = 0
net.ipv6.conf.docker0.accept_ra = 0

# и только после этого — форвардинг
net.ipv6.conf.all.forwarding = 1

Второй случай — у того же «Букет-Опта», тот же корень, другой повод. Для удалённого склада и ноутбуков торговых представителей админ клиента поднимал WireGuard на арендованном VPS по гайду, где первой строкой стоит net.ipv6.conf.all.forwarding=1. Такие гайды пишут под сценарий «у сервера статический IPv6», а хостер выдавал шлюз только по RA. Итог тот же: туннель поднялся, наружу по IPv6 сервер ходить перестал, две недели никто не замечал, потому что всё нужное резолвилось и в A-записи. Нашли случайно, когда выгрузка бэкапа базы 1С в S3-совместимое хранилище с AAAA-записью начала падать по таймауту.

Если IPv6-дефолт пропал, а вам нужна связность прямо сейчас — временный костыль: `ip -6 route add default via fe80::1 dev eth0`. Он переживёт purge (proto boot, не ADDRCONF), но не переживёт перезагрузку. Сразу после починки accept_ra его надо снять, иначе будете ловить дубли маршрута.

Кто на самом деле рулит вашим интерфейсом: sysctl.d, networkd, NetworkManager

Прежде чем править sysctl — определитесь, кто конфигурирует интерфейс. Это самая частая причина «я поставил двойку, а она обратно скидывается». Если интерфейсом управляет ifupdown, netplan-with-networkd, cloud-init или NetworkManager, ваши строки в /etc/sysctl.d могут применяться раньше появления интерфейса или переписываться менеджером после подъёма линка. systemd-sysctl отрабатывает при старте и по udev-событию, но для интерфейса, который появился позже (tun, bond, VLAN), запись по имени просто не найдёт цель и тихо пропустится.

systemd-networkd вообще играет по своим правилам, и это надо знать. В systemd.network(5) сказано прямо: реализация RA в ядре всегда выключена, независимо от настройки IPv6AcceptRA=, а при включённой опции работает userspace-клиент самого networkd. Значит, sysctl accept_ra на таком интерфейсе смотреть бессмысленно — там будет ноль, и это нормально: RA разбирает демон, а не ядро. При этом по умолчанию IPv6AcceptRA= выключается, если на интерфейсе включены IPv6SendRA=, IPv6Forwarding= или IPMasquerade=, и включён в остальных случаях. Логика та же, что у ядра, только ручки другие — крутить надо их: явное IPv6AcceptRA=yes перекрывает этот дефолт.

# /etc/systemd/network/10-uplink.network
[Match]
Name=eth0

[Network]
# systemd >= 256; на более старых (Debian 12 — systemd 252) — IPForward=ipv6
IPv6Forwarding=yes
IPv6AcceptRA=yes

# альтернатива без RA — статический шлюз:
# IPv6AcceptRA=no
# [Address]
# Address=2001:db8:10::2/64
# [Route]
# Gateway=2001:db8:10::1

Обратите внимание на версию. Параметры IPv6Forwarding= и IPv4Forwarding= появились в systemd 256; в более старых версиях форвардинг включался через IPForward=, и в дистрибутивах LTS вы встретите именно его. Кроме того, документация отдельно подчёркивает: включение IPv6Forwarding= на двух интерфейсах само по себе не делает так, чтобы пакеты IPv6 пересылались между ними — для этого нужен глобальный форвардинг. Не стоит ожидать от per-interface ключа того же поведения, что у IPv4.

NetworkManager ведёт себя аналогично, но об этом почти не пишут: на управляемых устройствах он выставляет accept_ra=0 в sysctl (это видно прямо в исходниках nm-device.c) и обрабатывает Router Advertisement сам, через libndp. Поэтому на RHEL-подобных системах с NM попытка «поправить accept_ra через sysctl» ничего не даёт: значение перетирается при активации соединения, а маршрут по умолчанию приезжает от демона. Здесь отправная точка — ipv6.method=auto в профиле соединения и проверка вывода nmcli device show eth0. Поведение NM при включённом форвардинге зависит от версии, поэтому после любого включения форвардинга на NM-хосте первым делом проверяйте, остался ли дефолт, и при необходимости задавайте шлюз в профиле статически (ipv6.gateway).

Проверка «кто хозяин»: `systemctl is-active systemd-networkd NetworkManager` и `nmcli device status`. Пока не ответите на этот вопрос, любые правки — стрельба вслепую.
Памятка: Кто на самом деле рулит вашим интерфейсом: sysctl.d, networkd, NetworkManager — схема
Памятка: Кто на самом деле рулит вашим интерфейсом: sysctl.d, networkd, NetworkManager. Открыть схему в полном размере

Когда accept_ra=2 — плохое решение, и чем закрывать риски

Скажу честно: accept_ra=2 — это компромисс, а не эталон. Для боевого периметрового шлюза с постоянным префиксом я предпочитаю другое: accept_ra=0 на аплинке и статический маршрут по умолчанию. Причина простая — маршрутизатор, который сам себе учит дефолт из широковещательных объявлений соседей, зависим от чужого поведения в канале. Любой мусорный RA (случайно поднятый Windows ICS, ноутбук с включённым sharing, «умный» роутер, воткнутый в свитч) может подсунуть шлюзу маршрут или префикс. RA-guard в дешёвых свитчах, как правило, отсутствует, а в L2-сегменте провайдера вы вообще ни на что не влияете.

А вот где accept_ra=2 — единственно верный вариант: VPS и облака, где дефолтный шлюз выдаётся только по RA и статикой не документируется; каналы с меняющимся link-local шлюза; стенды и офисные шлюзы с динамическим префиксом от провайдера. Тут статика превратится в грабли при первой же смене оборудования у оператора. В этом случае гибридный режим включаем осознанно и обвешиваем ограничителями: минимальный router lifetime, запрет на RA из собственных адресов, ограничение длины префикса в Route Information (RFC 4191), запрет RA на всех интерфейсах, кроме аплинка.

И сразу успокою насчёт страхов, которые мне регулярно транслируют. accept_ra=2 не превращает сервер обратно в хост и не выключает форвардинг — пакеты продолжают маршрутизироваться, флаг forwarding остаётся единицей, влияние строго на обработку входящих RA и отправку RS. Он также не заставляет ядро принимать RA на всех интерфейсах: это per-interface параметр, на br0 и docker0 значение останется своим. Ставить его на всю машину не нужно и вредно — только на тот интерфейс, откуда реально приходит ваш апстрим-роутер.

Отдельно про downstream: если на внутреннем интерфейсе шлюза остаётся accept_ra=1 и вы позже выключите форвардинг для отладки — шлюз мгновенно начнёт слушать RA из локалки и может подхватить чужой префикс. Явный ноль на внутренних интерфейсах стоит одну строку и снимает целый класс инцидентов.

Чек-лист на пять минут и что мониторить, чтобы не повторилось

Порядок действий, когда «IPv6 отвалился после чего-то». Шаг первый — ip -6 route show default. Пусто — идём дальше, есть маршрут — проблема не эта. Шаг второй — читаем два числа прямо из procfs для нужного интерфейса: accept_ra и forwarding. Комбинация 1+1 — диагноз поставлен. Шаг третий — смотрим, кто и когда включил форвардинг: journalctl -u docker -u wg-quick@wg0 --since -1d, время изменения файлов в /etc/sysctl.d, история команд. Шаг четвёртый — чиним в правильном порядке и закрепляем в конфиге того менеджера, который реально управляет интерфейсом. Шаг пятый — проверяем, что маршрут вернулся именно как proto ra и с ненулевым expires; если он статический, вы починили симптом, а не причину.

Мониторинг тут стоит копейки и окупается первым же случаем. Мне достаточно двух проверок на хосте: наличие IPv6-маршрута по умолчанию и фактическое значение accept_ra на аплинке. Обе — обычные UserParameter в Zabbix или строчка в любом самописном чекере. Третьей проверкой имеет смысл гонять реальный ICMP-эхо по IPv6 до внешнего адреса, а не только до шлюза: маршрут может присутствовать формально, а связность отсутствовать.

#!/bin/bash
# /usr/local/bin/check_ipv6_default.sh
IF=eth0
ip -6 route show default | grep -q . || { echo "CRIT: no IPv6 default route"; exit 2; }
RA=$(cat /proc/sys/net/ipv6/conf/$IF/accept_ra)
FW=$(cat /proc/sys/net/ipv6/conf/$IF/forwarding)
[ "$FW" = 1 ] && [ "$RA" != 2 ] && { echo "WARN: forwarding=1 accept_ra=$RA on $IF"; exit 1; }
# замените 2001:db8::53 на внешний IPv6-адрес, который вы контролируете
ping -6 -c2 -W2 2001:db8::53 >/dev/null || { echo "WARN: no IPv6 reachability"; exit 1; }
echo "OK"

И про приоритеты, раз уж об этом спрашивают. В первую очередь — правильный порядок применения (accept_ra=2 до forwarding=1) и явный per-interface конфиг там, где его читает ваш сетевой менеджер. Во вторую — accept_ra=0 на внутренних интерфейсах и мониторинг наличия дефолта. На accept_ra_min_hop_limit, приватные адреса, тонкую настройку rtr_solicit_interval и прочую экзотику можно спокойно забить: к этому классу аварий они отношения не имеют, а времени съедают много. Главное — понимать, что единица в accept_ra контекстно зависима, и что включение форвардинга в Linux — это не «плюс одна возможность», а смена роли машины с хоста на маршрутизатор со всеми вытекающими.

Правило, которое я вбиваю в свои чек-листы деплоя: любое включение IPv6-форвардинга на машине, получающей адрес по SLAAC, — это изменение маршрутизации, а не «галочка в конфиге». Всегда сопровождайте его строкой accept_ra=2 на аплинке, применённой ПЕРЕД форвардингом.
Порядок действий: Чек-лист на пять минут и что мониторить, чтобы не повторилось — схема
Порядок действий: Чек-лист на пять минут и что мониторить, чтобы не повторилось. Открыть схему в полном размере

Частые вопросы

Почему accept_ra=1 не работает при включённом форвардинге — это баг?

Нет, это документированное поведение. Значение 1 означает «принимать RA, если форвардинг выключен», а не «принимать RA всегда». Как только машина становится маршрутизатором (forwarding=1), ядро считает, что маршруты ей задаёт администратор или протокол маршрутизации, и перестаёт слушать чужие Router Advertisement. Гибридный режим — это accept_ra=2.

Я выставил net.ipv6.conf.all.accept_ra=2, а маршрут всё равно не приходит. Почему?

Потому что ядро при разборе RA читает значение конкретного интерфейса, а не ветку all: у accept_ra нет специальной обработки «all», в отличие от forwarding. Ставьте по имени интерфейса — net.ipv6.conf.eth0.accept_ra=2 — и проверяйте через /proc/sys/net/ipv6/conf/eth0/accept_ra. Ветка default тоже не поможет: она применяется только к интерфейсам, созданным после её изменения.

Маршрут пропал, я включил accept_ra=2 — почему связность вернулась не сразу?

Потому что удалённый маршрут восстановится только со следующим Router Advertisement, а роутеры шлют их периодически — у radvd MaxRtrAdvInterval по умолчанию 600 секунд. Ускорить можно временным статическим маршрутом через link-local шлюза либо перезапуском интерфейса, чтобы ядро отправило Router Solicitation — при accept_ra=2 оно снова имеет на это право.

Docker/WireGuard сами включают форвардинг. Как защититься заранее?

Пропишите accept_ra=2 на аплинке в конфиге сетевого менеджера или в /etc/sysctl.d так, чтобы значение применялось при подъёме интерфейса, а не только при загрузке. Тогда при любом последующем включении форвардинга чистка маршрутов по умолчанию обойдёт ваш аплинк стороной: ядро явно пропускает интерфейсы с accept_ra равным 2.

У меня systemd-networkd или NetworkManager — sysctl не помогает. Что делать?

Оба менеджера обрабатывают RA в userspace и держат kernel-настройку accept_ra в нуле, поэтому sysctl тут не рычаг. В systemd-networkd пропишите в .network-файле аплинка явное IPv6AcceptRA=yes — оно перекрывает дефолт «выключено при IPv6Forwarding=» (на systemd старше 256 форвардинг включается через IPForward=). В NetworkManager отправная точка — ipv6.method=auto в профиле соединения; если после включения форвардинга дефолт пропадает, проще задать шлюз статически (ipv6.gateway) или перевести аплинк под networkd.

Не безопаснее ли вообще отключить RA и прописать маршрут статикой?

На периметровом шлюзе с постоянным префиксом — да, я так и делаю: accept_ra=0 плюс статический дефолт. Это снимает зависимость от чужих объявлений в канале. Но если провайдер или облако выдают шлюз только по RA и link-local может смениться, статика превратится в мину замедленного действия — тогда честнее accept_ra=2 с ограничителями (accept_ra_min_lft, accept_ra_from_local=0, accept_ra_rt_info_max_plen=0).

Почему libvirt отказывается запускать IPv6-сеть, а Docker запускается и ломает маршрут?

libvirt перед включением форвардинга сам проверяет, есть ли на хосте маршруты, полученные из RA (флаг RTF_ADDRCONF), и какой accept_ra у их интерфейса. При значении 1 он останавливается с ошибкой «Check the host setup… without accept_ra set to 2», чтобы не сломать хост. Docker такой проверки не делает. Решение одинаковое: accept_ra=2 на аплинке до включения форвардинга или статический маршрут по умолчанию с accept_ra=0.

Поможет ли net.ipv6.conf.eth0.autoconf=1, чтобы вернуть SLAAC-адрес?

Нет. По документации ядра функциональное значение autoconf зависит от accept_ra_pinfo, а тот — от accept_ra. При forwarding=1 и accept_ra=1 весь RA отбрасывается до разбора Prefix Information, так что autoconf просто нечего обрабатывать. Существующий адрес доживёт до valid_lft и исчезнет. Возвращает SLAAC только accept_ra=2 на интерфейсе либо переход на статический адрес.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи