Server-Daten: Small DDOS and some results - Kleiner DDOS (Distributed Denial of Service) und die Folgen
Wer Webanwendungen betreibt, der kennt das Problem: Manchmal funktioniert plötzlich etwas nicht. Es ist völlig unklar, was dahintersteckt.
So ging es am Sonntagvormittag mit Server-Daten: Eine Abfrage lieferte nicht das gewünschte Ergebnis. Ich baute eine Weile (direkt auf dem DbServer) daran herum. Als das fertig war, wollte ich die Abfrage in die zugehörige Datenbank einspielen - ganz regulär über die internen blauen Seiten.
Oh: Der Webserver reagierte nicht. Hing irgendwie. Aber warum?
Auf dem Webserver läuft noch ein zweites System, ein Testsystem. Im Prinzip derselbe Code (außer bei Tests). Nur ein anderer Port - 442, von außen her nicht zugänglich. Das funktionierte problemlos. Die Kommunikation zwischen Web- und Datenbankserver funktionierte also, der Webserver war ja auch online. Aber auch der Aufruf einer Seite auf dem Hauptsystem vom Webserver her - hing.
Da nichts funktionierte: Den Application Pool neu gestartet. Ging weiterhin nicht. Auch das Neustarten des Webservers änderte nichts.
Seit September läuft Server-Daten auf neuen Servern mit SSD-Platten. Seither dauert ein richtiger Reboot nur noch etwa eine Minute. Das ist so schnell, daß die Alternative "ohne Ergebnis herumsuchen" nicht so sinnvoll ist.
Also Reboot. Der funktionierte problemlos. Danach - dasselbe. Das Testsystem funktionierte, das Hauptsystem war erneut "irgendwie blockiert". Grund: Nun gänzlich unklar. Dabei war gar keine so großartige Auslastung zu sehen.
Eine Suche mit
> netstat -a -n
listete schließlich ungewöhnlich viele TCP-Verbindungen. Ok, da gibt es einen Stapel. Aber für einen Sonntagmorgen waren das viele. Und: Diverse im Status CLOSE_WAIT / SCHLIESSEN_WARTEN. Einzelne IP-Adressen kurz überprüft: Weltweite Adressen.
Webserver-Log: Lauter verschiedene IP-Adressen, teils 10 pro Sekunde, alle auf die Startseite der inzwischen relativ beliebten https://check-your-website.server-daten.de/ . Da kann man seine Domain, die Nameserver-Konfiguration und einiges mehr testen. Die Seite entstand eigentlich nebenbei Ende Oktober 2018. Wurde ergänzt und erfreut sich inzwischen einer deutlichen Beliebtheit (über 400 Checks pro Tag im Schnitt, werktags teils über 600).
Wurde der Application Pool neu gestartet, ging die Zahl der TCP-Verbindungen kurz runter. Um sofort wieder anzuwachsen, erneut mit CLOSE_WAIT.
Schließlich hing sich die Verwaltung des Application Pools "irgendwie auf". Wirkung: Der Dienst konnte nicht mehr regulär verwaltet werden. "Er kann gerade keine Steuerungsanforderungen entgegennehmen". Wenn ein grundlegendes Windows-Werkzeug blockiert ist, hilft nur noch ein Reboot. Also ein zweites Mal neu gebootet.
Und danach - funktionierte alles wieder. Als ob nichts gewesen wäre. Die ersten fingen an, ihre Domains zu testen. Alles so, wie es sein soll.
--
Technisch kann man das als einen kleinen Distributed Denial of Service werten. Als einen Angriff mit unterschiedlichsten IP-Adressen. Der Versuch, die Webanwendung durch pure Überlastung in die Knie zu zwingen. Praktisch muß man damit rechnen. Und vor allem: Warum hat das System so empfindlich reagiert?
Daß sich bsp. die Windows-Werkzeuge aufhängen bzw. daß sich ein Standard-Dienst nicht mehr stoppen läßt, das darf eigentlich nicht passieren. Bug beim letzten Patchday? Irgendwelche schrägen Bugs, die nur temporär in speziellen Situationen auftreten? Oder hatte Windows irgendwelche Wartungsarbeiten durchgeführt, bei denen etwas schief ging? Auch das läßt sich nicht ausschließen.
Insofern ist das immer die Suche nach der Nadel im Heuhaufen. Alles, was man findet, kann auch gänzlich unrelevant sein, wenn es tatsächlich ein Bug wäre.
Weiteres Nachforschen führte zu einigen Einsichten:
1. CLOSE_WAIT: Zu einer TCP-Verbindung zum Server gehören ein Port und ein Socket. Eine eingehende Anfrage -> der Webserver macht einen Port und einen Socket auf, um über diesen Port die Anfrage entgegenzunehmen. Wenn die Gegenseite fertig ist, kann sie die Verbindung schließen. Wenn aber das Programm (hier: Der Webserver) den Socket nicht schließt, verbleibt dieser im Status CLOSE_WAIT. Viele CLOSE_WAIT -> Verbindungen -> irgendetwas hängt lokal. Die Gegenseite hat sich schon verabschiedet, lokal "hängt" womöglich etwas.
Einen kleinen Dienst geschrieben, der alle 30 Sekunden auf dem Webserver prüft, ob es CLOSE_WAIT - Verbindungen gibt. Falls ja, soll der Dienst eine Mail schicken. Ergebnis: Praktisch nichts, einmal ein CLOSE_WAIT vom Mailserver.
2. Die aufgerufene Seite hat sehr viele Abfragen. Das ist auf der Hauptseite nicht direkt sichtbar, da sieht man nur vier Abfragen. Aber die ganzen Details sind ebenfalls nur Abfragen auf derselben Seite. Da der Webserver diese Abfragen parallel zum DbServer schickt, sind das knapp 30 Verbindungen zum DbServer.
3. Das vom Testsystem her getestet. Ein kleines Programm geschrieben, das pro Sekunde 5 - 10 mal die Seite öffnet. Das führte zu einer sichtbaren Last auf dem DbServer. Allerdings wurden die Anfragen abgearbeitet, damit Verbindungen wieder freigegeben. Das wirkte nicht so wirklich kritisch.
4. Die kritischen Anfragen hatten alle denselben Referer. Der läßt sich direkt blocken, gleich am Anfang der Seitenverarbeitung. Damit würde zumindest dieses Botnetz nicht mehr durchkommen. Ohnehin wirkte das eher wie in Referer-Spam mit einem schlecht programmierten System als wie ein tatsächlicher Angriff. Der hätte mit Sicherheit nicht immer denselben Referer geschickt.
5. Schließlich die Frage: Wo könnte es einen Flaschenhals geben? Der Blick in die Konfiguration lehrte: Per ConnectionString waren maximal 500 gleichzeitige Verbindungen vom Webserver zum DbServer zulässig (Max Pool Size=500). Bei 30 benötigten Verbindungen produzieren 10 Anfragen in einer Sekunde schon 300 Verbindungen.
6. Ursprünglicher Standardwert für diese Maximalzahl: 100 Verbindungen. Aber ein MS-SqlServer kann bis zu 32768 gleichzeitige Verbindungen verarbeiten. Warum das beschränken? Das massiv hochgesetzt.
7. Dann schließlich die Einsicht: Es wurde nur ein einziger Connection-Pool genutzt. Heißt: Maximal 500 Verbindungen stehen zur Verfügung. Ein gesonderter Dienst braucht am Anfang der Seitenverarbeitung eventuell Verbindungen. Dann werden Metadaten geholt, teils aus dem Cache, teils von der Datenbank. Schließlich werden die Abfragen ausgeführt. Wenn nun 20 Aufrufe der Seite innerhalb von etwa einer Sekunde produziert werden: Dann blockieren die 600 Verbindungen, 100 müssen also bereits warten. Da alle Zugriffe anonym erfolgen, muß in diesem Fall der gesonderte Dienst nicht mehr auf die Datenbank zugreifen, alle Daten sind im Cache. Wenn aber nun (Sonntagvormittag!) ein Kunde eine Seite erstmals abruft, dann muß der zusätzliche Dienst ermitteln, welche Seite das ist und ob der Nutzer darauf zugreifen darf. Dafür braucht er eine Verbindung aus dem Verbindungspool - und wartet.
Das war genau die eigene Beobachtung: Es gab keinen 500-Fehler (Überlastung), sondern die Seite kam einfach nicht. Aber es gab auch auf dem Webserver keine wahnsinnig große Auslastung. Kein Wunder, Warten kostet keine Ressourcen.
8. Die Konsequenz: Der zusätzliche Dienst hat nun seinen eigenen Verbindungspool. Damit kommen diese Anfragen immer zum DbServer durch. Und für Systemadministratoren: Die von Microsoft vorgeschlagene Begrenzung auf maximal 100 Verbindungen von Webserver zum DbServer ist Quatsch. Das kann so einen Flaschenhals produzieren. Die nach einigen Jahren Betrieb eingeführte Parallelverarbeitung von Abfragen erhöht auch den Bedarf an Verbindungen deutlich.
9. Das getestet: Auf dem Testsystem die maximale Zahl der Verbindungen deutlich runtergesetzt. Erneut die Seite massiv abgerufen. Prompt mußten Aufrufe warten. Das Programm crashte schließlich, weil es Timeouts beim Verbindungsaufbau gab. Und voilà - der neue Dienst auf dem Webserver schickte eine CLOSE_WAIT - Mail.
--
Sprich: Man nutzt Konfigurationen, die man teils aus Dokumentationen ableitet. 100 als Maximalwert, 500 sollte doch reichen. Im "normalen Alltag" funktioniert alles ohne Probleme. Die allermeisten Seiten haben auch keine knapp 30 Abfragen, höchstens 3 - 5. Aber dann knallt das. Und man findet solche Flaschenhälse, die im Gesamtsystem geschlummert haben.
Ähnlich das Verbindungspooling: Eigentlich ist das eine gute Sache. Aber bei einem hinreichend großen System lohnt es sich, absichtlich getrennte Verbindungspools zu verwenden. So daß sichergestellt ist, daß bestimmte Anfragen immer durchkommen.
Man könnte sogar so weit gehen, einen Verbindungspool pro Datenbank zu definieren. Wenn sich dann irgendjemand auf einer öffentlichen Domain austobt: Dann mag die etwas lahm sein. Die anderen Datenbanken sind davon nicht betroffen.
Mal sehen, ob sich so etwas in Zukunft nochmals wiederholt.