Der cgroup-Bypass in nftables braucht zusätzlich eine Korrektur der Quelladresse
Das Markieren einer cgroup schickt sie in eine andere Tabelle, und der Socket behält die Adresse, die er bereits hatte. Klassifizieren, routen und übersetzen als drei getrennte Schritte.
Den Verkehr aus einer cgroup zu markieren ist der einfache Teil. Drei getrennte Entscheidungen müssen zusammenpassen, bevor eine einzige Anwendung über das physische Interface hinausgeht, während alles andere im Tunnel bleibt: welcher Socket zur Ausnahme gehört, welche Routing-Suche die Markierung auslöst und welche Quelladresse das Paket beim Verlassen trägt. Die meisten fehlgeschlagenen Versuche bekommen die erste richtig hin, die zweite halb und schauen sich die dritte nie an.
Drei Entscheidungen, nicht eine
Die Klassifizierung beantwortet, wer ausgenommen ist. Das Routing beantwortet, wohin das markierte Paket stattdessen geht. Die Quellwahl beantwortet, was die Gegenseite sieht und wohin die Antwort zurückkehren soll.
Diese scheitern unabhängig voneinander. Ein Paket kann klassifiziert, auf das physische Interface umgeroutet und trotzdem eine private Adresse tragen, die kein vorgelagerter Router zurückbringen kann, was einen Timeout erzeugt, der genau wie ein Firewall-Block aussieht. Nichts im Klassifizierungsschritt warnt davor. Nichts im Routing-Schritt auch nicht.
Auch die Reihenfolge zählt. Die Klassifizierung geschieht bei der Erzeugung des Pakets, das Routing danach, und die Übersetzung am Austrittspunkt.
Wenn du nicht benennen kannst, welche der drei Stufen kaputt ist, debuggst du nicht, sondern ordnest Regeln um und hoffst.
Triff eine cgroup, die es morgen noch gibt
nftables kann die Zugehörigkeit eines Sockets zu cgroup v2 prüfen, was wirklich nützlich ist, weil es dem Prozess folgt und nicht dem Ziel. Der Haken ist die Kennung. Eine Desktop-Anwendung, die aus einem Menü gestartet wird, lebt oft in einem flüchtigen Scope, dessen Name sich beim nächsten Start ändert.
nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroupEinen Scope-Namen aus einer Sitzung zu kopieren und fest zu verdrahten erzeugt eine Regel, die heute funktioniert und morgen stillschweigend nicht mehr greift. Setze die Anwendung stattdessen in einen bewusst stabilen Ausführungskontext, etwa einen eigenen Scope oder eine Unit, die du kontrollierst, und triff diesen. Dann überlebt die Regel Neustarts und Paketaktualisierungen. Sie wird außerdem zu etwas, worüber ein Kollege ohne deine Sitzungshistorie nachdenken kann.
Sei auch präzise dabei, welchen Mechanismus du verwendest. Die net_cls-Klassifizierung in cgroup v1 und das Socket-Matching in cgroup v2 sind verschiedene Dinge, und ein ähnlich benanntes Feld in der Dokumentation macht sie nicht austauschbar.
Eine Kette vom Typ route löst die zweite Suche aus
Bei lokal erzeugtem Verkehr entscheidet der Kettentyp, ob nach dem Setzen der Markierung noch etwas geschieht. Eine Output-Kette vom Typ filter kann ein Paket markieren, und der Kernel nutzt weiterhin die bereits gewählte Route. Eine Kette vom Typ route ist der Mechanismus, der eine neue Suche verlangt.
table inet bypass {
chain mark {
type route hook output priority mangle; policy accept;
socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
}
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200Die Markierung braucht dann eine Policy-Regel, die sie in eine Tabelle schickt, und diese Tabelle muss vollständig sein: eine Default-Route, alle lokalen Ziele, die die Anwendung noch benötigt, und ein vorgelagertes Gateway, das tatsächlich antwortet.
Wähle den Markierungswert bewusst. WireGuard nutzt sein eigenes fwmark für den äußeren Transportverkehr, und versehentlich auf derselben Zahl zu landen kann die eigene Routing-Ausnahme des Tunnels auf Arten stören, die schmerzhaft zu diagnostizieren sind.
Die Quelladresse folgt der Route nicht
Ein Socket, der bereits eine Quelladresse für den Tunnelpfad gewählt hat, behält diese Adresse nach dem Umrouten. Nichts an der Routing-Änderung schreibt sie um, weshalb das Paket das physische Interface mit einer privaten Quelle verlässt, die das vorgelagerte Netz nicht zurückbringen kann.
Das sichtbare Symptom ist eine Anwendung, die Daten zu senden scheint und nie eine Antwort erhält, und ein Firewall-Log, das nichts zeigt, weil das Paket nie zurückkam.
Die enge Korrektur ist eine Quellübersetzungsregel, begrenzt auf die Markierung dieser Anwendung und ihr reales Ausgangsinterface, integriert in das vorhandene NAT statt es zu ersetzen:
table inet bypass {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta mark 0x1 oifname "wlan0" masquerade
}
}Übersetze die Ausnahme und nichts sonst. Eine pauschale masquerade-Regel behebt das Symptom und zerstört deine Fähigkeit zu sagen, welcher Verkehr welchen Pfad genommen hat. Triff die Markierung, das reale Ausgangsinterface und, wenn möglich, die eigene Adresse der Anwendung. Breite Regeln sind leicht geschrieben und später kaum zu prüfen.
Was der Ausnahme noch entgeht
Die Namensauflösung kann von einem separaten Resolver-Prozess in einer anderen cgroup kommen, sodass die Klassifizierung des Browser-Sockets seine Abfragen nicht klassifiziert. Bleiben die Abfragen des Resolvers im Tunnel, während die Verbindung außerhalb verläuft, können Split-Horizon-Antworten nicht zum Pfad passen. Diese Diskrepanz erzeugt Fehler, die wie DNS-Bugs aussehen und keine sind.
Kindprozesse erben die cgroup in den meisten Setups, ein Helfer, der sich in einen eigenen Scope verschiebt, aber nicht. Prüfe, statt anzunehmen, besonders bei allem, was sich selbst erneut ausführt.
Entscheide schließlich, was passieren soll, wenn der Tunnel ausgefallen ist. Eine Ausnahme, die stillschweigend zu einem vollständigen Bypass wird, ist eine andere Sicherheitshaltung als eine, die geschlossen fehlschlägt, und nur eine von beiden ist das, was du gemeint hast.