Containers antwoorden op het kantoor-LAN en stoppen met antwoorden op het moment dat de bedrijfs-VPN verbindt. In de container is niets veranderd: geen herstart, geen image-update, geen configuratiewijziging. Wat veranderd is, is de routetabel van de host, waar een route die de tunnel toevoegde nu het adres dekt dat Docker uitdeelde. Voordat je een subnet aanpast of om een VPN-uitzondering vraagt, leg je beide adresplannen naast elkaar, want de reparaties kosten heel verschillende hoeveelheden tijd.

Lees de route voordat je de configuratie wijzigt

Begin bij het netwerk dat Docker werkelijk heeft gebouwd. Het IPAM-blok is het adresplan, en het subnet is de regel die ertoe doet:

docker network inspect project_default --format '{{json .IPAM.Config}}'
# [{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]

Compose wijst standaard een /16 toe aan elk nieuw project, dus twee services kunnen binnen een bereik van 65,536 adressen vallen. Die breedte is waarom overlappingen vaak voorkomen: een willekeurig gekozen /16 is groot genoeg om een site die je via de tunnel bereikt op te slokken, en niemand merkt het tot de tunnel opkomt.

Vergelijk daarna de routekeuze voor één containeradres, één keer met de tunnel omlaag en één keer met de tunnel omhoog:

ip route get 172.18.0.7
# 172.18.0.7 dev br-3f2a9c src 172.18.0.1

Twee soorten antwoord leiden naar verschillende plekken. Als de uitvoer tun0, wg0 of een ander tunnelapparaat noemt, is een VPN-prefix minstens zo specifiek als de brugroute en bereikt het pakket de container nooit. Noemt de uitvoer nog steeds de brug, dan kan de host de container bereiken, en zit je fout in filtering, DNS of een retourpad dat maar in één richting faalt.

Waarom een /16-botsing makkelijk optreedt

Docker haalt brugnetwerken uit de privépools, en de standaardpool komt uit het blok 172.16.0.0/12 in stukken van /16. Bedrijfsnetwerken gebruiken vaak dezelfde privéruimte, en een remote-access-VPN duwt die prefixen naar elke client die verbindt. Wanneer beide kanten menen dat zij 172.18.0.0/16 bezitten, bevat één routetabel twee tegenstrijdige claims.

De prefixlengte bepaalt de winnaar, niet de volgorde waarin routes staan. Een tunnel die 10.0.0.0/8 duwt, verslaat een brugroute van 172.18.0.0/16 niet, omdat /8 minder specifiek is. Een tunnel die 172.18.0.0/16 duwt, of 172.18.5.0/24 daarbinnen, doet dat wel. Daarom werkt de ene laptop op het kantoornetwerk en faalt hij op hotel-Wi-Fi met dezelfde VPN: de client ontvangt andere prefixen.

Dezelfde tabel bepaalt de andere richting. Een container die een interne host op 10.8.4.20 nodig heeft, stuurt het pakket naar zijn brug-gateway, en de host routeert het daarna de tunnel in. Dat pad werkt meestal omdat de host de NAT uitvoert, maar het faalt wanneer de bedrijfskant alleen het VPN-adres van de laptop accepteert, of wanneer reverse-path-filtering het asymmetrische antwoord laat vallen.

Vergelijk de drie reparaties

Er zijn maar drie uitwegen, en ze verschillen in wie ze moet goedkeuren en wat er breekt terwijl je er een toepast.

RepairWho owns itWhat it costs
Renumber the container networkYouRecreates the network, restarts services, invalidates literal addresses in configs and scripts
Exclude your range from the tunnelNetwork teamChanges what every laptop sees, needs a change request and a client profile update
Reach the service through a published portYou and the clientOne port only, no direct container-to-container routing across the tunnel

Hernummeren is de enige reparatie die je zonder ticket kunt afronden, en daarom stoppen de meeste stukken daar. Het is goedkoop op een jong project en duur zodra een migratietool of een opgeslagen verbindingsstring naar een letterlijk adres wijst. Tel die verwijzingen voordat je YAML aanpast, niet erna.

Een uitzondering op de tunnel is het schoonste antwoord wanneer de overlap in een bereik ligt dat je bedrijf echt bezit. Het is ook het traagst, en het is nutteloos voor een contractor die het clientprofiel niet kan wijzigen. Een gepubliceerde poort of een kleine reverse proxy vermijdt de discussie: de client praat met een adres dat de tunnel al correct routeert, en alleen die poort hoeft bereikbaar te zijn.

Leg een bereik bewust vast

Voor één project declareer je het subnet in het compose-bestand, zodat het project niet meer uit de gedeelde pool put:

networks:
  default:
    ipam:
      config:
        - subnet: 10.201.7.0/24

Neem dat bereik uit een register dat je team bijhoudt, niet uit een voorbeeld dat je tijdens het zoeken vond. Controleer het tegen de prefixen die de VPN duwt, de thuisnetwerken waar mensen werken, en de andere containernetwerken op dezelfde host. Plan daarna de hercreatie, want een bestaand netwerk houdt zijn oude subnet: docker compose down, dan up -d. docker network rm weigert zolang er endpoints zijn aangesloten, en elke ipv4_address-vermelding in het bestand moet binnen het nieuwe bereik vallen.

Voor netwerken die Docker zelf aanmaakt, stel je een daemon-pool in /etc/docker/daemon.json in:

{
  "default-address-pools": [
    { "base": "10.201.0.0/16", "size": 24 },
    { "base": "10.202.0.0/16", "size": 24 }
  ]
}

De pool geldt voor netwerken die na een herstart van de daemon worden gemaakt, en bruggen die al bestaan houden hun adressen. Dat is handig en makkelijk te vergeten, dus behandel een oud project als nog steeds overlappend tot iemand het netwerk opnieuw aanmaakt. Dockers netwerkdocumentatie voor de engine beschrijft de toewijzingsregels als je de pool in een review moet verdedigen.

De Linux-brug en Docker Desktop falen anders

Op een Linux-host staan containers op bruggen in de namespace van de host. Eén tabel beslist alles, dus de vergelijking hierboven is het hele verhaal: lees de brugroute, lees de tunnelroute, en kijk welke wint voor een echt containeradres.

Docker Desktop zet containers achter een VM met een eigen NAT. De host heeft helemaal geen route naar het containeradres; hij bereikt de service via een gepubliceerde poort, meestal gebonden aan 127.0.0.1. Een VPN-client kan de service dus breken zonder enige subnetoverlap, door een route voor het interne bereik van de VM te installeren of door loopbackverkeer te blokkeren terwijl de tunnel verbonden is. Als ip route get op de host geen route voor het containeradres meldt, is dat verwacht en niet de bug.

De praktische consequentie is waar je eerst kijkt. Op Linux begin je bij de routetabel. Op Desktop begin je bij de gepubliceerde poort en de firewallregels van de VPN-client, en controleer je daarna of het interne subnet van de VM botst met iets wat de tunnel duwt.

Verifieer met de tunnel omhoog

Verbind de VPN twee keer opnieuw voordat je het opgelost noemt, want een profiel dat bij verbinden routes duwt en bij verbreken weer intrekt, kan na de eerste poging correct lijken. Draai daarna de controles die de client werkelijk uitvoert:

  1. ip route get vanaf de host, met het echte adres van de container.
  2. Een verzoek vanaf een andere container, om te bewijzen dat de service zelf gezond is.
  3. curl -sv http://127.0.0.1:8080/ vanaf de client met de tunnel omhoog, daarna traceroute -n naar het containeradres vanaf een machine op het LAN.
  4. sysctl net.ipv4.conf.all.rp_filter als het verzoek de host bereikt maar het antwoord verdwijnt.

Schrijf ten slotte het gekozen bereik in hetzelfde register dat je gebruikte om het te kiezen, met de datum en het project dat het bezit. De volgende overlap komt uit een project dat niemand documenteerde, en de enige verdediging is dat het bereik met opzet is gereserveerd in plaats van willekeurig getrokken.

Gerelateerde artikelen