Docker без IPv4-NAT: выдаем контейнерам глобальные IPv6-адреса из /64

В этой статье мы разберем, как выдать контейнерам Docker публичные адреса IPv6 из подсети, выделенной виртуальному серверу, чтобы каждый сервис работал на собственном адресе и стандартном порту без трансляции адресов. Также мы рассмотрим, как распределить подсеть между сетями Docker, как защитить контейнеры правилами фильтрации и как проверить результат с внешнего устройства. 

Назначение

Каждому виртуальному серверу может быть выделена подсеть IPv6 размером /64. Это 18 квинтиллионов адресов, поэтому собственный публичный адрес можно выдать каждому контейнеру Docker.

Настройка решает задачу разделения сервисов. При работе только по IPv4 контейнеры делят один публичный адрес: занять порт 443 может лишь один сервис, остальные переносятся на нестандартные порты или размещаются за реверс-прокси. С подсетью IPv6 каждый контейнер получает собственный адрес и слушает стандартный порт, а трансляция адресов и проброс портов не используются.

IPv6 не заменяет IPv4: часть пользователей интернета подключается только по нему, поэтому публичные сайты обычно должны оставаться доступны по обоим протоколам. Описанная схема полезна там, где собственный адрес у контейнера Docker упрощает работу: при разделении сервисов между собой, доступе к служебным интерфейсам и тестовым стендам, взаимодействии с системами, которые уже работают по IPv6. 

Принцип работы

От привычной работы Docker схема отличается тем, как контейнер обменивается трафиком с интернетом. Ниже разобраны оба режима – знание этой разницы понадобится при создании сетей и запуске контейнеров.

При работе по IPv4 контейнер получает внутренний адрес вида 172.17.0.2, недоступный из интернета. Команда -p 8080:80 создает правило трансляции: запросы на порт 8080 адреса сервера перенаправляются на порт 80 контейнера. Исходящие соединения контейнера подменяются адресом сервера. Такой режим называется NAT и включен по умолчанию.

При работе по IPv6 контейнер получает адрес из вашей подсети. Этот адрес маршрутизируется в интернете так же, как адрес сервера, поэтому перенаправление не требуется: запрос приходит напрямую на адрес контейнера и на его порт. Такой режим называется routed.

Режим задается отдельно для каждого протокола, поэтому в одной сети адрес IPv6 может работать в режиме routed, а IPv4 – в режиме NAT. Меняется при этом и смысл публикации порта: в режиме routed она не перенаправляет порт, а разрешает доступ к нему, и порт сервера не используется.

Обе настройки выполняются штатными командами Docker: режим задается при создании сети, публикация порта – при запуске контейнера. Как именно, описано в разделах «Создание сетей» и «Запуск контейнеров».

Подключение подсети

Подсеть подключается в панели управления, в настройках виртуального сервера. После подключения первый адрес подсети назначается на публичный интерфейс автоматически.

Проверьте, что адрес назначен:

ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2001:db8:1a2b:3c4d::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::249:1fff:fe68:18b3/64 scope link
       valid_lft forever preferred_lft forever

Адрес 2001:db8:1a2b:3c4d::1/64 – первый адрес выделенной подсети. Здесь и далее в примерах используется подсеть 2001:db8:1a2b:3c4d::/64, подставляйте собственную. Адрес, начинающийся с fe80:, – служебный, он есть у каждого интерфейса и в настройке не участвует.

Проверьте маршрут по умолчанию и связность:

ip -6 route show default
default via fe80::1 dev eth0 metric 1024 onlink pref medium
ping -6 -c3 -n ipv6.google.com
PING ipv6.google.com (2a00:1450:4008:803::200e) 56 data bytes
64 bytes from 2a00:1450:4008:803::200e: icmp_seq=1 ttl=115 time=27.0 ms
64 bytes from 2a00:1450:4008:803::200e: icmp_seq=2 ttl=115 time=26.3 ms
64 bytes from 2a00:1450:4008:803::200e: icmp_seq=3 ttl=115 time=26.5 ms
--- ipv6.google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 26.294/26.613/27.011/0.297 ms

Ключ -n отключает разрешение имен: в выводе остаются только адреса.

Первый адрес подсети должен постоянно оставаться на публичном интерфейсе. Без него перестает работать вся подсеть, а не только сам адрес. Отсюда три ограничения:

  • префикс /64 у этого адреса не изменяется;
  • адрес не переносится на сетевой мост Docker;
  • адрес не удаляется и не заменяется другим адресом подсети.

По этой причине неприменим прием, встречающийся в руководствах по маршрутизируемому IPv6, – передать всю подсеть /64 одному мосту Docker. Такие руководства рассчитаны на конфигурации, где адрес сервера находится за пределами подсети.

Как записывается адрес IPv6

Адрес состоит из 128 бит и записывается восемью группами по четыре шестнадцатеричные цифры, разделенными двоеточиями. Одна группа – это 16 бит; такую группу также называют хекстетом. В полной записи первый адрес подсети из примера выглядит так:

2001:0db8:1a2b:3c4d:0000:0000:0000:0001

Запись /64 означает, что первые 64 бита адреса – это первые четыре группы – определены при выделении подсети и не изменяются. Оставшиеся четыре группы находятся в вашем распоряжении.

Сокращенная запись отбрасывает ведущие нули в группах, а одну последовательность нулевых групп заменяет на два двоеточия. Поэтому тот же адрес записывается как 2001:db8:1a2b:3c4d::1.

Распределение подсетей

Свою часть адреса удобно делить по границе группы. Если зафиксировать пятую группу, получится подсеть /80: 64 бита выделенной подсети и 16 бит пятой группы.

Пятая группа принимает значения от 0000 до ffff. Таким образом, из подсети /64 получается 65 536 подсетей /80, и пятая группа выполняет роль номера сети. Это основной принцип разметки, он не зависит от того, какая подсеть выделена вашему серверу.

Номер 0000 использовать нельзя: подсеть 2001:db8:1a2b:3c4d::/80 содержит первый адрес, закрепленный за сервером. Нумерация начинается с единицы.

Сети создаются не под отдельный контейнер, а под группу контейнеров с одинаковыми требованиями к доступу извне. Количество сетей определяется количеством таких групп, а не количеством контейнеров. Для типового проекта достаточно трех. Например:

  • 2001:db8:1a2b:3c4d:1::/80 – сеть web, сервисы, доступные из интернета;
  • 2001:db8:1a2b:3c4d:2::/80 – сеть app, приложения без прямого доступа извне;
  • 2001:db8:1a2b:3c4d:3::/80 – сеть data, базы данных и кеши, изолированная сеть.

Адреса контейнерам назначаются вручную, последней группой: 2001:db8:1a2b:3c4d:1::10, далее …:1::20, …:1::30. Значения произвольны, но их следует записывать: на эти адреса указывают записи DNS и правила фильтрации.

Разметку удобно хранить в документации проекта. Дальнейшие примеры используют приведенный вариант.

Подготовка сервера

Чтобы сервер передавал пакеты между публичным интерфейсом и сетевыми мостами Docker, включается маршрутизация IPv6. Docker задает этот параметр самостоятельно при создании сетей, но указать его явно надежнее.

Создайте файл конфигурации:

cat > /etc/sysctl.d/99-docker-ipv6.conf <<'EOF'
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.default.forwarding = 1
EOF

Примените параметры:

sysctl --system

Проверьте значения:

sysctl net.ipv6.conf.all.forwarding
net.ipv6.conf.all.forwarding = 1

Убедитесь, что маршрут по умолчанию сохранился:

ip -6 route show default
default via fe80::1 dev eth0 metric 1024 onlink pref medium

Настройка Docker

Статья рассчитана на готовое решение Docker: в него входят Ubuntu 24.04, Docker CE и Docker Compose. Проверьте версию на своем сервере:

docker version --format '{{.Server.Version}}'
29.8.0
Обратите внимание!
В версиях до 28.0 контейнер, до адреса которого есть маршрут извне, мог быть доступен на всех портах, которые слушает, независимо от публикации. Начиная с 28.0 снаружи доступны только опубликованные порты. Руководства, написанные раньше, исходят из прежнего поведения, поэтому переносить их команды без изменений не следует.

Поведение Docker настраивается в файле /etc/docker/daemon.json. Полный перечень параметров приведен в документации Docker, в разделе о конфигурации демона. Для описанной схемы достаточно одного параметра:

cat /etc/docker/daemon.json
{
  "ip6tables": true
}

Перезапустите Docker:

systemctl restart docker

Проверьте, что цепочка правил фильтрации создана:

ip6tables -L DOCKER-USER -n
Chain DOCKER-USER (1 references)
target     prot opt source               destination

Цепочка пустая – это нормально, правила в нее добавляются в разделе «Правила фильтрации».

Параметр ip6tables включает управление правилами фильтрации IPv6. В текущих версиях он включен по умолчанию, но указывается явно: при выключенном параметре сети остаются без фильтрации и все порты всех контейнеров становятся доступны из интернета.

Еще три параметра из документации Docker часто встречаются в руководствах по IPv6, но в описанной схеме не используются.

Параметры ipv6 и fixed-cidr-v6 включают IPv6 на стандартной сети bridge – той, к которой подключаются контейнеры, запущенные без указания сети. Публичный адрес такому контейнеру не нужен. Сетям, которые создаются в следующем разделе, эти параметры не требуются.

Параметр default-address-pools задает диапазоны, из которых Docker выбирает подсети для сетей, созданных без указания подсети. В описанной схеме подсеть указывается всегда. При отсутствии пула сеть без указанной подсети получает служебный адрес из диапазона fd00::/8 – по такому адресу сразу видно, что подсеть не была задана.

Если в другом руководстве встречается default-address-pools с подстановкой собственной подсети, обратите внимание на значение base. Пример в документации Docker рассчитан на подсеть /56, из которой нарезаются подсети /64. При переносе на /64 в base попадает вся выделенная подсеть, и первая же автоматически созданная сеть получает подсеть 2001:db8:1a2b:3c4d::/80 со шлюзом 2001:db8:1a2b:3c4d::1. Docker занимает адрес, закрепленный за сервером, и подсеть перестает работать.

Создание сетей

Сети создаются по разметке из раздела «Распределение подсетей». Каждой сети задаются подсеть IPv6, подсеть IPv4 для внутреннего взаимодействия и имя моста. Подставьте собственные подсети:

docker network create web --ipv6 --subnet 2001:db8:1a2b:3c4d:1::/80 --subnet 172.30.1.0/24 -o com.docker.network.bridge.name=br-web -o com.docker.network.bridge.gateway_mode_ipv6=routed
docker network create app --ipv6 --subnet 2001:db8:1a2b:3c4d:2::/80 --subnet 172.30.2.0/24 -o com.docker.network.bridge.name=br-app -o com.docker.network.bridge.gateway_mode_ipv6=routed
docker network create data --internal --ipv6 --subnet 2001:db8:1a2b:3c4d:3::/80 --subnet 172.30.3.0/24 -o com.docker.network.bridge.name=br-data

Назначение параметров:

  • --ipv6 – включает IPv6 в создаваемой сети;
  • --subnet – подсеть из вашей разметки; указывается дважды, для IPv6 и для IPv4;
  • gateway_mode_ipv6=routed – отключает трансляцию адресов для IPv6. Контейнер доступен извне под собственным адресом и на собственном порту, исходящие соединения устанавливаются с адреса контейнера. Для IPv4 сохраняется режим NAT;
  • --internal – сеть без выхода в интернет и недоступная извне. Применяется для баз данных, к которым обращаются только приложения из той же сети. Режим шлюза такой сети не задается;
  • bridge.name – имя сетевого моста. Без него Docker присваивает имена вида br-35e78414358b, что затрудняет чтение маршрутов и правил фильтрации.

Проверьте параметры созданной сети:

docker network inspect web --format '{{json .Options}}'
{"com.docker.network.bridge.gateway_mode_ipv6":"routed","com.docker.network.bridge.name":"br-web"}

Проверьте маршруты:

ip -6 route show | grep br-
2001:db8:1a2b:3c4d:1::/80 dev br-web proto kernel metric 256 linkdown pref medium
2001:db8:1a2b:3c4d:2::/80 dev br-app proto kernel metric 256 linkdown pref medium
2001:db8:1a2b:3c4d:3::/80 dev br-data proto kernel metric 256 linkdown pref medium

Каждой подсети соответствует свой маршрут. Пометка linkdown означает, что в сети пока нет контейнеров, – после их запуска она исчезнет.

Параметры существующей сети не изменяются. Чтобы изменить режим шлюза или подсеть, сеть удаляется и создается заново.

Запуск контейнеров

В примере используется контейнер traefik/whoami: он отвечает на HTTP-запросы сведениями о себе и удобен для проверки. В своем проекте подставьте нужный образ, адрес из своей разметки и порты своего приложения.

docker run -d --name whoami --restart unless-stopped --network web --ip6 2001:db8:1a2b:3c4d:1::10 -p '[::]::80' traefik/whoami

Назначение параметров:

  • --restart unless-stopped – контейнер запускается после перезагрузки сервера. Без этого параметра он остается остановленным;
  • --network – сеть из предыдущего раздела;
  • --ip6 – закрепляет за контейнером конкретный адрес. Без закрепления адрес меняется при каждом пересоздании контейнера, а запись DNS остается прежней;
  • -p '[::]::80' – публикует порт контейнера по IPv6. Запись расшифровывается как [адрес хоста]:[порт хоста]:[порт контейнера]. Порт хоста не указан, поскольку в режиме routed для IPv6 он не используется: входящие подключения направляются непосредственно на IPv6-адрес контейнера.

Значение -p 8080:80 дополнительно создаст проброс по IPv4 через порт 8080 сервера.

Проверьте, что контейнер запущен:

docker ps
CONTAINER ID   IMAGE            COMMAND     CREATED          STATUS         PORTS     NAMES
519c362068b0   traefik/whoami   "/whoami"   11 minutes ago   Up 5 minutes             whoami

Проверьте, как опубликован порт:

docker inspect whoami --format '{{json .NetworkSettings.Ports}}'
{"80/tcp":[{"HostIp":"::","HostPort":""}]}

Пустое значение HostPort подтверждает, что режим routed применен: порт сервера не занят.

Проверьте ответ сервиса с самого сервера:

curl -s "http://[2001:db8:1a2b:3c4d:1::10]/" | head -3
Hostname: 519c362068b0
IP: 127.0.0.1
IP: ::1

Проверка работы

Проверка выполняется с устройства, имеющего адрес IPv6.

Проверьте доступность контейнера извне:

ping -6 -c3 2001:db8:1a2b:3c4d:1::10
64 bytes from 2001:db8:1a2b:3c4d:1::10: icmp_seq=1 ttl=62 time=0.889 ms
--- 2001:db8:1a2b:3c4d:1::10 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2061ms
curl -s "http://[2001:db8:1a2b:3c4d:1::10]/" | head -2
Hostname: 519c362068b0
IP: 127.0.0.1

Отдельно проверяется, с какого адреса контейнер устанавливает исходящие соединения. Запустите на проверочном устройстве наблюдение за трафиком:

tcpdump -ni eth0 'icmp6 and host 2001:db8:1a2b:3c4d:1::60'

На сервере выполните команду, подставив адрес проверочного устройства:

docker run --rm --network web --ip6 2001:db8:1a2b:3c4d:1::60 alpine ping -6 -c3 2001:db8:c0ff:ee::1

В выводе tcpdump на проверочном устройстве появится:

IP6 2001:db8:1a2b:3c4d:1::60 > 2001:db8:c0ff:ee::1: ICMP6, echo request, id 1, seq 0, length 64
IP6 2001:db8:c0ff:ee::1 > 2001:db8:1a2b:3c4d:1::60: ICMP6, echo reply, id 1, seq 0, length 64

Отправитель – адрес контейнера. Если вместо него указан адрес сервера, применяется трансляция адресов и режим routed не был задан.

Выход контейнеров в Интернет:

docker run --rm --network web alpine ping -6 -c3 ipv6.google.com
PING ipv6.google.com (2a00:1450:4008:803::200e): 56 data bytes
64 bytes from 2a00:1450:4008:803::200e: seq=0 ttl=116 time=27.490 ms
64 bytes from 2a00:1450:4008:803::200e: seq=1 ttl=116 time=26.846 ms
64 bytes from 2a00:1450:4008:803::200e: seq=2 ttl=116 time=26.924 ms
--- ipv6.google.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 26.846/27.086/27.490 ms

Защита контейнеров

Контейнер с публичным адресом доступен из интернета напрямую. При работе через трансляцию адресов недоступность непроброшенных портов обеспечивалась самой схемой, здесь за это отвечают правила фильтрации.

Начиная с версии 28.0 Docker блокирует доступ извне к неопубликованным портам контейнеров. Опубликованные порты остаются доступны, в режиме routed – на адресе контейнера. Контейнеры из разных сетей не видят порты друг друга.

Эта защита не покрывает распространенную ситуацию, когда порт публикуется во время отладки и остается опубликованным. Второй уровень настраивается вручную.

UFW для этого не подходит. Утилита обрабатывает трафик, адресованный самому серверу, – цепочки INPUT и OUTPUT. Пакеты к контейнерам проходят транзитом через цепочку FORWARD, куда Docker добавляет собственные правила. Правило вида ufw deny 5432 отображается в выводе ufw status как действующее, но порт контейнера остается доступен. UFW применяется для сервисов самого сервера: SSH, веб-сервера на хосте.

Для контейнеров в документации Docker предусмотрена цепочка DOCKER-USER. Она создана специально для пользовательских правил: Docker обрабатывает ее раньше собственных правил и не очищает при перезапуске.

Правила фильтрации

Правила размещаются в цепочке DOCKER-USER и строятся по принципу разрешительного списка: явно разрешается то, что должно быть доступно извне, остальной трафик к вашей подсети отбрасывается. Набор ниже основан на рекомендациях документации Docker и подходит для большинства проектов – при переносе на свой проект изменяются только значения переменных и перечень разрешенных портов.

Создайте скрипт, подставив собственную подсеть, подсеть сети web и имя публичного интерфейса:

cat /usr/local/sbin/docker-ipv6-firewall.sh
#!/bin/bash
set -e

PREFIX="2001:db8:1a2b:3c4d::/64"
WEB="2001:db8:1a2b:3c4d:1::/80"
WAN="eth0"

# очистить цепочку, чтобы правила не дублировались при повторном запуске
ip6tables -F DOCKER-USER
# пропустить ответы на установленные соединения:
# без этого правила контейнеры не получат ответы на собственные запросы
ip6tables -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
# пропустить ICMPv6: на нем работают поиск соседей и определение размера пакета
ip6tables -A DOCKER-USER -p ipv6-icmp -j RETURN
# не обрабатывать трафик, пришедший не из интернета:
# связь между контейнерами и обращения с самого сервера
ip6tables -A DOCKER-USER ! -i "$WAN" -j RETURN
# разрешить нужные порты – этот блок вы изменяете при добавлении сервисов
ip6tables -A DOCKER-USER -i "$WAN" -d "$WEB" -p tcp --dport 80  -j RETURN
ip6tables -A DOCKER-USER -i "$WAN" -d "$WEB" -p tcp --dport 443 -j RETURN
# отбросить остальной трафик к вашей подсети
ip6tables -A DOCKER-USER -i "$WAN" -d "$PREFIX" -j DROP

Сделайте скрипт исполняемым:

chmod +x /usr/local/sbin/docker-ipv6-firewall.sh

Выполните скрипт – команда применяет правила к текущей конфигурации:

/usr/local/sbin/docker-ipv6-firewall.sh

Проверьте результат:

ip6tables -L DOCKER-USER -n -v --line-numbers
Chain DOCKER-USER (1 references)
num   pkts bytes target     prot opt in     out    source   destination
1        0     0 RETURN     0    --  *      *      ::/0     ::/0          ctstate RELATED,ESTABLISHED
2        0     0 RETURN     58   --  *      *      ::/0     ::/0
3        0     0 RETURN     0    --  !eth0  *      ::/0     ::/0
4        0     0 RETURN     6    --  eth0   *      ::/0     2001:db8:1a2b:3c4d:1::/80  tcp dpt:80
5        0     0 RETURN     6    --  eth0   *      ::/0     2001:db8:1a2b:3c4d:1::/80  tcp dpt:443
6        0     0 DROP       0    --  eth0   *      ::/0     2001:db8:1a2b:3c4d::/64

Шесть правил в том же порядке, что и в скрипте. Столбец pkts показывает количество обработанных пакетов – он используется при проверке в разделе «Проверка защиты».

Назначение правил:

  • правило 1 пропускает ответы на уже установленные соединения. Это единственный способ сопоставить трафик с исходным запросом: к моменту, когда пакет доходит до цепочки DOCKER-USER, он уже прошел трансляцию адресов, и исходный адрес назначения в нем недоступен. Без этого правила контейнеры не получат ответы на собственные запросы;
  • правило 2 пропускает ICMPv6. В IPv6 на этом протоколе работают поиск соседей – аналог ARP – и определение размера пакета на маршруте. При его блокировке возникают трудно диагностируемые сбои: небольшие страницы загружаются, а крупные файлы передаются не полностью;
  • правило 3 возвращает без обработки трафик, пришедший не с публичного интерфейса. Это связь между контейнерами и обращения с самого сервера – ограничивать их не требуется;
  • правила 4 и 5 разрешают порты 80 и 443 в сети web. Этот блок вы изменяете при добавлении сервисов: адрес подсети берется из вашей разметки, порты – из требований приложения;
  • правило 6 отбрасывает остальной трафик, адресованный вашей подсети. Именно оно закрывает порты, опубликованные по ошибке. Обратите внимание: правило ограничено вашей подсетью, поэтому транзитные пакеты и трафик к другим адресам под него не попадают.

Порядок правил имеет значение: они просматриваются сверху вниз до первого совпадения. Поэтому в скрипте используется ключ -A, добавляющий правило в конец цепочки. При использовании ключа -I, вставляющего правило в начало, порядок оказался бы обратным и последнее правило отбросило бы весь трафик.

Чтобы открыть доступ к новому сервису, добавьте строку в блок разрешенных портов и выполните скрипт повторно.

Доступ можно ограничить по адресу источника. Например, панель администрирования на порту 9000, доступная только из вашей домашней сети:

ip6tables -A DOCKER-USER -i eth0 -s 2001:db8:c0ff:ee::/64 -d 2001:db8:1a2b:3c4d:2::/80 -p tcp --dport 9000 -j RETURN

Домашним подключениям обычно выделяется постоянная подсеть IPv6, поэтому такое правило не требует регулярного обновления.

Сохранение правил

Правила цепочки DOCKER-USER не сохраняются автоматически. Пакет iptables-persistent для этой задачи не подходит: сохраненные правила восстанавливаются до запуска Docker, который затем пересоздает собственные цепочки. Скрипт должен выполняться после запуска Docker – для этого создается юнит systemd:

cat /etc/systemd/system/docker-ipv6-firewall.service
[Unit]
Description=Custom DOCKER-USER rules for IPv6
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/docker-ipv6-firewall.sh

[Install]
WantedBy=multi-user.target

Активируйте юнит:

systemctl daemon-reload
systemctl enable --now docker-ipv6-firewall.service

Проверьте состояние:

systemctl status docker-ipv6-firewall --no-pager
● docker-ipv6-firewall.service - Custom DOCKER-USER rules for IPv6
     Loaded: loaded (/etc/systemd/system/docker-ipv6-firewall.service; enabled; preset: enabled)
     Active: active (exited) since Tue 2026-09-15 10:36:57 UTC; 12s ago
    Process: 1892 ExecStart=/usr/local/sbin/docker-ipv6-firewall.sh (code=exited, status=0/SUCCESS)

Состояние active (exited) и код 0/SUCCESS означают, что скрипт отработал без ошибок.

Параметры, отключающие защиту

В документации Docker описаны параметры, упрощающие доступ к контейнерам извне. Они снимают фильтрацию и в описанной схеме не применяются.

  • allow-direct-routing в daemon.json – отключает фильтрацию пакетов, приходящих извне и адресованных напрямую контейнерам, для всех сетей сервера;
  • gateway_mode_ipv6=nat-unprotected – снимает фильтрацию портов для выбранной сети: из интернета становится доступен каждый порт, который слушают контейнеры этой сети;
  • "ip6tables": false – не даёт Docker создавать большую часть правил фильтрации.

Если во время отладки требуется доступ к дополнительному порту, разрешите этот порт в скрипте.

Проверка защиты

Проверка выполняется только с внешнего устройства. С самого сервера результат недостоверен: порты контейнеров не отображаются в выводе ss -tlnp, поскольку открыты не на адресе сервера.

Запустите три проверочных контейнера: без публикации портов, с намеренно опубликованным лишним портом и в изолированной сети.

docker run -d --name redis-closed --restart unless-stopped --network web --ip6 2001:db8:1a2b:3c4d:1::50 redis:7
docker run -d --name redis-open --restart unless-stopped --network web --ip6 2001:db8:1a2b:3c4d:1::51 -p '[::]::6379' redis:7
docker run -d --name pg-test --restart unless-stopped --network data --ip6 2001:db8:1a2b:3c4d:3::10 -e POSTGRES_PASSWORD=testonly postgres:16

Убедитесь, что все контейнеры находятся в состоянии Up:

docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED          STATUS          PORTS      NAMES
463586d2e2df   postgres:16      "docker-entrypoint.s…"   6 seconds ago    Up 5 seconds    5432/tcp   pg-test
eedc942e94de   redis:7          "docker-entrypoint.s…"   53 seconds ago   Up 52 seconds              redis-open
aaebd40b5794   redis:7          "docker-entrypoint.s…"   1 minute ago     Up 1 minute     6379/tcp   redis-closed
519c362068b0   traefik/whoami   "/whoami"                1 minute ago     Up 1 minute                whoami

Если контейнер не запустился, сканирование покажет закрытые порты из-за отсутствия сервиса по адресу, и результат будет истолкован неверно.

Обнулите счетчики правил – повторный запуск скрипта очищает цепочку и добавляет правила заново:

/usr/local/sbin/docker-ipv6-firewall.sh

На проверочном устройстве установите nmap:

apt install -y nmap

Проверьте сервис, который должен быть доступен:

nmap -6 -Pn -p 80,443,6379 2001:db8:1a2b:3c4d:1::10
Nmap scan report for 2001:db8:1a2b:3c4d:1::10
Host is up (0.00097s latency).
PORT     STATE    SERVICE
80/tcp   open     http
443/tcp  filtered https
6379/tcp filtered redis

Открыт только порт 80. Порт 443 разрешен правилами, но его не публиковал ни один контейнер, поэтому доступ заблокирован средствами Docker. Уровней защиты два, и разрешение в правилах само по себе порт не открывает.

Проверьте контейнер без публикации портов:

nmap -6 -Pn -p 80,443,6379 2001:db8:1a2b:3c4d:1::50
Nmap scan report for 2001:db8:1a2b:3c4d:1::50
Host is up.
PORT     STATE    SERVICE
80/tcp   filtered http
443/tcp  filtered https
6379/tcp filtered redis

Проверьте контейнер с опубликованным портом 6379:

nmap -6 -Pn -p 80,443,6379 2001:db8:1a2b:3c4d:1::51
Nmap scan report for 2001:db8:1a2b:3c4d:1::51
Host is up.
PORT     STATE    SERVICE
80/tcp   filtered http
443/tcp  filtered https
6379/tcp filtered redis

Это основная проверка. Порт 6379 опубликован, Docker его открыл, и недоступным он остается только благодаря правилам фильтрации. Так воспроизводится ситуация, когда порт публикуется при отладке и остается опубликованным.

Проверьте контейнер в изолированной сети:

nmap -6 -Pn -p 5432 2001:db8:1a2b:3c4d:3::10
Nmap scan report for 2001:db8:1a2b:3c4d:3::10
Host is up.
PORT     STATE    SERVICE
5432/tcp filtered postgresql

Состояние filtered, в отличие от closed, означает, что пакет отброшен без ответа.

Сразу после сканирования посмотрите счетчики на сервере:

ip6tables -L DOCKER-USER -n -v --line-numbers
num   pkts bytes target     prot opt in     out    source   destination
1       11  1237 RETURN     0    --  *      *      ::/0     ::/0          ctstate RELATED,ESTABLISHED
2        0     0 RETURN     58   --  *      *      ::/0     ::/0
3        0     0 RETURN     0    --  !eth0  *      ::/0     ::/0
4        6   400 RETURN     6    --  eth0   *      ::/0     2001:db8:1a2b:3c4d:1::/80  tcp dpt:80
5        6   384 RETURN     6    --  eth0   *      ::/0     2001:db8:1a2b:3c4d:1::/80  tcp dpt:443
6        8   512 DROP       0    --  eth0   *      ::/0     2001:db8:1a2b:3c4d::/64

Восемь пакетов в шестом правиле – обращения к портам 6379 и 5432, каждое из которых сканер повторил из-за отсутствия ответа. Шесть пакетов в четвертом правиле – сканирование порта 80 по трем адресам. Одиннадцать пакетов в первом правиле – обратный трафик.

Ненулевой счетчик шестого правила подтверждает, что порты закрыты правилами, а не отсутствием сервиса по адресу. При нулевом значении проверку следует повторить.

Изоляция внутренней сети проверяется изнутри контейнера:

docker exec pg-test sh -c 'timeout 5 getent hosts ipv6.google.com' || echo "изоляция работает"
изоляция работает

Проверка после перезагрузки

Перезагрузите сервер командой reboot или через панель управления, раздел управления виртуальным сервером.

После подключения убедитесь, что контейнеры запустились:

docker ps
CONTAINER ID   IMAGE            COMMAND                  CREATED         STATUS              PORTS      NAMES
463586d2e2df   postgres:16      "docker-entrypoint.s…"   6 minutes ago   Up About a minute   5432/tcp   pg-test
eedc942e94de   redis:7          "docker-entrypoint.s…"   6 minutes ago   Up About a minute              redis-open
aaebd40b5794   redis:7          "docker-entrypoint.s…"   7 minutes ago   Up About a minute   6379/tcp   redis-closed
519c362068b0   traefik/whoami   "/whoami"                7 minutes ago   Up About a minute              whoami

Проверьте, что правила восстановлены:

ip6tables -L DOCKER-USER -n | wc -l
8

Восемь строк – это заголовок цепочки, строка с названиями столбцов и шесть правил. Счетчики после перезагрузки нулевые.

Проверьте, что первый адрес подсети на месте и мосты пересозданы:

ip -6 addr show dev eth0 | grep global
    inet6 2001:db8:1a2b:3c4d::1/64 scope global
ip -6 route show | grep br-
2001:db8:1a2b:3c4d:1::/80 dev br-web proto kernel metric 256 pref medium
2001:db8:1a2b:3c4d:2::/80 dev br-app proto kernel metric 256 linkdown pref medium
2001:db8:1a2b:3c4d:3::/80 dev br-data proto kernel metric 256 pref medium

Пометка linkdown осталась только у сети app, в которой нет контейнеров.

Повторите сканирование с проверочного устройства и сравните результат с предыдущим – он должен совпасть. После этого удалите проверочные контейнеры:

docker rm -f redis-closed redis-open pg-test

Список адресов работающих контейнеров выводится командой:

docker ps -q | xargs -I{} docker inspect {} --format '{{.Name}}{{range .NetworkSettings.Networks}} {{.GlobalIPv6Address}}{{end}}'
/whoami 2001:db8:1a2b:3c4d:1::10

Периодическое сканирование этих адресов позволяет вовремя обнаружить порт, открытый при отладке и оставшийся открытым.

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

Перестала работать вся подсеть

Проверьте, находится ли первый адрес подсети на публичном интерфейсе: ip -6 addr show dev eth0. Без него подсеть не работает целиком. Чаще всего адрес теряется при попытке передать подсеть мосту Docker или изменить префикс адреса.

Новая сеть получила подсеть, шлюзом которой стал первый адрес подсети

В параметре default-address-pools указана вся выделенная подсеть. Уберите пул IPv6 из daemon.json и указывайте подсеть каждой сети явно.

Контейнер получил адрес, начинающийся с fd00

Сеть создана без указания подсети. Укажите параметр --subnet; существующую сеть потребуется пересоздать.

Маршрут сети помечен linkdown

В сети нет запущенных контейнеров. После запуска первого контейнера пометка исчезает.

После перезагрузки сервисы недоступны, хотя правила фильтрации есть

Контейнеры запущены без параметра --restart unless-stopped. При проверке защиты это особенно опасно: сканирование покажет закрытые порты, и результат можно принять за работающую фильтрацию.

Правило добавлено в UFW, но порт контейнера остается открытым

UFW не обрабатывает транзитный трафик к контейнерам. Используйте цепочку DOCKER-USER.

Команда ip6tables -L выводит пустые цепочки, хотя контейнеры работают

Либо в daemon.json задано "ip6tables": false, либо включен режим работы через nftables. В последнем случае правила из DOCKER-USER не применяются.

Сканирование показывает открытый порт базы данных.

Порт опубликован явно, проверьте вывод docker inspect. Уберите публикацию и подключите контейнер к сети, созданной с параметром --internal.

Если возникнут вопросы, напишите нам, пожалуйста, тикет из панели управления аккаунта (раздел “Помощь и поддержка”), а если вы захотите обсудить эту статью или наши продукты с коллегами по цеху и сотрудниками Beget – ждем вас в нашем сообществе в Telegram.