В этой статье мы разберем, как выдать контейнерам 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 mediumping -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Поведение 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=routeddocker 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=routeddocker 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 2061mscurl -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:7docker run -d --name redis-open --restart unless-stopped --network web --ip6 2001:db8:1a2b:3c4d:1::51 -p '[::]::6379' redis:7docker 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 globalip -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 и указывайте подсеть каждой сети явно.
Сеть создана без указания подсети. Укажите параметр --subnet; существующую сеть потребуется пересоздать.
В сети нет запущенных контейнеров. После запуска первого контейнера пометка исчезает.
Контейнеры запущены без параметра --restart unless-stopped. При проверке защиты это особенно опасно: сканирование покажет закрытые порты, и результат можно принять за работающую фильтрацию.
UFW не обрабатывает транзитный трафик к контейнерам. Используйте цепочку DOCKER-USER.
Либо в daemon.json задано "ip6tables": false, либо включен режим работы через nftables. В последнем случае правила из DOCKER-USER не применяются.
Порт опубликован явно, проверьте вывод docker inspect. Уберите публикацию и подключите контейнер к сети, созданной с параметром --internal.
Если возникнут вопросы, напишите нам, пожалуйста, тикет из панели управления аккаунта (раздел “Помощь и поддержка”), а если вы захотите обсудить эту статью или наши продукты с коллегами по цеху и сотрудниками Beget – ждем вас в нашем сообществе в Telegram.