Ho passato gran parte di una mattina di questa primavera a inseguire un container che non riusciva a risolvere un solo nome interno, mentre il browser accanto apriva il wiki senza lamentarsi. Il tunnel era attivo, il browser lo dimostrava, e il container restava in un altro spazio dei nomi di rete con un'altra tabella di routing e un altro resolver. Quello che segue è l'ordine che controllo ora: prima le tabelle di routing, poi la risoluzione, quindi l'inoltro e infine la piattaforma host. Fermati al primo passo che già fallisce, perché aggiungere regole più in basso nello stack nasconde il confine invece di spostarlo.

Confronta le tre tabelle di routing prima di cambiare una regola

Un container sul bridge predefinito di Docker ha una sola route predefinita verso il gateway docker0 e non sa che tun0 esiste. L'host ha tun0, le route spinte dalla VPN e forse una regola di policy che manda il traffico di prova su un'interfaccia diversa da quella che ti aspetti. WSL aggiunge una terza tabella dietro il proprio adattatore virtuale. Stampale tutte e tre prima di modificarne una.

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

Se l'unica route del container è il bridge e tun0 non compare mai nel suo spazio dei nomi, nulla di quanto segue aiuterà: nessun cambio di nameserver crea una route, e nessuna route crea l'identità di tunnel che il peer accetterà. L'ordine conta perché un guasto DNS e un guasto di route sembrano identici dall'interno di una shell.

Punta il resolver al tunnel invece che a uno pubblico

Sull'host, resolvectl status elenca i server DNS per collegamento, e readlink -f /etc/resolv.conf ti dice se il file è lo stub di systemd, un file NetworkManager o qualcosa che un client di un fornitore ha riscritto. Dentro un container su una rete definita dall'utente, /etc/resolv.conf nomina 127.0.0.11, il resolver integrato di Docker, che inoltra all'upstream con cui è stato configurato il demone. Un container sul bridge predefinito ha invece ricevuto una copia del file dell'host quando è stato creato, e la conserva.

È quella copia il motivo per cui un tunnel che sale dopo l'avvio del container lascia il container a interrogare il vecchio upstream. O passi il resolver della VPN esplicitamente con docker run --dns 10.8.0.53, oppure imposti "dns": ["10.8.0.53"] in /etc/docker/daemon.json e riavvii il demone, oppure ricrei il container una volta che il tunnel è attivo. Ricrearlo di solito costa meno che fare debug di un resolver che era corretto un'ora fa.

Sostituire con un resolver pubblico è la riparazione che conserva il sintomo. Risolve internet e fallisce in ogni zona privata, e può aggirare in silenzio il filtraggio che il tunnel esiste per imporre. Se provi a scrivere dentro /etc/resolv.conf di un container in esecuzione, aspettati Device or resource busy: Docker monta quel file in bind, quindi la modifica richiede prima uno smontaggio. Impostare --dns alla creazione è la sequenza che sopravvive a un riavvio.

Inoltra e traduci quando i container prendono in prestito il tunnel

Con il bridge predefinito, l'host è un router tra docker0 e tun0, e per impostazione predefinita non fa nessuno dei due lavori. Attiva l'inoltro e traduci l'indirizzo di origine perché il peer veda un indirizzo di tunnel dell'host invece di uno verso cui non ha una route di ritorno (sostituisci tun0 con il nome che il tuo client dà all'interfaccia):

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

Le regole FORWARD contano sugli host la cui policy di inoltro è DROP. Docker inserisce le proprie catene, quindi prova con un pacchetto invece di leggere iptables -S: dal container, ip route get 10.8.0.20 dovrebbe nominare il gateway del bridge, e nc -vz 10.8.0.20 443 dovrebbe connettersi o andare in timeout sul tunnel invece che sul resolver.

Senza MASQUERADE il peer ha bisogno di una route di ritorno verso 172.17.0.0/16, che la maggior parte delle VPN di accesso remoto non spingerà. È anche qui che un client basato su policy rompe il piano: se cattura solo il traffico di determinati UID o cgroups, le tue regole di inoltro non vedono mai il pacchetto dentro il tunnel, e la correzione appartiene alla policy di routing del client e non a iptables. Le regole aggiunte a mano svaniscono al riavvio, quindi mettile in un'unità systemd o in una tabella netfilter-persistent e ritesta dopo un riavvio, dato che un tunnel che sale tardi può spazzarle via.

Metti il tunnel dentro il container quando l'obiettivo è l'isolamento

Se un container deve uscire attraverso un nodo specifico, esegui il client in quel container. Serve --cap-add NET_ADMIN e --device /dev/net/tun, e allora possiede il proprio tun0, le proprie route e il proprio firewall. Se altri container ci passano attraverso, serve anche l'inoltro e una regola MASQUERADE dentro il container, e quei peer hanno bisogno di una route o di un proxy esplicito che punta a lui; il bridge predefinito di Docker non aggiunge quella route per te.

Il networking dell'host, docker run --network host, è la versione economica: il container entra nello spazio dei nomi dell'host e vede direttamente le route del tunnel. Però elimina l'isolamento di rete, invita a collisioni di porte ed è in pratica un'opzione Linux. Una porta pubblicata non è un sostituto. -p 8080:80 rende un servizio raggiungibile dall'host; non rende raggiungibili gli indirizzi interni 10.x dall'interno del container, e confondere le due cose è il modo in cui un problema di routing diventa una porta di database esposta.

Distingui un guasto di nome da una sottorete sovrapposta

Il bridge predefinito di Docker è 172.17.0.0/16, e molti pool VPN aziendali stanno dentro 172.16.0.0/12. Una volta che il tunnel possiede quell'intervallo, il traffico destinato a un container può essere instradato nel tunnel. I nomi falliscono, l'errore sembra DNS, e il problema sono gli indirizzi. Controlla la destinazione, non il resolver: ip route get 172.17.0.5 sull'host, poi confronta il prefisso del bridge con il pool VPN e con docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'.

La riparazione è spostare uno degli intervalli: o un altro pool di indirizzi predefinito per Docker in daemon.json o un altro pool sul concentratore. Aggiungere un altro nameserver non fa nulla qui, e non lo fa nemmeno ssh -b: vincolare il client a un'interfaccia cambia l'indirizzo di origine, non una route di destinazione che punta nel tunnel. Rapida divisione del lavoro: un nome che si risolve sull'host ma non nel container è un problema di resolver, mentre una connessione IP diretta che si blocca nel container ma funziona sull'host è un problema di route o di catena di inoltro.

WSL e Docker Desktop non seguono le regole di Linux

Nella sua modalità NAT predefinita, WSL sta dietro il proprio adattatore virtuale e non riceve le route interne né i suffissi DNS della VPN di Windows, ed è per questo che il browser funziona e ping dentro la distribuzione no. Il networking a specchio, disponibile nelle build recenti di Windows 11, condivide invece le interfacce dell'host, ma va abilitato in .wslconfig e la distribuzione va riavviata con wsl --shutdown. Conferma la modalità attiva con wslinfo --networking-mode prima di applicare una correzione, perché la risposta NAT è sbagliata per la modalità a specchio e viceversa.

Docker Desktop aggiunge un altro livello. I container Linux su Windows stanno dietro un'implementazione NAT storicamente chiamata vpnkit, e il loro percorso differisce da quello di un host Linux nativo. I container Windows si collegano tramite uno switch virtuale Hyper-V, e un client aziendale può bloccare il traffico da quello switch, così i container Linux sulla stessa macchina possono raggiungere la rete mentre i container Windows non hanno nulla. Uno switch esterno legato all'adattatore fisico è la soluzione abituale, e cambia l'identità di rete del container in modi che la policy potrebbe non consentire.

Prima di scrivere un'altra regola, percorri i quattro livelli in ordine e fermati dove uno di essi è sbagliato:

  • Dentro il container: ip route raggiunge il prefisso privato, e /etc/resolv.conf nomina un upstream che lo risolve?
  • Sull'host: l'inoltro è attivo, NAT è legato all'interfaccia del tunnel, e la catena di inoltro accetta il bridge verso il tunnel?
  • Sul peer: conosce un percorso di ritorno, o ti affidi a NAT per nascondere la sottorete del container?
  • Sulla piattaforma: WSL è in modalità NAT o a specchio, e il container è Linux o Windows?

Riesegui ogni test in un container o una shell nuovi. Un processo di lunga durata con una risposta in cache o una connessione stabilita continuerà a fallire dopo che la configurazione è corretta, e quel guasto sembra esattamente una correzione che non ha funzionato.

Articoli correlati