Зачем вообще IPv6 в контейнерах (и почему тут всё сложнее, чем с IPv4)
Если просто включить IPv6 в Docker или Podman, быстро выясняется: IPv6 задумывался как «каждому узлу — публичный адрес», а контейнерные сети у многих годами жили по логике IPv4-NAT. Отсюда типовые вопросы: как раздать контейнерам реальные IPv6-адреса из вашего routed-префикса, как обеспечить входящую доступность, что делать с Neighbor Discovery (NDP) и почему публикация портов по IPv6 иногда ведёт себя не так, как ожидается.
Ниже разберём три схемы, которые чаще всего нужны на сервере/виртуалке с IPv6:
- Маршрутизация routed /64 в контейнерные сети (рекомендуемый вариант без NAT).
- Bridge + NDP-proxy, когда провайдер «видит» только MAC хоста (частый кейс в виртуализации).
- NAT66 (нежелательно, но иногда единственный выход при ограничениях).
По пути затронем bridge с IPv6, нюансы публикации портов и минимально адекватный firewall для IPv6.
База: что должно быть на хосте, чтобы IPv6 для контейнеров заработал
Проверяем IPv6 на самом сервере
Сначала убедитесь, что у хоста есть глобальный IPv6-адрес и дефолтный маршрут:
ip -6 addr show
ip -6 route show
ping -6 -c 3 2606:4700:4700::1111
Если ping -6 не проходит — контейнеры ни при чём: чините IPv6 на уровне ОС/сети (адрес, маршрут, фильтры у провайдера).
Включаем форвардинг IPv6
Для схем с маршрутизацией в контейнерные сети нужен IPv6 forwarding:
sysctl -w net.ipv6.conf.all.forwarding=1
sysctl -w net.ipv6.conf.default.forwarding=1
Чтобы закрепить после перезагрузки:
printf '%s\n' 'net.ipv6.conf.all.forwarding=1' 'net.ipv6.conf.default.forwarding=1' > /etc/sysctl.d/99-ipv6-forwarding.conf
sysctl --system
Осторожно с RA на хосте
Во многих датацентрах IPv6 выдаётся как «адрес на интерфейсе + routed /64 (или /56)». В таких случаях предсказуемее опираться на статическую адресацию контейнеров/подсетей и L3-маршрутизацию, а не на SLAAC/RA внутри контейнерных сетей.
Если вы всё же планируете раздавать адреса по RA (например, через radvd), заранее проверьте, что ваш контейнерный стек и firewall не режут ICMPv6 и что вы понимаете модель маршрутизации между внешним интерфейсом и bridge.
На практике удобнее всего начинать с сервера, где у вас полный контроль над сетевыми настройками и sysctl. Если выбираете площадку под контейнерные сервисы, посмотрите тарифы VDS и заранее уточните, выдаёт ли провайдер routed-префикс (например, /64) и как он маршрутизируется.

Схема №1 (рекомендуется): routed /64 и маршрутизация в контейнерные сети без NAT
Идея простая: провайдер маршрутизирует на ваш сервер префикс routed /64 (например, 2001:db8:1000:2000::/64). Вы выделяете из него подсети под контейнерные сети (часто /80 или /96), назначаете подсеть на container bridge и включаете маршрутизацию между WAN-интерфейсом и bridge. NAT не нужен: каждый контейнер получает «настоящий» IPv6.
Главный плюс: end-to-end IPv6 без маскарадинга. Входящие соединения и часть прикладной логики (в т.ч. allowlist/ACL) становятся проще и прозрачнее.
План адресации: почему лучше /80 (или /96), а не весь /64 в один bridge
Технически можно отдать контейнерному bridge весь /64, но на практике удобнее дробить:
- проще сопровождать маршруты и правила firewall;
- легче изолировать окружения (prod/stage, разные стеки);
- меньше шансов на «случайные» конфликты адресов и неочевидные зависимости.
Пример: routed /64 — 2001:db8:1000:2000::/64. Под Docker сеть — 2001:db8:1000:2000:10::/80, под Podman — 2001:db8:1000:2000:20::/80.
Docker: включаем IPv6 и задаём фиксированный IPv6 pool
В Docker IPv6 настраивается в /etc/docker/daemon.json. Параметр fixed-cidr-v6 должен быть внутри вашего routed-префикса:
{
"ipv6": true,
"fixed-cidr-v6": "2001:db8:1000:2000:10::/80",
"ip6tables": true
}
Применяем и проверяем:
systemctl restart docker
docker network inspect bridge
Дальше проверьте IPv6 внутри контейнера:
docker run --rm -it alpine sh
ip -6 addr show
ping -6 -c 3 2606:4700:4700::1111
Podman: IPv6 в CNI/Netavark и что проверить в первую очередь
У Podman встречаются разные сетевые бэкенды. Сначала выясните текущий:
podman info --format '{{.Host.NetworkBackend}}'
podman network ls
podman network inspect podman
Для rootful Podman обычно достаточно создать сеть с IPv6-подсетью из routed /64:
podman network create --subnet 10.10.0.0/24 --ipv6 --subnet 2001:db8:1000:2000:20::/80 v6net
podman run --rm --network v6net alpine ip -6 addr
Для rootless Podman чаще используется user-space сеть (по смыслу ближе к NAT), поэтому «публичные IPv6 как у контейнеров» обычно не получают. Если нужны публичные сервисы по IPv6, выбирайте rootful-режим или заводите вход через reverse proxy на хосте.
Firewall для routed IPv6: минимум, без которого будет больно
В IPv6 нельзя «просто запретить ICMP». Для нормальной работы нужны как минимум NDP и сообщения для Path MTU Discovery (в частности Packet Too Big). Поэтому ICMPv6 лучше разрешить явно (а уже потом точечно ужесточать по типам, если понимаете последствия).
Пример минимального каркаса на nftables (адаптируйте интерфейсы и политику под себя):
nft add table inet filter
nft 'add chain inet filter input { type filter hook input priority 0; policy drop; }'
nft 'add chain inet filter forward { type filter hook forward priority 0; policy drop; }'
nft 'add chain inet filter output { type filter hook output priority 0; policy accept; }'
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input iif lo accept
nft add rule inet filter input ip6 nexthdr icmpv6 accept
nft add rule inet filter input tcp dport 22 accept
nft add rule inet filter forward ct state established,related accept
nft add rule inet filter forward ip6 nexthdr icmpv6 accept
Если у вас Docker с включённым ip6tables, он добавит свои правила. Полезно понимать, как это пересекается с вашим nftables-политиками. По этой теме пригодится материал: как подружить Docker с iptables/nftables и не сломать firewall.
Публикация портов по IPv6 в routed-схеме: что реально происходит
Когда контейнер имеет глобальный IPv6-адрес, у вас обычно два рабочих подхода:
- Давать доступ напрямую на IPv6 контейнера (без публикации порта на хосте).
- Использовать публикацию портов (DNAT на хосте), чтобы входили на IPv6 хоста, а дальше попадали в контейнер.
На практике первый вариант часто проще (меньше «магии»): открыли порт в firewall и обращаетесь к адресу контейнера. Второй вариант удобен, если нужен единый внешний адрес и привычная схема «порт на хосте», но это уже проксирование или переназначение на уровне хоста.
Схема №2: bridge IPv6 и NDP-proxy (когда провайдер не видит контейнеры)
Эта схема нужна, когда вы назначаете контейнеру «публичный» IPv6 из routed /64, но внешний мир не может корректно разрешить соседство: провайдер видит только MAC вашего виртуального интерфейса и не принимает NDP-ответы от «чужих» MAC (контейнеров). В итоге адрес у контейнера вроде бы из вашего префикса, но снаружи он не пингуется и соединения не доходят.
Решение: NDP proxy. Хост отвечает на Neighbor Solicitation за контейнерные адреса и принимает трафик на себя, после чего отправляет его внутрь.
Где здесь чаще всего ошибаются
- Включают proxy только на внешнем интерфейсе, забывая про настройки на стороне bridge.
- Проксируют «весь /64» без контроля и получают мусор или конфликты в neighbor-таблицах.
- Режут ICMPv6 в
inputилиforwardи ломают NDP.
Включаем NDP proxy на Linux
Включите proxy_ndp на внешнем интерфейсе (например, eth0) и на интерфейсе, куда смотрят контейнеры (например, docker0):
sysctl -w net.ipv6.conf.eth0.proxy_ndp=1
sysctl -w net.ipv6.conf.docker0.proxy_ndp=1
Дальше добавьте прокси-записи для конкретных IPv6 контейнеров (это обычно безопаснее, чем «разрешить всё префиксом»):
ip -6 neigh add proxy 2001:db8:1000:2000:10::10 dev eth0
ip -6 neigh add proxy 2001:db8:1000:2000:10::11 dev eth0
Теперь внешний сосед получит NDP-ответ от хоста, а дальше трафик будет доставлен до контейнера через вашу внутреннюю маршрутизацию или bridge.
Как автоматизировать NDP proxy для динамических контейнеров
Если адреса статические (вы задаёте их явно контейнеру) — держите список ip -6 neigh add proxy для нужных адресов и применяйте при старте системы.
Если адреса динамические — на практике есть три пути:
- перейти на схему маршрутизации подсетей (Схема №1), чтобы NDP proxy не требовался;
- повесить автоматизацию на события Docker/Podman и добавлять или удалять proxy-записи при старте или остановке контейнера;
- сделать вход только через reverse proxy на хосте (а контейнеры держать «внутренними»).
Если контейнеры получают «публичные» IPv6, часто всплывает второй вопрос: как правильно терминировать TLS (особенно когда сервисов много). В таких случаях удобнее держать сертификаты централизованно и регулярно обновлять их — посмотрите SSL-сертификаты, если нужен коммерческий сертификат под прод.

Firewall и NDP-proxy: критичные разрешения
Для NDP-proxy жизненно важен ICMPv6. Если у вас политика «drop всё», убедитесь, что ICMPv6 разрешён как минимум на input и forward, иначе симптомы будут максимально неприятными: «IPv6 адрес есть, но не пингуется и сервисы не открываются».
Схема №3: NAT66 для контейнеров (когда routed /64 недоступен или есть жёсткие ограничения)
NAT66 в IPv6 — компромисс. Он бывает нужен, если у вас нет routed-префикса (только один IPv6 на хосте), либо провайдер жёстко ограничивает использование дополнительных IPv6-адресов или маршрутов.
Если у вас есть routed /64 (или больше) — почти всегда лучше маршрутизация без NAT. NAT66 оставляйте как запасной план.
NAT66 на nftables: пример маскарадинга исходящих
Предположим, контейнеры живут в ULA-подсети fd00:10:10::/64. Тогда можно сделать SNAT masquerade на внешний IPv6 хоста:
nft add table ip6 nat
nft 'add chain ip6 nat postrouting { type nat hook postrouting priority 100; policy accept; }'
nft add rule ip6 nat postrouting oif eth0 ip6 saddr fd00:10:10::/64 masquerade
Входящие подключения при NAT66 обычно реализуются через DNAT (публикация портов) или через reverse proxy на хосте.
Docker/Podman и NAT66: что будет с публикацией портов
В NAT66-модели всё становится похоже на привычный IPv4-сценарий:
- контейнеры «прячутся» за адресом хоста;
- доступ снаружи делаете через публикацию портов на хосте.
Минус — сложнее дебажить и мониторить источники, а некоторые протоколы или приложения в IPv6-мире рассчитывают на end-to-end доступность.
Отладка: как быстро понять, где сломалось
1) Есть ли маршрут до контейнерной подсети
Проверьте маршруты на хосте:
ip -6 route show
Вы должны видеть маршрут вашей контейнерной подсети через docker0 или другой bridge-интерфейс.
2) Работает ли NDP (если используете NDP-proxy)
Посмотрите прокси-записи:
ip -6 neigh show proxy
И поймайте ICMPv6/NDP пакеты на внешнем интерфейсе:
tcpdump -n -i eth0 icmp6
Если NDP-пакеты есть, но адрес снаружи «молчит», чаще всего причина в firewall (режется ICMPv6) или в том, что не добавлены proxy-записи под реальные адреса контейнеров.
3) Проверяем, что сервис слушает на IPv6
Даже при идеальной сети приложение может слушать только IPv4. Проверяйте на хосте и в контейнере:
ss -lntup
ss -lnup
4) MTU и «странные таймауты»
В IPv6 зависания часто связаны с MTU и блокировкой ICMPv6 Packet Too Big. Если страницы грузятся частично или TLS-рукопожатие подвисает — проверьте, что ICMPv6 не режется, и при необходимости подберите MTU на контейнерном bridge.
Практические рекомендации по выбору схемы
- Есть routed /64 и нужны публичные контейнеры: берите маршрутизацию подсетей без NAT (Схема №1).
- Адреса из routed /64 не пингуются снаружи при bridge-схеме: вероятно, нужен NDP-proxy (Схема №2) или вы построили L2 там, где должен быть L3-route.
- Нет routed-префикса, только один IPv6 на хосте: NAT66 как компромисс (Схема №3) плюс публикация портов или reverse proxy.
Если вы разворачиваете это на сервере под прод, полезно фиксировать адреса и подсети, хранить firewall как код и иметь стенд, где вы спокойно проверяете NDP и маршрутизацию до релиза.
Частые вопросы
Можно ли просто включить IPv6 в Docker — и всё заработает?
Если у вас корректно маршрутизируемый routed-префикс и не режется ICMPv6, часто «почти заработает». Но затем всплывают нюансы: правила firewall для ICMPv6, выбор модели доступа (напрямую на адрес контейнера или через публикацию порта) и возможная необходимость NDP-proxy при L2-ограничениях.
Почему Podman IPv6 в rootless режиме обычно не даёт публичных адресов?
Rootless чаще использует user-space сеть (например, slirp4netns), которая по смыслу ближе к NAT и не даёт простую модель «публичный IPv6 на каждый контейнер». Для end-to-end IPv6 обычно выбирают rootful Podman и bridge или маршрутизацию, либо терминируют вход на хосте.
ip6tables или nftables — что лучше?
На современных дистрибутивах чаще удобнее nftables: один язык правил и таблицы inet. Но Docker исторически активно работает с iptables/ip6tables. Если ваш основной firewall — nftables, заранее проверьте, нет ли конфликта политик и что ICMPv6 не отрезается. Ещё один полезный материал по контейнерной безопасности: когда уместны gVisor и Firecracker для изоляции контейнеров.
Итог
IPv6 в контейнерах — это не «галочка», а выбор сетевой модели. При наличии routed /64 самый устойчивый путь — маршрутизировать отдельные подсети в container bridge и жить без NAT. NDP-proxy помогает, когда вы упираетесь в L2-ограничения виртуализации. NAT66 стоит оставлять на крайний случай.


