IT-Tagebuch: „iptables -nvL | less“ considered harmful

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.

Im IT-Tagebuch schreibt ein Kollege aus unserem IT-Team in loser Folge über Themen, Probleme und Lösungen, die ihm im Arbeitsalltag begegnen.


Mehr über die Creative-Commons-Lizenz erfahren