Статические маршруты в контейнерных сетях нужны реже, чем принято думать. Штатный кластер Kubernetes получает маршруты к pod-сетям от CNI-плагина, а Docker сам создаёт bridge docker0 и правила NAT в iptables. Руками таблицу маршрутов правят в четырёх ситуациях: кластер собран без overlay, два Docker-хоста нужно связать напрямую, появилась внешняя подсеть (NFS, Ceph, сеть резервного копирования), сторонний клиент переписал default route и контейнеры перестали видеть внешний мир.
Короткий ответ: маршрут к pod-сети добавляют на узле командой ip route add 10.244.1.0/24 via 192.168.1.11 dev eth0. Маршруты к service-сетям (ClusterIP) на хостах не прописывают: виртуальные IP обслуживает kube-proxy через iptables или IPVS, и статический маршрут в тот же диапазон создаёт петлю. Любая правка живёт до перезагрузки, пока не записана в systemd-networkd, netplan или /etc/network/interfaces, а ошибка в шлюзе или default route легко отрезает доступ к узлу по SSH.
Зачем нужны статические маршруты в контейнерных средах
Автоматика закрывает большинство задач. Демон Docker при старте создаёт bridge docker0, пару veth на каждый контейнер и правила MASQUERADE, поэтому контейнер выходит в интернет, но не видит контейнеры на соседнем хосте. Kubernetes идёт дальше: kube-controller-manager выдаёт каждому узлу собственный pod CIDR, CNI-плагин объявляет этот диапазон остальным узлам, а kube-proxy отвечает за виртуальные адреса сервисов.
Ручной маршрут нужен там, где автоматика не дотягивается: между Docker-хостами без CNI, к внешним подсетям хранилища, при отладке и при конфликте с программами, которые меняют default route.
| Параметр | Docker | Kubernetes |
|---|---|---|
| Кто создаёт маршруты | демон Docker: bridge, veth, iptables | CNI-плагин |
| Типовая подсеть | 172.17.0.0/16 (docker0) | pod CIDR на узел, например 10.244.1.0/24 |
| Связь между хостами | настраивают вручную | BGP, VXLAN, host-gw, native routing |
| Service-сети | нет такого понятия | ClusterIP через kube-proxy |
| Типичный ручной маршрут | ip route add 172.18.0.0/16 via 192.168.1.20 dev eth0 | ip route add 10.244.1.0/24 via 192.168.1.11 dev eth0 |
Чем отличается маршрутизация в Docker и Kubernetes
В Docker таблица маршрутов строится вокруг одного bridge docker0 с подсетью 172.17.0.0/16. Контейнер получает адрес из этой подсети, наружу трафик уходит через NAT хоста, обратный доступ организуют пробросом портов. Маршруты к контейнерным сетям других хостов Docker не создаёт: связность между двумя демонами настраивают руками либо переходят на overlay и macvlan. Механику veth-пар, MASQUERADE и DNAT разбирает статья Маршрутизация в Docker: bridge-сети, NAT и настройка маршрутов на практике.
В Kubernetes узел держит не один bridge, а набор маршрутов к pod-сетям. Узел получает pod CIDR, например 10.244.1.0/24, и CNI-плагин сообщает остальным узлам, что этот диапазон доступен через IP узла. Адреса сервисов (обычно 10.96.0.0/12) в таблице маршрутов хоста не появляются: ClusterIP живёт в правилах kube-proxy, которые подменяют адрес назначения на конкретный pod. Как это выглядит в таблицах, подробно показано в статье Маршрутизация в Kubernetes: таблицы маршрутов между подами и нодами.
Когда ручные маршруты оправданы, а когда опасны
Оправданы:
- Кластер без overlay: Calico в режиме BGP или Flannel в режиме host-gw, узлы в одной L2/L3-сети.
- Прямая связка Docker-хостов без overlay, когда нужна предсказуемая задержка.
- Маршрут к внешней подсети: NFS, Ceph, сеть резервного копирования, соседняя стойка.
- Временный маршрут для отладки, пока не найден корень проблемы.
Опасны:
- Дублирование маршрута, который уже добавил CNI: получаете петлю и «мигающую» связность.
- Пересекающиеся CIDR у двух узлов или двух bridge-сетей: пакеты уходят не туда.
- Правка default route: ошибка в шлюзе отрезает узел целиком.
- Маршрут в service CIDR: он конфликтует с правилами kube-proxy.
Перед первой правкой убедитесь, что есть консольный доступ (IPMI, iDRAC, KVM) или второй сетевой интерфейс.
Как добавить статический маршрут в Linux через ip route
Базовый инструмент это iproute2. Общий синтаксис: ip route add <сеть>/<маска> via <шлюз> dev <интерфейс> metric <число>. Параметр via задаёт шлюз, dev фиксирует интерфейс, metric управляет приоритетом: чем меньше число, тем раньше маршрут попадёт в выбор.
Временные маршруты: ip route add и ip route del
ip route add 10.244.1.0/24 via 192.168.1.11 dev eth0 ip route show ip route show table all ip route get 10.244.1.5 ip route del 10.244.1.0/24
Вывод ip route get 10.244.1.5 показывает, какой маршрут реально сработает: например, «10.244.1.5 via 192.168.1.11 dev eth0 src 192.168.1.10». Если ядро отвечает ошибкой Network is unreachable, шлюз недоступен напрямую и маршрут не будет установлен. После добавления проверьте связность: ping 10.244.1.1.
Для IPv6 применяют тот же синтаксис с флагом: ip -6 route add fd00:10:244:1::/64 via fd00:192:168:1::11 dev eth0. Чтобы откатить временную правку, удалите маршрут той же сети: ip route del 10.244.1.0/24.
Постоянные маршруты: systemd-networkd, netplan, ifupdown
Команды ip route add действуют до перезагрузки. Для постоянного маршрута создайте файл /etc/systemd/network/10-static-route.network:
[Match] Name=eth0 [Network] DHCP=yes [Route] Destination=10.244.1.0/24 Gateway=192.168.1.11
Примените конфигурацию: systemctl restart systemd-networkd, затем проверьте networkctl status eth0 и ip route show.
В netplan маршрут описывают в /etc/netplan/01-netcfg.yaml:
network:
version: 2
ethernets:
eth0:
dhcp4: true
routes:
- to: 10.244.1.0/24
via: 192.168.1.11
После правки выполните netplan apply. Ошибка в отступах YAML ломает конфигурацию целиком, поэтому применяйте изменения в момент, когда у вас есть консольный доступ.
В системах с ifupdown маршрут добавляют строкой в /etc/network/interfaces: up ip route add 10.244.1.0/24 via 192.168.1.11. Правка заработает после ifdown eth0 && ifup eth0.
Настройка маршрутов к pod-сетям и service-сетям в Kubernetes
В работающем кластере маршруты к pod-сетям уже есть. Полезно понять, кто их создал, до того как добавлять свои.
Как узнать pod CIDR и service CIDR
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
kubectl cluster-info dump | grep -i service-cluster-ip-range
kubectl cluster-info dump | grep -i -- --cluster-cidr
Диапазоны pod-сетей раздаёт kube-controller-manager флагами --allocate-node-cidrs и --cluster-cidr: первый указывает, следует ли выделять CIDR для pod'ов, второй задаёт сам диапазон и используется только при --allocate-node-cidrs=true. Cilium в своей документации отдельно подчёркивает, что kube-controller-manager важно запускать с --allocate-node-cidrs, чтобы Kubernetes выделял диапазоны PodCIDR (Kubernetes Host Scope — Cilium). Описание флагов приведено в справочнике kube-controller-manager | Kubernetes. Диапазон сервисов задаётся на стороне kube-apiserver флагом --service-cluster-ip-range; в использованных материалах его точное описание не подтверждено, поэтому сверяйтесь с reference для вашей версии Kubernetes. Типовая картина: pod CIDR 10.244.0.0/16, по 10.244.N.0/24 на узел, service CIDR 10.96.0.0/12. Менять эти диапазоны после установки кластера нельзя без пересоздания или миграции, поэтому проверяйте их до первых правок.
Взаимодействие с CNI-плагинами: Calico, Flannel, Cilium
| Плагин | Как строит маршруты | Что важно при ручной правке |
|---|---|---|
| Calico | BGP между узлами; IPIP или VXLAN как туннели | в режиме BGP маршруты объявляются автоматически, лишний статический даст конфликт |
| Flannel | VXLAN по умолчанию, host-gw как альтернатива | в host-gw маршруты добавляются напрямую, пересечение с ними ломает связность |
| Cilium | native routing или туннель, обработка через eBPF | правки в ip route могут не влиять на пути, которые считает eBPF |
Если CNI работает штатно, статические маршруты не нужны. Пример для кластера из двух узлов, pod CIDR 10.244.0.0/16, узлы 192.168.1.10 и 192.168.1.11:
# узел 192.168.1.10 ip route add 10.244.1.0/24 via 192.168.1.11 dev eth0 # узел 192.168.1.11 ip route add 10.244.0.0/24 via 192.168.1.10 dev eth0
Дополнительный маршрут к тому же pod CIDR через неверный узел превращается в чёрную дыру, поэтому сверяйте результат с выводом ip route show и с диапазонами, которые публикует плагин. Туннельные режимы уменьшают MTU: в примере конфигурации Flannel в Kubernetes задан FLANNEL_MTU=1450, а в руководстве по Flannel VXLAN все интерфейсы Flannel VXLAN устанавливаются на MTU 1450, и это значение считается типовым для большинства систем Flannel (Как pod в Kubernetes получает IP-адрес). Точную величину накладных расходов VXLAN в байтах в использованных материалах подтвердить не удалось, поэтому при нестандартном внешнем MTU проверяйте значение по документации вашего плагина, иначе крупные пакеты молча теряются.
Статические маршруты на хостах Docker
Задачи две: дать контейнерам доступ во внешние подсети и связать контейнеры, живущие на разных хостах. Общая схема сетей Docker и типовых сбоев разобрана в статье Сетевые модели в Docker и Kubernetes.
Маршрут к внешней подсети для контейнеров Docker
В bridge-сети контейнер выходит наружу через NAT хоста. Маршрут к внешней подсети нужен самому хосту, а контейнер получит доступ после подмены адреса источника на IP хоста:
ip route add 10.0.0.0/8 via 192.168.1.1 dev eth0 docker run --rm alpine ping -c 2 10.0.0.1
Если нужен прямой доступ без NAT, маршрут к подсети контейнеров прописывают на внешнем маршрутизаторе и исключают эту подсеть из masquerade. Для фильтрации трафика контейнеров Docker предоставляет цепочку DOCKER-USER: она обрабатывается раньше цепочки DOCKER, и правила в неё добавляют так, чтобы они выполнялись до правил Docker, а завершающий RETURN возвращал обработку к стандартному порядку (Docker with nftables | Docker Docs). Готовые конфигурации адресации и прямой маршрутизации собраны в статье Продвинутая маршрутизация в Docker и Compose.
Связность контейнеров между хостами без overlay
Возьмём два хоста: 192.168.1.10 с сетью контейнеров 172.17.0.0/16 и 192.168.1.20 с сетью 172.18.0.0/16.
# на обоих хостах sysctl -w net.ipv4.ip_forward=1 # хост 192.168.1.10 ip route add 172.18.0.0/16 via 192.168.1.20 dev eth0 # хост 192.168.1.20 ip route add 172.17.0.0/16 via 192.168.1.10 dev eth0
Сохраните параметр ядра в /etc/sysctl.d/99-docker-routes.conf строкой net.ipv4.ip_forward=1, иначе после перезагрузки маршруты останутся, а пересылка выключится. Проверьте фильтрацию: iptables -L FORWARD -n -v. Docker добавляет свои правила, но при политике DROP в цепочке FORWARD трафик между bridge-сетями не пройдёт. Проверка связности: docker exec -it container1 ping -c 3 172.18.0.2.
Подсети bridge-сетей не должны пересекаться между хостами: задавайте их явно через docker network create --subnet. В сетях с rp_filter асимметричные пути между хостами иногда блокируются, тогда проверяют net.ipv4.conf.all.rp_filter. Отдельный вариант прямой связности это macvlan или ipvlan, но они требуют поддержки со стороны коммутатора и конфликтуют с DHCP, поэтому адреса назначают статически.
Диагностика маршрутов и проверка связности подов
ip route show и ip route get: чтение таблицы маршрутов
ip route show ip route get 10.244.1.5 traceroute -n 10.244.1.5 tcpdump -ni eth0 host 10.244.1.5
В выводе ip route show строки вида 10.244.1.0/24 via 192.168.1.11 dev eth0 означают маршрут через соседний узел, а 10.244.0.0/24 dev cni0 это локальная pod-сеть. Если нужного маршрута нет, пакет уйдёт через default gateway и, скорее всего, потеряется. Команда ip route get отвечает на вопрос «куда уйдёт конкретный пакет» и показывает выбранный исходный адрес.
Проверка связности подов через kubectl exec и ping
kubectl run test --image=alpine --restart=Never -- sleep 3600 kubectl get pods -o wide kubectl exec -it test -- ip route show kubectl exec -it test -- ping -c 3 10.244.1.5 kubectl exec -it test -- nc -zv 10.244.1.5 80
Сначала получите адрес и узел целевого пода через kubectl get pods -o wide, затем проверьте маршрут внутри тестового пода и только после этого смотрите таблицу маршрутов на узле. Если ICMP не проходит, проверьте тот же адрес утилитой nc или curl: некоторые CNI и NetworkPolicy блокируют ICMP, а TCP при этом работает. Сетевые политики и команды диагностики для межузлового трафика разбирает материал таблицы маршрутизации контейнеров и диагностика сетевых проблем.
Влияние режимов перехвата трафика на маршрутизацию контейнеров
Клиенты Xray и Sing-box работают в одном из трёх режимов: System Proxy, TUN или Mixed. Выбор режима меняет таблицу маршрутов хоста, и контейнеры чувствуют это первыми. Разбор режимов с примерами приведён в материале Режимы работы Xray и Sing-box: System Proxy, TUN или Mixed?
Как TUN меняет таблицу маршрутизации
В режиме TUN клиент создаёт виртуальный адаптер и меняет таблицу маршрутизации, добавляя запись вида default dev happ-xray metric 1. Metric 1 делает маршрут приоритетным, поэтому весь исходящий IP-трафик системы уходит в туннель. Перехватывается 100% трафика: браузеры, фоновые демоны, консоль, Docker, DNS-запросы и системные службы. Контейнер отправляет пакеты на default gateway хоста, попадает в тот же туннель, и если правила разделения трафика не покрывают подсеть 172.17.0.0/16, связность рвётся. Симптом описан прямо: при неверном выборе режима консольные утилиты и Docker перестают видеть внешнюю сеть.
Что делать: добавить исключающий маршрут для контейнерных подсетей, например ip route add 172.17.0.0/16 dev docker0, либо развести трафик через policy routing (ip rule и отдельная таблица), либо вернуться в режим, который не трогает default route, если прокси нужен только браузеру.
System Proxy и Docker: почему контейнеры не используют прокси
В режиме System Proxy настройки читают только приложения, которые ориентируются на настройки ОС: браузеры, Telegram, часть десктопных клиентов. Контейнеры Docker, базы данных, IDE и консольные утилиты в этот список не входят, поэтому прокси для них задают явно: docker run -e HTTP_PROXY=http://proxy:8080 -e HTTPS_PROXY=http://proxy:8080 -e NO_PROXY=localhost,127.0.0.1. В Kubernetes те же переменные передают через env в спецификации пода. Не все приложения уважают эти переменные, так что после настройки проверьте фактический выход в сеть.
Предупреждения и безопасное применение изменений
Порядок действий перед правкой: сохраните текущее состояние командой ip route show > /root/routes-backup.txt, убедитесь в наличии консольного доступа к узлу и добавьте страховку в виде отложенного отката, например at now + 5 minutes с командой удаления нового маршрута. Для постоянных маршрутов правьте конфигурационные файлы, а не init-скрипты.
Как откатить изменения маршрутов
ip route del 10.244.1.0/24 ip route show | diff - /root/routes-backup.txt
Если маршрут добавлен командой, его снимают той же сетью. Если правка внесена в файл systemd-networkd или netplan, верните предыдущую версию файла и перезапустите службу: systemctl restart systemd-networkd или netplan apply. Маршруты, которые создал CNI-плагин или демон Docker, удалять вручную не стоит: демон вернёт их при следующем запуске, а в промежутке вы получите разрыв связности.
Проверка после перезагрузки
ip route show sysctl net.ipv4.ip_forward kubectl get nodes kubectl get pods -o wide --all-namespaces
Дальше повторите ping между подами на разных узлах. Если маршрут исчез, проверьте, применилась ли конфигурация сети и не перезаписал ли её CNI при старте: некоторые плагины при старте агента на узле полностью пересобирают таблицу маршрутов.
Актуальность команд для разных версий Kubernetes и Docker
Синтаксис iproute2 стабилен годами, а окружение вокруг него меняется: набор флагов kube-apiserver и kube-controller-manager, режимы работы CNI, версия спецификации CNI. Перед правкой сверяйтесь с release notes вашей версии Kubernetes и документацией плагина. В Docker поведение iptables и параметры демона задают в daemon.json: установка ключей iptables или ip6tables в false предотвращает создание Docker'ом большинства его правил iptables или nftables (Packet filtering and firewalls | Docker Docs), а флаг --ip6tables включает правила IPv6 iptables (только если включён experimental). Docker Engine 20.10 содержит обновлённые версии Docker Compose, Docker Scan, containerd и ряд исправлений (Docker Engine 20.10 release notes | Docker Docs); какие именно ветки считать актуальными сегодня, в использованных материалах не подтверждено, поэтому ориентируйтесь на актуальные release notes Docker. Проверять версии удобно командами kubectl version, docker version и ip -V: расхождение версий клиента и сервера часто объясняет, почему команда из старой инструкции не срабатывает.