Wir haben ein paar wenige Systeme, auf denen intensiver Gebrauch von iptables gemacht wird. Das heißt, dass einerseits eine große Anzahl an Regeln existiert und sich andererseits diese Regeln häufig ändern – etwa alle paar Sekunden tut sich dort etwas. Möchte man also den aktuellen Regelsatz betrachten, wird man naiverweise zuerst iptables -nvL
eingeben und dann merken, dass die Ausgabe zu lang ist. Also nimmt man iptables -nvL | less
, damit man komfortabler scrollen und suchen kann.
Ein Problem bekommt man unter Umständen, wenn währenddessen der Regelsatz geändert werden soll.
Der Lock von iptables ...
Seit einigen Jahren benutzt das iptables-Tool Locking. Bei jedem Aufruf wird versucht, den Lock zu bekommen. Schlägt das fehl, dann beendet sich iptables mit einem Fehler – und dem netten Hinweis, dass man vermutlich doch iptables -w
benutzen will, was dann mehrfach versucht, den Lock zu bekommen. Eigentlich sollte das in meinen Augen der Default sein, ist es aber leider nicht. Wir benutzen deswegen iptables -w
an den Stellen, wo es darauf ankommt.
Es gibt auch keinen Weg, iptables das Locking auszureden. Was auch immer man tut, es wird gelockt. Das betrifft dementsprechend auch die reine Anzeige der aktuellen Regeln. Das ergibt auch durchaus Sinn, da man ja ein konsistentes Bild erhalten und ausschließen will, dass ein anderer Prozess den Regelsatz verändert, während man selbst gerade dabei ist, ihn zu betrachten.
Wir halten also fest: iptables lockt.
... und die Pipe nach less
Ein iptables -nvL
funktioniert nun so:
... 12:43:13.019592 open("/run/xtables.lock", O_RDONLY|O_CREAT, 0600) = 3 12:43:13.019645 flock(3, LOCK_EX|LOCK_NB) = 0 12:43:13.019676 socket(PF_INET, SOCK_RAW, IPPROTO_RAW) = 4 12:43:13.019708 fcntl(4, F_SETFD, FD_CLOEXEC) = 0 12:43:13.019746 getsockopt(4, SOL_IP, IPT_SO_GET_INFO, "filter\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., [84]) = 0 ... 12:43:13.027281 write(1, " 0 0 ACCEPT all -- "..., 89) = 89 12:43:13.027390 write(1, " 0 0 ACCEPT all -- "..., 89) = 89 12:43:13.027490 write(1, " 0 0 ACCEPT all -- "..., 89) = 89 12:43:13.027601 write(1, " 3717 413K all -- "..., 89) = 89 12:43:13.027694 write(1, " 0 0 ACCEPT all -- "..., 89) = 89 12:43:13.027791 write(1, " 3717 413K all -- "..., 89) = 89 12:43:13.027918 write(1, " 3717 413K all -- "..., 89) = 89 12:43:13.028023 write(1, " 3717 413K all -- "..., 89) = 89 12:43:13.028116 write(1, "62680 4174K all -- "..., 89) = 89 12:43:13.028210 write(1, "62680 4174K all -- "..., 89) = 89 12:43:13.028283 write(1, "62680 4174K all -- "..., 89) = 89 12:43:13.028371 write(1, "62680 4174K all -- "..., 89) = 89 12:43:13.028447 write(1, " 11 4176 ACCEPT icmp -- "..., 89) = 89 12:43:13.028516 write(1, " 0 0 ACCEPT icmp -- "..., 89) = 89 12:43:13.028583 write(1, " 0 0 ACCEPT icmp -- "..., 89) = 89 12:43:13.028649 write(1, " 0 0 ACCEPT icmp -- "..., 89) = 89 ... 12:43:13.436317 exit_group(0) = ? 12:43:13.436531 +++ exited with 0 +++
Sprich: Der Lock wird ergriffen, der aktuelle Regelsatz vom Kernel erfragt und dann die Ausgabe erzeugt und geschrieben. Der Lock bleibt dabei die ganze Zeit über bestehen und wird erst implizit beim Verlassen des Programms wieder freigegeben.
Wir pipen diese Ausgabe nach less
. Pipes haben die schöne Eigenschaft, irgendwann "voll" zu sein. Das vordere Programm kann also nur eine begrenzte Anzahl an Daten schreiben, danach blockiert es so lange, bis das hintere Programm ein paar Daten gelesen hat. Dieser Puffer ist relativ klein: Wie man 7 pipe
verrät, ist die Standardgröße heutzutage 64 Kibibytes.
Die Gesamtgröße der Ausgabe auf unseren Systemen liegt bei etwa einem Megabyte. Und da beginnt das Problem. Ein beherztes iptables -nvL | less
reserviert und hält den internen iptables-Lock, bis dieses gesamte Megabyte durch die Pipe geschoben wurde. Das alles wäre bis hierhin nicht so unglaublich dramatisch, würde less
nicht recht früh aufhören, Daten aus der Pipe zu lesen: Es beobachtet, ob und wohin der Benutzer scrollt und liest daher erstmal nur so viel, bis die ersten paar Bildschirmseiten anzeigbar wären. Liest less
nicht weiter, so kann iptables ...
nicht schreiben – und blockiert an der Pipe, während weiterhin der Lock gehalten wird.
Versucht nun währenddessen ein zweiter iptables-Prozess irgendeine Aktion auszuführen – Regeln hinzufügen, löschen, anzeigen, was auch immer –, so wird dieser sich entweder mit einem Fehler beenden und nichts tun, weil der Lock nicht aufgenommen werden konnte, oder der zweite iptables-Prozess benutzt -w
und wartet damit so lange nichtstuend, bis der Lock endlich verfügbar ist.
Fazit
Ist das System darauf angewiesen, mit sehr geringer Latenz Firewall-Regeln während des Betriebs ändern zu können, so kann ein unbedachtes iptables -nvL | less
alles zum Stehen bringen.
Was macht man stattdessen? Nun, vielleicht legt man sich einfach einen Alias an:
alias iptables-show='t=$(mktemp); iptables -nvL >"$t"; less "$t"; rm "$t"'
Damit erhält man dieselbe Funktionalität, strapaziert den Lock aber nicht – zumindest nicht so lange.