Этой весной я потратил большую часть утра на поиски контейнера, который не мог разрешить ни одного внутреннего имени, хотя браузер рядом открывал вики без жалоб. Туннель был поднят, браузер это доказывал, а контейнер находился в другом сетевом пространстве имён, с другой таблицей маршрутов и другим резолвером. Далее — порядок, который я проверяю сейчас: сначала таблицы маршрутов, затем разрешение имён, потом пересылка и в конце платформа хоста. Остановитесь на первом шаге, который уже не проходит, потому что добавление правил ниже в стеке скрывает границу вместо того, чтобы её сдвинуть.

Сравните три таблицы маршрутов, прежде чем менять правило

Контейнер на стандартном мосту Docker имеет один маршрут по умолчанию к шлюзу docker0 и понятия не имеет, что tun0 существует. У хоста есть tun0, маршруты, протолкнутые VPN, и, возможно, правило политики, которое отправляет пробный трафик через другой интерфейс, чем вы ожидаете. WSL добавляет третью таблицу за своим виртуальным адаптером. Выведите все три, прежде чем править любую из них.

ContextCommandWhat a wrong answer looks like
Hostip route get 10.8.0.20Leaves a physical interface instead of tun0
Containerip routeOnly 172.17.0.0/16 via 172.17.0.1
Container DNScat /etc/resolv.confA public resolver, or an upstream the tunnel never pushed
WSLip addr; ip routeOnly the WSL subnet, no Windows VPN adapter

Если единственный маршрут контейнера — мост, а tun0 никогда не появляется в его пространстве имён, ничто ниже не поможет: никакое изменение сервера имён не создаёт маршрут, и никакой маршрут не создаёт идентичность туннеля, которую примет партнёр. Порядок важен, потому что сбой DNS и сбой маршрута изнутри оболочки выглядят одинаково.

Направьте резолвер на туннель, а не на публичный

На хосте resolvectl status перечисляет DNS-серверы по подключениям, а readlink -f /etc/resolv.conf сообщает, является ли файл заглушкой systemd, файлом NetworkManager или чем-то, что переписал клиент поставщика. Внутри контейнера в сети, заданной пользователем, /etc/resolv.conf называет 127.0.0.11, встроенный резолвер Docker, который пересылает тому upstream, с которым был настроен демон. А контейнер на стандартном мосту получил при создании копию файла хоста и хранит её.

Именно эта копия — причина, по которой туннель, поднятый после запуска контейнера, оставляет контейнер опрашивать старый upstream. Либо передайте резолвер VPN явно с docker run --dns 10.8.0.53, либо задайте "dns": ["10.8.0.53"] в /etc/docker/daemon.json и перезапустите демон, либо пересоздайте контейнер, когда туннель поднят. Пересоздание обычно дешевле отладки резолвера, который был верным час назад.

Замена на публичный резолвер — это ремонт, сохраняющий симптом. Он разрешает интернет и отказывает в каждой частной зоне, и может тихо обойти фильтрацию, ради которой существует туннель. Если вы попытаетесь писать внутрь /etc/resolv.conf работающего контейнера, ожидайте Device or resource busy: Docker делает bind-mount этого файла, поэтому правка сначала требует размонтирования. Задать --dns при создании — это последовательность, которая переживает перезапуск.

Пересылайте и транслируйте, когда контейнеры одалживают туннель

Со стандартным мостом хост является маршрутизатором между docker0 и tun0 и по умолчанию не выполняет ни одной из этих задач. Включите пересылку и транслируйте адрес источника, чтобы партнёр видел адрес туннеля хоста, а не адрес, обратного маршрута к которому у него нет (замените tun0 на то, как ваш клиент называет интерфейс):

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
iptables -A FORWARD -i docker0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Правила FORWARD важны на хостах, чья политика пересылки — DROP. Docker вставляет собственные цепочки, поэтому проверяйте пакетом, а не чтением iptables -S: из контейнера, ip route get 10.8.0.20 должен называть шлюз моста, а nc -vz 10.8.0.20 443 должен подключаться или истекать по времени на туннеле, а не на резолвере.

Без MASQUERADE партнёру нужен обратный маршрут к 172.17.0.0/16, который большинство VPN удалённого доступа не протолкнут. Здесь же ломается план у клиента на основе политик: если он перехватывает только трафик определённых UID или cgroups, ваши правила пересылки никогда не видят пакет внутри туннеля, и исправление относится к политике маршрутизации клиента, а не к iptables. Правила, добавленные вручную, исчезают при перезагрузке, поэтому положите их в unit systemd или таблицу netfilter-persistent и повторите проверку после перезагрузки, так как поздно поднятый туннель может их смыть.

Поместите туннель внутрь контейнера, когда цель — изоляция

Если один контейнер должен выходить через определённый узел, запустите клиент в этом контейнере. Ему нужны --cap-add NET_ADMIN и --device /dev/net/tun, и тогда он владеет своим tun0, маршрутами и межсетевым экраном. Если другие контейнеры проходят через него, ему также нужны пересылка и правило MASQUERADE внутри контейнера, а этим партнёрам нужен явный маршрут или прокси, указывающий на него; стандартный мост Docker не добавляет этот маршрут за вас.

Сеть хоста, docker run --network host, — дешёвый вариант: контейнер присоединяется к пространству имён хоста и видит маршруты туннеля напрямую. Правда, он лишается изоляции сети, провоцирует конфликты портов и на практике является вариантом Linux. Опубликованный порт не заменяет этого. -p 8080:80 делает сервис доступным с хоста; он не делает внутренние адреса 10.x доступными изнутри контейнера, и смешение этих двух вещей — путь, на котором проблема маршрутизации превращается в открытый порт базы данных.

Отличите сбой имени от перекрывающейся подсети

Стандартный мост Docker — 172.17.0.0/16, и многие корпоративные пулы VPN лежат внутри 172.16.0.0/12. Как только туннель владеет этим диапазоном, трафик, предназначенный контейнеру, может направляться в туннель. Имена отказывают, ошибка выглядит как DNS, а проблема в адресах. Проверяйте назначение, а не резолвер: ip route get 172.17.0.5 на хосте, затем сравните префикс моста с пулом VPN и с docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'.

Ремонт — сдвинуть один из диапазонов: либо другой пул адресов по умолчанию для Docker в daemon.json , либо другой пул на концентраторе. Добавление ещё одного сервера имён здесь ничего не даёт, и ssh -b тоже: привязка клиента к интерфейсу меняет адрес источника, а не маршрут назначения, ведущий в туннель. Быстрое разделение труда: имя, которое разрешается на хосте, но не в контейнере, — проблема резолвера, тогда как прямое IP-соединение, которое зависает в контейнере, но работает на хосте, — проблема маршрута или цепочки пересылки.

WSL и Docker Desktop не следуют правилам Linux

В своём стандартном режиме NAT WSL находится за собственным виртуальным адаптером и не получает внутренние маршруты или DNS-суффиксы VPN Windows, поэтому браузер работает, а ping внутри дистрибутива — нет. Зеркальная сеть, доступная в последних сборках Windows 11, вместо этого разделяет интерфейсы хоста, но её нужно включить в .wslconfig и перезапустить дистрибутив командой wsl --shutdown. Подтвердите активный режим командой wslinfo --networking-mode перед применением исправления, потому что ответ NAT неверен для зеркального режима и наоборот.

Docker Desktop добавляет ещё один уровень. Контейнеры Linux в Windows находятся за реализацией NAT, исторически называемой vpnkit, и их путь отличается от пути нативного хоста Linux. Контейнеры Windows подключаются через виртуальный коммутатор Hyper-V, и корпоративный клиент может блокировать трафик с этого коммутатора, поэтому контейнеры Linux на той же машине могут достигать сети, тогда как у контейнеров Windows нет ничего. Внешний коммутатор, привязанный к физическому адаптеру, — обычный обходной путь, и он меняет сетевую идентичность контейнера так, как политика может не разрешать.

Прежде чем писать ещё одно правило, пройдите четыре уровня по порядку и остановитесь там, где один из них неверен:

  • Внутри контейнера: достигает ли ip route частного префикса и называет ли /etc/resolv.conf upstream, который его разрешает?
  • На хосте: включена ли пересылка, привязан ли NAT к интерфейсу туннеля и принимает ли цепочка пересылки переход от моста к туннелю?
  • У партнёра: знает ли он обратный путь или вы полагаетесь на NAT, чтобы скрыть подсеть контейнера?
  • На платформе: WSL в режиме NAT или зеркальном, и контейнер — Linux или Windows?

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

Похожие статьи