J'ai passé la majeure partie d'une matinée ce printemps à traquer un conteneur qui n'arrivait pas à résoudre un seul nom interne, alors que le navigateur d'à côté ouvrait le wiki sans se plaindre. Le tunnel était monté, le navigateur le prouvait, et le conteneur restait dans un autre espace de noms réseau avec une autre table de routage et un autre résolveur. Ce qui suit est l'ordre que je vérifie maintenant : d'abord les tables de routage, ensuite la résolution, puis le transfert, et enfin la plateforme hôte. Arrêtez-vous à la première étape qui échoue déjà, car ajouter des règles plus bas dans la pile masque la frontière au lieu de la déplacer.

Comparez les trois tables de routage avant de changer une règle

Un conteneur sur le pont par défaut de Docker a une seule route par défaut vers la passerelle docker0, et aucune idée que tun0 existe. L'hôte a tun0, les routes poussées par le VPN, et peut-être une règle de politique qui envoie le trafic de sonde par une interface différente de celle que vous attendez. WSL ajoute une troisième table derrière son propre adaptateur virtuel. Affichez les trois avant d'en modifier une.

ContextCommandWhat a wrong answer looks like
Hostip route get 10.8.0.20Leaves a physical interface instead of tun0
Containerip routeOnly 172.17.0.0/16 via 172.17.0.1
Container DNScat /etc/resolv.confA public resolver, or an upstream the tunnel never pushed
WSLip addr; ip routeOnly the WSL subnet, no Windows VPN adapter

Si la seule route du conteneur est le pont et que tun0 n'apparaît jamais dans son espace de noms, rien de ce qui suit n'aidera : aucun changement de serveur de noms ne crée de route, et aucune route ne crée l'identité de tunnel que le pair acceptera. L'ordre compte, car une panne DNS et une panne de route sont identiques depuis l'intérieur d'un shell.

Pointez le résolveur vers le tunnel plutôt que vers un résolveur public

Sur l'hôte, resolvectl status liste les serveurs DNS par lien, et readlink -f /etc/resolv.conf vous dit si le fichier est le stub de systemd, un fichier NetworkManager ou quelque chose qu'un client d'éditeur a réécrit. Dans un conteneur sur un réseau défini par l'utilisateur, /etc/resolv.conf nomme 127.0.0.11, le résolveur intégré de Docker, qui transmet vers l'upstream avec lequel le démon a été configuré. Un conteneur sur le pont par défaut a en revanche reçu une copie du fichier de l'hôte à sa création, et la conserve.

Cette copie explique pourquoi un tunnel qui monte après le démarrage du conteneur laisse le conteneur interroger l'ancien upstream. Soit passez le résolveur du VPN explicitement avec docker run --dns 10.8.0.53, soit définissez "dns": ["10.8.0.53"] dans /etc/docker/daemon.json et redémarrez le démon, soit recréez le conteneur une fois le tunnel monté. Recréer est généralement moins coûteux que de déboguer un résolveur qui était correct il y a une heure.

Remplacer par un résolveur public est la réparation qui conserve le symptôme. Il résout Internet et échoue dans chaque zone privée, et il peut contourner silencieusement le filtrage que le tunnel existe pour appliquer. Si vous essayez d'écrire dans /etc/resolv.conf d'un conteneur en cours d'exécution, attendez-vous à Device or resource busy : Docker monte ce fichier en bind, donc la modification nécessite d'abord un démontage. Définir --dns à la création est la séquence qui survit à un redémarrage.

Transférez et traduisez quand les conteneurs empruntent le tunnel

Avec le pont par défaut, l'hôte est un routeur entre docker0 et tun0, et il ne fait par défaut ni l'une ni l'autre de ces tâches. Activez le transfert et traduisez l'adresse source pour que le pair voie une adresse de tunnel de l'hôte plutôt qu'une adresse vers laquelle il n'a pas de route de retour (remplacez tun0 par le nom que votre client donne à l'interface) :

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
iptables -A FORWARD -i docker0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Les règles FORWARD comptent sur les hôtes dont la politique de transfert est DROP. Docker insère ses propres chaînes, donc testez avec un paquet plutôt qu'en lisant iptables -S : depuis le conteneur, ip route get 10.8.0.20 devrait nommer la passerelle du pont, et nc -vz 10.8.0.20 443 devrait se connecter ou expirer au niveau du tunnel plutôt que du résolveur.

Sans MASQUERADE, le pair a besoin d'une route de retour vers 172.17.0.0/16, que la plupart des VPN d'accès distant ne pousseront pas. C'est aussi là qu'un client basé sur des politiques casse le plan : s'il ne capture que le trafic de certains UID ou cgroups, vos règles de transfert ne voient jamais le paquet à l'intérieur du tunnel, et la correction appartient à la politique de routage du client plutôt qu'à iptables. Les règles ajoutées à la main disparaissent au redémarrage, alors placez-les dans une unité systemd ou une table netfilter-persistent et retestez après un redémarrage, car un tunnel qui monte tard peut les balayer.

Placez le tunnel dans le conteneur quand l'isolation est le but

Si un conteneur doit sortir par un nœud précis, exécutez le client dans ce conteneur. Il a besoin de --cap-add NET_ADMIN et de --device /dev/net/tun, et il possède alors son propre tun0, ses routes et son pare-feu. Si d'autres conteneurs passent par lui, il a aussi besoin du transfert et d'une règle MASQUERADE dans le conteneur, et ces pairs ont besoin d'une route ou d'un proxy explicite pointant vers lui ; le pont par défaut de Docker n'ajoute pas cette route pour vous.

La mise en réseau de l'hôte, docker run --network host, est la version bon marché : le conteneur rejoint l'espace de noms de l'hôte et voit directement les routes du tunnel. Elle supprime aussi l'isolation réseau, invite aux collisions de ports et, en pratique, est une option Linux. Un port publié n'est pas un substitut. -p 8080:80 rend un service joignable depuis l'hôte ; il ne rend pas les adresses internes 10.x joignables depuis l'intérieur du conteneur, et confondre les deux est la façon dont un problème de routage devient un port de base de données exposé.

Distinguez une panne de nom d'un sous-réseau qui se chevauche

Le pont par défaut de Docker est 172.17.0.0/16, et beaucoup de pools VPN d'entreprise se situent dans 172.16.0.0/12. Une fois que le tunnel possède cette plage, le trafic destiné à un conteneur peut être routé vers le tunnel à la place. Les noms échouent, l'erreur ressemble à du DNS, et les adresses sont le problème. Vérifiez la destination, pas le résolveur : ip route get 172.17.0.5 sur l'hôte, puis comparez le préfixe du pont avec le pool VPN et avec docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'.

La réparation consiste à déplacer l'une des plages : soit un autre pool d'adresses par défaut pour Docker dans daemon.json soit un autre pool sur le concentrateur. Ajouter un autre serveur de noms ne fait rien ici, et ssh -b non plus : lier le client à une interface change l'adresse source, pas une route de destination qui pointe dans le tunnel. Répartition rapide du travail : un nom qui se résout sur l'hôte mais pas dans le conteneur est un problème de résolveur, tandis qu'une connexion IP directe qui bloque dans le conteneur mais fonctionne sur l'hôte est un problème de route ou de chaîne de transfert.

WSL et Docker Desktop ne suivent pas les règles de Linux

Dans son mode NAT par défaut, WSL se trouve derrière son propre adaptateur virtuel et ne reçoit pas les routes internes ni les suffixes DNS du VPN Windows, ce qui explique pourquoi le navigateur fonctionne et ping dans la distribution non. La mise en réseau en miroir, disponible dans les builds récents de Windows 11, partage à la place les interfaces de l'hôte, mais elle doit être activée dans .wslconfig et la distribution redémarrée avec wsl --shutdown. Confirmez le mode actif avec wslinfo --networking-mode avant d'appliquer une correction, car la réponse NAT est fausse pour le mode miroir et inversement.

Docker Desktop ajoute une autre couche. Les conteneurs Linux sous Windows se trouvent derrière une implémentation NAT historiquement appelée vpnkit, et leur chemin diffère de celui d'un hôte Linux natif. Les conteneurs Windows se connectent via un commutateur virtuel Hyper-V, et un client d'entreprise peut bloquer le trafic de ce commutateur, si bien que les conteneurs Linux de la même machine peuvent atteindre le réseau alors que les conteneurs Windows n'ont rien. Un commutateur externe lié à l'adaptateur physique est la solution habituelle, et il change l'identité réseau du conteneur d'une manière que la politique peut interdire.

Avant d'écrire une autre règle, parcourez les quatre couches dans l'ordre et arrêtez-vous là où l'une d'elles est fausse :

  • Dans le conteneur : ip route atteint-il le préfixe privé, et /etc/resolv.conf nomme-t-il un upstream qui le résout ?
  • Sur l'hôte : le transfert est-il activé, NAT est-il lié à l'interface du tunnel, et la chaîne de transfert accepte-t-elle le pont vers le tunnel ?
  • Sur le pair : connaît-il un chemin de retour, ou comptez-vous sur NAT pour masquer le sous-réseau du conteneur ?
  • Sur la plateforme : WSL est-il en mode NAT ou miroir, et le conteneur est-il Linux ou Windows ?

Relancez chaque test dans un conteneur ou un shell neufs. Un processus de longue durée avec une réponse en cache ou une connexion établie continuera d'échouer alors que la configuration est correcte, et cette panne ressemble exactement à une correction qui n'a pas fonctionné.

Articles similaires