Почему контейнер Docker не достигает хостов за VPN
У контейнеров в мостовой сети своё пространство имён, поэтому туннель хоста им не виден. Сравните сеть хоста, устройство tun и macvlan, прежде чем менять DNS.
Контейнер, который не достигает ничего за корпоративным VPN, обычно живёт в другом сетевом пространстве имён, чем туннель. Маршруты, DNS-серверы и метки межсетевого экрана клиента хоста принадлежат стеку хоста; у контейнера в мостовой сети свой стек. Этот единственный факт объясняет большинство обращений, которые доходят до команды платформы, и он же причина того, что «VPN сломан» и «Docker обходит VPN» могут быть неверны одновременно. За одним симптомом скрываются две совершенно разные конфигурации, поэтому определите, какая у вас, прежде чем менять настройку.
Выясните, кому принадлежит пространство имён туннеля
В Docker Desktop для macOS или Windows контейнеры работают внутри виртуальной машины Linux. VPN-клиент, установленный на настольном компьютере, создаёт туннель в операционной системе рабочего стола, а виртуальная машина его никогда не видит. Никакая конфигурация контейнера это не исправит, потому что интерфейса, который понадобился бы контейнеру, в его сетевом пространстве имён попросту нет.
На хосте Linux туннель живёт в пространстве имён хоста, и контейнеры мостовой сети обычно достигают его через таблицу маршрутизации хоста с применением NAT источника на выходе. Когда этот путь отказывает, причина обычно не в отсутствующем маршруте, а в политике пересылки, фильтрации обратного пути или перекрывающейся подсети. Подтвердите, в каком из двух миров вы находитесь, прежде чем читать дальше:
docker info --format '{{.OperatingSystem}}'
docker run --rm --net=host alpine ip route | head -5
docker run --rm alpine ip route | head -5
Разница между этими двумя таблицами маршрутизации — вся проблема на одном экране. Если в представлении хоста есть интерфейс туннеля, а в представлении моста нет, вы смотрите на изоляцию пространств имён, и всё остальное — выбор того, как её преодолеть.
Сеть хоста — это проверка, а не проект
Запуск контейнера с сетью хоста, которую Docker описывает как совместное использование сетевого пространства имён хоста, заставляет его напрямую наследовать интерфейс туннеля и его маршруты. Если тот же запрос после этого удаётся, теория маршрутов подтверждена, и сбой — это изоляция пространств имён, а не учётные данные, DNS или правило доступа.
docker run --rm --net=host curl -sS -o /dev/null -w '%{http_code}\n' https://api.internal/health
Оставьте это как диагностику. Сеть хоста отказывается от публикации портов и большей части изоляции, которую вы хотели получить от контейнера, а в Docker Desktop «host» означает виртуальную машину, а не рабочий стол, поэтому успех или неудача там могут ввести в заблуждение, если вы забудете, к какому хосту обращались.
Устройство tun и привилегии, которые с ним идут
VPN-клиенту внутри контейнера нужны туннельное устройство и разрешение настроить его. Обычное сокращение — --privileged, которое даёт доступ ко всем устройствам и ослабляет политику безопасности во всём контейнере. Это гораздо больше, чем требует эта задача, и трудно оправдать на ревью.
sudo modprobe tun
docker run --rm -it --device /dev/net/tun --cap-add NET_ADMIN myvpn-client
Более узкая форма сопоставляет единственный узел устройства и добавляет только возможность администрирования сети. Сначала загрузите модуль на хосте, чтобы узел существовал. Если контейнер запускается и клиент сообщает, что не может открыть туннельное устройство, проверьте сопоставление, прежде чем подозревать профиль, потому что эта ошибка обычно буквальна.
Этот подход также означает, что клиент работает для каждого контейнера. Это правильный ответ, когда каждой рабочей нагрузке нужен свой туннель, и неправильный, когда вы просто хотели, чтобы туннель хоста был виден, — именно это команда платформы, вероятно, и предполагала.
NAT моста меняет источник, который видит VPN
При стандартном мосте исходящий трафик контейнера транслируется в адрес хоста. Поэтому за VPN корпоративная сторона видит адрес туннеля хоста, а не контейнера, правила доступа по источнику ведут себя так, будто запрос сделал хост, и два контейнера становятся неразличимы для VPN-сервера.
macvlan или ipvlan, наоборот, даёт контейнеру адрес LAN, что помогает обнаружению в LAN и заставляет контейнер выглядеть равноправным участником сегмента. Он также полностью обходит туннель хоста, поэтому такой контейнер не достигнет префиксов только-VPN, если VPN недоступен и из этой LAN.
Обе формы законны; они отвечают разным требованиям. Если требование — «этот контейнер должен достигать сервиса только-VPN», вы выбираете между маршрутизацией через хост и завершением туннеля внутри контейнера. Запишите этот выбор до развёртывания, а не после первого неудачного запроса. Что бы вы ни выбрали, зафиксируйте адрес источника, который увидит удалённая сторона, потому что именно его покажут её правила доступа и её журналы. Дизайн, который не может назвать этот адрес, не завершён.
DNS следует за демоном, а не за туннелем
Контейнер получает конфигурацию резолвера при запуске, скопированную из демона или из образа. Адреса, которые VPN проталкивает после подключения, приходят для этой копии слишком поздно, и они проталкиваются резолверу хоста, а не в контейнер.
Задайте резолвер явно для сервиса или направьте его на внутренний резолвер, доступный через туннель. Контейнер, который разрешает частное имя в публичный адрес, обычно использует публичный резолвер, а не страдает от сломанного маршрута, и исправление относится к файлу compose или конфигурации демона.
Те же рассуждения применимы к вопросу об адресе выхода. Если каждый контейнер должен появляться с другого адреса VPN, один туннель хоста не может это обеспечить, потому что NAT сводит их к одному источнику. Рабочие формы — клиент в пространстве имён каждого контейнера или sidecar, который владеет туннелем и делит свой стек, и обе меняют то, как адресуется приложение.
Контрольный список перед закрытием заявки: определите пространство имён, которому принадлежит туннель, докажите путь сетью хоста, сопоставьте только нужное устройство tun, запомните адрес источника после NAT, задайте резолвер явно и рассматривайте адреса выхода для каждого контейнера как архитектурное решение, а не как флаг.