Warum Site-to-Site-Tunnel nach einer harmlosen Änderung instabil wurden: vier Fallen in strongSwan

Ein Hosting-Server mit sieben Site-to-Site-Tunneln zu Kundenstandorten, seit Jahren ohne Alarm. Dann eine kleine, gut begründete Konfigurationsänderung, ein Neustart der Tunnel, und innerhalb von zwei Tagen mehrere Ausfälle von zehn bis dreißig Minuten, Alarme in der Nacht, ein Standort, der bei „ESTABLISHED“ keinen einzigen Ping durchließ. Am Ende waren es vier voneinander unabhängige Fallen, die sich gegenseitig verstärkt haben. Keine davon ist exotisch. Jede kann in einer strongSwan-Installation stecken, die jahrelang „einfach lief“, weil niemand daran gedreht hat.

Die Änderung, die alles auslöste

Der Anlass war ein echtes Problem: Ein Standort war eine halbe Stunde offline, weil die Gegenstelle den Tunnel per DELETE geschlossen hatte und unsere Seite mit close_action = none passiv blieb. dpd_action = restart hilft nur bei einer Gegenstelle, die verstummt, nicht bei einer, die sich ordentlich abmeldet. Die naheliegende Korrektur: close_action = start in allen Child-SA-Blöcken. Ausgerollt, alle Tunnel neu gestartet. Und damit begann es.

Falle 1: das Start-Skript baut jede Child-SA zweimal auf

swanctl --load-all initiiert von sich aus jede Child-SA, deren Connection dabei neu geladen oder geändert wurde (start_action = start). Bei unveränderter Konfiguration meldet charon nur updated vici connection und tut nichts. Unser Start-Skript rief nach dem Laden zusätzlich swanctl --initiate für die erste Child-SA jeder Site auf. Nach einem Reboot oder einer Config-Änderung ergab das zwei Child-SAs für denselben Selektor.

Die meisten Gegenstellen tolerieren das. Eine nicht: Sie bediente nur die erste SA, der Kernel sendete auf der zweiten. Im Log ESTABLISHED, im Monitoring INSTALLED, und trotzdem 81 Pakete raus, null zurück. So erkennt man es:

swanctl --list-sas | grep -oE '^  [a-z0-9-]+: #[0-9]+' | awk '{print $1}' | sort | uniq -c | awk '$1>1'

Jede Zeile mit Zähler größer eins ist ein Kandidat. Die Lösung im Skript: nach --load-all kurz warten, dann je Connection nur initiieren, was weder INSTALLED ist noch gerade von charon aufgebaut wird (IKE_SA im Zustand CONNECTING). Children derselben Site nacheinander, weil zwei gleichzeitige --initiate ohne bestehende IKE_SA zwei IKE_SAs erzeugen.

Falle 2: ein Subnetz, das die Gegenstelle nicht bedient, kostet die ganze Site

In der Konfiguration standen Child-SAs für Subnetze, die auf der Gegenstelle nie eine Policy hatten. Jeder Aufbauversuch bekam ein NO_PROPOSAL_CHOSEN, hier sogar fehlerhaft kodiert (invalid notify data length). Was dann passiert, steht in keinem Handbuch prominent:

  1. charon wiederholt den CREATE_CHILD_SA fünfmal, gibt auf,
  2. erklärt daraufhin die gesamte IKE_SA für tot und startet sie neu,
  3. und nimmt den offenen Child-Auftrag in die neue IKE_SA mit.

Eine Endlosschleife alle drei Minuten, für den ganzen Standort, mit einem kurzen Aussetzer pro Runde. Eine Site lief so nachweislich seit Tagen mit über tausend Neustarts täglich, ohne dass das Monitoring anschlug, weil der Ping zwischen den Neustarts immer kurz funktionierte. Zählen lässt sich das im Journal:

journalctl -u strongswan --since yesterday --until today | grep -c "restarting CHILD_SA"
journalctl -u strongswan --since yesterday --until today | grep -c "establishing CHILD_SA <child>{"

Wichtig: start_action = none in der Konfiguration beendet eine laufende Schleife nicht. Der Auftrag lebt im Speicher von charon und wandert von IKE_SA zu IKE_SA. Nur ein Neustart des Daemons räumt ihn ab. Danach verhindert start_action = none für die betroffenen Children, dass Reboot oder Config-Änderung ihn erneut pflanzen. Besser noch: Blöcke, die die Gegenstelle nicht bedient, gehören aus der Konfiguration heraus, oder die Policy auf der Gegenstelle nachgezogen.

Falle 3: close_action = start ist nicht für jede Gegenstelle richtig

Die Regel, die wir am Ende hatten:

Gegenstelle close_action Grund
passiv, initiiert nie start sonst bleibt der Tunnel nach einem DELETE der Gegenstelle unten
initiiert selbst innerhalb von Sekunden none start rennt mit ihr um den Wiederaufbau, Ergebnis doppelte SAs und INVALID_SPI
erneuert Child-SAs im Sekundentakt (Stürme) none jedes DELETE löst bei uns ein weiteres Child aus, 42 parallele SAs in einer Stunde

Ob eine Gegenstelle selbst initiiert, sieht man nach einem swanctl --terminate --ike <site>: kommt sie innerhalb von 20 Sekunden von allein zurück (swanctl --list-sas zeigt eine IKE_SA mit _r*, also uns als Responder), ist sie aktiv. Für solche Sites ist der saubere Weg zum Bereinigen von Doppelungen auch nur das Terminieren, ohne eigenes --initiate.

Falle 4: Stop-Skripte, die auf Antworten warten

swanctl --terminate ohne --timeout wartet bis zu 165 Sekunden auf die DELETE-Antwort. Antwortet die Gegenstelle nicht, hängt das ganze Neustart-Skript. Unsere „Force“-Schleife terminierte danach nacheinander alles, was wieder ESTABLISHED war, und das waren genau die Gegenstellen, die sich sofort selbst wieder aufbauen. Solange diese Schleife lief, kam die Start-Phase nie, und die Standorte mit passiver Gegenstelle blieben dreizehn Minuten unten. Die Korrektur: --terminate --timeout 15 je Site parallel, danach nur melden, was die Gegenstellen zurückgebracht haben. Das Start-Skript füllt ohnehin nur Lücken.

Und eine Beobachtung ohne fertige Erklärung

Auf einem älteren charon (5.6.x mit kernel-libipsec) haben swanctl --reload-settings und ipsec rereadall im laufenden Betrieb kurz darauf DPD-, Rekey- und DELETE-Exchanges auf mehreren Tunneln ins Leere laufen lassen; charon konnte eigene SAs nicht mehr sauber beenden. Ob Ursache oder Zufall, wir behandeln es seither so: Konfiguration ablegen, Neustart planen, nicht live nachladen.

Die Checkliste, die geblieben ist

  • Vor jeder Änderung: journalctl -u strongswan --since yesterday | grep -c "restarting CHILD_SA". Eine hohe Zahl heißt, die Installation ist bereits krank, egal was das Monitoring sagt.
  • Nach jedem Neustart: Doppelungen zählen (Befehl oben), Pings von der Server-Seite, nicht nur ESTABLISHED.
  • Zehn Minuten Messfenster ohne Eingriff, bevor der Abend für beendet erklärt wird: Neustarts, giving up, INVALID_SPI, Ping-Quote je Site.
  • Monitoring, das nur pingt, sieht Neustart-Schleifen nicht. Die Zahl der Neustarts pro Tag gehört als Metrik dazu.

Vier Fallen, jede für sich klein. Zusammen haben sie aus einer Zeile Konfiguration zwei unruhige Tage gemacht.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert