Контейнер с двумя сетями выбирает не тот шлюз
Два подключения, один маршрут по умолчанию, и неверная сеть побеждает каждый раз. Закрепите выход через Compose gw_priority и проверьте это изнутри контейнера.
Ваш контейнер по-прежнему отвечает на своём управляющем мосту, но каждый исходящий пакет уходит через офисный macvlan. Ничего не сломалось. Два подключения дают по одному шлюзу, и единственный маршрут по умолчанию ушёл к сети, которую вы не имели в виду, поэтому сервис продолжает работать, а его трафик идёт более длинным путём. К тому времени, когда кто-то это заметит, доказательством будет таблица маршрутизации, а не сообщение об ошибке.
Обычное объяснение — Docker выбрал сеть случайно. На практике старое решение следовало алфавитному порядку имён подключённых сетей, что превращало маршрутизацию в побочный эффект метки. gw_priority заменяет это числом, которое вы выбираете намеренно, и переход занимает около минуты после того, как схема записана.
Почему маршрут по умолчанию попадает в неверную сеть
Каждая сеть, к которой подключается сервис, может предложить собственный шлюз, а у контейнера всё равно остаётся ровно один маршрут по умолчанию. До Compose 2.33.1 равенство разрешалось сортировкой имён сетей, поэтому метка вроде control, идущая перед office, определяла, куда пойдёт трафик. Внутри контейнера ничто не записывает, что решение было произвольным, и поэтому оно проходит ревью кода и позже всплывает как странный путь. Записать политику в файл compose — единственный способ сделать выбор проверяемым.
Какая сеть победит, действительно меняют две вещи: назначенные вами значения gw_priority и то, какие сети вообще подключены. Добавьте третью сеть — набор кандидатов вырастет; уберите macvlan — и соревноваться с мостом станет нечему. Ни один из шагов не виден в журналах приложения, потому что неверный маршрут по умолчанию редко разрывает соединение полностью. Он лишь заставляет ответы приходить с адреса, которого другая сторона может не ждать.
Закрепите шлюз через gw_priority
Compose показывает gw_priority на каждом подключении сервиса к сети начиная с версии 2.33.1, и побеждает наибольшее число. Отдельного ключа шлюза для сервиса нет, поэтому значение относится к блоку networks сервиса. Пример ниже отдаёт маршрут по умолчанию control, оставляя office доступной для её напрямую подключённой LAN.
services:
vpn:
networks:
control:
gw_priority: 1
office:
gw_priority: 0
networks:
control:
driver: bridge
ipam:
config:
- subnet: 172.30.50.0/24
office:
driver: macvlan
driver_opts:
parent: eno1.120
macvlan_mode: bridge
ipam:
config:
- subnet: 192.168.120.0/24
ip_range: 192.168.120.192/27
gateway: 192.168.120.1
Читайте блок как два отдельных управления: gw_priority решает маршрут по умолчанию, а раздел ipam — адресацию. Изменение только subnet оставляет выбор выхода нетронутым, и это самый частый способ применить эту правку наполовину. Держите приоритеты рядом с сетью, которой они управляют, чтобы следующему читателю не пришлось искать решение.
Не путайте gw_priority с обычным свойством priority рядом. Оно упорядочивает создание подключений и ничего не говорит о маршрутизации, поэтому не может выбрать шлюз по умолчанию и не исправляет имена интерфейсов. Если кто-то советует поднять priority, чтобы направить трафик, совет неверен, даже когда получившийся порядок случайно выглядит правильно.
Проверьте, какая сеть на самом деле несёт маршрут по умолчанию
Проверяйте внутри работающего контейнера, потому что таблица маршрутизации хоста описывает хост, а не пространство имён, которое использует сервис. Сначала запустите проверку конфигурации, чтобы опечатка выявилась до перезапуска, а затем прочитайте маршрут, который контейнер действительно установил.
docker compose config --quiet
docker exec vpn ip -4 route show default
docker exec vpn cat /proc/net/route
Правильный результат называет устройство моста и 172.30.50.1; неверный называет 192.168.120.1 и устройство macvlan. Если в образе нет пакета iproute2, файл /proc всё равно сообщает маршрут по умолчанию, с назначением 00000000 и байтами шлюза в обратном порядке. Сравните столбец интерфейса с docker inspect, когда имена устройств неясны.
Macvlan — это физический порт, а не клон моста
Macvlan даёт контейнеру вторую личность в проводе, и именно это делает работоспособным доступ к локальным устройствам. Это также значит, что порт коммутатора и гипервизор должны принять дополнительный MAC-адрес, и многие облачные сети молча отказывают в этом. Проверьте один контейнер, прежде чем планировать флот, потому что отклонённый MAC обычно выглядит как связь, которая поднята и ничего не пропускает.
Настройка macvlan_mode: bridge — это режим драйвера macvlan, а не программный мост Docker, а passthru — отдельный специализированный режим, а не значение по умолчанию. Linux также не даёт контейнеру macvlan достичь собственного хоста через тот же родительский интерфейс, и именно поэтому управляющий мост сохраняют, а не ждут, что одно подключение выполнит обе задачи.
Назовите родительский интерфейс VLAN до того, как Compose его увидит
Docker трактует родителя с точкой вроде eno1.120 как субинтерфейс 802.1Q и сам создаёт устройство VLAN. У этого удобства есть границы: производное имя зависит от имени физического интерфейса, а длинные имена могут вывести результат за пределы ожиданий окружающих инструментов. Создание именованного интерфейса VLAN в сетевой конфигурации хоста и указание parent на него делает оба конца предсказуемыми.
Помните, что ключ под networks — это имя Compose, а не устройство Linux. Поиск на хосте интерфейса с именем office не удастся, потому что ядро видит только родительский интерфейс и пару veth за контейнером.
Освободите место для VPN сверху
Клиент VPN подключается после того, как эти сети существуют, поэтому может добавить более конкретные маршруты, правила политики и фильтры независимо от gw_priority. Планируйте удалённые префиксы относительно обеих локальных подсетей, включая ответы, которые нужны управляющему пути. Когда удалённый префикс пересекается с офисной LAN, исправляйте план адресов, а не вырывайте целый приватный диапазон из туннеля.
Перепроверьте адреса, маршруты и путь сервиса после запуска VPN и ещё раз после его переподключения, потому что именно при переподключении исчезает маршрут, добавленный вручную. Запишите ожидаемую таблицу до первого теста, чтобы последующее сравнение было diff, а не упражнением на память. Держите постоянное владение сетью в Compose и позвольте конфигурации VPN владеть только своей политикой.