Docker «No route to host» у контейнерах: виправлення маршрутизації й iptables, що зберігаються

Було корисно?

Ви в контейнері, намагаєтеся дістатися чогось нудного, але необхідного — внутрішнього API, VIP бази даних, хоста в іншій підмережі — і отримуєте мережевий еквівалент знехтування: No route to host. Тим часом той самий пункт призначення працює з хоста. Ваш сервіс впав, сигналізація гучна, а Docker-мережі вирішили провести філософський семінар.

Цю помилку часто звинувачують у «дивному Docker», що — втішна омана. Насправді майже завжди це чітка проблема Linux-мереж: маршрути, форвардинг, правила файрволу, зворотне фільтрування шляху, або очікування NAT, які не збігаються з реальністю. Трюк у тому, щоб виправити це таким чином, щоб зміни пережили перезавантаження, рестарт демона, оновлення ОС і колегу, який «оптимізує» ваш файрвол у п’ятницю.

Швидкий план діагностики

Коли контейнер видає No route to host, не починайте з перезавантаження Docker. Не починайте з перевстановлення Docker. Почніть з визначення, який шар бреше.
Ось порядок дій, який швидко знайде вузьке місце.

1) Підтвердіть режим відмови зсередини контейнера

  • Якщо ping каже «Network is unreachable», у вас проблема з маршрутизацією в неймспейсі контейнера.
  • Якщо ping каже «No route to host», ядро вважає, що маршрут існує, але не може доставити пакет (часто проблема ARP/neighbor, відмова файрволу або маршрутизація + rp_filter).
  • Якщо TCP-з’єднання таймаутиться, підозрюйте drop у файрволі, відсутній NAT/masquerade або проблеми з шляхом повернення.

2) Порівняйте маршрути неймспейсу контейнера з маршрутами хоста

Зазвичай контейнери мають маршрут за замовчуванням через шлюз мосту Docker (часто 172.17.0.1). Якщо на хості є спеціальні маршрути (policy routing, статичні маршрути до RFC1918 сегментів, інтерфейси VPN), контейнер може не успадкувати те, що ви очікуєте.

3) Перевірте форвардинг і політику фільтрації на хості

Пакети контейнера виходять із його неймспейсу і потрапляють у хостовий ланцюг FORWARD. Якщо FORWARD має політику DROP (поширено в захищених базових образах), ваш контейнер може мати ідеальні маршрути, але все одно нікуда не дійде.

4) Перевірте очікування NAT

Якщо пункт призначення поза підмережею Docker-bridge, Docker зазвичай покладається на NAT (MASQUERADE), якщо ви не використовуєте маршрутизовану мережу. Відсутність NAT — класичний сценарій «працює на хості, не працює в контейнері».

5) Перевірте rp_filter, якщо є асиметричні шляхи

Якщо трафік виходить через один інтерфейс (наприклад, VPN) і повертається іншим (наприклад, дефолтний шлюз), суворе зворотне фільтрування шляху може відкидати його. Це болісно часто трапляється в корпоративних мережах зі split-tunnel і внутрішньою маршрутизацією.

Що насправді означає «No route to host» (і чого воно не означає)

Ця фраза виглядає як «відсутній запис у таблиці маршрутів», але Linux використовує її для кількох ситуацій. Ядро може повернути EHOSTUNREACH («No route to host»),
коли:

  • Маршрут існує, але next-hop недоступний (помилки neighbor/ARP на L2).
  • ICMP host unreachable повернувся від маршрутизатора і відзеркалився в сокеті.
  • Файрвол явно відкидає з icmp-host-prohibited або подібним.
  • Політична маршрутизація шле пакет у чорну діру або недоступний тип маршруту.
  • rp_filter відкидає зворотній трафік настільки агресивно, що стек поводиться так, ніби шлях зламаний.

Це не завжди означає «ви забули додати маршрут». Іноді маршрут є, але мережа все одно відмовляється співпрацювати. Linux чесний, але не завжди балакучий.

Практична цитата, яка витримує випробування інцидент-румами: Надія — не стратегія. — генерал Гордон Р. Салліван.
Коли помилка «No route», дебаг на основі надії вкрай затратний.

Як трафік контейнера справді залишає вузол (режим bridge)

Більшість Docker-хостів досі працюють із за замовчуванням bridge-мережею (docker0) або з користувацьким bridge. Всередині контейнера ви отримуєте пару veth: один кінець у неймспейсі контейнера (часто eth0), інший на хості (імена на кшталт vethabc123). Хост підключає його до docker0.

Звідти пакети йдуть через звичайну Linux-маршрутизацію на хості, плюс iptables/nftables. Це критичний момент: мережі контейнерів — не «магія Docker», це Linux-мережі з додатковими правилами.

У режимі bridge вихідний доступ до інтернету/внутрішніх мереж зазвичай залежить від NAT:

  • IP джерела контейнера: 172.17.x.y
  • Host MASQUERADE переписує його на egress IP хоста
  • Повернений трафік приходить на хост і де-NAT-иться до контейнера

Якщо ви очікуєте маршрутизовану мережу (без NAT), потрібно забезпечити реальні маршрути в upstream-мережі до підмереж контейнерів і дозволити форвардинг.
Багато команд випадково створюють «напівмаршрутизовану, напів-NAT» конфігурацію, яка працює до першого збою.

Цікаві факти й історичний контекст (чому ця проблема постійно повертається)

  1. Мережеві неймспейси Linux з’явилися в основній гілці ядра 2008–2009 років. Контейнери — це здебільшого неймспейси плюс cgroups; Docker не винайшов шлях пакета.
  2. Спочатку Docker сильно покладався на iptables, бо це було повсюдно та скриптується; nftables з’явився пізніше й ускладнив картинку.
  3. Підмережа bridge за замовчуванням у Docker (172.17.0.0/16) частіше конфліктує з реальними корпоративними мережами, ніж хотілося б визнати.
  4. Політика за замовчуванням FORWARD = DROP стала більш поширеною з жорсткішими безпековими базами. Це добре для хостів, доки ви не забудете, що трафік контейнерів — це «пересилка».
  5. firewalld та ufw стали популярними як «зручніші» менеджери файрволу; обидва можуть перезаписувати або змінювати порядок Docker-прав, якщо їх явно не інтегрувати.
  6. Зворотне фільтрування шляху (rp_filter) створене для зменшення IP-спуфінгу. На сучасних багатошляхових серверах (VPN, кілька аплінків) воно може карати легітимну асиметричну маршрутизацію.
  7. conntrack зробив можливим stateful-файрволінг у масштабі, але також ввів режими відмов: виснаження таблиці виглядає як рандомна мережева нестабільність.
  8. З ростом хмарної мережі policy routing (кілька таблиць маршрутизації, правила за джерелом) став звичним. Контейнери рідко відповідають припущенням, закладеним у цих правилах.

Жарт №1: Docker-мережі як офісний Wi‑Fi — коли ламається, всі раптом стають мережевими інженерами на 20 хвилин.

Практичні завдання: команди, виводи та яке рішення приймати

Нижче — реальні завдання, які я виконую під час інциденту. Кожне містить команду, типовий вивід, що це означає, і наступне рішення. Виконуйте їх по черзі, поки не знайдете брехню.
Ви швидше виправите проблему і будете знати, що саме виправили.

Завдання 1: Відтворіть зсередини контейнера (не вгадуйте)

cr0x@server:~$ docker exec -it app1 sh -lc 'ip route; echo ---; ping -c1 -W1 10.20.30.40; echo ---; nc -vz -w2 10.20.30.40 5432'
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 scope link  src 172.18.0.12
---
PING 10.20.30.40 (10.20.30.40): 56 data bytes
ping: sendto: No route to host
---
nc: connect to 10.20.30.40 port 5432 (tcp) failed: No route to host

Значення: Контейнер вважає, що має маршрут за замовчуванням, але ядро не може доставити пакети до цього пункту призначення. Це не «відсутній шлюз за замовчуванням».

Рішення: Перемикайте увагу на хост: форвардинг, bridge, відмова файрволу, розв’язання сусідів або політика маршрутизації хоста.

Завдання 2: Підтвердіть мережу Docker і відповідність шлюзу

cr0x@server:~$ docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.NetworkID}} {{.IPAddress}} gw={{.Gateway}}{{end}}' app1
/app1 9c2d3a7f6c8a 172.18.0.12 gw=172.18.0.1

Значення: Ви знаєте підмережу контейнера (172.18.0.0/16) і IP шлюзу.

Рішення: Подивіться на відповідний bridge і veth на хості.

Завдання 3: Ідентифікуйте інтерфейс bridge і його стан

cr0x@server:~$ ip -br link show | egrep 'docker0|br-'
docker0           UP             5a:3f:2b:aa:bb:cc <BROADCAST,MULTICAST,UP,LOWER_UP>
br-9c2d3a7f6c8a    UP             02:42:7c:8e:11:22 <BROADCAST,MULTICAST,UP,LOWER_UP>

Значення: Користувацька мережа створила міст br-9c2d3a7f6c8a, і він увімкнений.

Рішення: Перевірте, що міст має очікувану IP-адресу та маршрути.

Завдання 4: Перевірте IP-адреси моста (шлюз має існувати на хості)

cr0x@server:~$ ip -4 addr show br-9c2d3a7f6c8a
8: br-9c2d3a7f6c8a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-9c2d3a7f6c8a
       valid_lft forever preferred_lft forever

Значення: Шлюз існує і правильний.

Рішення: Якби шлюз був відсутній, перезапустіть мережу Docker або виправте старт демона. Оскільки він присутній, рухаємося далі.

Завдання 5: Перевірте sysctl форвардингу на хості

cr0x@server:~$ sysctl net.ipv4.ip_forward net.ipv4.conf.all.forwarding
net.ipv4.ip_forward = 0
net.ipv4.conf.all.forwarding = 0

Значення: Хост не форвардить IPv4-пакети. Контейнери можуть спілкуватися з хостом, але не через нього.

Рішення: Увімкніть форвардинг постійно (не одноразовим sysctl, який зникає після перезавантаження).

Завдання 6: Увімкніть форвардинг (постійно) і перевірте

cr0x@server:~$ sudo install -d -m 0755 /etc/sysctl.d
cr0x@server:~$ printf '%s\n' 'net.ipv4.ip_forward=1' 'net.ipv4.conf.all.forwarding=1' | sudo tee /etc/sysctl.d/99-docker-forwarding.conf
net.ipv4.ip_forward=1
net.ipv4.conf.all.forwarding=1
cr0x@server:~$ sudo sysctl --system | tail -n 5
* Applying /etc/sysctl.d/99-docker-forwarding.conf ...
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1

Значення: Форвардинг увімкнений і збережеться після перезавантаження.

Рішення: Повторно протестуйте з контейнера. Якщо все ще не працює — переходимо до файрволу/NAT/rp_filter.

Завдання 7: Перевірте політику FORWARD і Docker-ланцюги (iptables)

cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-9c2d3a7f6c8a -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-9c2d3a7f6c8a -j DOCKER
-A FORWARD -i br-9c2d3a7f6c8a ! -o br-9c2d3a7f6c8a -j ACCEPT
-A FORWARD -i br-9c2d3a7f6c8a -o br-9c2d3a7f6c8a -j ACCEPT

Значення: Політика за замовчуванням DROP підходить лише якщо правила ACCEPT покривають ваш трафік. -i br-... ! -o br-... ACCEPT має дозволяти egress контейнера.

Рішення: Якщо ці правила ACCEPT відсутні, вставлення правил Docker блокується або переважається. Дослідіть інтеграцію з firewalld/ufw/nftables.

Завдання 8: Перевірте ланцюг DOCKER-USER (часто тут живе політика безпеки)

cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i br-9c2d3a7f6c8a -d 10.0.0.0/8 -j REJECT --reject-with icmp-host-prohibited
-A DOCKER-USER -j RETURN

Значення: Хтось явно відкидає трафік контейнерів до 10/8 з повідомленням ICMP host-prohibited. Багато додатків бачать це як «No route to host».

Рішення: Видаліть або змініть правило REJECT, або додайте виняток allow для реальної підмережі/портів.

Завдання 9: Підтвердіть, що NAT/MASQUERADE існує для підмережі контейнера

cr0x@server:~$ sudo iptables -t nat -S POSTROUTING | egrep 'MASQUERADE|172\.18\.0\.0/16'
-A POSTROUTING -s 172.18.0.0/16 ! -o br-9c2d3a7f6c8a -j MASQUERADE

Значення: NAT налаштований для egress (все, що виходить не через міст — маскарадується).

Рішення: Якщо відсутній — або Docker не управляє iptables, або інший менеджер файрволу очистив nat-правила. Вирішіть: потрібен NAT чи маршрутизована мережа.

Завдання 10: Перевірте налаштування Docker щодо iptables (чому він не програмує правила?)

cr0x@server:~$ docker info --format '{{json .SecurityOptions}} {{.Name}}' | head -n 1
["name=seccomp,profile=default","name=cgroupns"] server
cr0x@server:~$ sudo cat /etc/docker/daemon.json 2>/dev/null || echo "no /etc/docker/daemon.json"
{
  "iptables": false
}

Значення: Docker налаштований так, щоб не чіпати iptables. Це може бути навмисно, але тепер ви відповідаєте за всі правила для NAT і форвардингу.

Рішення: Або встановіть "iptables": true (і налаштуйте інтеграцію правильно), або реалізуйте еквівалентні правила і зробіть їх постійними самостійно.

Завдання 11: Проаналізуйте вибір маршруту до пункту призначення з хоста

cr0x@server:~$ ip route get 10.20.30.40
10.20.30.40 via 10.20.0.1 dev tun0 src 10.20.10.5 uid 0
    cache

Значення: Хост маршрутує це через tun0 (VPN). Трафік контейнера також, ймовірно, буде виходити через tun0 після форвардингу/NAT.

Рішення: Якщо залучений VPN, негайно підозрівайте rp_filter і невідповідність policy routing. Продовжуйте з перевіркою rp_filter і правил.

Завдання 12: Перевірте правила policy routing (контейнери можуть потрапити в іншу таблицю)

cr0x@server:~$ ip rule show
0:      from all lookup local
100:    from 10.20.10.5 lookup vpn
32766:  from all lookup main
32767:  from all lookup default

Значення: Є правило на основі джерела: тільки трафік з 10.20.10.5 використовує таблицю vpn. NAT може переписувати джерело на інше значення.

Рішення: Вирішіть, чи хочете ви NAT (трафік контейнера стає IP хоста і потрапляє під правило) або маршрутизацію без NAT (джерело контейнера — 172.18/16 і не співпаде).
Якщо друге, потрібно додати ip rule для підмереж контейнерів або налаштувати маршрути на відповідних таблицях.

Завдання 13: Перевірте налаштування rp_filter (глобально й для інтерфейсу)

cr0x@server:~$ sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter net.ipv4.conf.tun0.rp_filter
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.tun0.rp_filter = 1

Значення: Суворе rp_filter увімкнено. Якщо шлях повернення для форварденого/NAT-ованого трафіку не збігається з тим, що ядро вважає «найкращим маршрутом», пакети відкидаються.

Рішення: Для багатошляхових/VPN-хостів встановіть rp_filter у loose-режим (2) принаймні на залучених інтерфейсах, і зробіть це постійним.

Завдання 14: Слідкуйте за лічильниками під час відтворення (хіти правил iptables)

cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n --line-numbers
Chain DOCKER-USER (1 references)
num   pkts bytes target     prot opt in              out  source        destination
1      120  7200 REJECT     all  --  br-9c2d3a7f6c8a  *   0.0.0.0/0     10.0.0.0/8 reject-with icmp-host-prohibited
2      900 54000 RETURN     all  --  *               *   0.0.0.0/0     0.0.0.0/0

Значення: Правило REJECT активно збирає трафік (pkts/bytes зростають). Це не теоретично.

Рішення: Змініть це правило. Не чіпайте Docker. Не чіпайте маршрути. Виправте політику, що викликає відмову.

Завдання 15: Підтвердіть поведінку neighbor/ARP на хостовому мосту

cr0x@server:~$ ip neigh show dev br-9c2d3a7f6c8a | head
172.18.0.12 lladdr 02:42:ac:12:00:0c REACHABLE

Значення: Хост бачить MAC контейнера і запис neighbor у стані REACHABLE.

Рішення: Якщо бачите FAILED/INCOMPLETE часто — підозрюйте L2-проблеми: некоректний міст, флапінг veth або дивні проблеми з MTU.

Завдання 16: Захопіть один шлях пакета (tcpdump, що дає відповідь на одне питання)

cr0x@server:~$ sudo tcpdump -ni br-9c2d3a7f6c8a host 10.20.30.40
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on br-9c2d3a7f6c8a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:21:18.019393 IP 172.18.0.12.48522 > 10.20.30.40.5432: Flags [S], seq 1234567890, win 64240, options [mss 1460,sackOK,TS val 123 ecr 0,nop,wscale 7], length 0

Значення: Пакет виходить із контейнера і досягає мосту. Далі перевірте egress-інтерфейс (наприклад, eth0 або tun0), щоб побачити, чи він форвардиться/NAT-иться.

Рішення: Якщо бачите його на мосту, але не на egress — блокування/форвардинг хоста. Якщо бачите на egress, але немає відповіді — upstream маршрутизація/шлях повернення.

Жарт №2: Найшвидший спосіб знайти зламане правило файрволу — сказати в каналі інцидентів вголос «це не може бути файрвол».

iptables vs nftables vs firewalld: хто насправді керує

Найстійкіші «no route to host» відмови — політичні: кілька систем вважають, що вони володіють файрволом. Docker пише правила. firewalld пише правила. Ваш агент безпеки
пише правила. Хтось додає nftables прямо, бо прочитав блог. Потім оновлення ОС змінює бекенд з iptables-legacy на iptables-nft і всі роблять вигляд, що нічого не сталося.

Знайте свій бекенд: iptables-legacy чи iptables-nft

На багатьох сучасних дистрибутивах iptables — це сумісний обгорток над nftables. Docker покращив тут ситуацію, але «покращив» — не те саме, що «не чутливий до локальної політики».

cr0x@server:~$ sudo update-alternatives --display iptables 2>/dev/null | sed -n '1,12p'
iptables - auto mode
  link best version is /usr/sbin/iptables-nft
  link currently points to /usr/sbin/iptables-nft

Значення: Ви використовуєте бекенд nft. Правила можуть бути видимі через nft list ruleset.

Рішення: Якщо ви відлагоджуєте через iptables, але продакшен-набір застосовує nftables у іншій таблиці, ви будете ганятися за привидами. Перевіряйте і nft теж.

cr0x@server:~$ sudo nft list ruleset | sed -n '1,40p'
table inet filter {
  chain forward {
    type filter hook forward priority filter; policy drop;
    jump DOCKER-USER
    ct state related,established accept
  }
}

Значення: nftables застосовує політику forward drop. Docker може вставляти правила, але базова політика має значення.

Рішення: Переконайтеся, що ACCEPT від Docker присутні й у правильному порядку, або явно дозволяйте підмережі мостів у вашому керованому шарі файрволу.

Інтеграція з firewalld: не боріться, налаштуйте

firewalld не є злим. Він просто впевнений у собі. Якщо firewalld запущений і ви також очікуєте, що Docker вільно керує iptables, потрібен явний план. Зазвичай план такий:
дозвольте Docker управляти своїми ланцюгами, і переконайтеся, що ваші зони і політики дозволяють форвардинг із Docker-bridges.

cr0x@server:~$ sudo systemctl is-active firewalld
active
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
  interfaces: eth0

Значення: firewalld активний і керує, принаймні, eth0. Docker-bridges можуть не бути у жодній зоні або мати обмежувальну зону за замовчуванням.

Рішення: Додайте інтерфейси Docker-bridge у зону trusted/дозволену зону (або створіть виділену зону) і зробіть це постійно.

cr0x@server:~$ sudo firewall-cmd --permanent --zone=trusted --add-interface=br-9c2d3a7f6c8a
success
cr0x@server:~$ sudo firewall-cmd --reload
success

Значення: Міст тепер у trusted-зоні після перезавантаження/перегрузки конфігурації.

Рішення: Повторно протестуйте з контейнера. Якщо у вас є вимоги комплаєнсу, не використовуйте trusted; створіть кастомну зону з явними правилами.

Виправлення маршрутизації, що зберігаються (не тільки «ip route add»)

Найспокусливіше виправлення — найменш довговічне: додати статичний маршрут на хості, побачити, що працює, і оголосити перемогу. Потім відбувається перезавантаження. Або NetworkManager перевстановлює профілі. Або VPN-клієнт переконфігурує маршрути. Ваш інцидент повертається, але вже о 3-й ранку.

Категорія виправлення A: Підмережа контейнера конфліктує з реальною мережею

Якщо у вашій корпоративній мережі широко використовується 172.16/12, дефолтні налаштування Docker — мінне поле. Маршрутизація стає неоднозначною. Пакети, які мали йти до віддаленої підмережі, можуть інтерпретуватися як локальні на мосту, або навпаки.

Краща практика: обирайте site-specific CIDR для контейнерів, що не конфліктує, і закріплюйте його в конфігурації демона на початку.

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "bip": "10.203.0.1/16",
  "default-address-pools": [
    { "base": "10.203.0.0/16", "size": 24 }
  ]
}

Значення: IP мосту Docker і користувацькі мережі братимуться з 10.203.0.0/16 з масивом /24.

Рішення: Це руйнівна зміна. Виконуйте її на нових хостах або у вікно міграції; існуючі мережі/контейнери можуть потребувати пересоздання.

Категорія виправлення B: Потрібні маршрутизовані контейнери (без NAT)

Деякі середовища ненавидять NAT. Аудитори хочуть реальні IP-адреси джерела. Мережеві команди хочуть маршрутизувати CIDR контейнерів як будь-яку іншу підмережу. Це можливо, але потрібно зобов’язатися.

  • Upstream-маршрутизатори повинні мати маршрути до підмереж контейнерів через Docker-хости.
  • Docker-хост має дозволяти форвардинг і не мусить маскарадувати ці підмережі.
  • Повернення має бути достатньо симетричним, щоб уникнути відкидань rp_filter.

Якщо ви намагаєтесь «майже маршрутизувати» і при цьому залишаєте NAT «на всяк випадок», отримаєте нестабільну поведінку, через яку почнете сумніватися у своєму виборі професії.

Категорія виправлення C: Policy routing має включати джерела контейнерів

Якщо ваш хост використовує кілька таблиць маршрутизації, контейнери — не виняток. Вони просто додаткові мережі джерела. Додавайте правила свідомо.

cr0x@server:~$ sudo ip rule add from 172.18.0.0/16 lookup vpn priority 110
cr0x@server:~$ sudo ip rule show | sed -n '1,6p'
0:      from all lookup local
100:    from 10.20.10.5 lookup vpn
110:    from 172.18.0.0/16 lookup vpn
32766:  from all lookup main
32767:  from all lookup default

Значення: Тепер трафік із джерел контейнера використовуватиме таблицю vpn, що відповідає намірам хоста.

Рішення: Зробіть це постійним через інструменти вашого дистрибутиву (systemd-networkd, NetworkManager dispatcher, або статичні скрипти). Одноразові ip rules зникають після перезавантаження.

Шаблони для збереження конфігурації

  • systemd-networkd: оголосіть маршрути й політику маршрутизації у файлах .network.
  • NetworkManager: використовуйте nmcli connection modify для додавання маршрутів і правил у профіль.
  • VPN-клієнти: якщо вони пушать маршрути, налаштуйте hook-и «route-up», щоб також обробляти джерела контейнерів, або вимкніть пуш і керуйте маршрутами централізовано.

Якщо ви не знаєте, яке саме туло керує вашими маршрутами, у вас немає конфігурації маршрутів. У вас є «відчуття» маршрутизації.

Виправлення iptables/nftables, що зберігаються

Постійні виправлення файрволу — це про власність. Оберіть одну систему, яка володіє правилами, і інтегруйте інші. Найгірший варіант — «Docker володіє частково, агент безпеки володіє частково, і ми вручну додаємо кілька правил під час інцидентів».

Опція 1 (поширена): Дозвольте Docker керувати своїми правилами, але обмежуйте через DOCKER-USER

Docker вставляє свої ланцюги й переходи. Підтримуване місце для вашої політики — ланцюг DOCKER-USER. Він обробляється перед власними правилами Docker.
Саме там додавайте allowlist/denylist без конфлікту з генератором правил Docker.

Якщо ви використовуєте DOCKER-USER, будьте обережні з REJECT проти DROP. REJECT дає миттєві помилки на кшталт «No route to host», що зручно для UX але погано для діагностики. DROP дає таймаути — гірше для UX, але іноді краща безпекова поведінка. Обирайте свідомо і документуйте.

Опція 2: Docker iptables вимкнено; ви відповідаєте за все

Це валідно в жорстко контрольованих середовищах, але тоді ви маєте реалізувати:

  • Правила прийняття форвардингу між bridge та egress інтерфейсами
  • NAT/masquerade для підмереж контейнерів (якщо потрібен NAT)
  • Прийняття established/related для поверненого трафіку
  • Правила ізоляції, якщо вам потрібна сегментація мереж між bridge

Плюс: передбачувана політика. Мінус: ви тепер — інженер файрволу для Docker, чи вам це подобається, чи ні.

Як правильно зберігати правила iptables

На Debian/Ubuntu часто використовують iptables-persistent. На RHEL-подібних системах зазвичай firewalld/nftables. Мета: правила з’являються після перезавантаження у правильному порядку.

cr0x@server:~$ sudo iptables-save | sed -n '1,35p'
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A DOCKER-USER -j RETURN
COMMIT

Значення: Це те, що буде відновлено, якщо ви його зафіксуєте. Також зверніть увагу: важливий порядок.

Рішення: Якщо ваш механізм збереження відновлює правила до старту Docker, можливо, потрібна корекція ordering у systemd, щоб Docker встиг створити ланцюги перед застосуванням політики (або ваша політика має бути толерантною до відсутніх ланцюгів).

NAT, що переживає перезавантаження файрволу

Перезавантаження firewalld може очищати й перевкладати правила. Якщо NAT Docker зникає після reload, ви побачите «вчора працювало, сьогодні після вікна змін — зламалося». Якщо використовуєте firewalld, краще налаштувати masquerade і політику форвардингу через сам firewalld для релевантних зон, або переконатися, що інтеграція Docker підтримується у вашому дистрибутиві.

cr0x@server:~$ sudo firewall-cmd --zone=public --query-masquerade
no
cr0x@server:~$ sudo firewall-cmd --permanent --zone=public --add-masquerade
success
cr0x@server:~$ sudo firewall-cmd --reload
success
cr0x@server:~$ sudo firewall-cmd --zone=public --query-masquerade
yes

Значення: Маскарадування увімкнено на рівні менеджера файрволу, тому перезавантаження не видалить поведінку egress NAT.

Рішення: Робіть це тільки якщо ваше середовище очікує NAT для цієї зони; не вмикайте masquerade на хостах, які мають бути строго маршрутизованими без NAT.

rp_filter і асиметрична маршрутизація: тихий убивця контейнерів

Якщо ви працюєте на простому однопрограмному хості з дефолтним шлюзом, rp_filter можна здебільшого ігнорувати. Проте production-системи рідко залишаються такими простими. Додайте VPN-інтерфейс, другий аплінк, policy routing з ip rules, і суворий rp_filter перетворюється на машину для drop-ів.

Як це ламається: Трафік контейнера входить на хост через br-*, форвардиться назовні через tun0, а відповіді повертаються через eth0 (або навпаки). Суворий rp_filter перевіряє, чи була б джерельна адреса спрямована назад тим самим інтерфейсом; якщо ні — пакет може бути відкинутий.
Ви бачите «No route to host» або таймаути. Логи тихі. Усі звинувачують Docker.

Loose-режим зазвичай правильний компроміс

rp_filter=2 (loose) перевіряє, чи джерело досяжне через який-небудь інтерфейс, а не обов’язково через той, на якому пакет прийшов. Це загалом прийнятно для серверів у складних маршрутизованих середовищах.

cr0x@server:~$ printf '%s\n' \
'net.ipv4.conf.all.rp_filter=2' \
'net.ipv4.conf.default.rp_filter=2' \
'net.ipv4.conf.tun0.rp_filter=2' \
| sudo tee /etc/sysctl.d/99-rpfilter-loose.conf
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
net.ipv4.conf.tun0.rp_filter=2
cr0x@server:~$ sudo sysctl --system | tail -n 6
* Applying /etc/sysctl.d/99-rpfilter-loose.conf ...
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.tun0.rp_filter = 2

Значення: Loose rp_filter збережено постійно.

Рішення: Якщо ви ворожому мережевому середовищі, де спуфінг значна загроза, розгляньте альтернативи: більш явну симетрію маршрутів або послаблення rp_filter тільки на конкретних інтерфейсах, що беруть участь у форвардингу.

Три корпоративні міні-історії з практики

1) Інцидент через хибне припущення: «Хост досяжний, отже й контейнери»

Платформна команда розгорнула новий внутрішній pipeline метрик. Колектор працював у Docker, опитував сервіси в кількох RFC1918 діапазонах і відправляв дані до центрального кластера. Перший день усе працювало в staging і на невеликій частині продакшену.

Потім розгортання поширилося. Частина хостів почала логувати No route to host, коли колектор намагався дістатися набору сервісів за VPN-інтерфейсом. Сам хост міг curl ці сервіси нормально. Припущення команди закріпилося: «проблема в Docker routing».

Насправді: хост мав policy routing правила, що застосовувалися тільки для VPN IP хоста. Трафік, що походив з хоста, мав потрібне джерело, потрапляв у таблицю vpn і йшов тунелем. Трафік контейнера іноді NAT-ився, іноді ні (залежно від хоста), і у випадку відмови він не потрапляв під правило. Йшов через дефолтний шлюз, потрапляв на маршрутизатор, який його відхиляв, і колектор повідомляв «No route to host».

Виправлення не було «перезапустіть Docker». Воно полягало в додаванні policy routing правила для підмереж контейнерів на ураженій групі вузлів і в тому, щоб зробити його постійним у тому ж механізмі, що керував VPN-маршрутами. Після цього контейнери поводилися як хост, бо мережева політика застосовувалась до них також.

Постмортем-дія, яка мала значення: коли хост використовує маршрутизацію на основі джерела, CIDR контейнерів повинні розглядатися як повноцінні джерела в мережевому дизайні. Не особливі, не як післямова, не «чужа проблема».

2) Оптимізація, що відкусила хвіст: «Вимкнемо iptables Docker для продуктивності»

Команда безпеки і інженер, що піклувався про продуктивність, погодилися: встановити Docker "iptables": false, щоб Docker не міняв файрвол, і замінити його централізованим набором правил nftables. Ідея проста: менше рухомих частин, швидша обробка правил, краща відповідність вимогам.

Роллаут пройшов добре на кількох вузлах з простими HTTP-сервісами. Через тижні інший клас сервісів — контейнери, яким треба було дістатися внутрішніх баз даних по нестандартних маршрутах — почав падати під час звичного reload файрволу. Аплікації логували таймаути і зрідка No route to host. Інженери перезавантажували контейнери, хости, навіть міняли ноди. Симптоми переміщувалися.

Відштовхування було тонким: централізований набір правил мав accept для форвардингу, але NAT був неповним для одного з пулів користувацьких bridge. За певних комбінацій джерел/призначень пакети виходили з невірним адресним простором 172.x. Деякі upstream маршрутизатори відразу відхиляли (виглядало як «no route»), інші — тихо скидали (таймаути). Після reload змінювався порядок ланцюгів і іноді REJECT опинявся перед accept.

Виправлення не було «включити iptables назад» (хоч би це й спрацювало). Вони натомість закріпили NAT і форвард-логіку для кожного пулу адрес у конфігурації nftables, додали regression-тести, що перевіряють присутність правил після reload, і стандартизували bridge CIDR у флоті, щоб зменшити дрейф.

Урок: ви можете повністю володіти файрволом. Але як тільки ви забираєте кермо від Docker, не дивуйтеся, коли влетите у NAT-проблеми.

3) Нудна, але правильна практика, що врятувала день: «Ми написали runbook і зафіксували інваріанти»

Команда, що працює поруч із платіжними системами, управляла флотом Docker-хостів із сумішшю легаси-сервісів і нових контейнерів. Їхнє середовище включало firewalld, HIPS і корпоративний VPN-клієнт, що іноді оновлював маршрути. Інакше: ідеальний шторм.

Після одного неприємного інциденту вони впровадили нудну практику: кожен вузол при завантаженні запускав перевірку здоров’я, яка валідовувала набір інваріантів — ip_forward=1,
FORWARD містить ACCEPT для Docker-bridge, NAT masquerade існує для кожного пулу, rp_filter послаблений на VPN-інтерфейсах і bridge CIDR не перекриваються з маршрутами сайту. Перевірка виводила один рядок статусу і виводила вузол з ротації, якщо щось зламалось.

Через місяці оновлення базового образу змінило профіль sysctl і вимкнуло форвардинг. Половина флоту могли б стати непрацездатними. Натомість перевірка здоров’я відзначила вузли відразу після перезавантаження, до того як вони почали брати трафік. Команда більш-менш швидко виправила drop-in для sysctl і вклала оновлення без помітних наслідків для користувачів.

Нагород ніхто не отримав за «ми перевірили sysctl». І правильно. Нудна правильність купує вам вихідні.

Типові помилки: симптом → корінь → виправлення

1) Симптом: «No route to host» лише з контейнерів; хост працює

Корінь: net.ipv4.ip_forward=0 або форвардинг вимкнений у sysctl-базі.

Виправлення: Увімкніть форвардинг постійно через /etc/sysctl.d/*.conf і застосуйте sysctl --system.

2) Симптом: миттєва відмова, не таймаут; лічильники iptables ростуть на REJECT

Корінь: Ланцюг DOCKER-USER містить REJECT (часто «блокувати приватні мережі від контейнерів»).

Виправлення: Замініть на allowlist-політику або звузьте її по підмережі/порту; перевірте лічильники хітів.

3) Симптом: Працювало до reload firewalld; потім контейнери втратили вихід

Корінь: NAT/forward правила очищуються; правила Docker не повторно вставляються або їх переважають.

Виправлення: Інтегруйте Docker-bridges у зони firewalld і налаштуйте masquerade/forwarding там, або дозвольте Docker керувати правилами.

4) Симптом: Лише певні призначення відмовляються (особливо через VPN); інтермітентний «No route»

Корінь: Policy routing не відповідає CIDR джерела контейнера; трафік виходить через неправильний інтерфейс і відхиляється.

Виправлення: Додайте ip rule для CIDR контейнерів у правильну таблицю маршрутизації, збережіть через мережевий менеджер.

5) Симптом: SYN виходить з хоста, немає відповідей; curl хоста працює

Корінь: Відсутній MASQUERADE; upstream не знає, як маршрутувати 172/10 підмережі назад.

Виправлення: Додайте NAT для пулу контейнерів або реалізуйте маршрутизовані підмережі з upstream-маршрутами.

6) Симптом: Відповіді приходять іншим інтерфейсом; conntrack показує drop; є VPN

Корінь: Суворий rp_filter відкидає асиметричний зворотній трафік.

Виправлення: Встановіть rp_filter у loose (2) для глобального/ключових інтерфейсів, або зробіть маршрути симетричними.

7) Симптом: Контейнери можуть дістатися деяких приватних підмереж, але не інших; «Network is unreachable»

Корінь: Колізія маршрутів (bridge Docker перекриває реальну мережу), що призводить до неправильного вибору маршруту.

Виправлення: Змініть адресні пулі Docker (bip, default-address-pools) на не перекриваючіся діапазони і пересоздайте мережі.

8) Симптом: Після оновлення ОС правила видно в iptables, але не застосовуються (або навпаки)

Корінь: Несумісність бекенду (iptables-legacy vs iptables-nft); дебаг в неправильній площині.

Виправлення: Підтвердіть бекенд через alternatives; використовуйте nft list ruleset, коли nft активний; стандартизуйте флот.

Контрольні списки / покроковий план

Покроково: Виправити «No route to host» у контейнері стійко

  1. Відтворіть усередині контейнера: виконайте ip route, потім мінімальне TCP-з’єднання до точної IP:порт. Запишіть, чи це «No route» чи таймаут.
  2. Визначте підмережу контейнера і міст: через docker inspect і ip link. Запишіть ім’я мосту і CIDR.
  3. Перевірте форвардинг хоста: перевірте net.ipv4.ip_forward. Якщо виключено — увімкніть постійно через sysctl drop-in.
  4. Перевірте політику FORWARD і DOCKER-USER: шукайте DROP за замовчуванням без accept-правил, та явні REJECT-и. Використовуйте лічильники, щоб отримати доказ.
  5. Підтвердіть NAT (якщо очікуєте NAT): перевірте POSTROUTING MASQUERADE для підмережі контейнера. Вирішіть: NAT чи маршрутизовані контейнери?
  6. Перевірте маршрут до призначення з хоста: ip route get <dest>. Якщо використовується VPN або нестандартний шлях, перевірте policy routing.
  7. Перевірте rp_filter: якщо багатошляховість або VPN — встановіть loose там, де треба, і збережіть.
  8. Інтегруйте менеджер файрволу: якщо є firewalld/ufw, призначте Docker-bridges у зону і налаштуйте masquerade/forwarding у тім самому менеджері.
  9. Повторно протестуйте і зробіть packet-capture: tcpdump на мосту й egress, щоб підтвердити, де пакети зупиняються.
  10. Закріпіть зміни: зафіксуйте зміни у конфігураційному менеджменті (sysctl.d, профілі NetworkManager, постійна конфігурація firewalld, docker daemon.json).

Операційний чеклист: інваріанти, які варто моніторити

  • net.ipv4.ip_forward=1 на хостах з контейнерами
  • FORWARD містить accepts для bridge→egress трафіку і established повернення
  • DOCKER-USER не містить широких REJECT-ів без явних allow-виключень
  • NAT masquerade існує для кожного пулу контейнерів, якщо використовується bridge з NAT
  • CIDR контейнерів не перекриваються з корпоративними/приватними підмережами або VPN-маршрутами
  • rp_filter налаштований відповідно до багатошляхового дизайну
  • Є одне джерело істини для файрволу (Docker + DOCKER-USER або firewalld/nftables — але не бійка між ними)

FAQ

Чому я отримую «No route to host» замість таймауту?

Тому що щось активно відкидає або оголошує хост недосяжним (iptables REJECT, ICMP unreachable від маршрутизатора, помилка neighbor). Таймаути зазвичай означають DROP.

Контейнери можуть виходити в інтернет, але не в приватну підмережу. Що особливого в приватних діапазонах?

Приватні діапазони часто залучають policy routing (VPN), явну політику файрволу або перекриття CIDR. Інтернет зазвичай — це дефолтний маршрут + NAT; приватні мережі — це місце, де починається ваша «специфіка».

Чи добра ідея вимикати управління iptables у Docker?

Тільки якщо ви готові повністю володіти NAT/forwarding/isolation правилами і тестувати їх після reload-ів і оновлень. Це підходить у дисциплінованих середовищах; в ад-хок — це хаос.

Який найшвидший спосіб довести, що це файрвол?

Перевірте лічильники DOCKER-USER і FORWARD під час відтворення. Якщо лічильники зростають на REJECT/DROP — маєте доказ без філософських дебатів.

Чи потребує зміна CIDR мосту Docker пересоздання контейнерів?

Зазвичай так. Існуючі мережі й контейнери створені з старими пулами. Плануйте міграцію: виведіть навантаження, пересоздайте мережі, розгорніть заново.

Як rp_filter проявляє себе як проблема контейнера?

Контейнери — це форвардений трафік. Якщо відповіді приходять по несподіваному інтерфейсу, суворий rp_filter може їх відкинути. Додаток відчуває це як недосяжність або таймаут.

Чи відрізняється це для macvlan або host networking?

Так. macvlan обходить Docker-bridge і модель NAT і покладається на L2-аджасенсі і поведінку upstream-комутаторів; host networking обходить неймспейси, але стикається з файрволом/політикою хоста. Самі симптоми «No route» змінюються, але підхід дебагу (маршрути, форвардинг, файрвол, rp_filter) лишається релевантним.

Я на nftables. Чи припинити використання iptables-команд взагалі?

Використовуйте інструменти, що відповідають шарові примусового виконання. Якщо iptables — це обгортка для nft, iptables-команди часто працюють, але можуть ховати специфічну структуру nft. У сумнівах інспектуйте обидва і стандартизуйте флот.

Чому після перезапуску Docker воно працює, а потім ламається?

Тому що щось інше перевстановлює політику файрволу або sysctl після старту Docker (reload firewalld, агент безпеки, cloud-init, конфігураційний менеджмент). Вирішіть власність і порядок завантаження.

Висновок: наступні кроки, що зменшать майбутній біль

«No route to host» з контейнерів рідко є загадкою Docker і майже завжди — правда Linux-мереж, яку ви ще не задокументували. Стійке вирішення — не магічна команда; це явне й постійне визначення вашого наміру щодо маршрутизації і файрволу.

  • Оберіть неконфліктні CIDR для контейнерів і стандартизуйте їх на початку.
  • Визначте модель: NAT чи маршрутизовані контейнери, і реалізуйте послідовно.
  • Зафіксуйте форвардинг, позицію rp_filter і правила policy routing у постійних конфігураціях.
  • Оберіть власника файрволу (Docker + DOCKER-USER або firewalld/nftables) і інтегруйте замість конкуренції.
  • Автоматизуйте перевірки інваріантів, щоб «працювало до перезавантаження» перестало бути постійним жанром.

Якщо зробите лише одну річ: додайте перевірку здоров’я, яка підтверджує форвардинг, наявність NAT і політику DOCKER-USER після кожного завантаження і reload файрволу. Це нудно. Це правильно. Це врятує вас пізніше.

← Попередня
Версії модулів ZFS: як уникнути конфліктів між ядром і ZFS
Наступна →
Налаштування ZFS для SSD-пулів: уникнення краху через збір сміття

Залишити коментар