Rätta Docker- och WSL-trafik som ignorerar värdens VPN
Containrar och WSL håller egna rutter, resolvrar och vidarebefordringsvägar, så en fungerande värdtunnel lämnar dem med inaktuell DNS eller utan utgång. Rätta varje lager.
Jag tillbringade större delen av en förmiddag den här våren med att jaga en container som inte kunde lösa ett enda internt namn, medan webbläsaren bredvid öppnade wikin utan klagomål. Tunneln var uppe, webbläsaren bevisade det, och containern låg i en annan nätverksnamnrymd med en annan routtabell och en annan resolvrar. Följande är den ordning jag kontrollerar nu: först routtabeller, sedan namnmatchning, därefter vidarebefordran och sist värdplattformen. Stanna vid det första steget som redan misslyckas, eftersom regler längre ner i stacken döljer gränsen i stället för att flytta den.
Jämför de tre routtabellerna innan du ändrar en regel
En container på Dockers standardbrygga har en enda standardrutt mot docker0-gatewayen och anar inte att tun0 finns. Värden har tun0, rutterna som VPN:en sköt in och möjligen en policyregel som skickar probetrafik ut genom ett annat gränssnitt än du förväntar dig. WSL lägger till en tredje tabell bakom sin egen virtuella adapter. Skriv ut alla tre innan du redigerar någon av dem.
| Context | Command | What a wrong answer looks like |
|---|---|---|
| Host | ip route get 10.8.0.20 | Leaves a physical interface instead of tun0 |
| Container | ip route | Only 172.17.0.0/16 via 172.17.0.1 |
| Container DNS | cat /etc/resolv.conf | A public resolver, or an upstream the tunnel never pushed |
| WSL | ip addr; ip route | Only the WSL subnet, no Windows VPN adapter |
Om containerns enda rutt är bryggan och tun0 aldrig dyker upp i dess namnrymd hjälper inget nedan: ingen namnserverändring skapar en rutt, och ingen rutt skapar den tunnelidentitet som peeren accepterar. Ordningen spelar roll eftersom ett DNS-fel och ett ruttfel ser identiska ut inifrån ett skal.
Peka resolvern mot tunneln i stället för mot en offentlig
På värden, resolvectl status listar DNS-servrar per länk, och readlink -f /etc/resolv.conf berättar om filen är systemd-stubben, en NetworkManager-fil eller något en leverantörsklient skrivit om. Inne i en container på ett användardefinierat nätverk, /etc/resolv.conf namnger 127.0.0.11, Dockers inbyggda resolvrar, som vidarebefordrar till den upstream som daemonen konfigurerades med. En container på standardbryggan fick däremot en kopia av värdens fil när den skapades, och behåller den.
Den kopian är anledningen till att en tunnel som kommer upp efter att containern startat lämnar containern frågande den gamla upstreamen. Antingen skickar du VPN-resolvern uttryckligen med docker run --dns 10.8.0.53, eller sätter "dns": ["10.8.0.53"] i /etc/docker/daemon.json och startar om daemonen, eller återskapar containern när tunneln är uppe. Återskapande är oftast billigare än att felsöka en resolvrar som var korrekt för en timme sedan.
Att byta till en offentlig resolvrar är reparationen som behåller symptomet. Den löser internet och misslyckas i varje privat zon, och den kan tyst kringgå den filtrering som tunneln finns för att upprätthålla. Om du försöker skriva inne i /etc/resolv.conf på en körande container, förvänta dig Device or resource busy: Docker bind-monterar den filen, så ändringen kräver först en avmontering. Att sätta --dns vid skapandet är sekvensen som överlever en omstart.
Vidarebefordra och översätt när containrar lånar tunneln
Med standardbryggan är värden en router mellan docker0 och tun0, och som standard gör den ingen av de två uppgifterna. Slå på vidarebefordran och översätt källadressen så att peeren ser en tunneladress på värden i stället för en den inte har någon returrutt till (ersätt tun0 med det som din klient kallar gränssnittet):
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 ACCEPTFORWARD-reglerna spelar roll på värdar vars vidarebefordringspolicy är DROP. Docker infogar egna kedjor, så testa med ett paket i stället för att läsa iptables -S: från containern bör ip route get 10.8.0.20 namnge bryggans gateway, och nc -vz 10.8.0.20 443 bör ansluta eller timed out vid tunneln i stället för vid resolvern.
Utan MASQUERADE behöver peeren en returrutt till 172.17.0.0/16, vilket de flesta fjärråtkomst-VPN:er inte skjuter in. Här går också en policybaserad klient sönder planen: om den bara fångar trafik från vissa UID:n eller cgroups ser dina vidarebefordringsregler aldrig paketet inne i tunneln, och rättelsen hör hemma i klientens routingpolicy och inte i iptables. Regler som läggs till manuellt försvinner vid omstart, så lägg dem i en systemd-enhet eller en netfilter-persistent-tabell och testa igen efter en omstart, eftersom en tunnel som kommer upp sent kan spola bort dem.
Lägg tunneln inne i containern när isolering är målet
Om en container måste gå ut genom en viss nod, kör klienten i den containern. Den behöver --cap-add NET_ADMIN och --device /dev/net/tun, och då äger den sin egen tun0, sina rutter och sin brandvägg. Om andra containrar passerar genom den behöver den också vidarebefordran och en MASQUERADE-regel inne i containern, och de peerarna behöver en uttrycklig rutt eller proxy som pekar på den; Dockers standardbrygga lägger inte till den rutten åt dig.
Värdnätverk, docker run --network host, är den billiga versionen: containern går med i värdens namnrymd och ser tunnelrutterna direkt. Det släpper också nätverksisoleringen, bjuder in portkollisioner och är i praktiken ett Linux-alternativ. En publicerad port är ingen ersättning. -p 8080:80 gör en tjänst nåbar från värden; det gör inte interna 10.x-adresser nåbara inifrån containern, och att blanda ihop de två är hur ett routingproblem blir en exponerad databasport.
Skilj ett namnfel från ett överlappande subnät
Dockers standardbrygga är 172.17.0.0/16, och många företags-VPN-pooler ligger inom 172.16.0.0/12. När tunneln äger det intervallet kan trafik avsedd för en container routas in i tunneln i stället. Namn misslyckas, felet ser ut som DNS, och problemet är adresserna. Kontrollera destinationen, inte resolvern: ip route get 172.17.0.5 på värden, jämför sedan bryggans prefix med VPN-poolen och med docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'.
Reparationen är att flytta ett av intervallen: antingen en annan standardadresspool för Docker i daemon.json eller en annan pool på koncentratorn. Att lägga till ytterligare en namnserver gör ingenting här, och det gör ssh -b inte heller: att binda klienten till ett gränssnitt ändrar källadressen, inte en destinationsrutt som pekar in i tunneln. Snabb arbetsfördelning: ett namn som löses på värden men inte i containern är ett resolverproblem, medan en direkt IP-anslutning som hänger i containern men fungerar på värden är ett rutt- eller vidarebefordringskedjeproblem.
WSL och Docker Desktop följer inte Linux-reglerna
I sitt standard-NAT-läge sitter WSL bakom sin egen virtuella adapter och får inte Windows-VPN:ens interna rutter eller DNS-suffix, vilket är varför webbläsaren fungerar och ping inne i distributionen inte. Speglat nätverk, tillgängligt i nyare Windows 11-byggen, delar i stället värdens gränssnitt, men det måste aktiveras i .wslconfig och distributionen startas om med wsl --shutdown. Bekräfta det aktiva läget med wslinfo --networking-mode innan du tillämpar en rättelse, eftersom NAT-svaret är fel för speglat läge och tvärtom.
Docker Desktop lägger till ytterligare ett lager. Linux-containrar på Windows sitter bakom en NAT-implementering som historiskt kallas vpnkit, och deras väg skiljer sig från en inhemsk Linux-värd. Windows-containrar ansluter via en virtuell Hyper-V-växel, och en företagsklient kan blockera trafik från den växeln, så Linux-containrar på samma maskin kan nå nätverket medan Windows-containrar inte har något. En extern växel bunden till den fysiska adaptern är den vanliga lösningen, och den ändrar containerns nätverksidentitet på sätt som policyn kanske inte tillåter.
Innan du skriver ytterligare en regel, gå igenom de fyra lagren i ordning och stanna där ett av dem är fel:
- Inne i containern: når
ip routedet privata prefixet, och namnger/etc/resolv.confen upstream som löser det? - På värden: är vidarebefordran på, är NAT bundet till tunnelns gränssnitt, och accepterar vidarebefordringskedjan brygga till tunnel?
- Hos peeren: känner den en returväg, eller förlitar du dig på att NAT döljer containerns subnät?
- På plattformen: är WSL i NAT- eller speglat läge, och är containern Linux eller Windows?
Kör om varje test i en ny container eller ett nytt skal. En långkörande process med cachat svar eller en etablerad anslutning fortsätter att misslyckas efter att konfigurationen är korrekt, och det felet ser precis ut som en rättelse som inte fungerade.