Das Paket des Containers erreicht tun0, und zusätzliche Capabilities am Container ändern daran nichts. Zwischen Gast und Tunnel liegen zwei Routen-Lookups: einer im Netzwerk-Namespace des Gasts, einer auf dem Host, nachdem der Frame auf der Bridge angekommen ist, und der zweite fragt eine andere Tabelle mit anderen Match-Regeln ab. Mein Referenzaufbau ist ein Linux-Host mit einem OpenVPN Client mit 10.8.0.10 auf tun0, einer Docker-Bridge auf 172.18.0.0/16, und einer privaten Adresse, die nur die Tunnel-Seite erreichen kann. Das Ziel bleibt eng: Dieses Gast-Subnetz verlässt den Host durch den Tunnel, während der Host seinen gewohnten Uplink behält.

Den VPN-Client im Container laufen zu lassen ist ein anderes Design, keine Abkürzung. Der Container besitzt dann seinen Tunnel und seine Zugangsdaten, und die Arbeit wandert in ein Dockerfile, statt in iptables zu bleiben.

Weiterleitung wählt die Route nach dem Ziel, dann schreibt NAT die Quelle um

Mit net.ipv4.ip_forward=1 wird ein weitergeleitetes Paket in der Haupttabelle ausschließlich nach dem Ziel geroutet. Die Quelle spielt bei dieser Entscheidung keine Rolle. Erst nach der Routenwahl läuft im Kernel POSTROUTING, und dort sitzt die Docker-eigene Regel: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. Alles, was die Bridge in Richtung einer anderen Schnittstelle verlässt, wird übersetzt, und welche Adresse es annimmt, hängt von der Schnittstelle ab, an die die Route es geschickt hat.

Diese Regel erklärt die meisten Symptome, nach denen ich gefragt werde. Hat die Haupttabelle keine Route für das private Ziel, verlässt das Paket den Uplink mit der physischen Adresse des Hosts. Dein Provider verwirft das Ziel, oder die Gegenseite verwirft eine RFC-1918-Quelle, die sie nie erwartet hat, und der Container läuft in einen Timeout, während der Tunnel steht und gesund ist. Ein Mitschnitt auf tun0 zeigt nichts, und das ist der Hinweis. Zeigt die Route dagegen auf tun0, schreibt MASQUERADE die Quelle auf 10.8.0.10 um, und derselbe Container funktioniert, ohne dass du etwas anfassen musst.

Frage den Lookup ab, der die Gast-Quelle nennt. Ohne from und iif antwortet der Host für seinen eigenen Socket und meldet einen Pfad, den das weitergeleitete Paket nie nehmen wird:

ip route get 192.168.11.222 from 172.18.0.5 iif br-appnet

Nennt diese Antwort den Uplink, ist der Tunnel nicht im Spiel, und keine Einstellung auf der Tunnel-Seite hilft.

Den Gast auf die eigene Adresse des Tunnels übersetzen

Die kleinste funktionierende Änderung ist eine ausdrückliche Quell-Umschreibung beim Hinausgehen. Füge sie ein, statt sie anzuhängen: Docker sitzt bereits am Kopf von POSTROUTING, daher wird eine mit -A hinzugefügte Regel hinter der von Docker ausgewertet und greift nie.

iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10

Zwei Bedingungen entscheiden, ob das hält. Die Adresse muss eine sein, die der Client wirklich hält, also fixiere sie mit einem Eintrag pro Client, der ifconfig-push 10.8.0.10 255.255.255.0; eine Regel, die eine nie zugewiesene Adresse nennt, erzeugt stille Drops, keinen Fehler. Und die Gegenseite muss diese Quelle akzeptieren und Antworten zurückrouten, deshalb nennt eine Allowlist dort die Tunneladresse statt des Gast-Subnetzes. Ist die Client-Adresse dynamisch, -j MASQUERADE mit -o tun0 folgt dem, was die Schnittstelle gerade hält, um den Preis einer Remote-Policy, die du nicht gegen einen festen Wert schreiben kannst.

Filtern ist die andere Hälfte. Docker setzt die FORWARD Policy auf DROP und leert bei jedem Daemon-Neustart die Chains, die es besitzt, also lege lokale Policy in DOCKER-USER, das überlebt:

iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Die Rückregel zählt: Eine Antwort, die auf tun0 für eine über die Bridge geöffnete Verbindung eintrifft, wird nicht schon deshalb akzeptiert, weil die ausgehende Richtung es war.

Den Host mit einer Quellregel direkt halten

Wenn die ganze Bridge den Tunnel nutzen soll, sonst aber nichts auf dem Host, überspringe NAT und gib dieser Quelle eine eigene Tabelle. Gast-Adressen bleiben dann durchgehend sichtbar.

echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnbox

Regeln werden vor der Haupttabelle befragt, und from trifft die Quelle. Die äußeren Pakete des Tunnels tragen die physische Quelle des Hosts, daher passen sie nie auf from 172.18.0.0/16 und der Tunnel kann sich nicht selbst in sich zurückrouten. Diese Selbstausnahme macht den Aufbau stabil.

Der Rückweg braucht jetzt Hilfe. Die Gegenseite sieht eine unübersetzte 172.18.0.x und hat keinen Grund zu wissen, wo sie liegt. Auf dem Server braucht eine Datei pro Client iroute 172.18.0.0 255.255.0.0 damit die interne Tabelle des Servers dieses Präfix an diesen Client gibt, und eine gepushte route 172.18.0.0 255.255.0.0 damit andere Clients wissen, dass es sie gibt.

Zwei Prüfungen, bevor du ihm traust. Führe ip route show default: mit redirect-gateway def1 hält die Tabelle 0.0.0.0/1 und 128.0.0.0/1 über tun0, und der Host ist überhaupt nicht direkt, weil diese Halb-Defaults gewinnen, ohne Ersetzen von default. Regeln, die mit ip rule add hinzugefügt werden, verschwinden beim Neustart ebenfalls, also trage sie in ein Up-Skript ein.

Die Paketgröße ist der zweite Fehler, auf den du stößt

Eine Bridge-Schnittstelle mit 1500 Bytes gibt dem Tunnel mehr, als er tragen kann, sobald die äußeren UDP- und IP-Header hinzukommen. Kleine Transaktionen gehen durch, also löst DNS auf und der TLS-Handshake läuft durch, dann hängt ein größerer Upload ohne Fehlermeldung. Der Sender wiederholt weiter dieselbe Segmentgröße, weil die ICMP-Meldung „Fragmentation Needed“, die ihm sagen würde, dass er schrumpfen soll, nie ankommt.

Behebe es an der Schnittstelle, an der MSS oder an beidem. Das Klemmen braucht keine Mitarbeit des Gasts:

ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Das Klemmen schreibt nur TCP um. UDP, einschließlich QUIC, braucht stattdessen eine niedrigere Gast-Schnittstelle, mit --opt com.docker.network.driver.mtu=1400 bei der Netzwerkerstellung oder dem passenden Compose-Schlüssel.

DNS und die Prüfungen, die sich zu wiederholen lohnen

Die Namensauflösung folgt demselben Pfad. Dockers eingebetteter Resolver unter 127.0.0.11 leitet an die konfigurierten Server des Hosts weiter, ein Container erbt also, was der Host nutzt. Wird die private Zone nur von einem durch den Tunnel erreichbaren Resolver beantwortet, richte den Gast auf diese Adresse, und teste mit getent hosts im Container statt auf dem Host.

  • Führe die fehlschlagende Anfrage im Gast aus; ein Test auf dem Host nutzt eine andere Quelladresse und verdeckt das Problem.
  • Lies die Zähler der NAT- und Filterregeln, die du hinzugefügt hast. Eine Regel ohne Treffer ist ein Routing-Fehler, kein Policy-Fehler.
  • Bestätige, dass die Tunneladresse in deiner SNAT-Regel eine ist, die der Client wirklich hält.
  • Senke die Gast-MTU oder klemme die MSS, bevor du den Server für einen Hänger verantwortlich machst.

Arbeite die Liste ab und stoppe beim ersten Fehler. Diese Schicht ist die, die repariert werden muss; alles, was weiter unten hinzugefügt wird, verdeckt nur die Grenze, die bricht.

Ähnliche Artikel