Wie Linux performant als Firewall mit sehr vielen Interface-basierten Regeln benutzt werden kann, und welche Hardware-Tweaks dafür nötig sind.
Um Linux zum Routing zu überreden bedarf es nicht viel:
sysctl net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1
Doch damit fangen die Probleme erst an…
Performance von iptables bei vielen Interface-Regeln
Wie in der Übersicht unserer Netzstruktur erwähnt benutzten unsere auf Linux basierenden Kernrouter früher das wohl allen Linux-Admins bekannte iptables. Das lief so lange gut, bis Debian Buster iptables durch eine Kompatibilitätsschicht von nftables ersetzt hat. Plötzlich kam es zu Packet Loss, weil das Abarbeiten der Firewalls für jedes Paket schlicht zu lange dauerte.
Das ist vermutlich der richtige Moment, etwas genauer darauf zu schauen, wie sich das Netzwerk aus Sicht des Servers darstellt. Es handelt sich natürlich um “Router on a Stick”: Die Server verbinden sich durch VLANs mit den Routern, von denen sich am Router dann hunderte auf ein und dem selben physischen Interface sammeln. Jedes dieser Interfaces hat eigene Regeln definiert, welche neuen Verbindungen in das VLAN aufgebaut werden dürfen. Das heißt, dass die iptables-Regeln im Wesentlichen so aussahen:
iptables -A FORWARD -o vlan1 -j vlan1
iptables -A FOWRARD -o vlan2 -j vlan2
...
iptables -A FORWARD -o vlan300 -j vlan300
iptables -A FORWARD -m state --state established -j ACCEPT
In den einzelnen Chains stehen dann die Regeln des jeweiligen VLANs. Aber alleine diese Jump-Regeln für jedes einzelne Paket abzuarbeiten dauerte mit der nftables-Kompatibilitätsschicht viel zu lang.
Nach viel Debugging, womit der Kernel eigentlich seine Zeit verbringt, waren wir schlauer: Jede dieser Regeln führt einen Vergleich der Interface-Namen durch, um herauszufinden, durch welches Interface ein Paket den Router verlassen würde. Da Namen Zeichenketten sind ist das eher ineffizient.
Umstieg auf nftables
Also war es an der Zeit, sich nftables anzusehen. Es wurde schnell klar, dass ein Umstieg sich auch angesichts der Features lohnen würde. Das am meisten gewünschte Feature waren anonyme ad-hoc Sets von IP-Adressen, um etwa
iptables -A vlan1 -s 192.0.2.1 -p tcp --dport 80 -j ACCEPT
iptables -A vlan1 -s 198.51.100.2 -p tcp --dport 80 -j ACCEPT
iptables -A vlan1 -s 203.0.113.3 -p tcp --dport 80 -j ACCEPT
einfach ersetzen zu können durch
nft add rule inet vlan1 ip saddr { 192.0.2.1, 198.51.100.2, 203.0.113.3 } tcp dport 80 accept
ohne dafür ein neues ipset anlegen zu müssen (was durch unsere Automatisierung gar nicht möglich war). Auch einheitliche Regeln für IPv4 und IPv6 waren mit iptables nicht möglich.
Also wurde das System zum Konfigurieren der Server langwierig von iptables auf nftables umgestellt. Alle Firewall-Regeln sind in einem zentralen git-Repository gespeichert (mehr dazu in Teil 3). Dadurch mussten wir “nur” die Übersetzung in Konfiguration auf den Routern neu schreiben, damit sie nftables-Regeln statt iptables-Regeln erzeugt.
nftables sollte natürlich nicht wieder das selbe Problem der Zeichenkettenvergleiche haben. Also mussten wir die ausgehenden Interfaces dort direkt mit ihrem Index in den Regeln hinterlegen anstatt als Namen. Und das ist leichter gesagt als getan, da sich die Interfaces des Systems ja ändern können, wenn wir ein neues Netz in Betrieb nehmen oder ein altes löschen. Der erste naive Ansatz war also, die Regeln alle neu zu laden wenn sich etwas an den Netzwerk Interfaces des Systems ändert. Doch gerade beim Start des ganzen Systems ist das zum Scheitern verurteilt, da das System in sehr kurzer Zeit hunderte Interfaces anlegt.
nftables-Regeln sinnvoll aufteilen
Wir haben uns also für einen geteilten Ansatz entschieden: Das Gros der Regeln bleibt dauerhaft geladen, da sie einfach in einer nach dem Interface benannten Chain “vlan1” sitzen, dieser Name aber für nftables keinerlei Bedeutung hat. Den Teil der Regeln, der bei Veränderung der Netzwerkinterfaces angepasst werden muss, lagern wir in eine extra Datei aus:
# cat /etc/nftables.d/interfaces/vlan1.conf
add element inet filter oif_jump {
vlan1 : goto vlan1,
}
# nft list chain inet filter forward
table inet filter {
chain forward {
type filter hook forward priority filter; policy drop;
oif vmap @oif_jump
}
}
nftables übersetzt beim Laden der ersten Datei den Interface-Namen “vlan1” in den Interface-Index (z.B. 42) und speichert nur den im Kernel. In der Forward-Chain nutzen wir dann die Map-Funktionalität von nftables aus und springen abhängig vom Index des ausgehenden Interfaces direkt in die entsprechende Chain – und zwar ohne linear alle Interfaces durchzugehen!
Das Laden der Interface-Dateien (und noch einiges andere) übernimmt dann udev für uns:
# cat /etc/udev/rules.d/99-load-nftables.rules
SUBSYSTEM=="net", ACTION=="add", RUN+="/usr/local/lib/udev/load-nftables.sh"
# cat /usr/local/lib/udev/load-nftables.sh
#!/bin/sh
nft -f "/etc/nftables.d/interfaces/${INTERFACE}.conf"
Als Übung für zu Hause muss noch das Initscript von nftables angepasst werden. Regeln für bereits existente Interfaces sollen gleich mit geladen werden (z.B. auch beim Reload der Regeln), aber nicht die der noch nicht angelegten (da ansonsten der ganze Ladevorgang abbricht).
Routing und Firewalling mit Linux: Runter auf die Hardware-Ebene
In den Tests sah alles gut aus. Im Produktivbetrieb kam es trotzdem zu Paketverlusten, die wir uns sehr lange nicht erklären konnten. Immerhin zeigten die Statistiken auf der Netzwerkkarte, dass überhaupt Pakete verworfen wurden. Wir hatten also ein messbares Phänomen!
Die Recherche förderte viele mögliche Ursache für solchen Paketverlust zu Tage. Wir stellten also z.B. sicher, dass Pakete nicht erst zwischen den verschiedenen Prozessoren eines Mehrprozessorsystems hin und her gereicht werden mussten, doch leider ohne Erfolg.
Intensives Monitoring im Sekundenabstand zeigte dann irgendwann einen Zusammenhang: Wenn plötzlich viele Pakete ankommen (ein “Burst”) und die CPU vorher nicht auf voller Leistung lief ist sie zu langsam im Hochtakten um den Ansturm zu verarbeiten. Die Lösung war also einfach, sämtliche Energiesparfunktionen der Prozessoren zu deaktivieren.
Und noch einen weiteren Trick haben wir ausgespielt: Anstatt die Absenderadressen der eingehenden Pakete anhand der Routing-Tabelle zu überprüfen (uRPF aka rp_filter) haben wir diese Information mit in nftables-Regeln verpackt! Denn um den kürzesten möglichen Weg zu wählen haben alle Router eine vollständige Sicht der globalen Routingtabelle. Und zusätzlich zur Empfänger- auch noch die Absenderadresse jedes Paket gegen alle 800.000 Routen zu prüfen hat noch einmal ordentlich Leistung geschluckt.
# nft list table netdev ingress
table netdev ingress {
chain common {
meta pkttype { broadcast, multicast } accept
meta protocol arp accept
ip6 saddr fe80::/64 accept
}
chain vlan1 {
type filter hook ingress device "vlan1" priority filter; policy drop;
ip saddr 192.0.2.0/24 accept
goto common
}
}
nftables bearbeitet diese Regeln der netdev-Klasse sehr früh auf dem Weg des Pakets durch den Kernel. Er verwirft Pakete von ungültigen Adressen so bereits bevor ihre Empfängeradressen in der Routingtabelle aufgelöst werden. Außerdem können wir so auch Adressen von einem Interface erlauben, die aktuell vielleicht einen anderen Weg nehmen (asymmetrisches Routing).
Ausblick
Jetzt haben wir einen Linux-Server zum Routen und Firewalling konfiguriert. Aber so ein zentrales System kommt natürlich nicht ohne Redundanz aus! Im nächsten Artikel (wird demnächst veröffentlicht) werden wir uns also um Redundanz via VRRP kümmern.