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

Прочитайте маршрут, прежде чем менять конфигурацию

Начните с сети, которую Docker действительно построил. Блок IPAM — это план адресации, а подсеть — та строка, которая важна:

docker network inspect project_default --format '{{json .IPAM.Config}}'
# [{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]

По умолчанию Compose выделяет /16 каждому новому проекту, поэтому два сервиса могут уместиться в диапазоне из 65,536 адресов. Именно из-за этой ширины перекрытия так часты: случайно выбранный /16 достаточно велик, чтобы поглотить сайт, до которого вы добираетесь через туннель, и никто этого не замечает, пока туннель не поднимется.

Затем сравните выбор маршрута для одного адреса контейнера — один раз с выключенным туннелем и один раз с включённым:

ip route get 172.18.0.7
# 172.18.0.7 dev br-3f2a9c src 172.18.0.1

Две формы ответа ведут в разные места. Если вывод называет tun0, wg0 или другое устройство туннеля, префикс VPN как минимум не менее конкретен, чем маршрут моста, и пакет никогда не доходит до контейнера. Если он по-прежнему называет мост, хост может достучаться до контейнера, и ваша неисправность — это фильтрация, DNS или обратный путь, который отказывает только в одном направлении.

Почему коллизию /16 легко получить

Docker берёт мостовые сети из частных пулов, а пул по умолчанию выделяется из блока 172.16.0.0/12 кусками по /16. Корпоративные сети часто используют то же частное пространство, а VPN удалённого доступа раздаёт эти префиксы каждому подключающемуся клиенту. Когда обе стороны считают, что владеют 172.18.0.0/16, в одной таблице маршрутизации оказываются два противоречащих утверждения.

Победителя определяет длина префикса, а не порядок маршрутов. Туннель, продвигающий 10.0.0.0/8, не побеждает мостовой маршрут 172.18.0.0/16, потому что /8 менее конкретен. Туннель, продвигающий 172.18.0.0/16 или 172.18.5.0/24 внутри него, побеждает. Поэтому один ноутбук работает в офисной сети и отказывает в Wi-Fi отеля с тем же VPN: клиент получает разные префиксы.

Та же таблица управляет и обратным направлением. Контейнер, которому нужен внутренний хост по адресу 10.8.4.20, отправляет пакет на свой мостовой шлюз, а хост затем направляет его в туннель. Этот путь обычно работает, потому что хост выполняет NAT, но отказывает, когда корпоративная сторона принимает только VPN-адрес ноутбука или когда фильтрация обратного пути отбрасывает асимметричный ответ.

Сравните три исправления

Есть только три выхода, и они различаются тем, кто должен их утвердить и что ломается, пока вы применяете один из них.

RepairWho owns itWhat it costs
Renumber the container networkYouRecreates the network, restarts services, invalidates literal addresses in configs and scripts
Exclude your range from the tunnelNetwork teamChanges what every laptop sees, needs a change request and a client profile update
Reach the service through a published portYou and the clientOne port only, no direct container-to-container routing across the tunnel

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

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

Закрепите диапазон намеренно

Для одного проекта объявите подсеть в файле compose, чтобы проект перестал брать из общего пула:

networks:
  default:
    ipam:
      config:
        - subnet: 10.201.7.0/24

Берите этот диапазон из реестра, который ведёт ваша команда, а не из примера, найденного при поиске. Проверьте его по префиксам, которые продвигает VPN, по домашним сетям, из которых работают люди, и по другим сетям контейнеров на том же хосте. Затем запланируйте пересоздание, потому что существующая сеть сохраняет свою старую подсеть: docker compose down, затем up -d. docker network rm отказывается, пока подключены конечные точки, а любая запись ipv4_address в файле должна попадать в новый диапазон.

Для сетей, которые Docker создаёт сам, задайте пул демона в /etc/docker/daemon.json:

{
  "default-address-pools": [
    { "base": "10.201.0.0/16", "size": 24 },
    { "base": "10.202.0.0/16", "size": 24 }
  ]
}

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

Мост Linux и Docker Desktop отказывают по-разному

На хосте Linux контейнеры находятся на мостах в пространстве имён хоста. Всё решает одна таблица, поэтому приведённое выше сравнение — это вся история: прочитайте маршрут моста, прочитайте маршрут туннеля и посмотрите, какой победит для настоящего адреса контейнера.

Docker Desktop помещает контейнеры за виртуальной машиной с собственным NAT. У хоста вообще нет маршрута к адресу контейнера; он достигает сервиса через опубликованный порт, обычно привязанный к 127.0.0.1. Поэтому клиент VPN может сломать сервис вообще без перекрытия подсетей: либо установив маршрут для внутреннего диапазона виртуальной машины, либо заблокировав loopback-трафик, пока туннель подключён. Если ip route get на хосте сообщает, что маршрута к адресу контейнера нет, это ожидаемо, а не ошибка.

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

Проверьте с включённым туннелем

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

  1. ip route get с хоста, используя настоящий адрес контейнера.
  2. Запрос из другого контейнера, чтобы доказать, что сам сервис здоров.
  3. curl -sv http://127.0.0.1:8080/ с клиента при включённом туннеле, затем traceroute -n к адресу контейнера с машины в локальной сети.
  4. sysctl net.ipv4.conf.all.rp_filter, если запрос доходит до хоста, но ответ исчезает.

Наконец, запишите выбранный диапазон в тот же реестр, из которого вы его выбирали, с датой и проектом, которому он принадлежит. Следующее перекрытие придёт из проекта, который никто не задокументировал, и единственная защита — диапазон был зарезервирован намеренно, а не выбран случайно.

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