Как Docker маршрутизирует трафик: базовая модель
Docker не изобретает собственный протокол маршрутизации. Он собирает стандартные механизмы ядра Linux в предсказуемую схему: network namespace, veth-пары, виртуальный мост и правила iptables. Контейнер получает изолированный сетевой стек с интерфейсом eth0, хост видит парный интерфейс vethXXXX, а связывает их мост docker0 с адресом 172.17.0.1/16.
Путь пакета от контейнера к внешнему адресу: eth0 внутри namespace, парный veth на хосте, мост docker0, таблица маршрутизации хоста, цепочка POSTROUTING с MASQUERADE, физический интерфейс, внешняя сеть. Обратный трафик к опубликованным портам проходит через DNAT в цепочке DOCKER nat-таблицы, а ответы на исходящие соединения возвращаются по записи в conntrack.
Между интерфейсами ядро передаёт пакеты только при включённом форвардинге. Проверка одной командой: sysctl net.ipv4.ip_forward. Ожидаемое значение 1. Включается на лету через sysctl -w net.ipv4.ip_forward=1 и закрепляется строкой net.ipv4.ip_forward = 1 в файле /etc/sysctl.d/99-docker.conf. Если параметр равен 0, контейнер получает адрес и шлюз, но наружу не выходит.
Что такое docker0 и veth-пары
docker0 это виртуальный программный мост, который Docker Engine создаёт при старте службы. Он существует только в стеке хоста и не имеет отношения к физическому коммутатору. Посмотреть его состояние можно командами ip link show docker0 и ip addr show docker0. В типовой установке интерфейс поднят, имеет адрес 172.17.0.1/16 и флаг UP.
veth-пара это два виртуальных интерфейса, соединённых друг с другом напрямую: всё, что входит в один, выходит из второго. Один конец помещается в network namespace контейнера и получает имя eth0, второй остаётся на хосте и подключается к мосту docker0. Имена на хосте выглядят как veth1a2b3c@if7, где число после @ это индекс интерфейса в чужом namespace. Полный список пар видно командой ip link show type veth, а привязку к мосту и адрес контейнера показывает docker network inspect bridge.
Из этого устройства следуют два практических вывода. Первый: у каждого контейнера свой стек, свои маршруты и свой файл resolv.conf. Второй: для трафика внутри одной bridge-сети NAT не нужен, пакет идёт от одного veth к другому через мост и не покидает хост.
Роль iptables и NAT в маршрутизации Docker
Docker создаёт собственные цепочки, чтобы не смешивать свои правила с пользовательскими. В filter-таблице появляются DOCKER-USER, DOCKER-ISOLATION-STAGE-1, DOCKER-ISOLATION-STAGE-2 и DOCKER, в nat-таблице DOCKER и правила в POSTROUTING. Ключевое правило маскарада выглядит так: -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE. Адрес источника подменяется на адрес хоста, поэтому внешние сети не видят 172.17.0.2 и не должны знать о контейнерных подсетях.
На дистрибутивах, где ядро и пакеты перешли на nftables, Docker создаёт цепочки nft с той же логикой. Вывод iptables -L на таких системах может выглядеть пустым или показывать трансляцию через слой совместимости. Проверить backend просто: iptables --version отвечает nf_tables или legacy. Для nftables полный набор правил покажет nft list ruleset.
Ручная чистка этих цепочек ломает связность. Команды iptables -F и iptables -t nat -F удаляют правила Docker целиком: контейнеры теряют доступ во внешнюю сеть, перестают видеть друг друга через публикацию портов, а иногда и вовсе остаются без DNS-ответов. Для собственных ограничений используйте цепочку DOCKER-USER, которую Docker не пересоздаёт.
Режимы сети Docker и их влияние на маршрутизацию
Режим сети определяет, нужен ли NAT, виден ли контейнер из локальной сети и работает ли схема в пределах одного хоста. Взаимодействие контейнеров с сетями и внешними сервисами разбирается в учебных программах по инструментам промышленной разработки, например в курсе НИУ ВШЭ (hse.ru), но на практике выбор режима диктует конкретная задача: веб-сервис, база данных, VPN-шлюз или кластер из нескольких машин.
| Режим | NAT | Виден извне | Мультихост | Типовой сценарий |
|---|---|---|---|---|
| bridge (default) | Да | Только через -p | Нет | Простые веб-сервисы на одном хосте |
| user-defined bridge | Да | Только через -p | Нет | Связка из нескольких контейнеров с DNS по именам |
| host | Нет | Напрямую, порты хоста | Нет | Высоконагруженные сетевые приложения |
| none | Нет | Нет | Нет | Изолированные задачи без сети |
| container | Как у соседа | Как у соседа | Нет | Sidecar-контейнеры |
| overlay | Да, плюс VXLAN | Через -p на узле | Да | Docker Swarm, распределённые сервисы |
| macvlan / ipvlan | Нет | Напрямую по IP в LAN | Зависит от сети | Контейнер с адресом физической сети |
Bridge и пользовательские сети: в чём разница
Default bridge подходит для быстрых экспериментов и почти не годится для продакшена. Встроенный DNS по именам контейнеров в нём не работает, связывать сервисы приходится через устаревший --link или по IP, а адреса выдаются из общей подсети 172.17.0.0/16. Пользовательская bridge-сеть даёт три преимущества: DNS-сервер Docker на 127.0.0.11 отвечает по именам контейнеров, каждый проект получает изолированную подсеть, а параметры моста задаются явно.
Создание сети с контролируемыми параметрами: docker network create --subnet=10.10.0.0/24 --gateway=10.10.0.1 mynet. Контейнеры в одной такой сети общаются напрямую через мост, без NAT и без публикации портов. Сравнение драйверов и сценарии их применения для продакшена собраны в материале о сетях bridge, host, overlay и macvlan.
Host, none и container: когда маршрутизация не нужна
В режиме host контейнер использует сетевые интерфейсы хоста напрямую: docker run --network host nginx. Отдельного namespace с eth0 нет, NAT не применяется, маршруты берутся из таблицы хоста. Выигрыш заметен на сетевых приложениях с большим числом коротких соединений, где цепочки iptables и docker-proxy добавляют задержку. Плата за это - потеря изоляции и конфликты портов: два контейнера не смогут слушать один и тот же порт.
Режим none оставляет контейнеру только loopback. Он полезен для офлайн-обработки данных, сборки артефактов и задач, которым сеть не нужна вовсе. Режим container подключает новый контейнер к namespace уже запущенного: docker run --network container:c1 logger. Порты, маршруты и адрес у них общие, что удобно для sidecar-схем, например для сборщика логов или прокси.
Поведение host различается между платформами. На Linux контейнер видит реальные интерфейсы сервера. В Docker Desktop для macOS и Windows он получает интерфейсы виртуальной машины, поэтому ожидания по доступности сервиса из локальной сети могут не совпасть.
Overlay, macvlan и ipvlan для мультихоста и LAN
Overlay нужен, когда контейнеры расположены на разных машинах. Трафик инкапсулируется в VXLAN и передаётся по UDP на порт 4789, служебные данные Swarm идут через 7946/tcp и 7946/udp. Сеть создаётся командой docker network create --driver overlay --attachable mynet и работает в кластере Swarm. Адресация внутри overlay не пересекается с физической сетью, а наружу трафик выходит через NAT на узле.
macvlan решает обратную задачу: контейнер получает MAC и IP из физической сети и становится её равноправным участником. Пример: docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 -o parent=eth0 macnet. Маршрут до контейнера строит коммутатор, NAT не участвует, публикация портов через -p не нужна. Ограничения два: порт коммутатора должен пропускать несколько MAC-адресов, а хосту не всегда доступен контейнер через тот же родительский интерфейс.
ipvlan в режимах L2 и L3 использует один MAC на все контейнеры и позволяет маршрутизировать трафик между подсетями. В Kubernetes аналогичные задачи решают CNI-плагины, о чём ниже.
Пошаговая настройка маршрутизации для контейнеров
Сценарий ниже воспроизводится на любом Linux-хосте с Docker Engine. Проверяйте конфигурацию на стенде: неверная подсеть или лишний маршрут легко нарушают связность рабочего сервера.
- Проверьте форвардинг: sysctl net.ipv4.ip_forward. Значение должно быть 1.
- Убедитесь, что выбранная подсеть не пересекается с локальной сетью: ip route и ip addr show.
- Создайте пользовательскую bridge-сеть с явными параметрами.
- Запустите два контейнера в этой сети.
- Проверьте связность между контейнерами, доступ во внешнюю сеть и маршруты.
- Добавьте статический маршрут, если схема выходит за пределы одной подсети.
Создание пользовательской bridge-сети с заданной подсетью
Команда с явным именем моста упрощает диагностику: в выводе ip link вы сразу увидите br-custom, а не случайный br-1a2b3c4d5e6f.
docker network create --driver bridge --subnet=10.20.0.0/24 --gateway=10.20.0.1 --opt com.docker.network.bridge.name=br-custom mynet
Проверка: docker network inspect mynet показывает драйвер, подсеть, шлюз и список подключённых контейнеров, а ip addr show br-custom подтверждает адрес 10.20.0.1 на хосте. Подробный разбор параметров подсети, шлюза и статических адресов дан в руководстве по настройке пользовательской bridge-сети.
Подключение контейнеров и проверка маршрутов
Запуск двух контейнеров: docker run -d --name c1 --network mynet nginx и docker run -d --name c2 --network mynet nginx. Имена разрешаются автоматически: docker exec c1 ping -c 2 c2 отвечает адресом вида 10.20.0.3.
Далее проверяется выход наружу и таблица маршрутизации:
- docker exec c1 ping -c 2 8.8.8.8 - проверка маршрута по умолчанию и NAT.
- docker exec c1 ip route - ожидаемый ответ: default via 10.20.0.1 dev eth0 и строка 10.20.0.0/24 dev eth0.
- docker exec c1 cat /etc/resolv.conf - адрес DNS-сервера 127.0.0.11 и внешние резолверы.
- ip route show | grep 10.20.0.0 на хосте - маршрут вида 10.20.0.0/24 dev br-custom proto kernel scope link src 10.20.0.1.
Если контейнер видит соседа по имени, но не пингует 8.8.8.8, проблема почти всегда в форвардинге или в отсутствующем правиле MASQUERADE, а не в самом мосте.
Добавление статического маршрута на хосте или в контейнере
Статический маршрут нужен, когда за Docker-хостом есть ещё одна подсеть, например туннель до филиала или вторая сеть стенда. На хосте маршрут до контейнерной подсети другого сервера добавляется так: ip route add 10.30.0.0/24 via 10.20.0.1 dev br-custom. Внутри контейнера аналогичная команда выглядит как ip route add 10.30.0.0/24 via 10.20.0.1, но требует capabilities NET_ADMIN: docker run --cap-add=NET_ADMIN ...
Маршруты внутри контейнера не сохраняются после перезапуска: namespace создаётся заново. Рабочих вариантов два. Первый: задать маршрут на хосте, где он живёт постоянно. Второй: добавить скрипт инициализации в образ или entrypoint. Для постоянных маршрутов на хосте используйте systemd-networkd, netplan или NetworkManager, а не ручной ip route add. Практические схемы статической адресации и прямой маршрутизации с хоста разобраны в материале о продвинутой маршрутизации в Docker и Compose.
Главный риск на этом шаге - пересечение подсетей. Если корпоративная сеть уже использует 10.20.0.0/24, Docker-сеть с теми же адресами приведёт к странным сбоям: часть узлов отвечает, часть теряет пакеты. Перед созданием сети сверьтесь с таблицей маршрутизации и схемой сети предприятия.
Доступ к контейнерам извне: публикация портов и прямой маршрут
Клиенты из внешней сети не видят контейнерные адреса напрямую. Есть два пути: публикация порта через NAT или выдача контейнеру адреса в физической сети через macvlan и ipvlan.
Как работает -p и DNAT
Флаг -p 8080:80 создаёт правило в nat-таблице: -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80. Пакет, пришедший на порт 8080 любого интерфейса хоста, переписывается на адрес контейнера. Проверить набор правил можно командой iptables -t nat -L DOCKER -n -v.
Одновременно Docker запускает процесс docker-proxy, который слушает порт в пространстве пользователя и пересылает соединения. Отключение userland-proxy=false убирает этот процесс и оставляет маршрутизацию целиком на iptables, что снижает накладные расходы при большом числе соединений. На Linux такой режим работает штатно, но проверяйте его на стенде: часть сценариев с обращением к localhost с самого хоста может потребовать дополнительных правил.
Альтернатива без NAT: контейнер в macvlan-сети получает адрес вида 192.168.1.50 и доступен всем участникам LAN напрямую. Порты публиковать не нужно, а балансировка и фильтрация выполняются на коммутаторе или на маршрутизаторе.
Взаимодействие с firewalld и ufw
Docker вставляет свои правила в цепочки DOCKER, которые обрабатываются раньше пользовательских. Из этого следует неприятный эффект: порт, опубликованный через -p, доступен из сети даже при включённом ufw с политикой deny. Свои ограничения добавляйте в DOCKER-USER, например: iptables -I DOCKER-USER -i eth0 -p tcp --dport 8080 -j ACCEPT, а трафик на остальные порты контейнеров закрывайте отдельными правилами DROP.
Для ufw удобный способ - дописать блок в /etc/ufw/after.rules, чтобы цепочка DOCKER-USER создавалась при загрузке правил. Firewalld управляет зонами, и контейнерный трафик попадает в зону docker, поэтому проверку делайте после каждого перезапуска firewall: iptables -S DOCKER-USER и iptables -t nat -L DOCKER -n.
Диагностика типичных проблем со связностью
Порядок проверок от простого к сложному экономит время. Сначала убедитесь, что контейнер в нужной сети и получил адрес, затем проверяйте маршруты, после - правила трансляции адресов и DNS, и лишь в конце беритесь за MTU.
- sysctl net.ipv4.ip_forward - форвардинг включён.
- docker inspect --format '{{json .NetworkSettings.Networks}}' c1 - адрес, шлюз, имя сети.
- docker exec c1 ip route - маршрут по умолчанию указывает на шлюз моста.
- iptables -t nat -L POSTROUTING -n -v - правило MASQUERADE для контейнерной подсети на месте.
- iptables -t nat -L DOCKER -n -v - правила DNAT для опубликованных портов.
- docker exec c1 cat /etc/resolv.conf - DNS-сервер отвечает.
- Проверка MTU: ping -M do -s 1472 8.8.8.8.
Контейнер не видит внешнюю сеть
Симптом: ping между контейнерами проходит, ping внешнего адреса и установка пакетов зависают. Причины по частоте: net.ipv4.ip_forward равен 0, отсутствует правило MASQUERADE в POSTROUTING, маршрут по умолчанию в контейнере указывает не на шлюз bridge-сети. Проверка: docker exec c1 ip route, docker exec c1 ping 8.8.8.8, iptables -t nat -L POSTROUTING -n -v. Отсутствие счётчиков у правила MASQUERADE означает, что трафик до него не доходит: смотрите таблицу маршрутизации хоста и FORWARD-цепочку.
Внешняя сеть не видит контейнер
Симптом: с хоста сервис открывается по localhost:8080, а с другого компьютера нет. Проверьте правило DNAT в цепочке DOCKER, а затем то, на каком адресе слушает приложение внутри контейнера. Если процесс занял 127.0.0.1, он не примет трафик, переписанный на адрес eth0 контейнера: приложение обязано слушать 0.0.0.0. Полезные команды: ss -tlnp | grep 8080 на хосте, iptables -t nat -L DOCKER -n -v, а также проверка облачных security groups и сетевых ACL, которые стоят вне сервера.
Конфликт подсетей и проблемы MTU
Конфликт подсетей проявляется плавающими сбоями: часть соединений проходит, часть обрывается. Причина в том, что адреса 172.17.0.0/16 или выбранной вами подсети уже заняты в LAN или VPN. Решение - изменить пул адресов Docker в /etc/docker/daemon.json, задав default-address-pools с базой 10.99.0.0/16 и размером сети 24, после чего перезапустить службу docker. Перезапуск пересоздаст docker0 и правила iptables.
Проблемы MTU выглядят иначе: небольшие запросы и рукопожатия проходят, крупные ответы зависают. Так бывает в overlay-сетях, VPN и в облаках с туннелями. Проверка: ping -M do -s 1472 8.8.8.8 (внутри контейнера, если утилита установлена). Решение: задать меньший MTU при создании сети, например через --opt com.docker.network.driver.mtu=1400, или настроить его в daemon.json для всех сетей сразу.
Маршрутизация Docker в Kubernetes: что меняется
Принципы остаются: тот же network namespace, те же veth-пары, тот же NAT для исходящего трафика. Меняется управляющий слой: сети создаёт не Docker Engine, а CNI-плагин. Переход от контейнеров к оркестратору разобран в курсе по Kubernetes (youtube.com).
CNI и маршрутизация между подами
Каждому поду CNI-плагин выдаёт IP из Pod CIDR, создаёт veth-пару и прописывает маршрут на ноде. Дальше схемы расходятся: Calico анонсирует подсети подов по BGP и маршрутизирует трафик на уровне L3, Flannel инкапсулирует его в VXLAN, Cilium использует eBPF и умеет обходиться без части iptables-правил. Общее правило: маршруты между нодами настраивает плагин, и вмешиваться в них вручную не нужно.
kube-proxy и Service: DNAT на стероидах
kube-proxy создаёт правила для Service трёх типов: ClusterIP, NodePort и LoadBalancer. По сути это тот же DNAT, что и при публикации портов в Docker, только цели выбираются из набора подов, а правила пересобираются при изменении объектов кластера. Проверить режим можно командой kubectl get configmap kube-proxy -n kube-system -o yaml. Режим IPVS эффективнее iptables при сотнях сервисов, поскольку не перебирает цепочки линейно.
При миграции с Docker Compose на Kubernetes не переносите вручную старые iptables-правила. CNI-плагин ожидает, что цепочки находятся под его контролем, а лишние правила дают трудноуловимые сбои. Если нужен управляемый кластер, подойдёт облачная инфраструктура вроде Timeweb Cloud с готовым Kubernetes и сетевой конфигурацией, где CNI уже настроен.
Типичные ошибки и как их избежать
- Ручной flush iptables. Команды iptables -F или iptables -t nat -F удаляют правила Docker. Связность контейнеров пропадает мгновенно. Лечение: systemctl restart docker, служба пересоздаёт docker0 и цепочки.
- Default bridge в продакшене. Нет DNS по именам и изоляции между проектами. Правильно: отдельная пользовательская сеть на каждый стек сервисов.
- Пересечение подсетей с LAN. Самая частая причина плавающих сбоев. Проверяйте диапазоны до создания сети и меняйте default-address-pools при необходимости.
- Отключение userland-proxy без проверки. Ставится осознанно, после теста на стенде, иначе ломаются отдельные сценарии доступа к localhost.
- Игнорирование MTU в overlay и VPN. Симптомы похожи на потерю пакетов, а причина в размере кадра. Решение: com.docker.network.driver.mtu=1400 или значение, согласованное с провайдером.
- Перезапуск Docker без сохранения пользовательских маршрутов. Маршруты, добавленные вручную, исчезают. Храните их в конфигурации системы.
- Публикация портов без firewall. -p открывает порт на всех интерфейсах в обход правил ufw. Ограничения добавляйте в DOCKER-USER.
Опасные операции с iptables и Docker
Критичный набор команд, которых стоит избегать на рабочем сервере: iptables -F, iptables -X, iptables -t nat -F, iptables -P FORWARD DROP без анализа правил Docker. Если политика FORWARD становится DROP, контейнеры теряют доступ наружу, хотя локальная сеть между ними продолжает работать. Собственные правила добавляйте только в DOCKER-USER и не трогайте цепочки DOCKER и DOCKER-ISOLATION-STAGE-*.
Как откатить изменения и восстановить сеть
Перед экспериментами сохраните текущее состояние: iptables-save > /root/iptables-backup.rules и iptables-restore при необходимости вернуть правила. Перезапуск службы docker пересоздаёт docker0, veth-пары и правила трансляции адресов. Если связность не возвращается, проверьте /etc/docker/daemon.json на синтаксические ошибки, затем удалите проблемные пользовательские сети командой docker network rm и подключите контейнеры заново. Сравнение драйверов и разбор типового поведения моста и overlay-сетей собраны в статье об устройстве сетевых драйверов Docker.
Проверка конфигурации и адаптация под версию Docker
Имена цепочек, поведение прокси и backend трансляции адресов зависят от версии Engine и дистрибутива. Пройдите по короткому чек-листу перед тем, как переносить команды из статьи на свой сервер.
Как определить, что изменилось в вашей версии
- docker version - точная версия клиента и демона.
- docker info | grep -i iptables - какой backend трансляции использует демон.
- docker info | grep -i proxy - значение userland-proxy.
- iptables --version - nf_tables или legacy в системе.
- nft list ruleset - полный набор правил там, где работает nftables.
В ветках Docker Engine 20.10 и 24+ поведение прокси по умолчанию и способ интеграции с nftables различаются, поэтому конкретные значения сверяйте с официальной документацией вашей версии. Полезно фиксировать рабочую конфигурацию в репозитории: файл daemon.json, compose-файлы и сохранённые правила iptables дают воспроизводимость при пересборке стенда.
Особенности Docker Desktop и альтернативных сред
Docker Desktop на macOS и Windows запускает контейнеры в виртуальной машине LinuxKit. Команды ip route и iptables на хосте не показывают контейнерные сети, потому что их там нет. Чтобы увидеть маршруты внутри среды, выполните docker run --rm --net=host alpine ip route или переключитесь в нужный контекст через docker context ls и docker context use. В Windows с WSL2 дистрибутив работает в отдельной сети, и проброс портов между Windows и WSL идёт своим механизмом, что стоит учитывать при отладке доступа из локальной сети.
Практический порядок для любого окружения: зафиксируйте текущие правила iptables, создайте пользовательскую сеть с непересекающейся подсетью, проверьте маршруты внутри контейнера и на хосте, и только после этого публикуйте порты и открывайте доступ извне. Такой порядок исключает большинство сбоев связности ещё до того, как они попадут в прод.