Vous êtes dans un conteneur, vous essayez d’atteindre quelque chose d’ennuyeux mais essentiel — une API interne, une VIP de base de données, un hôte sur un autre sous‑réseau — et
vous obtenez l’équivalent réseau d’un haussement d’épaules : No route to host. Pendant ce temps, la même destination fonctionne depuis l’hôte. Votre service est en
panne, votre pager sonne, et le réseau Docker donne l’impression d’un séminaire de philosophie.
Cette erreur est souvent imputée à « Docker qui fait des siennes », ce qui est un mensonge rassurant. En pratique, il s’agit presque toujours d’un problème net et clair de réseau Linux :
routage, forwarding, politique de pare‑feu, filtrage de chemin inverse, ou attentes NAT qui ne collent pas à la réalité. L’astuce consiste à corriger cela de façon à ce que la
modification survive aux redémarrages, aux redémarrages du démon, aux mises à jour de l’OS et à un collègue « optimisant » votre pare‑feu un vendredi.
Playbook de diagnostic rapide
Quand des conteneurs lancent No route to host, ne commencez pas par redémarrer Docker. Ne commencez pas par réinstaller Docker. Commencez par déterminer quelle couche ment.
Voici l’ordre qui permet de trouver rapidement le goulot d’étranglement.
1) Confirmer le mode d’échec depuis l’intérieur du conteneur
- Si
pingindique « Network is unreachable », vous avez un problème de routage dans l’espace de noms du conteneur. - Si
pingindique « No route to host », le noyau croit qu’il existe une route mais ne peut pas livrer (souvent résolution ARP/voisin, rejet du pare‑feu, ou routage + rp_filter). - Si les connexions TCP expirent, suspectez un DROP par le pare‑feu, un NAT/masquerade manquant, ou des problèmes de chemin de retour.
2) Comparer les routes du namespace du conteneur à celles de l’hôte
Les conteneurs ont généralement une route par défaut via la passerelle du bridge Docker (souvent 172.17.0.1). Si l’hôte a des routes spéciales (routage par politique,
routes statiques vers des segments RFC1918, interfaces VPN), le conteneur pourrait ne pas hériter de ce que vous pensez.
3) Vérifier le forwarding et la politique filter sur l’hôte
Les paquets du conteneur quittent son espace de noms et arrivent dans la chaîne FORWARD de l’hôte. Si FORWARD est en DROP (courant avec des baselines renforcées), votre conteneur
peut avoir des routes parfaites et quand même ne pas aller nulle part.
4) Vérifier les attentes NAT
Si la destination est en dehors du subnet du bridge Docker, Docker s’appuie typiquement sur le NAT (MASQUERADE) sauf si vous utilisez un réseau routé. L’absence de NAT est un cas classique
« fonctionne sur l’hôte, échoue dans le conteneur ».
5) Vérifier rp_filter si vous avez des chemins asymétriques
Si le trafic sort par une interface (par exemple un VPN) et revient par une autre (par exemple la passerelle par défaut), le filtrage strict du chemin inverse va le bloquer. C’est
douloureusement courant dans les réseaux d’entreprise avec tunnels fractionnés et routage interne.
Ce que signifie réellement « No route to host » (et ce que cela ne signifie pas)
L’expression ressemble à « entrée manquante dans la table de routage », mais Linux l’utilise pour plusieurs situations. Le noyau peut renvoyer EHOSTUNREACH (« No route to host »)
lorsque :
- Une route existe mais le next‑hop est injoignable (échec de résolution voisin/ARP au niveau L2).
- Un ICMP host unreachable revient d’un routeur et est reflété vers le socket.
- Une règle de pare‑feu rejette explicitement avec
icmp-host-prohibitedou équivalent. - Le routage par politique envoie le paquet dans un trou noir ou vers un type de route unreachable.
- rp_filter jette le trafic de retour si violemment que la pile se comporte comme si le chemin était rompu.
Ce que cela ne veut pas dire de façon fiable : « vous avez oublié d’ajouter une route ». Parfois vous avez une route et le réseau refuse quand même de coopérer. Linux est honnête,
mais pas toujours bavard.
Une citation pratique qui tient dans les salles d’incident : « Hope is not a strategy. » — General Gordon R. Sullivan.
Quand l’erreur est « No route », le débogage basé sur l’espoir coûte cher.
Comment le trafic des conteneurs sort vraiment de la machine (mode bridge)
La plupart des hôtes Docker utilisent encore le réseau bridge par défaut (docker0) ou un bridge défini par l’utilisateur. À l’intérieur d’un conteneur vous récupérez une paire veth :
une extrémité dans le namespace du conteneur (souvent eth0), l’autre sur l’hôte (nommée comme vethabc123). L’hôte pont ces interfaces dans docker0.
De là, les paquets atteignent le routage Linux normal sur l’hôte, plus iptables/nftables. C’est le point critique : le réseau des conteneurs n’est pas de la « magie Docker », c’est du réseau
Linux avec des règles supplémentaires.
En mode bridge, l’accès sortant vers Internet/les réseaux internes dépend généralement du NAT :
- IP source du conteneur : 172.17.x.y
- Le MASQUERADE de l’hôte la réécrit en IP d’egress de l’hôte
- Le trafic de retour revient à l’hôte et est dé‑NATé vers le conteneur
Si vous attendez un réseau routé à la place (sans NAT), vous devez fournir de vraies routes en amont vers les sous‑réseaux de conteneurs, et autoriser le forwarding. Beaucoup d’équipes
construisent accidentellement une configuration « à moitié routée, à moitié NAT » qui fonctionne jusqu’au jour où elle ne fonctionne plus.
Faits intéressants et contexte historique (pourquoi ce problème réapparaît)
- Les namespaces réseau Linux sont arrivés dans le noyau principal en 2008–2009. Les conteneurs sont largement des namespaces plus des cgroups ; Docker n’a pas inventé le chemin du paquet.
- Docker s’appuyait à l’origine fortement sur iptables pour assembler le réseau parce que c’était omniprésent et scriptable ; nftables est arrivé plus tard et a compliqué l’histoire.
- Le subnet bridge Docker par défaut (172.17.0.0/16) entre en collision avec des réseaux d’entreprise réels bien plus souvent qu’on ne veut l’admettre.
-
La politique par défaut FORWARD à DROP est devenue plus courante à mesure que les baselines de sécurité se durcissaient. C’est bien pour les hôtes, jusqu’au moment où l’on oublie que les
conteneurs sont du trafic « forwarded ». - firewalld et ufw sont devenus populaires comme gestionnaires de pare‑feu « plus conviviaux » ; tous deux peuvent remplacer ou réordonner les règles de Docker si vous ne les intégrez pas.
-
Le filtrage de chemin inverse (rp_filter) a été conçu pour réduire le spoofing IP. Sur des serveurs multi‑homés modernes (VPNs, uplinks multiples), il peut pénaliser un routage asymétrique
légitime. - conntrack (suivi de connexions) a rendu le firewall stateful praticable à grande échelle, mais a aussi introduit des modes de défaillance : l’épuisement de la table ressemble à de la fragilité réseau aléatoire.
- Avec la croissance du réseau cloud, le routage par politique (tables multiples, règles par source) est devenu courant. Les conteneurs correspondent rarement aux hypothèses inscrites dans ces règles.
Blague #1 : le réseau Docker, c’est comme le Wi‑Fi du bureau — quand ça casse, tout le monde devient soudainement ingénieur réseau pendant 20 minutes.
Tâches pratiques : commandes, sorties et quelle décision prendre
Ci‑dessous se trouvent les vraies tâches que j’exécuterais pendant un incident. Chacune inclut une commande, une sortie représentative, ce que cela signifie et la décision suivante. Exécutez‑les
dans l’ordre jusqu’à trouver le mensonge. Vous réparerez le problème plus vite et saurez ce que vous avez corrigé.
Task 1: Reproduire depuis l’intérieur du conteneur (ne pas deviner)
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
Signification : Le conteneur croit avoir une route par défaut, mais le noyau ne peut pas livrer les paquets vers cette destination. Ce n’est pas « pas de passerelle par défaut ».
Décision : Déplacer l’attention vers l’hôte : forwarding, bridge, rejet du pare‑feu, résolution voisin ou politique de routage de l’hôte.
Task 2: Confirmer le réseau Docker et la correspondance de la passerelle
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
Signification : Vous connaissez maintenant le subnet du conteneur (172.18.0.0/16) et l’IP de la gateway.
Décision : Examiner le bridge correspondant et le veth sur l’hôte.
Task 3: Identifier l’interface bridge et son état
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>
Signification : Le réseau défini par l’utilisateur a créé un bridge br-9c2d3a7f6c8a, et il est UP.
Décision : Vérifier que le bridge possède l’IP et les routes attendues.
Task 4: Vérifier les adresses IP du bridge (la gateway doit exister sur l’hôte)
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
Signification : La gateway existe et est correcte.
Décision : Si la gateway manquait, redémarrez le réseau Docker ou corrigez le démarrage du daemon. Comme elle est présente, continuer l’investigation.
Task 5: Vérifier les sysctls de forwarding de l’hôte
cr0x@server:~$ sysctl net.ipv4.ip_forward net.ipv4.conf.all.forwarding
net.ipv4.ip_forward = 0
net.ipv4.conf.all.forwarding = 0
Signification : L’hôte ne fait pas de forwarding IPv4. Les conteneurs peuvent parler à l’hôte, mais pas à travers lui.
Décision : Activer le forwarding de façon persistante (pas une simple commande sysctl volatile qui disparaît au reboot).
Task 6: Activer le forwarding (persistant) et vérifier
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
Signification : Le forwarding est activé et survivra aux redémarrages.
Décision : Retester depuis le conteneur. Si toujours en panne, vous êtes dans la zone pare‑feu/NAT/rp_filter.
Task 7: Inspecter la politique de la chaîne FORWARD et les chaînes 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
Signification : Le DROP par défaut est acceptable seulement si les règles d’acceptation couvrent votre trafic. Le -i br-... ! -o br-... ACCEPT devrait permettre l’egress des conteneurs.
Décision : Si ces règles ACCEPT manquent, l’insertion de règles par Docker est bloquée/écrasée. Examiner l’intégration avec firewalld/ufw/nftables.
Task 8: Vérifier la chaîne DOCKER-USER (les règles de sécurité de votre org y vivent souvent)
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
Signification : Quelqu’un rejette explicitement le trafic des conteneurs vers 10/8 avec un ICMP host‑prohibited. Beaucoup d’apps rapportent cela comme « No route to host ».
Décision : Supprimer/ajuster la règle de rejet, ou ajouter une exception allow pour le sous‑réseau/ports réels de destination.
Task 9: Confirmer que le NAT/MASQUERADE existe pour le subnet du conteneur
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
Signification : Le NAT est configuré pour l’egress (tout ce qui sort sans passer par le bridge est masqueradé).
Décision : Si absent, soit Docker ne gère pas iptables, soit un autre gestionnaire de pare‑feu a flushé les règles nat. Décidez si vous voulez NAT ou routage.
Task 10: Vérifier le paramètre iptables de Docker (pourquoi n’a‑t‑il pas programmé les règles ?)
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
}
Signification : Docker est configuré pour ne pas toucher iptables. Cela peut être intentionnel, mais vous héritez alors de toute la responsabilité des règles NAT et forwarding.
Décision : Soit définir "iptables": true (et gérer l’intégration correctement), soit implémenter l’équivalent complet des règles vous‑même et les rendre persistantes.
Task 11: Inspecter la sélection de route vers la destination depuis l’hôte
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
Signification : L’hôte route cette destination via tun0 (VPN). Le trafic du conteneur sortira probablement aussi via tun0 après forwarding/NAT.
Décision : Si un VPN est impliqué, suspectez immédiatement rp_filter et des incompatibilités de routage par politique. Continuer avec les vérifications rp_filter et rules.
Task 12: Vérifier les règles de routage par politique (les conteneurs peuvent tomber dans la mauvaise table)
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
Signification : Il existe une règle basée sur la source : seul le trafic issu de 10.20.10.5 utilise la table vpn. Le NAT peut réécrire la source en autre chose.
Décision : Décidez si vous voulez NAT (le trafic des conteneurs devient l’IP hôte, correspond à la règle) ou routage sans NAT (la source conteneur est 172.18/16, ne correspondra pas).
Dans ce dernier cas, ajoutez des ip rule pour les subnets conteneurs ou routez‑les correctement.
Task 13: Vérifier les paramètres rp_filter (global et par interface)
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
Signification : rp_filter strict est activé. Si le chemin de retour pour du trafic forwardé/NATé ne correspond pas à l’idée du noyau du « meilleur route », les paquets sont rejetés.
Décision : Pour des hôtes multi‑homés/VPN, mettre rp_filter en mode loose (2) au moins sur les interfaces impliquées, de façon persistante.
Task 14: Surveiller les compteurs pendant la reproduction (hit counts 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
Signification : La règle de rejet matche activement le trafic (paquets/octets en hausse). Ce n’est pas théorique.
Décision : Changer cette règle. Ne touchez pas Docker. Ne touchez pas les routes. Corrigez la politique qui cause le rejet.
Task 15: Confirmer le comportement voisin/ARP sur le bridge de l’hôte
cr0x@server:~$ ip neigh show dev br-9c2d3a7f6c8a | head
172.18.0.12 lladdr 02:42:ac:12:00:0c REACHABLE
Signification : L’hôte voit le MAC du conteneur et l’entrée voisin est saine.
Décision : Si vous voyez FAILED/INCOMPLETE de façon répétée, suspectez des problèmes L2 : mauvaise configuration du bridge, veth instable, ou problèmes MTU étranges.
Task 16: Capturer un trajet de paquet (tcpdump qui répond à une question)
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
Signification : Le paquet quitte le conteneur et atteint le bridge. Ensuite, vérifiez l’interface d’egress (par ex. eth0 ou tun0) pour voir s’il est forwardé/NATé.
Décision : Si vous le voyez sur le bridge mais pas sur l’egress, le pare‑feu/forwarding de l’hôte bloque. Si vous le voyez en sortie mais pas de réponse, c’est du routage/chemin de retour en amont.
Blague #2 : La façon la plus rapide de trouver une règle de pare‑feu cassée est de dire « ce n’est pas le pare‑feu » à haute voix dans le canal d’incident.
iptables vs nftables vs firewalld : qui est vraiment en charge
Les pannes « no route to host » les plus durables sont politiques : plusieurs systèmes estiment qu’ils possèdent le pare‑feu. Docker écrit des règles. firewalld écrit des règles. Votre agent de sécurité
écrit des règles. Quelqu’un ajoute nftables directement parce qu’il a lu un article. Puis une mise à jour de l’OS change le backend d’iptables‑legacy à iptables‑nft et tout le monde fait comme si
rien n’avait changé.
Connaître votre backend : iptables‑legacy ou iptables‑nft
Sur beaucoup de distributions modernes, iptables est un wrapper de compatibilité sur nftables. Docker s’est amélioré ici, mais « s’être amélioré » n’est pas synonyme d’« immunisé »
face à la politique locale.
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
Signification : Vous utilisez le backend nft. Les règles peuvent être visibles via nft list ruleset.
Décision : Si vous déboguez avec iptables mais que l’application en production est nftables dans une table différente, vous courrez après des fantômes. Vérifiez aussi avec 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
}
}
Signification : nftables applique une politique forward drop. Docker peut toujours insérer des règles, mais votre politique de base compte.
Décision : Assurez‑vous que les acceptations de Docker sont présentes et dans le bon ordre, ou autorisez explicitement les subnets de bridge dans votre couche de pare‑feu gérée.
Intégration firewalld : ne luttez pas, configurez
firewalld n’est pas malveillant. Il est simplement sûr de lui. Si firewalld est actif et que vous vous attendez aussi à ce que Docker gère iptables librement, vous avez besoin d’un plan explicite.
Le plan consiste généralement à laisser Docker gérer ses chaînes, et à vous assurer que vos zones et politiques autorisent le forwarding depuis les bridges Docker.
cr0x@server:~$ sudo systemctl is-active firewalld
active
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
interfaces: eth0
Signification : firewalld est actif et gère au moins eth0. Les bridges Docker peuvent ne pas appartenir à une zone, ou appartenir par défaut à une zone restrictive.
Décision : Placez les interfaces bridge Docker dans une zone trusted/permise (ou créez une zone dédiée) et rendez‑le permanent.
cr0x@server:~$ sudo firewall-cmd --permanent --zone=trusted --add-interface=br-9c2d3a7f6c8a
success
cr0x@server:~$ sudo firewall-cmd --reload
success
Signification : Le bridge est maintenant dans la zone trusted à travers redémarrages et rechargements.
Décision : Retester la connectivité des conteneurs. Si vous avez des contraintes de conformité, n’utilisez pas trusted ; créez une zone personnalisée avec des règles explicites.
Corrections de routage durables (pas seulement « ip route add »)
La correction la plus tentante est la moins durable : ajouter une route statique sur l’hôte, voir que ça marche et proclamer la victoire. Puis survient un redémarrage. Ou NetworkManager réapplique
des profils. Ou le client VPN reconfigure les routes. Votre incident revient, mais maintenant à 3 h du matin.
Catégorie de correction A : votre subnet conteneur entre en collision avec le réseau réel
Si votre réseau d’entreprise utilise beaucoup 172.16/12, les valeurs par défaut de Docker sont une mine antipersonnel. Le routage devient ambigu. Des paquets qui devraient aller vers un sous‑réseau
distant réel peuvent être interprétés comme locaux sur le bridge, ou inversement.
Bonne pratique : choisissez un CIDR de conteneur spécifique au site qui ne risque pas d’entrer en collision, et inscrivez‑le dans la configuration du daemon dès le départ.
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 }
]
}
Signification : L’IP du bridge Docker et les réseaux définis par l’utilisateur tireront des adresses depuis 10.203.0.0/16 avec des /24.
Décision : C’est un changement perturbateur. Faites‑le sur des hôtes neufs ou pendant une fenêtre de migration ; les réseaux/containeurs existants peuvent devoir être recréés.
Catégorie de correction B : vous avez besoin de conteneurs routés (sans NAT)
Certains environnements détestent le NAT. Les auditeurs veulent des IP sources réelles. Les équipes réseau veulent router les CIDR de conteneurs comme n’importe quel autre subnet. C’est possible, mais
il faut s’y engager.
- Les routeurs en amont doivent avoir des routes vers les subnets conteneurs via les hôtes Docker.
- L’hôte Docker doit autoriser le forwarding et ne pas masquerader ces subnets.
- Le trafic de retour doit être suffisamment symétrique pour éviter les drops rp_filter.
Si vous essayez de « presque router » et gardez des règles NAT « au cas où », vous créerez un comportement intermittent qui vous fera douter de vos choix de carrière.
Catégorie de correction C : le routage par politique doit inclure les sources conteneurs
Si votre hôte utilise plusieurs tables de routage, les conteneurs ne sont pas spéciaux. Ce sont simplement des réseaux sources additionnels. Ajoutez des règles intentionnellement.
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
Signification : Le trafic issu des conteneurs utilisera maintenant la table vpn, correspondant à l’intention de l’hôte.
Décision : Rendre cela persistant avec l’outil réseau de votre distro (scripts dispatcher NetworkManager, systemd‑networkd, ou scripts statiques). Les règles ip one‑shot meurent au reboot.
Patrons de persistance qui ne s’effritent pas
- systemd-networkd : déclarez routes et policy routing dans des fichiers
.network. - NetworkManager : utilisez
nmcli connection modifypour ajouter routes et rules au profil. - Clients VPN : s’ils poussent des routes, configurez des hooks « route‑up » pour gérer aussi les sources conteneurs, ou désactivez les routes poussées et gérez‑les centralement.
Si vous ne savez pas quel outil gère vos routes, vous n’avez pas une configuration de routage. Vous avez une vibe de routage.
Corrections iptables/nftables durables
Les corrections persistantes du pare‑feu consistent à décider d’un propriétaire. Choisissez un système pour posséder les règles et intégrez les autres. La pire option est « Docker gère certaines règles,
l’agent de sécurité en gère d’autres, et on ajoute manuellement quelques règles en cas d’incident ».
Option 1 (courante) : laisser Docker gérer ses règles, mais contraindre via DOCKER-USER
Docker insère ses chaînes et ses jump. L’endroit supporté pour votre politique est la chaîne DOCKER-USER. Elle est évaluée avant les règles d’acceptation propres à Docker.
C’est là que vous ajoutez allowlists/denylists sans vous battre avec le générateur de règles de Docker.
Si vous utilisez DOCKER-USER, faites attention au choix REJECT vs DROP. REJECT produit des échecs immédiats comme « No route to host », ce qui est bon pour l’expérience utilisateur mais
mauvais pour le diagnostic. DROP produit des timeouts, pire pour l’expérience utilisateur mais parfois préférable pour la posture sécurité. Choisissez intentionnellement et documentez.
Option 2 : Docker iptables désactivé ; vous gérez tout
C’est valide dans des environnements verrouillés, mais alors vous devez implémenter :
- Règles d’acceptation de forwarding entre bridge et interfaces d’egress
- NAT masquerade pour les subnets conteneurs (si vous voulez NAT)
- Acceptation established/related pour le trafic de retour
- Règles d’isolation si vous tenez à la segmentation réseau entre bridges
L’avantage : politique prévisible. L’inconvénient : vous devenez l’ingénieur pare‑feu de Docker, que vous le vouliez ou non.
Persister les règles iptables correctement
Sur Debian/Ubuntu, on utilise souvent iptables-persistent. Sur les systèmes de type RHEL, firewalld/nftables est le chemin habituel. L’objectif : les règles réapparaissent après reboot
dans le bon ordre.
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
Signification : C’est ce qui sera restauré si vous le persistez. Notez aussi : l’ordre compte.
Décision : Si votre mécanisme de persistance restaure les règles avant le démarrage de Docker, vous pouvez avoir besoin d’un fix d’ordre systemd pour que Docker recrée ses chaînes avant
l’application de la politique (ou votre politique doit tolérer l’absence de chaînes).
NAT qui survit aux rechargements de pare‑feu
Les reloads firewalld peuvent flush et réappliquer des règles. Si le NAT de Docker disparaît après un reload, vous verrez « fonctionnait hier, cassé après la fenêtre de changement ». Si vous utilisez
firewalld, préférez configurer le masquerade et la politique de forward via firewalld lui‑même pour la/les zone(s) concernée(s), ou assurez‑vous que l’intégration Docker est supportée sur votre distro.
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
Signification : Le masquerade est activé au niveau du gestionnaire de pare‑feu, donc un reload ne supprimera pas silencieusement le comportement NAT d’egress.
Décision : Ne faites cela que si votre environnement attend du NAT pour cette zone ; n’activez pas le masquerade aveuglément sur des hôtes qui devraient être strictement routés.
rp_filter et routage asymétrique : le tueur silencieux des conteneurs
Si vous exécutez un hôte simple à NIC unique avec une gateway par défaut, vous pouvez généralement ignorer rp_filter. Les systèmes de production restent rarement aussi simples. Ajoutez une interface VPN,
un second uplink, une politique de routage de type VRF, et rp_filter strict devient un broyeur de paquets.
Comment ça casse : le trafic conteneur entre sur l’hôte via br-*, est forwardé vers tun0, et les réponses reviennent par eth0 (ou inversement).
rp_filter strict vérifie si l’adresse source d’un paquet entrant serait routée vers la même interface ; si non, le paquet peut être jeté. Vous voyez « No route to host » ou des expirations. Les logs restent
muets. Tout le monde accuse Docker.
Le mode loose est généralement le compromis correct
rp_filter=2 (loose) vérifie que la source est joignable via une quelconque interface, pas nécessairement celle par laquelle le paquet est arrivé. C’est généralement acceptable pour
des serveurs dans des environnements de routage complexes.
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
Signification : rp_filter en mode loose est maintenant persistant.
Décision : Si vous êtes dans un réseau hostile où le spoofing est une préoccupation majeure, envisagez des alternatives : routage symétrique plus explicite, ou ne relaxer rp_filter que sur les interfaces
impliquées dans le forwarding.
Trois mini‑histoires d’entreprise vécues sur le terrain
1) L’incident causé par une mauvaise hypothèse : « L’hôte y accède, donc les conteneurs aussi. »
Une équipe plateforme a déployé un nouveau pipeline métrique interne. Le collecteur tournait dans Docker, scrappait des services dans plusieurs plages RFC1918, et envoyait les données à un cluster central. Le premier jour
tout a marché en staging et sur une petite tranche de production.
Puis le déploiement s’est étendu. Une série d’hôtes a commencé à logger No route to host quand le collecteur essayait d’atteindre des services derrière une interface VPN. L’hôte lui‑même pouvait curl ces services sans problème.
L’hypothèse de l’équipe est devenue un diagnostic : « Docker routing est cassé. »
La réalité : l’hôte avait des règles de routage par politique qui ne s’appliquaient qu’à l’IP source VPN de l’hôte. Le trafic d’origine hôte utilisait cette source, matchait la table vpn et sortait par le tunnel.
Le trafic conteneur était parfois NATé, parfois routé différemment (selon le nœud et son baseline de pare‑feu), et dans le cas d’échec il ne matchait pas la règle. Il sortait par la gateway par défaut à la place, frappait un routeur qui le rejetait,
et le collecteur rapportait « No route to host ».
La correction n’a pas été « redémarrer Docker ». C’était d’ajouter une règle de routage par source pour les subnets conteneurs sur la classe de nœuds affectés, et la rendre persistante dans le même système qui gérait les routes VPN.
Après cela, les conteneurs se comportaient comme l’hôte parce que la politique réseau s’appliquait vraiment à eux.
Action postmortem qui a compté : chaque fois qu’un hôte utilise du routage basé sur la source, les CIDR conteneurs sont traités comme des sources de première classe dans la conception du routage. Pas spéciaux, pas un détail, pas
« le problème de quelqu’un d’autre ».
2) L’optimisation qui s’est retournée contre eux : « Désactivons iptables de Docker pour la performance. »
Une équipe sécurité et un ingénieur axé performance ont validé un changement : mettre "iptables": false afin que Docker ne mutile pas le pare‑feu, et le remplacer par un jeu de règles nftables géré centralement.
L’argument était net : moins de pièces mobiles, évaluation des règles plus rapide, meilleure conformité.
Le rollout semblait correct sur une poignée de nœuds exécutant des services HTTP simples. Des semaines plus tard, une autre classe de services — des conteneurs qui devaient atteindre des bases de données internes sur des routes non par défaut — a commencé à échouer
lors d’un reload de pare‑feu de routine. Les applis loguaient des timeouts et des No route to host occasionnels. Les ingénieurs ont redémarré des conteneurs, rebooté des hôtes, remplacé des nœuds. Les symptômes migraient.
Le retour de flamme était subtil : le jeu de règles centralisé avait des acceptations de forwarding, mais le NAT était incomplet pour une des pools de bridges définies par l’utilisateur. Sous certaines combinaisons source/destination, des paquets sortaient avec des adresses source 172.x non routables.
Certains routeurs en amont rejetaient rapidement (surface comme « no route »), d’autres dropaient (timeouts). Après un reload, l’ordre des chaînes a changé et a brièvement placé un REJECT avant l’accept.
La correction n’a pas été « réactiver iptables » (même si cela aurait marché). Ils ont plutôt codifié le NAT et le forwarding pour chaque pool d’adresses conteneur dans la configuration nftables, ajouté des tests de régression qui valident la présence des règles après reload,
et standardisé les CIDR de bridge à travers la flotte pour réduire la dérive.
Leçon : vous pouvez absolument prendre la main sur le pare‑feu vous‑même. Mais une fois que vous retirez le volant à Docker, vous n’êtes plus surpris quand vous vous prenez un mur NAT.
3) La pratique ennuyeuse mais correcte qui a sauvé la mise : « On a écrit un runbook et on a appliqué des invariants. »
Une équipe proche des paiements gérait une flotte d’hôtes Docker avec un mélange de services legacy et de conteneurs récents. Leur environnement incluait firewalld, un système HIPS et un client VPN qui mettait parfois à jour des routes.
En d’autres termes : un générateur parfait d’orage.
Après une panne désagréable, ils ont implémenté une pratique ennuyeuse : chaque nœud au boot lançait une vérification santé qui validait une poignée d’invariants — ip_forward=1,
la chaîne FORWARD contient des acceptations pour les bridges Docker, le masquerade NAT existe pour chaque pool configuré, rp_filter est en loose sur les interfaces VPN, et les CIDR de bridge Docker ne chevauchent pas les routes du site.
La vérification affichait une ligne de statut et mettait le nœud hors rotation si un invariant était brisé.
Des mois plus tard, une mise à jour d’image de base a inversé un profil sysctl qui désactivait le forwarding. La moitié de la flotte serait devenue inutilisable. À la place, la vérification santé a signalé les nœuds immédiatement après reboot, avant qu’ils ne prennent du trafic de production.
L’équipe a ajusté le drop‑in sysctl, a déroulé la mise à jour, et les utilisateurs n’ont rien remarqué.
Personne n’a reçu d’applaudissements pour « on a vérifié des sysctls ». C’est correct. La correction ennuyeuse est la façon d’acheter des week‑ends.
Erreurs fréquentes : symptômes → cause racine → correction
1) Symptom : « No route to host » uniquement depuis les conteneurs ; l’hôte fonctionne
Cause racine : net.ipv4.ip_forward=0 ou forwarding désactivé via un profil sysctl.
Correction : Activer le forwarding de manière persistante avec des fichiers /etc/sysctl.d/*.conf et appliquer avec sysctl --system.
2) Symptom : échec immédiat, pas d’expiration ; compteurs iptables augmentent sur un REJECT
Cause racine : La chaîne DOCKER-USER contient un REJECT (souvent « bloquer les réseaux privés depuis les conteneurs »).
Correction : Remplacer par une politique basée sur une allowlist, ou la restreindre par subnet/port ; vérifier les compteurs.
3) Symptom : fonctionne jusqu’au reload de firewalld ; puis les conteneurs perdent la sortie
Cause racine : Les règles NAT/forward sont flushées ; les règles Docker ne sont pas réinsérées ou sont écrasées.
Correction : Intégrer les bridges Docker dans les zones firewalld et configurer masquerade/forwarding dans firewalld, ou autoriser Docker à gérer les règles.
4) Symptom : seules certaines destinations échouent (surtout via VPN) ; « No route » intermittent
Cause racine : Les règles de routage par politique ne correspondent pas au CIDR source des conteneurs ; le trafic sort par la mauvaise interface et est rejeté.
Correction : Ajouter des ip rule pour les CIDR conteneurs vers la table de routage correcte, rendre persistant via l’outil réseau.
5) Symptom : SYN quitte l’hôte, pas de réponses ; curl hôte fonctionne
Cause racine : MASQUERADE manquant ; l’amont ne sait pas comment router le subnet 172/10 du conteneur en retour.
Correction : Ajouter du NAT pour le pool conteneur, ou implémenter des subnets conteneurs routés avec des routes en amont.
6) Symptom : les réponses arrivent sur une interface différente ; conntrack montre des drops ; VPN impliqué
Cause racine : rp_filter strict jette le trafic asymétrique de retour.
Correction : Mettre rp_filter en loose (2) pour all/default et interfaces clés, ou rendre le routage symétrique.
7) Symptom : les conteneurs atteignent certains subnets privés mais pas d’autres ; « Network is unreachable »
Cause racine : Collision de routes (le bridge Docker chevauche un réseau réel), provoquant une mauvaise sélection de route.
Correction : Changer les pools d’adresses Docker (bip, default-address-pools) pour des plages non chevauchantes et recréer les réseaux.
8) Symptom : après une mise à jour OS, les règles semblent présentes dans iptables mais ne sont pas appliquées (ou inversement)
Cause racine : Mismatch de backend (iptables‑legacy vs iptables‑nft) ; vous déboguez le mauvais plan.
Correction : Confirmer le backend avec alternatives ; utiliser nft list ruleset quand nft est actif ; standardiser sur la flotte.
Listes de contrôle / plan étape par étape
Étapes : réparer le « No route to host » conteneur de façon durable
-
Reproduire dans le conteneur : exécuter
ip route, puis une connexion TCP minimale vers l’IP:port exacte. Noter s’il s’agit de « No route » ou d’un timeout. -
Identifier le subnet conteneur et le bridge : via
docker inspectetip link. Noter le nom du bridge et le CIDR. -
Vérifier le forwarding de l’hôte : contrôler
net.ipv4.ip_forward. Si désactivé, activer de façon persistante avec un drop‑in sysctl. - Vérifier la politique FORWARD et DOCKER-USER : rechercher des DROP par défaut sans règles acceptantes, et des REJECTs explicites. Utiliser les compteurs pour prouver les correspondances.
- Confirmer le NAT (si vous attendez du NAT) : vérifier un MASQUERADE POSTROUTING pour le subnet conteneur. Décider : NAT ou routé ? Choisissez une stratégie.
-
Vérifier le routage vers la destination depuis l’hôte :
ip route get <dest>. Si ça utilise un VPN ou un chemin non‑défaut, vérifier le routage par politique. - Vérifier rp_filter : si multi‑homé ou VPN, passer en loose sur les interfaces pertinentes, de façon persistante.
- Intégrer le gestionnaire de pare‑feu : si firewalld/ufw est présent, assigner les bridges Docker à une zone et configurer masquerade/forwarding dans ce système.
- Retester et capturer le paquet une fois : tcpdump sur le bridge et l’egress pour confirmer où les paquets s’arrêtent.
- Rendre cela durable : encoder les changements dans la gestion de configuration (sysctl.d, profils NetworkManager, config permanente firewalld, docker daemon.json).
Checklist opérationnelle : invariants à surveiller
net.ipv4.ip_forward=1sur les hôtes conteneurs- La chaîne FORWARD accepte le trafic bridge→egress et le retour established
- La chaîne DOCKER-USER ne contient pas de REJECTs larges sans exceptions autorisées
- Le masquerade NAT existe pour chaque pool de conteneurs quand on utilise le networking bridge avec NAT
- Les CIDR conteneurs ne chevauchent pas les sous‑réseaux d’entreprise privés ni les routes VPN
- rp_filter réglé de manière appropriée pour des designs de routage multi‑homés
- Une seule source de vérité pour le pare‑feu (Docker + DOCKER-USER, ou firewalld/nftables centralisé — pas une bagarre)
FAQ
Pourquoi j’ai « No route to host » au lieu d’un timeout ?
Parce que quelque chose rejette activement ou déclare l’hôte injoignable (REJECT iptables, ICMP unreachable d’un routeur, échec voisin). Les timeouts signifient généralement DROP.
Les conteneurs atteignent Internet mais pas un subnet privé. Qu’y a‑t‑il de spécial avec les plages privées ?
Les plages privées impliquent souvent du routage par politique (VPN), des politiques de pare‑feu explicites, ou des CIDR qui se chevauchent. Internet est généralement juste une route par défaut + NAT ; les réseaux privés
sont l’endroit où commencent les spécificités de votre organisation.
Est‑ce une bonne idée de désactiver la gestion iptables de Docker ?
Seulement si vous êtes prêt à prendre entièrement en charge NAT/forwarding/isolation et à les tester après reloads et upgrades. C’est parfait dans des environnements disciplinés ; c’est le chaos dans des environnements ad hoc.
Quelle est la manière la plus rapide de prouver que c’est le pare‑feu ?
Vérifiez les compteurs DOCKER-USER et FORWARD pendant la reproduction du problème. Si les compteurs augmentent sur une règle REJECT/DROP, vous avez la preuve sans débat philosophique.
Changer le CIDR du bridge Docker exige‑t‑il de recréer les conteneurs ?
Généralement oui. Les réseaux et conteneurs existants ont été créés avec les anciens pools. Planifiez une migration : vider les charges, recréer les réseaux, redéployer.
Comment rp_filter apparaît‑il comme un problème de conteneur ?
Les conteneurs produisent du trafic forwardé. Si les réponses reviennent sur une interface inattendue, rp_filter strict peut les jeter. L’application perçoit cela comme injoignable ou des timeouts.
Est‑ce différent pour macvlan ou le mode host ?
Oui. macvlan contourne le bridge Docker et le modèle NAT, et dépend de l’adjacence L2 et du comportement du commutateur en amont ; le networking host contourne le routage du namespace mais subit toujours la politique du pare‑feu/routage de l’hôte.
Les causes de « No route » changent, mais l’approche de débogage (routes, forwarding, pare‑feu, rp_filter) reste pertinente.
Je suis sur nftables. Dois‑je cesser d’utiliser les commandes iptables entièrement ?
Utilisez l’outil qui correspond à l’application. Si iptables est un wrapper nft, les commandes iptables fonctionnent souvent mais peuvent cacher la structure spécifique nft. En cas de doute, inspectez les deux et standardisez sur la flotte pour réduire la confusion.
Pourquoi ça marche juste après le redémarrage de Docker puis casse plus tard ?
Parce que quelque chose d’autre réapplique la politique du pare‑feu ou des sysctls après que Docker a démarré (reload firewalld, agent de sécurité, cloud‑init, gestion de config). Corrigez la propriété et l’ordre d’exécution.
Conclusion : prochaines étapes pour réduire la douleur future
« No route to host » depuis des conteneurs est rarement un mystère Docker et presque toujours une vérité réseau Linux que vous n’avez pas encore formalisée. La correction durable n’est pas une commande magique ; c’est rendre explicite et persistant
votre intention de routage et de pare‑feu.
- Choisir des CIDR conteneurs non chevauchants et les standardiser tôt.
- Décider NAT vs conteneurs routés et l’appliquer de façon cohérente.
- Rendre explicites et persistants le forwarding, la posture rp_filter et les règles de routage par politique.
- Choisir un propriétaire du pare‑feu (Docker+DOCKER-USER, ou firewalld/nftables) et intégrer au lieu de faire concurrence.
- Automatiser des vérifications d’invariants pour que le « marche jusqu’au reboot » cesse d’être un genre récurrent.
Si vous ne faites qu’une seule chose : ajoutez une vérification santé qui valide le forwarding, la présence du NAT et la politique DOCKER-USER après chaque boot et reload de pare‑feu. C’est ennuyeux. C’est correct.
Ça vous sauvera plus tard.