Erweiterte Suche


Ein Kunde berichtete über eine Nichterreichbarkeit des Webservers der Piratenpartei. Die Störung sah aus wie ein Routingproblem, aber es war viel viel schlimmer.

Zusätzlich zu der Nichterreichbarkeit der Webseite waren auch zwei Traceroutes mitgeliefert worden. Einer aus Kundensicht und einer vom Webserver aus. Die sehen so aus (IP Adresse des Kunden durch die eines Routers ersetzt)

traceroute from 178.19.71.117 to 178.19.224.1
 1  178.19.71.1 (178.19.71.1) [AS29551]  0.673 ms * *
 2  * * *
 3  * * *
 4  80.81.193.74 (80.81.193.74) [AS6695]  0.660 ms * *
 5  * * *
 6  * * 217.71.99.134 (217.71.99.134) [AS13237]  8.215 ms
 7  109.73.31.194 (109.73.31.194) [AS196714]  8.227 ms * *
 8  * * *
 9  * * *

Sowie

traceroute from 178.19.224.1 to 178.19.71.117
 1  rudo1-g01-115 (217.17.207.161)  18.793 ms  1.234 ms  5.662 ms
 2  NUE-2.de.lambdanet.net (217.71.100.33)  4.153 ms  4.303 ms  4.192 ms
 3  xe0-0-0.irt1.fra44.de.as13237.net (217.71.96.73)  7.408 ms  7.421 ms  7.426 ms
 4  decix.fra2.tng.de (80.81.192.83)  12.095 ms  12.027 ms  12.094 ms
 5  t4-1.decix.rt02.of.aixit.net (83.141.1.187)  12.080 ms  12.286 ms  12.279 ms
 6  v7.rt03.boe.aixit.net (83.141.0.249)  12.015 ms  12.500 ms  13.041 ms
 7  v3-rt01.boe.aixit.net (83.141.1.97)  12.021 ms  12.247 ms  12.502 ms
 8  * * *
 9  * * *

Führt man Traces von anderen Systemen aus, so scheint alles ok zu sein.

Nach direktem Kontakt zu einem Admin der BundesIT (vielen Dank für die Geduld mit meinen doch sehr erstaunten Fragen) stellte sich heraus.

  • Eingehende ICMP-Pakete sind bei der Firewall der Piratenpartei rate-limited.
  • Einige Server dort hatten die falsche Netzmaske, deswegen glaubte der Webserver, die Antworten im lokalen Netz ausliefern zu können.

Wie kommt man zu einer falschen Netzmaske? Ganz einfach: Die Server waren classful konfiguriert. 178.19.224.1 und 178.19.71.117 liegen nun mal im gleichen /16 (ehemals Class-B Netz).

Ob des Ratelimits weist der Traceroute die Sterne unterwegs auf: Die Antworten erreichen die tracende Maschine gar nicht.

Nebenbei stellte sich auch heraus, warum Ping komplett gesperrt und ICMP rate-limited ist: Man könnte ja herausbekommen, welche IPs benutzt werden.

Und das Problem wird schon länger gesucht, wie eine Diskussion auf Twitter zeigt.

Carrier Grade NAT, also NAT auf Providerseite, hat besondere Anforderungen an die Technik. Eine Lösung mit OpenSource auf Basis von FreeBSD stößt dabei schnell an erstaunliche Grenzen.

Seltsame Reboots

Nach Inbetriebnahme einer neuen LNS-Plattform, bootete sporadisch einer der Server in der LNS Farm neu. Dabei wurden die gerade auf diesem LNS angemeldeten Kunden getrennt.

Wenn das System abstürzt und nicht von allein rebootet, steht der Absturzbericht auf der Konsole. Teile davon gelangen manchmal auch ins Logfile. Typischerweise schaut das dann so aus:

bsd-lasttest-panic

Was mag das sein?

Die erste Vermutung lautet, daß irgendein Kundensystem irgend etwas "falsch" macht: Es könnte so ungewöhnliche Daten schicken, daß der Kernel die Hufe reißt. Dafür lassen sich aber keine Korrelationen finden: Die Kunden die regelmäßig zum Absturzzeitpunkt online sind, sind die, die häufig Ihre Verbindung neu aufbauen und so mit hoher Wahrscheinlichkeit auf dem betreffenden System landen.

Eine kurzer Blick in die Kernelquellen offenbart, daß dort aber auch nicht alles ganz koscher ist. Es werden Locks getestet, bevor die Zeiger auf die Locks überhaupt initialisiert sind. Ein Bugreport für diesen offenkundigen Fehler löste mir aber auch nicht das Problem.

Was kann es noch sein? Vielleicht einige der Protokolle, die die neue Plattform mehr unterstützt? Schalten wir doch auf einem Server mal VanJacobsen Compression in IPCP und IPV6CP Aushandlung komplett ab. Die anderen Server bleiben, wie sie sind.

Es ist keine signifikante Änderung zu erkennen. Das war es wohl nicht.

Es gab noch Überreste des Umbaus beim Plattformwechsel, schließlich wurden beide System einige Zeit parallel betrieben. Vielleicht kommen die Fehler aus dieser doch hinreichend umständlichen Topologie. Wieder kein Erfolg.

Etwas Systematik

Zuerst einmal müssen Fakten her. Dazu werden Crashdumps ativiert.

Alle beobachtbaren Abstürze finden im Netzkartentreiber statt. Nun werden alle Maschinen also in Zukunft primär die andere Netzkartenart benutzen (em statt igb). Außerdem ist bei em-Karten die Systemlast niedriger. Darüberhinaus lassen sich die em-Karten bei Bedarf von Interrupt in Pollingmodus versetzen. Das wäre der nächste Schritt.

Aber auch das hilft nichts. Die Fehler treten alle paar Tage wieder und wieder auf: Quer über den Maschinenpark, aber immer im libalias Code. Es steht also wieder mal die Vermutung im Raum, daß dieser Fehler von einem Datenpaket, nicht von einer PPP-Anmeldung verursacht wird.

Hier mal ein Crashdump mit Verbindung zum Sourcecode:

#6  0xffffffff80fa2829 in _FindLinkOut (la=0xffffff8001436000, src_addr=
 {s_addr = 403271268}, dst_addr={s_addr = 4143199550}, src_port=17143,
 dst_port=59294, link_type=17, replace_partial_links=1)
 at /usr/src/sys/netinet/libalias/alias_db.c:1111

       LIBALIAS_LOCK_ASSERT(la);
       i = StartPointOut(src_addr, dst_addr, src_port, dst_port, link_type);
->   LIST_FOREACH(lnk, &la->linkTableOut[i], list_out) {
           if (lnk->dst_addr.s_addr == dst_addr.s_addr &&
               lnk->src_addr.s_addr == src_addr.s_addr &&
               lnk->src_port == src_port &&
               lnk->dst_port == dst_port &&
               lnk->link_type == link_type &&
               lnk->server == NULL) {
                 lnk->timestamp = la->timeStamp;
                 break;
               }
       } 

Sehr seltsam. Warum sollte ein solcher Code abstürzen? Das Lock ist gesetzt (und validiert). Die Datenstrukturen sind einfach und enthalten eigene Kopien der Originaldaten. Dafür gibt es keine logische Erklärung.

Abstürzende Abstürze

Wieder ist ein Server abgestürzt. Er hat aber nicht rebootet, sondern ist beim Anlegen des Absturzberichtes hängen geblieben. Remote wurde die Maschine rebootet. Der eigentliche Grund liegt an der gleichen Stelle: FindLinkOut in libalias.

Warum kein Crashdump? Stehen geblieben ist er beim Schreiben des Crashdumps auf die Platte. Die schnelle Lösung ist, per crontab nachts die Crashdumps abzuschalten. Dann bleibt nur auf das Glück zu hoffen und einen hängen bleibenden Crashdump live zu erleben.

Dumping xx out of yy MB: 1% … 11% ... 21% carp1: BACKUP -> MASTER

Was zur Hölle macht der carp Code da? Wir sind im Shutdown!

Ein Blick in den Kernelcode zeigt, daß die Interrupt-Verarbeitung und damit ein Teil des Kernelcodes aktiviert bleibt. Dies tut er, damit er überhaupt auf die Platte schreiben kann. Aber offenbar dauert das Schreiben zu lange.

Die Probleme, die beim Erstellen von Crashdumps auftreten, liegen also darin begründet, daß für die Erzeugung der Crashdumps ein Teil der Betriebssystemfunktionalität vorhanden sein muß. Diese ist aber schon zum Teil abgeschaltet. Damit kann es vorkommen, daß der Überrest des Systems einen Deadlock generiert, z.B. weil kein Kernelthread mehr da ist, der ihn auflösen kann.

Kann man schneller Dumpen? Ja, mit textdumps. Auch hier ist wieder ein neuer Kernel fällig, aber der lohnt sich wirklich. Denn nun schreibt er die Crashdumps zuverlässig und bootet auch danach korrekt.

Nach einiger Zeit bleibt wieder ein Crash hängen: Er steht im Kerneldebugger. Wieso das?

Der Kernel hatte keine panic, sondern einen Page Fault ausgelöst. Und ddb hat pro möglichem Event einen Handler, an den Scripte gebunden werden können. Defaultmäßig liefert FreeBSD eine ddb.conf aus, die nur kernel.enter.panic belegt.

# /etc/ddb.conf

script kdb.enter.default=textdump set; capture on; show pcpu; bt; ps; alltrace; capture off; call doadump; reset

Nachdem nun alle Events abgefangen werden, ist klappt das mit den Crashdumps endlich wie gewünscht. Ein ernsthaftes Debugging ist nun erstmals möglich.

Spurensuche

Immer und immer wieder treten diese Art von Fehlern auf:

#7  0xffffffff80fa0cf1 in DeleteLink (lnk=0xffffff018feab700)
 at /usr/src/sys/netinet/libalias/alias_db.c:859
#8  0xffffffff80fa0f01 in HouseKeeping (la=0xffffff80011fa000)
 at /usr/src/sys/netinet/libalias/alias_db.c:849 

Vielleicht klappt das mit dem Locking im NAT-Code nicht richtig? Vielleicht wurde der Fehler inzwischen behoben?

Ein Vergleich des Sourcecodes des für NAT zuständigen libalias Moduls zwischen der eingesetzten Version 8.3-RELEASE und der aktuellen Entwicklerversion HEAD ergibt, daß eigentlich nur die SVN Änderung 241648 dazu gekommen ist. Diese wird eingespielt, aber bringt keine Besserung.

Der Fehler wurde offenbar auch anderswo noch nicht gefunden, i.d.R. wird darauf verwiesen, daß der RAM defekt sei. Dies ist aber inzwischen sehr unwahrscheinlich. Es können nicht alle Systeme gleichartig kaputt sein.

Ist das Locking defekt? Wenn ja, dann müßte doch LIBALIAS_LOCK_ASSERT sich melden. Wieder zeigt ein Blick in den Source, wie irregeleitet eine solche Annahme sein kann:

 * $FreeBSD: releng/8.3/sys/netinet/libalias/alias_local.h

#ifdef _KERNEL
#define LIBALIAS_LOCK_INIT(l) \
        mtx_init(&l->mutex, "per-instance libalias mutex", NULL, MTX_DEF)
#define LIBALIAS_LOCK_ASSERT(l) mtx_assert(&l->mutex, MA_OWNED)
#define LIBALIAS_LOCK(l) mtx_lock(&l->mutex)
#define LIBALIAS_UNLOCK(l) mtx_unlock(&l->mutex)
#define LIBALIAS_LOCK_DESTROY(l)        mtx_destroy(&l->mutex)
#else
#define LIBALIAS_LOCK_INIT(l)
#define LIBALIAS_LOCK_ASSERT(l)
#define LIBALIAS_LOCK(l)
#define LIBALIAS_UNLOCK(l)
#define LIBALIAS_LOCK_DESTROY(l)
#endif 

Schaut doch gut aus, oder?

 * $FreeBSD: releng/8.3/sys/sys/mutex.h

/*
 * The INVARIANTS-enabled mtx_assert() functionality.
 *
 * The constants need to be defined for INVARIANT_SUPPORT infrastructure
 * support as _mtx_assert() itself uses them and the latter implies that
 * _mtx_assert() must build.
 */
#if defined(INVARIANTS) || defined(INVARIANT_SUPPORT)
#define MA_OWNED        LA_XLOCKED
#define MA_NOTOWNED     LA_UNLOCKED
#define MA_RECURSED     LA_RECURSED
#define MA_NOTRECURSED  LA_NOTRECURSED
#endif

#ifdef INVARIANTS
#define mtx_assert(m, what)  \
        _mtx_assert((m), (what), __FILE__, __LINE__)

#define GIANT_REQUIRED  mtx_assert(&Giant, MA_OWNED)

#else   /* INVARIANTS */
#define mtx_assert(m, what)     (void)0
#define GIANT_REQUIRED
#endif  /* INVARIANTS */

Der gesamte Code wird schon vom Präprozessor entsorgt! Na zum Glück kann man das libalias-Modul separat kompilieren und einbinden.

Oh, nein! Die Basisfunktionalität "INVARIANT_SUPPORT" ist nicht im Standardkernel eingebunden. Also wieder mal einen komplett neuen Kernel!

Mit einem neuen Kernel kann man ja mal schon präventiv Polling aktivieren. Aber das war ein extra Schuß in den Ofen.

Folge dem Lock

Nun ist also im betroffenen NAT Modul der Überwachungscode für Locking aktiviert. Wenn die Abstütze mit fehlerhaftem Locking zusammen hängen, sollten sie damit gefunden werden.

Alternativ kann es sein, daß NAT aufgrund einer bestimmten Protokollbenutzung eines Kunden durcheinander kommt. Dies kann ein defekter NAT-Helper sein, der auf bestimmte Protokollverletzungen nicht korrekt reagiert und dabei die Verwaltungsdaten zerstört.

Und tatsächlich gibt starke Hinweise auf ein Lockingproblem:

KDB: enter: panic mutex per-instance libalias mutex not owned
 at .../libalias/alias_db.c:861 

Es ist nicht klar, ob die Lockingprobleme durch die Interruptsynchronisation auf mehreren CPUs kommt, oder durch einen NAT Helper, der die falsche Tabelle anfaßt.

Es wurde ein neues NAT-Modul erstellt, das einige Vermutungen belegen oder widerlegen soll:

  • Wenn die internen Rücklinks der NAT Einträge auf die Verwaltungsstruktur nicht stimmen, wird das System mit einem panic anhalten und Details melden.
  • Bevor internen Verwaltungsstrukturen entsorgt werden, werden diese mit einem wiedererkennbaren Bitmuster ECECEC... überschrieben. Damit ist eine spätere Verwendung von bereits freigegebenen Speicherbereichen erkennbar.
--- bsd-head/sys/netinet/libalias/alias_db.c    2013-05-09 14:25:35.000000000 +0200
+++ 8.3/sys/netinet/libalias/alias_db.c 2013-05-20 22:36:07.125123000 +0200
@@ -828,6 +828,8 @@
                lnk = LIST_FIRST(&la->linkTableOut[i]);
                while (lnk != NULL) {
                        struct alias_link *link_next = LIST_NEXT(lnk, list_out);
+
+                       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                        DeleteLink(lnk);
                        lnk = link_next;
                }
@@ -845,6 +847,7 @@
        LIBALIAS_LOCK_ASSERT(la);
        LIST_FOREACH_SAFE(lnk, &la->linkTableOut[la->cleanupIndex++],
            list_out, lnk_tmp) {
+               KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                if (la->timeStamp - lnk->timestamp > lnk->expire_time)
                        DeleteLink(lnk);
        }
@@ -875,6 +878,7 @@
                head = curr = lnk->server;
                do {
                        next = curr->next;
+                       memset(curr, 0xEC, sizeof(*curr));     /* detect double free */
                        free(curr);
                } while ((curr = next) != head);
        }
@@ -921,6 +925,7 @@
        }

 /* Free memory */
+        memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
        free(lnk);

 /* Write statistics, if logging enabled */
@@ -994,6 +999,7 @@

                /* Determine alias port */
                if (GetNewPort(la, lnk, alias_port_param) != 0) {
+                       memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
                        free(lnk);
                        return (NULL);
                }
@@ -1026,6 +1032,7 @@
                                fprintf(stderr, "PacketAlias/AddLink: ");
                                fprintf(stderr, " cannot allocate auxiliary TCP data\n");
 #endif
+                               memset(lnk, 0xEC, sizeof(*lnk));     /* detect double free */
                                free(lnk);
                                return (NULL);
                        }
@@ -1135,6 +1142,7 @@
                            link_type, 0);
                }
                if (lnk != NULL) {
+                       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                        lnk = ReLink(lnk,
                            src_addr, dst_addr, lnk->alias_addr,
                            src_port, dst_port, lnk->alias_port,
@@ -1268,6 +1276,7 @@
                struct in_addr src_addr;
                u_short src_port;

+               KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
                if (lnk->server != NULL) {      /* LSNAT link */
                        src_addr = lnk->server->addr;
                        src_port = lnk->server->port;
@@ -2097,6 +2106,9 @@
 void
 SetExpire(struct alias_link *lnk, int expire)
 {
+       struct libalias *la = lnk->la;
+
+       LIBALIAS_LOCK_ASSERT(la);
        if (expire == 0) {
                lnk->flags &= ~LINK_PERMANENT;
                DeleteLink(lnk);
@@ -2141,6 +2153,7 @@

        LIBALIAS_LOCK_ASSERT(la);
        la->deleteAllLinks = 1;
+       KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
        ReLink(lnk, lnk->src_addr, lnk->dst_addr, lnk->alias_addr,
            lnk->src_port, cid, lnk->alias_port, lnk->link_type);
        la->deleteAllLinks = 0;
@@ -2432,6 +2446,7 @@

        LIBALIAS_LOCK(la);
        la->deleteAllLinks = 1;
+        KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p)\n", __FILE__, __LINE__, __FUNCTION__, la, lnk->la));
        DeleteLink(lnk);
        la->deleteAllLinks = 0;
        LIBALIAS_UNLOCK(la);
@@ -2570,6 +2586,7 @@
        LIST_REMOVE(la, instancelist);
        LIBALIAS_UNLOCK(la);
        LIBALIAS_LOCK_DESTROY(la);
+       memset(la, 0xEC, sizeof(*la));     /* detect double free */
        free(la);
 }

Ungereimtheiten

Es fällt auf, daß die Abstürze einige Zeit nach dem schnellen Verlust von Sessions auf einem Server auftreten, d.h. vor allem Nachts bei den (kundenseitig initierten) Trennungen bzw. nachdem ein Teil der Sessions getrennt wurden.

So besteht die Vermutung, daß beim Entfernen von Interfaces auch IP Adressen und damit zusammenhängende Daten im Kernel entfernt werden. Ein Teil der Daten wird auch von der NAT-Funktionalität nochmal verwaltet. Es ist möglich, daß die NAT-Bibliothek nicht ordnungsgemäß über das Wegräumen von Daten informiert wird, die sie ebenfalls verwaltet. Dadurch greift die NAT-Bibliothek auf eigentlich nicht mehr existente Speicherbereiche zu, die zufällig noch eine Weile die richtigen Daten enthalten. Irgendwann werden diese Speicherbereiche aber überschrieben und spätestens dann kommt es zum Absturz.

Eine solche Vermutung ist zu prüfen. Dazu ist der Kernelcode für das Entfernen von Interfaces zu sichten. Die Hoffnung besteht darin, eine Stelle zu finden, die auf die Vermutung paßt. Dies würde eine Fehlerbehebung gestatten.

Alternative Umgehungsmöglichkeit: Da die Abstürze fast ausschließlich im NAT-Code auftreten und damit den NAT-tenden Server zum Reboot zwingen, kann der Schaden für die Endkunden eingeschränkt werden. Dazu kann man die NAT und die LNS (Terminierung der PPP Verbindungen) Funktionalität trennen. Wenn nun der NAT Server abstürzt, übernimmt die Funktion ein gleichartiger NAT-Server.

Andere Server bilden ausschließlich den LNS, also die PPP-Terminierung ab. Diese wären von den NAT-Ausfällen nicht betroffen: Die Kunden würden also online bleiben. Für dieses Setup ist es notwendig, mit weitere Maschinen zu arbeiten, die Funktionscluster bilden.

Bevor aber für einen solchen Umbau Hardware beschafft beschafft wird, ist es notwendig, Die Vermutung zu untersetzen oder zu widerlegen. Und dazu muß das mit INVARIANTS kompilierte Modul ansprechen. Und mit etwas Glück liefert es einen Beleg, ob ein Use-After-Free Fehler vorliegt.

Es dauert aber erstmal Tage, bis alle Systeme den neuen Kernel mit dem "Fehlersuch-Modul" durch die zufälligen Reboots geladen haben. Neue Erkenntnisse sind bis dahin nicht aufgetreten: Wie immer sind die Abstürze im libalias-Code.

In der Zwischenzeit hat intensives Codestudium doch einige Erkenntisse gebracht:

  • An besonderen Protokollhelper im NAT sind nur GRE und SCTP einkompiliert.
  • Andere Protokollhelper von NAT sind nicht geladen (Modularer Aufbau).
  • Querreferenzen auf Datenstrukturen anderer Kernelbestandteile werden von der NAT-Bibliothek nicht verwaltet.

Damit entfällt die Vermutung, das es mit dem Entfernen von Interfaces zu tun hat.

Erste Ergebnisse

Die Fehlersuche schlägt an:

.../netinet/libalias/alias_db.c:853 IncrementalCleanup:
  la (0xffffff8001476000) != lnk->la (0xffffff80014c4000)

la ist der Zeiger auf die NAT-Struktur (von denen es 32 gibt). lnk ist der Zeiger auf den gerade aktiven NAT-Eintrag, der selbst auf seine NAT-Struktur zurückzeigen soll.

Erwartet war, daß lnk->la den Codewert für 0xececececec enthält, was einen Use-after-Free aufgezeigt hätte. Eine andere Erwartung war, daß lnk->la komplettes Chaos enthält, was eine Fremdnutzung des Speicherbereichs angezeigt hätte.

Die unerwartete Meldung besagt nun, daß ein NAT-Eintrag, die für ihn zuständige Struktur verlassen hat und bei einer anderen Struktur aufgenommen wurde. Dies deutet auf einen Fehler im originären NAT-Code hin.

Es ist grundsätzlich möglich, daß ein einzelnes Datenpaket durch die konfigurierten Firewallregeln, die das NAT ansteuern, zweimal an unterschiedliche NAT-Instanzen übergeben wird. Dies geschieht dann, wenn ein Kunde einen anderen Kunden von NAT zu NAT ansprechen will.

Dies sollte eigentlich kein Problem darstellen, korreliert aber mit dem Regelwerk.

for i in $(seq 0 31); do
  j=$(($i+128))
  ipfw nat $j config ip 198.51.100.$j same_ports
  ipfw add 1$j nat $j ip4 from any to 198.51.100.$j
  ipfw add 2$j nat $j ipv4 from 100.64.0.$i:255.192.0.31 to any
done

Die Firewallregeln werden nun dahingehend angepaßt, daß sie ausschließlich auf dem äußeren Interface (NAT ins Internet und unNAT aus dem Internet) aktiv werden. Erst durch den Abbau der alten Plattform war es überhaupt möglich, diese Einschränkung vorzunehmen. Zuvor gab es weitere, interne Übergänge ins Internet.

Da diese Bedingung nun entfallen ist, wurde das Regelwerk entschlackt und die NAT-Bindungen nur auf das Außeninterface beschränkt.

for i in $(seq 0 31); do
  j=$(($i+128))
  ipfw nat $j config ip 198.51.100.$j same_ports
  ipfw add 1$j nat $j ip4 from any to 198.51.100.$j recv extern in
  ipfw add 2$j nat $j ipv4 from 100.64.0.$i:255.192.0.31 to any xmit extern out
done

Dabei ist zu beachten, daß FreeBSD NAT nur ausgehend ausführt. Eingehende Datenpakete werden ausschließlich "entnattet".

Die Absturzhäufigkeit sinkt drastisch!

Mit etwas Abstand kann man feststellen, daß nach diesen Änderungen an den ipfw-Regeln die beobachteten Fehler nicht mehr auftreten. Das bedeutet natürlich nicht, daß alle Probleme gelöst sind. Denn es geht weiter …

Der MPD tut an sich zuverlässig für PPPoE Terminierung. Wenn es aber zu Abstürzen des zugrundeliegenden Systems kommt, bekommen die anderen LNS im Verbund die wegfallenden Teilnehmer mit einem Schlag ab. Und dann können andere System ebenfalls zusammenbrechen.

Genauere Beobachtung des Effektes zeigt, daß der nächste LNS ca. 10min später abstürzt. In einigen Fällen meldet er vorher noch den Verlust einer oder mehrerer L2TP Sessions. Als Gründe werden i.d.R. angegeben:

mpd: L2TP: Control connection X terminated: 6 (Control channel timeout - Remote peer dead)
mpd: L2TP: Control connection Y terminated: 6 (expecting reply; none received)

Rückfrage beim Carrier ergibt, daß er kurz vorher schon einen ähnlichen Eintrag in seinem Log hat. Dort wird ebenfalls die L2TP Verbindung für tot erklärt.

Wie kommt es dazu? Der MPD arbeitet doch weiter? Er meldet aber immer wieder im Log.

mpd: Daemon overloaded, ignoring request.

Normalerweise tritt dieser Eintrag selten auf. Wenn die Maschine nachts mit den von den Kunden in vorauseilendem Gehorsam verursachten "Zwangs"-Trennungen loslegt, tritt so ein Eintrag ein bis zehnmal pro Sekunde auf.

Nach dem Absturz eines LNS werden aber schlagartig alle Kunden auf die anderen LNS verteilt. Dann ist die Überlast fast übermächtig. Vierhundert bis sechshundert Verbindungsaufbauten pro Sekunde sind dann der Normalfall.

Im MPD ist ein Überlastschutz eingebaut. Er stellt sich einfach tot:

if (OVERLOAD()) {
   Log(LG_PHYS, ("Daemon overloaded, ignoring request."));
   goto failed;
}

Damit er nicht die Arbeit völlig einstellt, kommt eine zufällige Anzahl von Verbindungsversuchen trotzdem durch:

#define OVERLOAD()            (gOverload > (random() % 100))

Allerdings erreicht der Wert von gOverload schnell die 100. Dann stellt sich der Daemon wirklich tot.

  #define SETOVERLOAD(q)        do {                                    \
                                    int t = (q);                        \
                                    if (t > 60) {                       \
                                        gOverload = 100;                \
                                    } else if (t > 10) {                \
                                        gOverload = (t - 10) * 2;       \
                                    } else {                            \
                                        gOverload = 0;                  \
                                    }                                   \
                                } while (0)

Und genau das fällt dem LAC auf. Er trennt die L2TP Session zu dem vermeindlich "toten" Peer. Damit antwortet er auch selbst nicht mehr auf die Pakete des LNS im gerade geschlossenen Kanal. Und nun macht auch der LNS die Pforten dicht. Wieder fallen Tausende von Kunden aus dem Netz.

Die Lösung ist so einfach wie nur möglich. Ich habe die Berechnungsvorschrift für gOverload geändert:

/* SETOVERLOAD: can reach 0 .. 98 */
gOverload = (q < 10) ? 0 : (99.0 * (1.0 - 10.0/q));

Nun gibt es immer eine Chance eine neue Verbindung durchzubekommen.

Der LAC ist happy, weil er den Tunnel nicht länger verliert. Der LNS ist happy, weil er unter Last nicht zusammenbrechen muß, sondern weiterhin Aufgaben verweigern kann.

Leider ist das Verfahren doch zu einfach gestrickt. Es kommen unter Last so viele Neuverbindungen noch durch, daß deren Behandlung das betroffene System mächtig belastet. Die LACs erkennen trotzdem auf "Dead Peer" und das Spiel geht von vorne los. Nun müßte man die Implementierung des kommerziellen LAC kennen, um zu wissen, woran ein Peer als "tot" erkannt wird. Aber das definitiv zu erfahren, ist wohl unmöglich.

Was kann man tun? Zum einen ist zu kontrollieren, ob vom Overload auch die Pakete des Control Channels betroffen sind. Andererseits sollte der LNS sich im Überlastfall nicht tot stellen, sondern eine klare Fehlermeldung an den LAC schicken. Da hilft nur, die Standards zu lesen.

Aber zuerstmal zurück zu striktem Overload:

static void
update_overload(void) {
   int i,o=0;
   time_t now;
   static time_t last_reported = 0;
   static int peak[2] = {0};

   time(&now);

   for(i=0; i<sizeof(queue)/sizeof(*queue); i++)
     if(queue[i].port) {
        int l = mesg_port_qlen(queue[i].port);
        o += l * (2 + queue[i].max_workers);
        if(peak[i] < l)
          peak[i] = l;
     }

   /* SETOVERLOAD */
   gOverload = (o < 10) ? 0 : (o < 110) ? o-10 : 100;

   if(now > last_reported) {
      Log(LG_ALWAYS, ("Queues had up to %d (serial) and %d (parallel) entries."$
                      peak[0], peak[1]));
      peak[0] = 0;
      peak[1] = 0;
      last_reported = now + (60 - 1);  /* 1 second due to strict less then */
   }

}

Wirklich tot?

Eine genauere Überprüfung des MPD-Codes ergab, daß sich hinter dem goto failed doch eine Benachrichtigung des LAC findet. Diese meldet eine temporäre Überlastung ohne weitere Angaben. Trotzdem klassifiziert der LAC den LNS als tot.

Offenbar genügt der temporäre Fehler dem LAC nicht, um einen anderen LNS zu probieren. Immer und immer wieder wirft er ein und dieselbe Anmeldung dem gleichen LNS vor.

Aber es gibt ja "LNS guided LAC" und das sollte dort wirklich stehen.

--- mpd-5.6/src/l2tp.c  2011-12-21 15:58:49.000000000 +0100
+++ mpd-5.6-lutz/src/l2tp.c     2013-06-09 16:40:35.000000000 +0200
@@ -1112,6 +1123,7 @@
        Link    l = NULL;
        L2tpInfo pi = NULL;
        int     k;
+       char const * failreason = NULL;

        /* Convert AVP's to friendly form */
        if ((ptrs = ppp_l2tp_avp_list2ptrs(avps)) == NULL) {
@@ -1126,12 +1138,12 @@
            ppp_l2tp_sess_get_serial(sess), ctrl));

        if (gShutdownInProgress) {
-               Log(LG_PHYS, ("Shutdown sequence in progress, ignoring request."));
+               failreason = "Shutdown sequence in progress, ignoring request.";
                goto failed;
        }

        if (OVERLOAD()) {
-               Log(LG_PHYS, ("Daemon overloaded, ignoring request."));
+               failreason ="Daemon overloaded, ignoring request.";
                goto failed;
        }

@@ -1194,10 +1206,10 @@
                ppp_l2tp_avp_ptrs_destroy(&ptrs);
                return;
        }
-       Log(LG_PHYS, ("L2TP: No free link with requested parameters "
-           "was found"));
+       failreason = "L2TP: No free link with requested parameters was found";
 failed:
-       ppp_l2tp_terminate(sess, L2TP_RESULT_AVAIL_TEMP, 0, NULL);
+       Log(LG_PHYS, ("%s", failreason));
+       ppp_l2tp_terminate(sess, L2TP_RESULT_AVAIL_TEMP, L2TP_ERROR_TRY_ANOTHER, failreason);
        ppp_l2tp_avp_ptrs_destroy(&ptrs);
 }

Bei der Überprüfung einer anderen Verbindung kam die Rückmeldung vom Carrier, er würde in seinen Logfiles folgende Einträge sehen, die da bisher nicht standen:

%L2TP-7-SES: 16646:53888 Sending ICRQ
%L2TP-7-SES: 16646:53888 Rcvd CDN rid:0 (4,7) L2TP: No free link with requested parameters was found
%L2TP-7-SES: 16646:53888: peer failure, rerouting session
%L2TP-7-SES: 16646:53888 remote abort: peer failure, rerouting session (tc 23)

Das ist genau das, was der MPD nun dem LAC sendet. Der kommerzielle LAC (Redback) reagiert darauf genau wie gewünscht, er versucht die Session auf einem anderen L2TP Kanal abzukippen.

Damit ist die Änderung ein voller Erfolg:

  • Kunden kommen deutlich schneller wieder online, weil sie im Überlastfall auf andere, höher belegte L2TP-Tunnel verschoben werden.
  • Es ist nun möglich, die Annahme neuer Session gezielt zu verweigern, ohne den Carrier um Abschaltung des Tunnel bitten zu müssen. Damit kann man ein Komponente gezielt freiräumen, um unterbrechungsfrei arbeiten (z.B. Software tauschen) zu können.

Problem gelöst.

PPP-Verbindungen werden von Carrier zum ISP per L2TP-Tunnel zugeführt. Diese Tunnel tragen einige tausend Sessions gleichzeitig. Deswegen sollten sie gegen Störungen sehr nachsichtig sein. Und dazu hat man sich einiges einfallen lassen.

Der TCP-Nachbau

L2TP ist gemäß RFC 2661 ein Tunnelprotokoll mit gesichertem Kanal für Kontrollnachrichten.

Dazu führt jede Seite 16bit-Sequenznummern für die eignenen Nachrichten ein, die sich mit jeder Kontrollnachricht erhöhen. Empfängt der L2TP-Stack der Gegenstelle eine neue Nachricht, so sendet es selbst die fremde Sequenznummer wieder als Bestätigung bei der nächsten ausgehenden Kontrollnachricht mit. Liegt keine Kontrollnachricht vor, so kann auch eine leere Nachricht gesendet werden, die praktisch nur das ACK transportiert.

Damit die Verbindung nicht so stottert, vereinbaren beide Seiten ein Fenster an "unbestätigten" Nachrichten, die sich noch im Transport befinden dürfen. Dieses Fenster ist üblicherweise vier bis acht Nachrichten groß.

Das entspricht vollständig dem bekannten TCP Protokoll.

Um die Funktionsweise dieses L2TP-Kanals muß man sich normalerweise nicht kümmern: Man wird eine Kontrollnachricht ein und wartet auf die Antwort bzw. die Kontrollnachricht der Gegenseite. Dafür reicht ein Fenster für eine Handvoll Nachrichten völlig aus.

Eine explizite Möglichkeit, die Unerreichbarkeit der Gegenseite zu bemerken, gibt es nicht. (TCP hat dafür RST und FIN.) Deswegen definiert man sich einen Timeout, innerhalb dessen eine Nachricht von der Gegenseite bearbeitet bekommen haben möchte.

Wenn es kurz klemmt?

Wie immer bringt ein Blick in den Sourcecode (hier sys/netgraph/ng_l2tp.c von FreeBSD) Klarheit:

/* Some hard coded values */
#define L2TP_MAX_XWIN           128                     /* my max xmit window */
#define L2TP_MAX_REXMIT         5                       /* default max rexmit */
#define L2TP_MAX_REXMIT_TO      30                      /* default rexmit to */
#define L2TP_DELAYED_ACK        ((hz + 19) / 20)        /* delayed ack: 50 ms */

Insgesamt läßt sich das Fenster auf bis zu 128 Nachrichten vergrößern. Aber wie erfolgt die Wiederholung?

/* Restart timer, this time with an increased delay */
delay = (seq->rexmits > 12) ? (1 << 12) : (1 << seq->rexmits);
if (delay > priv->conf.rexmit_max_to)
    delay = priv->conf.rexmit_max_to;
ng_callout(&seq->rack_timer, node, NULL,
    hz * delay, ng_l2tp_seq_rack_timeout, NULL, 0);

Auf die ACKs von Nachrichten wird also bis zu 30 Sekunden gewartet. Und zwar immer expotentiell länger werdend:

Resend Delay
Versuch 1 2 3 4 5 6 7 ...
Wartezeit 1 2 4 8 16 30 30 30

Der Default liegt bei fünf Wiederholungen, was insgesamt eine Kommunikationslücke von 31 Sekunden abdecken kann.

Es nützt also nichts, einen globalen Timeout von mehr als diesen Zeitraum anzusetzen, da spätestens nach dieser halben Minute die Verbindung lokal für tot erklärt wird, wenn Nachrichten vorliegen.

Will man höhere Timeouts haben, so muß man die Wiederholungsraten auf beiden Seiten anheben. Diese Parameter werden ausgehandelt:

ncgtl> msg [0x00030854]: getconfig
Args:   { enabled=1 match_id=1 tunnel_id=0x6e9f peer_id=0x5685
         peer_win=8 rexmit_max=8 rexmit_max_to=10 }

Konkret werden hier also ein Fenster von acht Nachrichten, sowie acht Wiederholungen bis zu einem Maximalwert von 10 Sekunden pro Wiederholung vereinbart. Dies bedeutet also, daß diese Kontrollnachrichten nach 1+2+4+8+10+10+10+10 = 45 Sekunden bestätigt sein müssen.

Da L2TP Pakete über normales IP-Routing laufen, sollte die Konvergenzzeit dieser Netze kleiner sein als diese halbe Minute.

Überlastverhalten

Ganz anders ist die Situation, wenn auf einem L2TP-Tunnel einige tausend Sessions gleichzeitig laufen. Die Anzahl der hier benötigten Kontrollnachrichten ist dann sehr viel größer. Es erreicht schnell mal einige hundert Nachrichten auf einen Schlag.

Aber was passiert, wenn mehr Nachrichten gesendet werden sollen, als in den Puffer der Nachrichtenfensters passen?

Auch hier schult wieder ein Blick in den Quellcode die Sachkenntnis:

struct l2tp_seq {
        ...
        struct mbuf             *xwin[L2TP_MAX_XWIN];   /* transmit window */
};
/*
 * Handle an outgoing control frame.
 */
static int
ng_l2tp_rcvdata_ctrl(hook_p hook, item_p item)
{
        ...
        /* Find next empty slot in transmit queue */
        for (i = 0; i < L2TP_MAX_XWIN && seq->xwin[i] != NULL; i++);
        if (i == L2TP_MAX_XWIN) {
                mtx_unlock(&seq->mtx);
                priv->stats.xmitDrops++;
                m_freem(m);
                ERROUT(ENOBUFS);
        }
        seq->xwin[i] = m;
        ...
}

Eine Kontrollnachricht wird also zur Versendung in den 128 Nachrichten großen Kernelpuffer geworfen. Aus diesem werden soviele Nachrichten versendet, wie in das vereinbarte Sendefenster passen. Nach Bestätigung einer Aussendung werden die so bestätigten Nachrichten durch umkopieren nach vorn entfernt. Das Feld xwin ist also stets einseitig bündig mit Nachrichten gefüllt.

Kommen nun mehr Kontrollnachrichten herein als versendet werden können, puffert der Kernel bis zu 128 Nachrichten intern zwischen.

Reicht der Platz für diese 128 Nachrichten nicht aus, weil beispielsweise das Routing zwischen den Gegenstellen kurz gestört ist, oder weil die Gegenstelle gerade mächtig beschäftigt ist, so meldet der Kernel an den sendenden Prozess ENOBUFS "Kein Platz mehr".

Der MPD reagiert darauf verschnupft:

        if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1)
                goto fail;

        /* Done */
        goto done;

fail:
        /* Close up shop */
        Perror("L2TP: error sending ctrl packet");
        ppp_l2tp_ctrl_close(ctrl, L2TP_RESULT_ERROR,
            L2TP_ERROR_GENERIC, strerror(errno));

done:
        /* Clean up */

Unabhängig vom Grund des Fehlers wird die gesamte L2TP Verbindung hingeworfen. Im Log sieht das dann so aus.

mpd: L2TP: error sending ctrl packet: No buffer space available

Der offenkunde Fix besteht darin, es mehrfach zu probieren:

    int rtn, retry = 10, delay = 1000;

retry:
    if ((NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1) {
        if (errno == ENOBUFS && retry > 0) {
            Log(LG_ERR, ("[%s] XMIT stalled, retrying...", ctrl));
            usleep(delay);
            retry--;
            delay *= 2;
            goto retry;
        }

Selbstverständlich hilft das nicht viel. Denn in dieser Schleife kann der Prozeß nicht beliebig viel Zeit vergeuden. Er hat schließlich noch anderes zu tun.

Was man also braucht ist eine echte, dynamisch wachsende Warteschlange für diese Kontrollnachrichten.

--- mpd-5.6/src/l2tp_ctrl.c     2011-12-21 15:58:49.000000000 +0100
+++ mpd-5.6-lutz/src/l2tp_ctrl.c        2013-09-27 16:08:15.000000000 +0200
@@ -153,6 +153,14 @@
        int                     req_avps[AVP_MAX + 1];
 };

+/* Control message queue */
+struct ctrl_queue_entry {
+   void *                      data;
+   unsigned int                        len;
+   STAILQ_ENTRY(ctrl_queue_entry) next;
+};
+STAILQ_HEAD(l2tp_ctrl_queue, ctrl_queue_entry);
+
 /* Control connection */
 struct ppp_l2tp_ctrl {
        enum l2tp_ctrl_state    state;                  /* control state */
@@ -164,6 +172,7 @@
        char                    path[32];               /* l2tp node path */
        int                     csock;                  /* netgraph ctrl sock */
        int                     dsock;                  /* netgraph data sock */
+       struct l2tp_ctrl_queue  *dsock_queue;           /* queue if netgraph is
        u_char                  *secret;                /* shared secret */
        u_int                   seclen;                 /* share secret len */
        u_char                  chal[L2TP_CHALLENGE_LEN]; /* our L2TP challenge
@@ -533,6 +544,10 @@
            ctrl->mutex, ppp_l2tp_data_event, ctrl, PEVENT_READ,
            ctrl->dsock) == -1)
                goto fail;
+
+       /* Initialize send queue */
+       ctrl->dsock_queue = Malloc(CTRL_MEM_TYPE, sizeof(*ctrl->dsock_queue));
+       STAILQ_INIT(ctrl->dsock_queue);

        /* Copy initial AVP list */
        ctrl->avps = (avps == NULL) ?
@@ -616,5 +632,6 @@
        ppp_l2tp_avp_list_destroy(&ctrl->avps);
        ghash_remove(ppp_l2tp_ctrls, ctrl);
        ghash_destroy(&ctrl->sessions);
-       Freee(ctrl->secret);
+       l2tp_ctrl_queue_destroy(&ctrl->dsock_queue);
+       Freee(ctrl->secret);
        Freee(ctrl);
        if (ppp_l2tp_ctrls != NULL && ghash_size(ppp_l2tp_ctrls) == 0)
                ghash_destroy(&ppp_l2tp_ctrls);
@@ -1261,8 +1290,32 @@
                ppp_l2tp_ctrl_dump(ctrl, avps, "L2TP: XMIT(0x%04x) ",
                    ntohs(session_id));
        }
-       if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, data, 2 + len) == -1)
-               goto fail;
+
+        /* Schedule packet */
+        n = Malloc(CTRL_MEM_TYPE, sizeof(*n));
+       n->data = data; data = NULL;
+       n->len = 2 + len;
+       MUTEX_LOCK(gNgMutex);
+       STAILQ_INSERT_TAIL(ctrl->dsock_queue, n, next);
+       MUTEX_UNLOCK(gNgMutex);
+
+       MUTEX_LOCK(gNgMutex);
+       /* Try to send outstanding messages */
+       while(!STAILQ_EMPTY(ctrl->dsock_queue)) {
+          struct ctrl_queue_entry *o = STAILQ_FIRST(ctrl->dsock_queue);
+          if (NgSendData(ctrl->dsock, NG_L2TP_HOOK_CTRL, o->data, o->len) == -1
+             if (errno == ENOBUFS) {
+                Log(LG_ERR, ("[%p] L2TP: XMIT stalled, queueing ...", ctrl));
+                break;
+             }
+             MUTEX_UNLOCK(gNgMutex);
+             goto fail;
+          }
+          STAILQ_REMOVE_HEAD(ctrl->dsock_queue, next);
+          Freee(o->data);
+          Freee(o);
+       }
+       MUTEX_UNLOCK(gNgMutex);

        /* Done */
        goto done;
@@ -1277,7 +1330,6 @@
        /* Clean up */
        ppp_l2tp_avp_destroy(&avp);
        ppp_l2tp_avp_list_destroy(&avps);
-       Freee(data);
 }

 /*
@@ -1412,6 +1464,22 @@
 }

 /*
+ * Free all outstanding entries in the control message queue.
+ * Remove the queue.
+ */
+static void
+l2tp_ctrl_queue_destroy(struct l2tp_ctrl_queue ** q) {
+   while(!STAILQ_EMPTY(*q)) {
+      struct ctrl_queue_entry * n = STAILQ_FIRST(*q);
+      STAILQ_REMOVE_HEAD(*q, next);
+      Freee(n->data);
+      Freee(n);
+   }
+   Freee(*q);
+}
+
+
+/*
  * Notify link side that the control connection has gone away
  * and begin death timer.
  *

Damit ist es möglich, die Kontrollnachrichten beliebig lange zwischenzuspeichern, auch wenn es auf dem L2TP Server mal heiß hergeht.

An den Timeouts für die Verbindung ändert sich dabei noch nicht viel. Diese Resende-Timeouts müssen anderweitig angepaßt werden.

Die 5%-Hürde führte bei der diesjährigen Bundestagswahl spektakulär zu einem Verwerfen von fast sechs Millionen Stimmen. Auch ich hatte mir dazu Gedanken gemacht. Der überarbeitete Vorschlag besteht darin bis zu 5% der Stimmen im Sinne einer stabilen Mehrheitsfindung zu opfern. Doch was würde eine solche Änderung normalerweise bedeuten?

Für die Beantwortung der Frage, ob die Wahl 2013 ein Ausreißer war oder einem Trend folgt, ist ein Blick in die Vergangenheit notwendig. Dabei stehen ausschließlich die Stimmen und Parteien im Vordergrund, die es nicht in den Bundenstag geschafft haben: Die Sonstigen.

Ausgehend von den Daten des Bundeswahlleiters wurden für jedes Jahr die Bundesergebnisse erfaßt. Zu beachten sind zwei Ausnahmen:

  • Im Jahr 1949 gab es keine 5%.-Hürde
  • Im Jahr 1990 wurde in Ost und West getrennt eine 5%-Hürde angewandt.
Jahr Angetretene Parteien Gültige Stimmen Sonstige Parteien 5%-Hürde Verworfene Stimmen %-Anteil Sonstige
1949 15 23732398 Wahl ohne 5%-Hürde
1953 13 27551272 6 1377564 1803026 6,54
1957 13 29905428 6 1495271 2010826 6,72
1961 9 31550901 5 1577545 1796408 5,69
1965 11 32620442 7 1631022 1186449 3,64
1969 12 32966024 8 1648301 1801699 5,47
1972 8 37459750 4 1872988 348579 0,93
1976 16 37822500 12 1891125 333595 0,88
1980 12 37938981 8 1896949 749646 1,98
1983 13 38940687 8 1947034 201962 0,52
1987 16 37867319 11 1893366 512817 1,35
1990 24 46455772 18 2322789 3740292 8,05
1994 22 47105174 16 2355259 1698766 3,61
1998 33 49308512 27 2465426 2899822 5,88
2002 24 47996480 18 2399824 1459299 3,04
2005 25 47187988 19 2359399 1757610 3,72
2009 27 43371190 21 2168560 2606902 6,01
2013 30 43726856 25 2186343 6859439 15,69

Für die Trendanalyse sind Graphen aber sicher besser geeignet. Also mal schauen, wie die "Sonstigen Parteien" sich so verteilen. Die Anzahl der Stimmen, die unter die 5%-Hürde fallen habe ich im Sinne meines Vorschlags gleich gestrichen.

wahl-sonstige-historie

Es ist klar zu erkennen, daß der Trend zu immer mehr Parteien bei der Wahl geht. Erst 2013 ist ein so erheblicher Teil der Stimmen verworfen worden. Der Wert im Jahr 1990 geht maßgeblich auf den knappen Verlust der Grünen im Westgebiet mit 4,8% zurück. Insofern besteht in beiden Jahren eine ähnliche Situation: Es gibt in beiden Fällen Parteien, die knapp die 5%-Hürde verfehlen, deren Stimmen aber fast komplett über der summierten 5%-Hürde liegen.

Die nun offene Frage ist, wie sich die Begrenzung der 5%-Hürde auf maximal 5% der zu verwerfenden Stimmen auswirkt. Welche Parteien kommen somit wie stark in die Parlamente? Ist dann die damals geschlossene Koalition immer noch möglich?

Jahr Gebildete
Koalition
Zusätzliche Parteien beim Abschneiden
von unten
Zusätzliche Parteien beim Abschneiden
von allen
Alte Koalition weiterhin noch möglich?
1953 CDU/CSU,FDP, DP, GB/BHE   12 KPD    6 KPD
   3 BP
Ja/Ja
1957 CDU/CSU, DP   26 GB/BHE   10 GB/BHE Ja/Ja
1961 CDU/CSU, FDP   15 GDP (DP-BHE)    4 GDP (DP-BHE) Ja/Ja
1965 CDU/CSU, FDP 5%-Hürde nicht überschritten
1969 SPD, FDP   23 NPD    3 NPD Nein/Ja
1972 SPD, FDP 5%-Hürde nicht überschritten
1976 SPD, FDP
1980 SPD, FDP
1983 CDU/CSU, FDP
1987 CDU/CSU, FDP
1990 CDU/CSU, FDP   27 GRÜNE   17 GRÜNE
   5 REP
Ja/Ja
1994 CDU/CSU, FDP 5%-Hürde nicht überschritten
1998 SPD, GRÜNE   13 REP    5 REP
   1 DVU
Nein/Ja
2002 SPD, GRÜNE 5%-Hürde nicht überschritten
2005 CDU/CSU,SPD
2009 CDU/CSU, FDP   13 PIRATEN    5 PIRATEN
   2 NPD
Ja/Ja
2013 CDU/CSU, SPD
CDU/CSU, GRÜNE
SPD, LINKE, GRÜNE
  31 FDP
  31 AfD
  14 PIRATEN
  27 FDP
  27 AfD
  10 PIRATEN
   4 NPD
   2 FREIE WÄHLER
Ja/Ja
Ja/Ja
Nein/Nein

Fazit

Die Beschränkung der zu verwerfenden Stimmen auf maximal 5% der gültigen Stimmen ist historisch gesehen nicht schädlich.

Das Verfahren, die Parteien solange rauszuwerfen, bis die 5%-Hürde überschritten würde (Abschneiden von unten), verhindert in zwei Jahren (1969/1998) die doch knappe Koalition. Es kam in den letzten Jahren immer nur die stärkste der sonstigen Parteien hinzu, oft sogar fast in Fraktionsstärke.

Das Verfahren, allen sonstigen Parteien die Stimmen wegzunehmen, bis die 5%-Hürde erreicht ist (Abschneiden von allen), verändert an den historischen Regierungen nichts. In allen Fällen sind die zusätzlich hinzukommenden Parteien deutlich unter Fraktionsstärke und es kommen mehr Parteien ins Parlament.

Die Auswirkungen des "Abschneidens von allen" sind demokratischer (mehr sonstige Parteien, geringerere Einfluß für einzelene sonstige Parteien).

Wer Wordpress einfach nur nutzt, wundert sich manchmal, mit welcher Magie das System einen wieder erkennt. Akut habe ich einen Fall, bei dem ein geschützter Bereich eingerichtet wurde, aber tagelang nicht nach einem Passwort gefragt wird.

Cookies

Wordpress benutzt Cookies, um den Nutzer während einer Session immer wieder zu identifizieren. Solange der Nutzer sich also nicht abmeldet und seinen Browser offen läßt, solange sendet sein Browser eine Kennung an den Webserver. Anhand dieser Kennung identifiziert das CMS den Nutzer und weist ihm die richtigen Rechte zu.

Wählt man zusätzlich "Erinnere Dich an mich", wird ein Cookie konstruiert, der dauerhaft im Browser gespeichet wird. Die Voreinstellung für die Lebensdauer von Cookies ist zwei Wochen.

Natürlich hat sich der Nutzer bei der Erstellung des geschützten Seite angemeldet. Wordpress übersendet ihm einen Cookie, um ihn wiederzuerkennen. Als Eigentümer der Seite bekommt der Nutzer deswegen die Passwortabfrage nie zu sehen. Dies ist verwirrend.

Lebenserfahrung

Die implizite Annahme, eine Anmeldung sei jeden Tag neu nötig, wird durch die Voreinstellungen von Wordpress verletzt.

Diese Annahme entstammt der allgemeinen Lebenerfahrung, sich an seinem Betriebssystem jeden Tag anmelden zu müssen, nachdem man den Rechner eingeschaltet hat. Es ist nicht einzusehen, warum andere System nicht diesem Rhytmus unterliegen.

Andererseits ist es nicht einzusehen, warum man sich neu anmelden muß, wenn man den Browser beendet und neu startet. Schließlich ist der Browser ja solange ohne Passwortabfrage startbar, wie man am System selbst angemeldet ist.

Der Nutzer setzt also die Anwendung im Browser mit der Browsersoftware gleich.

Lebensdauer

Es wird also gewünscht die Lebensdauer einer automatischen Anmeldung (Wiedererkennung) auf die Arbeit eines Tages zu begrenzen. Wenn man an mehreren Tagen am CMS arbeiten will, muß man sich halt täglich neu anmelden.

Ein Blick in den Sourcecode von WordPress zeigt, wie die Cookies zu ihrer Lebensdauer kommen:

function wp_set_auth_cookie($user_id, $remember = false, $secure = '') {
  if ( $remember ) {
    $expiration =
        $expire = time() +
      apply_filters('auth_cookie_expiration', 1209600, $user_id, $remember);
  } else {
    $expiration = time() +
      apply_filters('auth_cookie_expiration',  172800, $user_id, $remember);
    $expire = 0;
  }

Mit "Erinnere Dich an mich" hat ein Cookie also 14 Tage, sonst 2 Tage zu leben. Und das muß geändert werden.

Glücklicherweise sieht WordPress bereits einen Hook für die Änderung vor. Es genügt also ein kleines Plugin zu schreiben, das diesen Wert abändert.

$ cat > wp-content/plugins/restrict-cookie.php
<?php
/**
 * @package Restrict_Cookie
 * @version 0.1
 */
/*
Plugin Name: Restrict Cookie
Plugin URI: http://lutz.donnerhacke.de/Blog/Restrict-Cookie
Description: Limit the lifetime of the cookie to a given value
Author: Lutz Donnerhacke
Version: 0.1
Author URI: http://lutz.donnerhacke.de/
*/

function restrict_cookie_set() {
        $years   = 0;
        $months  = 0;
        $weeks   = 0;
        $days    = 0;
        $hours   = 10;
        $minutes = 0;
        $seconds = 0;

        return ($seconds +
                60*($minutes +
                    60*($hours +
                        24*(    $days   +
                              7*$weeks  +
                             30*$months +
                            365*$years
                           )
                       )
                   )
               );
}


add_filter( 'auth_cookie_expiration', 'restrict_cookie_set' );

?> 

Das Plugin taucht sofort im Administrationsinterface auf und kann aktiviert werden.

Und wie paßt man die Werte an? Schließlich ist das Plugin minimal, es enthält keine neuen Webseiten für das Administrationsinterface, keine Einstellknöpfe, keine Regler.

Und da habe ich mir die Arbeit einfach gemacht. Man klickt im Administrationsinterface der Plugins auf "Bearbeiten".

Fertig.

Die Cisco ASA kann mit mehreren CAs umgehen, aber wann kommen welche Zertifikate zur Anwendung? Wie funktioniert das mit mehren CA? Wie überlebt man den Rollover von CA-Schlüsseln?

Hier kommt fast ausschließlich L2TP over IPSec zum Einsatz, weil dieses von praktisch allen Betriebssystemen nativ unterstützt wird. Es ist also keine Clientsoftware notwendig. Für diesen Artikel wird auch nur der Fall behandelt, wenn überhaupt eine Zertifikatsauthenisierung stattfindet.

Was im Hintergrund passiert

Wählt sich ein VPN-Client ein, präsentiert er zuerst sein Zertifikat (aggressive Mode). Zu diesem Zeitpunkt hat die ASA noch keinerlei weitere Informationen und prüft das Client-Zertifikat gegen alle konfigurierten trust-points.

Wurde das Nutzerzertifikat für gültig befunden, schaut die ASA anhand der Angaben des Client-Zertifikats in den certificate map nach, welche Kennung in der tunnel-group-map nachgeschlagen werden soll. Dort findet sich dann die entsprechende tunnel-group. Diese Indirektion gestattet es verschiedene Zertifikatseigenschaften zu analysieren die dann doch nur auf wenigetunnel-groups zusammenfallen.

Fehlt ein passender Eintrag in der certificate-map, wird versucht, den OU-Eintrag im Subject des Client-Zertifikats alstunnel-group Bezeichner zu finden.

Schlägt auch dieses fehl, so wird die Gruppe DefaultRAGroup genutzt.

Nun steht fest, welche tunnel-group genutzt werden soll. Von dort holt sich die ASA den zu verwendenden trust-point. Dieser legt fest, welches Server-Zertifikat ausgewählt wird.

Steht in der tunnel-group auch das Kommando chain, so wird nicht nur das Server-Zertifikat, sondern die ganze Zertifikatskette dieses trust-points mitgeschickt.

CA-Rollover

Es ist möglich, als CA-Zertifikat eines Trust-Points auch ein Zwischenzertifikat (intermediate) einzuspielen. In Verbindung mit derchain Option liefert die ASA dann die Zertifikatskette aus, die der Client zur Validierung benötigt.

Die bisherige aktive CA besteht, weil nach wie vor von ihr ausgestellte CA-Zertifikate im Umlauf sind, die validiert werden müssen. Das Zertifikate der ASA unter dieser CA ist allerdings abgelaufen.

asa-hosting# sh crypto ca certificates iks2013 | in ^C|cn|Name|ate:
Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2013
  Subject Name:
    cn=asa-hosting.net.iks-jena.de
  Validity Date:
    start date: 09:15:08 CET Jan 10 2013
    end   date: 09:15:08 CET Jan 10 2014
CA Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2013
  Subject Name:
    cn=CA der IKS Service GmbH (SIGN) 2013
  Validity Date:
    start date: 16:48:03 CET Jan 9 2013
    end   date: 16:48:03 CET Feb 8 2015

Zuerst einmal wird die CA des neuen Jahres von der alten CA signiert. Diese Zwischenzertifikat ist der Ersatz für die bisherige alte "iks2013". Überall, wo ikev1 trust-point iks2013, steht nun ikev1 trust-point iks2013-14.

asa-hosting# sh crypto ca certificates iks2013-14 | in ^C|cn|Name|ate:
Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2014
  Subject Name:
    cn=asa-hosting.net.iks-jena.de
  Validity Date:
    start date: 02:50:30 CET Jan 10 2014
    end   date: 02:50:30 CET Jan 10 2015
CA Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2013
  Subject Name:
    cn=CA der IKS Service GmbH (SIGN) 2014
  Validity Date:
    start date: 22:09:53 CET Jan 8 2014
    end   date: 22:09:53 CET Jan 18 2016

Dazu kommt noch das CA des neuen Jahres, die überall dort als trust-point verwendet wird, wo keine alten Zertifikate mehr im Umlauf sind.

asa-hosting# sh crypto ca certificates iks2014 | in ^C|cn|Name|ate:
Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2014
  Subject Name:
    cn=asa-hosting.net.iks-jena.de
  Validity Date:
    start date: 02:50:30 CET Jan 10 2014
    end   date: 02:50:30 CET Jan 10 2015
CA Certificate
  Issuer Name:
    cn=CA der IKS Service GmbH (SIGN) 2014
  Subject Name:
    cn=CA der IKS Service GmbH (SIGN) 2014
  Validity Date:
    start date: 22:08:13 CET Jan 8 2014
    end   date: 22:08:13 CET Feb 7 2016

Mittels der crypto ca certificate map Regeln werden die Tunnel-Gruppen so ausgewählt, daß dem Client das jeweils passende Server-Zertifikat vorgelegt wird.

Praktisch genügt es überall, iks2013-14 zu verwenden, weil jeder Client die alte oder neue CA kennt. Mit dem Zwischenzertifikat können beide Clientarten korrekt validieren. Man kann aber mit diesen drei Trust-Points die alten CA-Zertifikate stückweise und im laufenden Betrieb entfernen.

asa-aa-7

Mein Schreck war groß, als ich bei Fefe las, dass ein OpenPGP Key mit 4096 bit RSA faktorisiert worden sei. Kann das überhaupt sein? Ist die Kryptokalypse da?

Zuerst einmal die gute Nachricht: Es stellte sich heraus, dass jemand die Schlüssel auf den Keyservern mit einem weiteren Subkey versehen hatte. Und dieser Subkey hat die leicht zu findenden Primfaktoren 3 * 7 * 11 * … Dieser Subkey ist mit einer ungültigen (kopierten) Signatur an den Hauptschlüssel angepappt worden, so dass praktisch jede Implementation diesen Key nicht akzeptieren sollte.

Warum macht man so was? Zum einen besteht natürlich immer die Hoffnung, dass jemand doch den falschen Schlüssel benutzt und so Geheimnisse direkt für den Angreifer verschlüsselt. Natürlich in guten Glauben nur den gewünschten Partner zu erreichen. Zum anderen ist da ein Spieltrieb: Man kann halt zeigen, dass man es kann.

Hinter den Kulissen

Aber der eigentlich interessante Teil ist, was man dort versucht hat und welche Gefahren dieses Verfahren birgt. Es geht darum, die ca. zwei Millionen Schlüssel auf den Keyservern als Gesamtheit zu untersuchen.

In der Hoffnung zwei Schlüssel mit zufällig gleichem Primfaktor zu finden, vergleicht man paarweise die Schlüssel miteinander. Man bildet jeweils den größten gemeinsamen Teiler und hofft auf das große Los.

Bei 2 Mio. Schlüsseln sind das 2 Trillionen zu untersuchende Paare. Klingt viel? Mit einem Aufbau, der bei 2 GHz je ein Schlüsselpaar vergleichen kann, dauert es nur ein paar Minuten.

Es stimmt, aktuell gibt es keinen Prozessor, der in einem Taktzyklus eine ggT-Operation auf Langzahlen ausführen kann. Allerdings habe ich in der Schätzung auch alle Schlüssel unabhängig von ihrer Länge erfasst. Das würde man natürlich nicht tun, sondern gruppieren. Außerdem kann man das Verfahren bequem parallelisieren. Oder anders gesagt. Mit etwas Vorbereitung durchsucht man den kompletten Bestand der Keyserver in ein paar Tagen auf zufällige Übereinstimmungen der Primfaktoren.

Soweit so schlecht. Aber wie wahrscheinlich ist es, überhaupt Erfolg zu haben?

Unter der Annahme, die Primzahlen würden zufällig ausgewählt werden, kann man unter 10631 Zahlen wählen. Das ist im Vergleich zu den 2 Trillionen Paaren mehr als gigantisch. Die Wahrscheinlichkeit, einen Treffer zu landen ist also praktisch Null.

Ist der Zufallszahlengenerator aber nicht in Ordnung oder korreliert er zwischen verschiedenen Systemen (z.B. über eine gemeinsame Zeit initialisiert), dann steigen die Chancen ganz erheblich.

Und deswegen versucht man es halt.

Und manchmal findet man da sehr seltsame Dinge.

Gestern Abend hat sich ein Mailserver spontan dazu entschlossen, als offenes Relay eine besondere Willkommenskultur gegenüber Fremden an den Tag zu legen. Am nächsten Morgen fiel der Zustand zwar auf, konnte aber nicht mehr nachgestellt werden. Egal was man probierte, die Maschine lehnte gleichartige Versuche, den Spam einzuliefern einfach ab.

Es fanden sich über 4000 noch nicht zugestellte E-Mails im Spool, von den noch nicht zugestellten Rückläufern ganz zu schweigen. Aufräumen ist ja das eine, wichtiger ist die Ursache zu finden.

Ein Blick ins Logfile zeigt, daß ohne irgendwelche Authentisierung die E-Mail angenommen wurde:

19:05:03 xxx sm-mta[1930]: u0EI4iOC001930: from=clients@accounts1.com, size=38551, class=0, nrcpts=13, msgid=<22f19892829748b29072f669d147ed20@accounts1.com>, proto=ESMTP, daemon=MTA, relay=[120.24.4.184]
19:05:03 xxx sm-mta[1930]: u0EI4iOC001930: to=samoapim@gmail.com, delay=00:00:15, mailer=esmtp, pri=428551, dsn=4.4.3, stat=queued
19:05:03 xxx sm-mta[1930]: u0EI4iOC001930: to=samoawillovercome@hotmail.co.uk, delay=00:00:15, mailer=esmtp, pri=428551, dsn=4.4.3, stat=queued
...

Wie kann das sein?

Ein Test von einer externen Maschine aus gibt eine klare Ablehnung zurück:

$ telnet -4 xxx 25
Connected to xxx
Escape character is '^]'.
220 xxx ESMTP Sendmail ...
ehlo yyy
250-xxx Hello yyy, pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE 100000000
250-DSN
250-ETRN
250-AUTH PLAIN LOGIN
250-STARTTLS
250-DELIVERBY
250 HELP
mail from: <clients@accounts1.com>
250 2.1.0 <clients@accounts1.com>... Sender ok
rcpt to: <samoapim@gmail.com>
550 5.7.1 <samoapim@gmail.com>... Relaying denied. Proper authentication required.
quit
221 2.0.0 xxx closing connection

Am gestrigen Abend war das aber offenbar nicht der Fall.

Ein Bug in einer Crypto-Lib, der des dem Angreifer gestattet, den Speicher des Sendmail zu überschreiben? Ach nein, es war ja kein TLS aktiv.

Eine besonders kreative DNS Antwort für die reverse Auflösung, die das Sendmail austrickst? Ja, kann sein. Es ist zwar nichts von einer erfolgreichen DNS Auflösung geloggt worden, aber vielleicht ist das auf den Trick auch reingefallen.

Also den Debugger anwerfen und spielen:

# sendmail -bt -d21.4
> .D{client_addr}120.24.4.184
> .D{client_name}
> .D{client_resolve}OK
> check_rcpt <samoapim@gmail.com>
...
check_rcpt       returns: $# error $@ 5 . 7 . 1 $: "550 Relaying denied. Proper authentication required." 

Das war's nicht, selbst wenn ich über die erfolgreiche Hin- und Rückauflösung lüge (das OK). Also nächster Versuch. Was könnte das DNS noch zurück geliefert haben?

> .D{client_name}localhost
> check_rcpt <samoapim@gmail.com>
...
check_rcpt       returns: $# error $@ 5 . 7 . 1 $: "550 Relaying denied. Proper authentication required." 

Es gibt da aber innen drin, einige sehr interessante Domain-Namen, die das Sendmail zusammenbaut. Wer da Glück hat, kann sicher einiges an Schutzmaßnahmen aushebeln. Aber nicht jetzt.

Nächster Versuch:

> .D{client_name}xxx
> check_rcpt <samoapim@gmail.com>
...
rewritten as: < ? > samoapim < @ gmail . com > $| OK
rewritten as: < ? > samoapim < @ gmail . com >
check_rcpt       returns: < ? > samoapim < @ gmail . com >

Oops! Wenn sich der Einlieferer als der einlieferende Host ausgibt, dann kann er – unabhängig von seiner tatsächlichen Adresse – problemlos einliefern.

Panik

Das Problem ist nur, daß Sendmail eine Vorwärtsauflösung zur Rückwärtsauflösung hinzufügt, und die muß wieder die einlieferende IP ergeben. Das ist natürlich nicht der Fall, oder kann man das DNS austricksen?

Normalerweise sollte das nicht klappen, aber wenn man den Resolver des Mailserver überreden könnte – dann hätte man eine ganze Weile Narrenfreiheit.

Zum Glück hat dieser Resolver DNSSEC-Validierung aktiv, wenn er auch zum fraglichen Zeitpunkt unter mächtigem Beschuß stand. Kann es sein, daß es also doch gelungen ist, da eine falsche Auflösung unterzujubeln?

Eine genauere Analyse der fraglichen DNS Resolver brachte keine Aufklärung. Kann sein, sollte nicht, wäre ungewöhnlich, kann ich mir nicht vorstellen. Oder doch?

Verzweifelte Suche

Je mehr man sucht, desto weniger wahrscheinlich wird das Phantom, dem man nachjagt. Also zurück auf Start.

Was tut sendmail eigentlich beim check_rcpt?

Beim Durchblättern des Debug-Outputs fällt eine Zeile auf, die siedend heiße Erinnerungen weckt.

Basic_check_rcpt   input: < samoapim @ gmail . com >
rewrite: RHS $&{deliveryMode} => "i"
rewritten as: < i > < samoapim @ gmail . com >
rewritten as: < samoapim @ gmail . com >

deliveryMode? Daran habe ich gestern Nachmittag herum gespielt. Wegen einer seltsamen Verzögerung bei der Mailannahme.

Mit dem Setzen von deliveryMode auf deferred werden DNS-Anfragen nicht sofort gestellt, sondern erst, wenn der Server explizit zur Mailauslieferung aufgefordert wird. Der Modus ist dafür gedacht, auf einer isolierten Maschine die Mailannahme zu ermöglichen.

Sollte das etwa? Ein Test:

> .D{deliveryMode}d
> check_rcpt <samoapim@gmail.com>
...
rewritten as: < ? > samoapim < @ gmail . com > $| deferred
rewritten as: < ? > samoapim < @ gmail . com >
check_rcpt       returns: < ? > samoapim < @ gmail . com >

Autsch! Ja, ich hatte die Konfig geändert, aber habe ich nach dem Test den Dienst auch neu gestartet?

Wirklich?

Ganz, ganz sicher?

Wollen wir ins Logfile schauen?

Occams razor

Was ist wahrscheinlicher: Das ein Spammer einen gezielten DNS Angriff gegen DNSSEC validierende Resolver erfolgreich mit einer Spamverteilung koordinierte (so das ein ganzes Botnetz für zig Minuten einliefern kann) oder das der Admin einen Fehler gemacht hat?

Ok, also nochmal den Modus umstellen und von extern testen:

$ telnet -4 xxx 25
Connected to xxx
Escape character is '^]'.
220 xxx ESMTP Sendmail ...
ehlo yyy
250-xxx Hello yyy, pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE 100000000
250-DSN
250-ETRN
250-AUTH PLAIN LOGIN
250-STARTTLS
250-DELIVERBY
250 HELP
mail from: <clients@accounts1.com>
250 2.1.0 <clients@accounts1.com>... Sender ok
rcpt to: <samoapim@gmail.com>
250 2.1.5 <samoa@gmail.com>... Recipient ok (will queue)
quit
221 2.0.0 xxx closing connection

Autsch! Und wieder zurück.

$ telnet -4 xxx 25
Connected to xxx
Escape character is '^]'.
220 xxx ESMTP Sendmail ...
ehlo yyy
250-xxx Hello yyy, pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE 100000000
250-DSN
250-ETRN
250-AUTH PLAIN LOGIN
250-STARTTLS
250-DELIVERBY
250 HELP
mail from: <clients@accounts1.com>
250 2.1.0 <clients@accounts1.com>... Sender ok
rcpt to: <samoapim@gmail.com>
550 5.7.1 <samoapim@gmail.com>... Relaying denied. Proper authentication required.
quit
221 2.0.0 xxx closing connection

*seufz* Was für ein Mist.