Mehrere Wochen lang war ein von mir gepflegter Webserver Ziel einer
verteilten SYN-Flood. Sie führte mehrfach zu (teilweise massiven) Problemen bei der Auslieferung der Daten. Das fand ich… überraschend. Eigentlich hätte ich davon nämlich nichts merken dürfen.
Jede
TCP-Verbindung beginnt mit einem
Handshake: Rechner A schickt ein SYN-Paket (»Haage! Jemand za hage?«) an Rechner B. Wenn sich auf Rechner B ein Programm bereit erklärt hat, auf dem gewünschten Port Verbindungen anzunehmen, antwortet Rechner B mit SYNACK (»Haage! Jajaja! Jemand za hage?«). Rechner A bestätigt das wiederum mit einem ACK-Paket (»Jajaja!«) — und schon steht die Verbindung und die Nutzdaten (meist Spam oder Schweinegifs) können fließen.
Bei einer SYN-Flood (die Schreibung hab ich aus der Wikipedia, ich hatte mich eigentlich an »Synflood« gewöhnt) schickt der Angreifer eine Menge SYN-Pakete, ohne sich um die Antworten zu scheren. Der angegriffene Server reagiert auf jedes SYN mit einem SYNACK und steckt die »halb offene« Verbindung in eine Warteschlange, wo sie eine Weile auf das abschließende ACK wartet. Kommt das nicht, wird sie schließlich entsorgt. Das Ziel des Angreifers ist, diese Warteschlange zum Überlaufen zu bringen, damit der Server keine neuen Verbindungen mehr annehmen kann. In der Fachsprache bezeichen wir das als »clogging teh tubez«.
Gegen eine SYN-Flood gibt es in der Theorie eine einfache Abhilfe:
SYN-Cookies. Die werden automatisch eingesetzt, sobald die Warteschlange für halb offene Verbindungen voll ist: statt sich in der Warteschlange zu merken, dass er sich mit einer Gegenstelle mitten im Handshake befindet, markiert der Server das SYNACK-Paket so, dass er an der Antwort, am ACK-Paket, erkennen kann, dass es sich um einen gültigen Handshake handelt.
In der Praxis funktionierte das bei mir auch immer. Bis zu diesem Angriff auf diesen Server. War die Warteschlange voll, kamen nur noch vereinzelte Verbindungen beim Webserver an, es wurden kaum noch Daten ausgeliefert und die Serverlast ging gegen null. — »Er hat Jehova gesagt! Er hat Jehova gesagt! Jeder weiß doch, dass SYN-Cookies funktionieren
müssen!« — Nein, ich hatte nicht bloß vergessen, sie anzuschalten:
TcpExt:
190431 SYN cookies sent
159509 SYN cookies received
233521 invalid SYN cookies received
Die nächste Seltsamkeit war mein Unvermögen, die Warteschlange zu verlängern. Egal an welchen Rädchen ich drehte und wie heftig der Angriff gerade lief, »netstat« behauptete stur, dass ich während der Spitzen des Angriffs nicht mehr als 512 Verbindungen im Zustand »SYN_RECV« hatte. Ich bin schließlich bei folgenden Einstellungen angelangt, die zumindest nicht zu einer Verschlechterung der Situation führten (Linux-Kernel 2.6.x, 2 GB RAM):
echo 5000 >/proc/sys/net/core/netdev_max_backlog
echo 4096 >/proc/sys/net/ipv4/tcp_max_syn_backlog
echo 2048 >/proc/sys/net/core/somaxconn
echo »389952 519936 779904« > /proc/sys/net/ipv4/tcp_mem
echo 131072 > /proc/sys/net/netfilter/nf_conntrack_max
Im Angesicht meines Unvermögens, die Warteschlange zu verlängern, musste ich dafür sorgen, dass 1) weniger halbfertige Verbindungen in die Warteschlange eingestellt wurden, dass 2) die Verbindungen, die zum Angriff gehörten, schneller weggeräumt wurden und/oder dass 3) legitime, vollständig aufgebaute Verbindungen schneller vom Webserver angenommen wurden.
Den ersten Punkt löste ich durch das Filtern der aktiveren Teilnehmer des verteilten Angriffs: wer mir mehr als hundert SYNs in zehn Sekunden schickt, kann es nicht gut mit mir meinen und wird erst mal für eine Weile ignoriert. Vermutlich wäre es elegant gewesen, das mit »iptables --limit« zu lösen, aber es entstand bei mir quasi aus der Diagnose heraus und passierte daher im gleichen Shellskript, das mich permanent über den Verlauf des Angriffs informierte und dazu eh schon die Verbindungen durchzählte. Natürlich macht das den Server angreifbar für
Spoofing: wenn ein Angreifer die SYN-Pakete nicht mit seiner eigenen IP als Absender losschickt, sondern mit der IP eines Nachbarn (und der Provider das nicht erkennt und filtert), sperrt dieser Mechanismus den Falschen. Pech für den Nachbarn, aber das
Wohl der Vielen wiegt mehr als das Wohl der Wenigen oder des Einzelnen.
Das beschleunigte Weggräumen von halbfertigen Verbindungen, die nicht schnell genug fertig aufgebaut werden, und die dadurch in den Verdacht geraten, Teil der Attacke zu sein, besorgt der Kernel nach dem Herabsetzen von tcp_synack_retries auf 1 oder, wenn das nicht reicht, auf 0. Der Nachteil hieran ist, dass wohl auch die ein oder andere legitime Verbindung von einer trägen Gegenstelle über die Klinge springen muss. Aber das Wohl der Vielen…
Den dritten Schritt, der nach den ersten beiden Schritten vermutlich überflüssig gewesen wäre, hab ich leider als erstes getan — davon ausgehend, dass SYN-Cookies funktionieren müssen, suchte ich den Flaschenhals zuerst beim
Webserver.
Der angegriffene Server bekam auch schon im Normalfall sehr viele Verbindungen ab, weil er den gesamten statischen Content (hauptsächlich Bilder) für eine Reihe von anderen Sites ausliefert. Der bisher dahin dafür eingesetzte
thttpd schien überfordert, lief (auf einer Maschine mit zwei Doppelkern-CPUs) single-threaded und bot kein
KeepAlive.
Vielleicht hätte sich als Alternative
lighttpd angeboten, aber mir schien der Zeitpunkt nicht wirklich geeignet für erste Experimente mit einem neuen Webserver, also griff ich auf
Apache2 mit dem
Worker-MPM zurück. Das Vergnügen hatte ich bisher nicht so oft, dank PHP bin ich praktisch überall auf das
Prefork-MPM festgenagelt, das einen Pool von Prozessen verwaltet, die die Requests entgegennehmen, bearbeiten und dann für den nächsten Request frei sind. Das Worker-MPM arbeitet im Vergleich dazu mit einem zweistufigen System: es startet einen relativ kleinen Pool von Prozessen, die unentwegt Requests annehmen und dann jeweils in Threads abarbeiten. Beim Ausliefern statischer Dateien sollte das im Vergleich zum Prefork-MPM sowohl die Verzögerung beim Annehmen einer Verbindung als auch den Speicherverbrauch verringern.
Nach einigem Rumprobieren bin ich bei folgenden Einstellungen für das Worker-MPM angelangt, die ich vorher auch für übertrieben gehalten hätte:
StartServers 16
MaxClients 6400
ServerLimit 256
MinSpareThreads 25
MaxSpareThreads 6400
ThreadsPerChild 25
MaxRequestsPerChild 0
MaxSpareThreads auf MaxClients zu setzen führt übrigens dazu, dass ein einmal gestarteter Thread nie mehr weggeräumt wird. Wenn die Stärke des Angriffs zunahm, kamen die gültigen (!) Requests ab und an in heftigen Wellen, die es dann blitzschnell entgegenzunehmen galt, da wurden dann auch mal ein paar Tausend Threads losgetreten (während im »Normalbetrieb« ein paar Hundert ausreichen) — die nächste Welle wurde aber entsprechend leicht verdaut, weil die Threads bereits existierten und nur noch angeschubst werden mussten.
Der Angriff hält immer noch an, hat aber keine praktischen Auswirkungen mehr, im Gegenteil: durch die Umstellung auf den Apache (und das Einschalten von KeepAlive — natürlich nur auf ein paar Sekunden, ich will ja nur alle Bildchen, Javascripte und Stylesheets eines Seitenzugriffs in einer Verbindung abfackeln, für den nächsten Klick kann gerne ‘ne neue kommen) liefern wir jetzt noch zackiger aus als vor dem Angriff. Interessant wird’s erst wieder, wenn der Angreifer noch ‘nen Zahl zulegt, sprich: wenn er die SYNs von mehr Absendern, also aus einem größeren Botnet, schickt, oder die Bandbreite des Angriffs massiv erhöht.