Erweiterte Suche


Der Wechsel einer PPPoX-Plattform bringt vor allem dort Probleme, wo Alt- und Fremdgeräte im Einsatz sind. Besonders unangenehm sind die Speedports der Telekom. Mit Verzicht und Sorgfalt geht es trotzdem.

Als neues LNS soll MPD auf FreeBSD zum Einsatz kommen. Mit den vorgegebenen Fritzboxen tut alles wunderbar. In der Praxis kommen aber verschiedene Kunden nicht mehr vernünftig online.

Speedport W 700V

Das Gerät kann sich meist nicht anmelden. Alle zig Versuche gelingt es trotzdem und dann funktioniert die Anbindung stabil. Was ist da los?

Im LCP Debugging findet sich ein ganz normaler Vorgang:

  • Die Vorschläge werden verhandelt, abgelehnt, umformuliert und neu verhandelt.
  • Alle Umformulierungen finden Stück für Stück statt. Es wird nur soviel geändert, wie unbedingt nötig.
  • Nach drei Verhandlungsrunden sendet der Speedport eine Termination Request. Er legt auf.
  • In den seltenen Fällen, in denen es klappt, gelingt die Verständigung in der vierten Runde.
  • In diesen Fällen antwortet der Speedport nicht in Runde drei auf die Vorschläge. Stattdessen überspringt er diese Runde.

Der Speedport hat offenbar ein internes Limit auf drei Verhandlungsrunden (nach den ersten Runden mit dem LAC). Gelingt es ihm nicht binnen dieser drei Runden eine PPP Verbindung auszuhandeln, legt er auf. Fritzboxen machen dagegen bis zu zehn Durchläufe.

Die Erklärung liegt darin, daß Paketverluste auf der DSL Leitung dazu führen können, daß der dritte Vorschlag gar nicht beim Speedport ankommt. Dann verarbeitet er den Vorschlag der vierte Runde als dritten Vorschlag.

Daraus ergibt sich als Lösung, die "richtigen"(TM) Vorschläge gleich zu Anfang auszugeben. Es genügt in diesem Fall, die Authentisierungsprotokolle auf PAP einzuschränken. Der MPD schlägt dieses Authentisierungsprotokoll normalerweise als letztes vor, weil es den niedrigsten Sicherheitsgrad auf der Leitung bietet. Der Speedport macht aber nur PAP und durch die notwendigen Extrarunden kommt die Verbindung dann gar nicht mehr zustande.

create link template ... l2tp
set link no     chap-md5 chap chap-msv1 chap-msv2 eap
set link enable pap
set link deny   pap

Natürlich gibt es da ein neues Problem: Alle Kunden, die bisher ausschließlich CHAP machten, können nun gar nicht mehr arbeiten. Dafür gibt es zwei Ansätze:

  • Wenn der Carrier seinem LAC beibringt, die Anschlußkennung zu übertragen, kann der MPD den Stand der letzten fehlgeschlagenen Anmeldung vermerken. Beim nächsten Anmeldeversuch (von diesem Anschluß) beginnt er dann an der Abbruchstelle erneut. Dieses Verfahren ist selbstjustierend und kann eine weite Bandbreite von Parametern aushandeln.
  • Wenn der Carrier seinem LAC beibringt, die schon vorab ausgehandelten Parameter zu übertragen, kann der MPD auf diesen Stand aufsetzen und direkt mit den schon ausgehandelten Einstellungen weiter machen.

Beide Varianten wurden von mir schon testweise implementiert, jedoch ist die Versorgung mit den Parametern noch nicht produktiv.

Speedport W 723V Typ A

Das Gerät kann sich sauber anmelden. Es funktioniert allerdings keine eingehende Datenübertragung. Die Daten erreichen den Speedport offenkundig nicht.

Beim Sniffen findet man die ganz normale Kommunikation. Es gehen DNS Anfragen aus, es kommen Antworten zurück. Es gehen HTTP Anfragen aus, es kommen Antworten zurück. Aber es gibt nie mehr als das initiale Paket des Verbindungsaufbaus. Und das wird immer wiederholt. Als ob Paketverlust auftritt.

Im LCP Debugging sieht auch alles gut aus. Jedoch kommt nach einer Weile eine ungewöhnliche Meldung:

LCP: protocol 0x2145 was rejected

Auch hier ist die Erklärung so einfach wie überraschend:

  • Beide Seiten handeln PROTOCOMP aus.
  • Diese Kompression gestattet es, die normalerweise 16-bit langen Protokollfelder als ein Oktet zu übertragen, wenn diese dort hinein passen.
  • IPv4 Payload hat die Protokollkennung 0x0021, was wegen PROTOCOMP als 0x21 kodiert wird.
  • Der Speedport liest das Protokoll trotz gegenteiliger Aushandlung immer als zwei Oktets ein.
  • Er beschwert sich über das unbekannte Protokoll "0x2145", also das Protokoll 0x21 und das erste Byte des Payloads 0x45.
  • Der Speedport verwirft die Nutzdaten von IPv4 Paketen.

Die Lösung besteht darin, dieses Protokoll nicht mehr auszuhandeln.

create link template ... l2tp
set link no protocomp

Und schon geht's wieder. Trotzdem ist es unschön, auf wünschenswerte Protokolleigenschaften zu verzichten, nur weil ein verschwindend kleiner Anteil an Endgeräten solche katatrophalen Fehler macht.

Auch hier bietet sich an, individuell auf die Kundengeräte einzugehen. Da aber die PROTOCOMP Aushandlung noch vor dem Login erfolgt, geht es auch nur wieder mit Hilfe des Carriers.

  • Wenn der Carrier seinem LAC beibringt, die Anschlußkennung oder den Nutzernamen zu übertragen, kann der MPD diesem Anschluß beim nächsten Einwahlversuch PROTOCOMP verbieten.

Für eine Implementation des Verhaltens sind die o.g. Testimplementationen anpaßbar.

Für eine große Anzahl an DSL Kunden wurde mir für die LNS Funktionalität die Software MPD auf FreeBSD empfohlen. Ein Blick in die Suchergebnisse zeigt, dass vor allem in Russland wirklich große Installationen damit gefahren werden. Als Alternative stand OpenL2TP im Raum. Allerdings handelt OpenL2TP die PPP Sessions im Userspace ab, während MPD auf NetGraph zurück greift.

Erstkontakt

Das Setup ist einfach, wenn auch die Dokumentation von MPD spartanisch ist. Was ein Kommando tut, steht höchstens an einer Stelle. Wenn man die nicht kennt, ist man schlicht aufgeschmissen.

Darüberhinaus unterscheidet MPD strikt zwischen den Layern der Verbindung. Man kann also nur dort etwas konfigurieren (und nachlesen), wo diese Einstellung relevant ist. So findet sich VanJacobson Compression im IPCP-Teil eine (PPP-)Bundles. Protocol Field Compression als Bestandteil der PPP Session wird aber im Link-Layer ausgehandelt. Deswegen findet sich die Einstellung dort: wie auch die Authenisierungsprotokolle, die LCP verwenden soll.

Die Konfigurationsdatei ist strikt linear. Jedes Kommando kann den aktuellen Layer ändern. Damit sind die Folgekommandos in einem anderen Kontext auszuführen. Am besten ist es also, die Konfiguration manuell zu erstellen und parallel dazu die Eingaben mitzuschreiben (log +console).

Ernüchterung

Die ersten Tests waren das Grauen.

lasttest-mpd-patch1

Es ging ganz lustig los, brach aber dann mit "no more threads" und "Fatal message queue overflow!" ab. Der MPD stürzte kontrolliert ab, beendete alle Sessions und sich selbst.

Als Grund dafür fand sich schnell das Event-Handling für Radius Anfragen. Die Implementation erfolgt als threadinterne Pipe aus der gelesen und geschrieben wird. Parallel dazu gibt es einen Ringpuffer an Ereignisinformationen. Damit ein write auf die Pipe nicht blockt, dürfen nicht mehr Bytes (Dummywert für "Es gibt ein Ereignis") geschrieben werden, als in die Kernel-Puffer passen.

Der offenkundige Patch besteht darin, die Pipegröße und den Ringpuffer zu vergrößern. Aber damit hatte ich gar keinen Erfolg: Es knallte mit einem Segfault.

Da pro Layer verschiedene Ereignisse generiert werden, kann bei der Verarbeitung eines einzigen Ereignisses eine Flut von Folgeevents angefordert werden. Aber der Thread für die Eventverarbeitung darf nicht blockieren! Deswegen werden für externe Ereignisse ständig neue Threads aufgemacht, die dann für sich allein warten können. In diesem Fall hat Radius-Authenisierung und -Accounting die Grenze von 5000 Threads pro Prozess überschritten. Auf dem Radiusserver sah die Last dann ähnlich aus.

Wie schlimm diese Radiuskaskade werden kann, hat ein Zulieferer während eines solches Tests erlebt. Die kommerzielle LAC Lösung dort fragt pro Loginversuch einen Radiusserver nach dem Realm, um den passenden LNS zu ermitteln. Als spontan alles zusammenbrach, wurde er von so vielen Requests überrannt, dass der Radiusserver – genauer dessen Datenbank-Backend – in die Hände klatschte und die gesamte DSL-Zuführung den Dienst quittierte. Inzwischen läuft die Verteilung an die LNS statisch.

Schön langsam

Man soll also freundlich zu seinen Zulieferern sein, besonders zu seinen Radius-Servern. Ich habe das komplette Eventhandling des MPD entsorgt und auf Basis von mesg_queue neu geschrieben. Die verwendete Bibliothek war sowieso schon im MPD eingebunden.

Unterschieden wird nach "sequentiellen" und "parallelen" Ereignissen. Sequentielle Ereignisse laufen unter dem globalen Lock des MPD serialisiert und in der Reihenfolge, in der sie generiert wurden. So kommen die Layer nicht durcheinander und die Ereignisverarbeitung kann sich auf die vorbereitenden Aktionen verlassen. Die parallele Verarbeitung dient der externen Kommunikation. Dabei die die Anzahl der aktiv laufenden Worker-Threads limitiert.

Die Länge beider Warteschlangen wird dem MPD zur Overload Berechnung (Load = 2×queue_len(seriell) + 10×queue_len(parallel)) vorgelegt, so dass er bei Last die Annahme neuer Verbindungen verweigern kann.

So ausgerüstet war ich voller Hoffnung und wurde jäh enttäuscht. Der Kernel warf hin: Tief tief drin.

bsd-lasttest-panic-1

Und nicht nur einmal, sondern auch an ganz anderer Stelle.

bsd-lasttest-panic-2

War der NetGraph Code defekt? Kommt der Kernel nicht mit dem schnellen Anlegen und Löschen von Interfaces klar? Bin ich zu schnell?

Schon im Radius-Accounting gab es Probleme, weil des Testgeräten gelang dreimal innerhalb einer Sekunde eine Verbindung auf- und abzubauen. Das verletzte die unique-Constraints meines Datenschemas. Das Teil ist also definitiv zu schnell! Im Code habe ich also dann eine Pause von 20ms (konfigurierbar) zwischen zwei seriellen Ereignissen eingebaut.

Da der Thread für die serielle Abarbeitung nun vom Hauptthread getrennt läuft, was vorher nicht der Fall war, habe ich um die NetGraph Aufrufe ein separates Lock gelegt. Dies verhindert, dass konkurrierend auf den NetGraph-Sockets gearbeitet wird.

Damit wurde der Systemverhalten stabil.

Lasttests

Zum Testen wird von einem Linux mit OpenL2TP aus:

  • eine zufällige Anzahl von L2TP Tunneln aufgemacht und
  • auf diesen so schnell wie möglich reiherum bis zu 9000 Sessions aufgerissen.
  • Danach werden die Sessions mit einigen Dezisekunden Pause generiert, bis die Anzahl der der gewünschten Clients verbunden ist.
  • Anschließend wird ein Ausfall und Wiederherstellen von ca. 300 zufälligen Sessions mehrfach wiederholt.
  • Nach einer Weile Ruhe werden die Sessions kontrolliert abgebaut.

Ein typisches Beispiel dieses Tests sieht so aus: Die "Sessions" sind vom lastgenerierenden OpenL2TP, diese generieren auf den MPD L2TP-"Links" und darauf PPP-"Bundles". Mitgeplottet werden auch die Längen der Eventqueues.

mpd-logins-13040106

Initial ist die Länge der parallelen Queue groß, dann greift die Limitierung durch Overload. Die Kurven für Sessions, Links und Bundles liegen praktisch aufeinander, es gibt keine Differenzen zwischen den Systemen, denn nicht aufgebaute Sessions werden nicht mit aufgeschrieben.

Ebenso ist schön zu sehen, wie heftig die serielle Queue anwächst, wenn Interfaces wegfallen: Der MPD ist in seiner originalen Version definitiv anfällig gegen Queueoverrun. Tritt dieser Effekt ein, beendet sich der originale MPD. Ein inakzeptabler Zustand.

Die FreeBSD Kiste mit MPD hat dabei einen maximalen Load von 3 und benötigt 280 MB RAM. Die Linux Maschine hat einen Load von 20 beim Verbindungsaufbau und einen Load von 50 beim Verbindungsabbau. Der Speicherbedarf auf der Linux-Seite beträgt 2 GB, um die vielen PPPD Instanzen zu halten.

Knalleffekte

Und nun der Test mit einem heftigeren Abbruch: Anstatt die PPPD Instanzen auf Linux einzeln zu killen, wird der L2TP Kanal unterbrochen.

mpd-logins-13040211

Deutlich erkennbar ist, wie die "übergeordneten" Bundles zuerst abgebaut werden und dann die dazugehörigen L2TP-Links. Der Load auf BSD-Seite steigt auf 6.

Auf Linux-Seite nimmt eine Katastrophe ihren Lauf: Alle pppd-Instanzen konkurrieren um den Scheduler, um sich zu beenden. Der Load steigt auf über 700 und die Maschine braucht zu lange, um sich zu beruhigen. Ein beherztes "killall -9 pppd" erlöste die Maschine nach einer halben Stunde.

Aber wie weit kann man gehen? Mehr als 10000? Mehr als 20000? Probieren wir es aus:

mpd-logins-13040213

Die Kombination steigt rasant auf 11500 Sessions und hört spontan auf. Auf Linux-Seite meldet der OpenL2TP fehlerhafte "RPC"-Parameter: Er ist in die Limits seiner Implementation gelaufen. Die Maschine verheddert sich und muß neu gebootet werden. Dem BSD hat es nichts getan.

Und nun mehrfach hintereinander:

mpd-logins-130401

Hoch und runter, immer wieder. Ab und zu einen Absturz durch Dummheit, aber es tut!

Ab in die Produktion

In der produktiven Umgebung ist es erstaunlich ruhig angelaufen. Trotzdem stürzt das System weiter ab:

bsd-lasttest-panic-3

Alle Abstürze sind nun nur noch im NAT-Code. Obwohl der Code dem Stand von HEAD entspricht ... Dazu anderweitig mehr.

Änderungen am MPD haben sich noch weitere ergeben:

  • Um den Code in der Produktion schnell austauschen zu können, beendet sich der MPD selbst, wenn er keine Sessions mehr offen hat. Die Konfiguationsoption heißt "delayed-one-shot". Der Aufruf des MPD erfolgt nun in einem Loop (/usr/local/etc/rc.d/mpd5 enthält command="/usr/local/sbin/${name}.loop")
$ cat /usr/local/sbin/mpd5.loop
#! /usr/local/bin/bash

nohup /usr/local/bin/bash -c "
cd /
while true; do
  /usr/local/sbin/mpd5 -k -p /var/run/mpd5.pid -O
  sleep 5
done
"  >/dev/null 2>/dev/null </dev/null &
  • Das Radius-Accounting für die Abbruchgründe ist mangelhaft, die komplette Fehlermeldung wird nun ebenfalls reportet:
ATTRIBUTE      mpd-term-cause  23      string
  • Systemrelevantes Logging (Interfaces kommen und gehen, Nutzer melden sich an und ab) ist nun dauerhaft an und nicht mehr nur für Debugging. Ebenso sind relevante Fehler immer zu loggen. Andernfalls fährt der Server im Blindflug.

Achja, der Patch: Bitteschön. Wir liefern gern auch kommerziellen Support dafür.

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.

Immer wieder benötigt man im Privatkunden-Umfeld die Möglichkeit die Zuweisung der IP Adressen zu dynamisieren. Hier ist der Hauptgrund, daß die Kunden darauf konditioniert wurden und sich ohne regelmäßige Adreßwechsel unsicher fühlen. Erfolgreiches Marketing eben. Muß man halt haben.

Standardsoftware

Wir setzen auf den ISC-DHCP. Und der ist bezüglich der IP Vergabe sehr konservativ. Solange auch nur eine Anhaltspunkt gefunden werden kann, daß der Client die spezielle IP bekommen müßte, wird er die bekommen. Gründe sind obskure Historien, schon mal gesehenen MAC Adressen oder ganz einfach die Anfrage des Clients, diese IP zu erhalten.

Die Dynamisierung besteht im Prinzip aus zwei Schritten.

  • Zum einen darf ein Client seine vorher mal benutzte IP nicht erneut angeboten (DISCOVER) bekommen.
  • Zum anderen darf ein Client eine Lease mit einer zugewiesenen IP ab einem bestimmten Zeitpunkt nicht mehr verlängert (REQUEST) bekommen.

Konsequenterweise sind das zwei getrennte, poolspezifische Optionen:

  • avoid-reuse bool sorgt dafür, daß ein Client jedesmal eine andere IP angeboten bekommt, wenn er keine gültige IP hat. Das gestattet einen Adreßwechsel jedesmal, wenn der Client rebootet oder neue IPs anfordert.
  • force-nack hour minute dagegen sorgt dafür, daß ein Client zu einem bestimmten Zeitpunkt seine aktuell zugewiesene IP verliert. Überstreicht eine Lease den definierten Zeitpunkt, wird die niemals verlängert. Der Client muß dann nach einer neuen IP fragen.

Die Optionen können unabhängig voneinander agieren.

Es ist also möglich, nur den Adreßwechsel zu erzwingen, nicht aber die Zwangstrennung du8rchzuführen. Diese quasi-statische Zuordnung ist im Zusammenhang mit Triple-Play wichtig, wenn der ISP erhebliche Risiken eingehen, falls er ein wichtiges Telefonat (z.B. einen Notruf) oder eine Sportübertragung (Fußball, SuperBowl) unterbricht.

Es ist ebenso möglich, nur die Zwangstrennung zu aktivieren, falls das durch Beschränkungen der Abrechnungssoftware oder andere interne Mechanismen erforderlich ist.

Umsetzung

Zuerst einmal brauchtes ein paar Token, die beide Funktionen kennzeichnen sollen.

diff -pbBru ../ORIGINAL/includes/dhcpd.h includes/dhcpd.h
--- ../ORIGINAL/includes/dhcpd.h        2011-07-09 00:56:26.000000000 +0200
+++ includes/dhcpd.h    2012-03-06 15:54:40.000000000 +0100
@@ -713,6 +713,9 @@ struct lease_state {
 # define SV_LDAP_TLS_RANDFILE           77
 #endif
 #endif
+/* private options */
+#define SV_AVOID_REUSE                 200
+#define SV_FORCE_NACK                  201
 
 #if !defined (DEFAULT_PING_TIMEOUT)
 # define DEFAULT_PING_TIMEOUT 1
diff -pbBru ../ORIGINAL/includes/dhctoken.h includes/dhctoken.h
--- ../ORIGINAL/includes/dhctoken.h     2011-05-12 14:02:47.000000000 +0200
+++ includes/dhctoken.h 2012-03-06 15:54:06.000000000 +0100
@@ -362,6 +362,9 @@ enum dhcp_token {
        REWIND = 663,
        INITIAL_DELAY = 664,
        GETHOSTBYNAME = 665
+       /* Private usage. Prepend COMMA to keep patches context independant */
+     ,  AVOID_REUSE = 1000
+     ,  FORCE_NACK = 1001
 };
 
 #define is_identifier(x)       ((x) >= FIRST_TOKEN &&  \

Als nächstes sind diese Werte pro Pool aus der Konfiguration zu parsen:

diff -pbBru ../ORIGINAL/server/stables.c server/stables.c
--- ../ORIGINAL/server/stables.c        2011-05-20 16:21:11.000000000 +0200
+++ server/stables.c    2012-03-06 15:27:42.000000000 +0100
@@ -266,6 +266,8 @@ static struct option server_options[] = 
        { "ldap-tls-randfile", "t",             &server_universe,  77, 1 },
 #endif /* LDAP_USE_SSL */
 #endif /* LDAP_CONFIGURATION */
+       { "avoid-reuse", "f",                   &server_universe,  SV_AVOID_REUSE, 1 },
+       { "force-nack", "BB",                   &server_universe,  SV_FORCE_NACK, 1 },
        { NULL, NULL, NULL, 0, 0 }
 };
diff -pbBru ../ORIGINAL/common/conflex.c common/conflex.c
--- ../ORIGINAL/common/conflex.c        2011-05-11 16:20:59.000000000 +0200
+++ ./common/conflex.c  2012-03-06 15:56:22.000000000 +0100
@@ -782,6 +782,8 @@ intern(char *atom, enum dhcp_token dfv) 
                                return AUTO_PARTNER_DOWN;
                        break;
                }
+               if (!strcasecmp(atom + 1, "void-reuse"))
+                       return AVOID_REUSE;
                break;
              case 'b':
                if (!strcasecmp (atom + 1, "ackup"))
@@ -986,6 +988,8 @@ intern(char *atom, enum dhcp_token dfv) 
                        return FIXED_PREFIX6;
                if (!strcasecmp (atom + 1, "ddi"))
                        return TOKEN_FDDI;
+               if (!strcasecmp(atom + 1, "orce-nack"))
+                       return FORCE_NACK;
                if (!strcasecmp (atom + 1, "ormerr"))
                        return NS_FORMERR;
                if (!strcasecmp (atom + 1, "unction"))

Und dann braucht es noch Funktionalität. Dazu werden Hooks in die betreffenden Funktionen eingepaßt. Diese Hooks ermitteln, ob die betreffende Funktionalität jetzt im Moment für diese Anfrage aktiviert werden soll oder nicht.

diff -pbBru ../ORIGINAL/server/dhcp.c server/dhcp.c
--- ../ORIGINAL/server/dhcp.c   2011-07-20 00:22:49.000000000 +0200
+++ server/dhcp.c       2012-03-16 11:33:11.000000000 +0100
@@ -40,6 +40,8 @@
 static void commit_leases_ackout(void *foo);
 static void maybe_return_agent_options(struct packet *packet,
                                       struct option_state *options);
+static int avoid_reuse(struct packet *packet, struct lease * lease);
+static int force_nack(struct packet *packet, struct lease * lease);
 
 int outstanding_pings; 

Die Funktion von avoid-reuse besteht darin, jede zuvor gefundenen Lease während der DISCOVER Verarbeitung zu verwerfen. Deswegen steht sie am Ende aller Lease-Ermittlungen. Diese Platzierung gestattet es, alle anderen Funktionen unverändert zu belassen.

diff -pbBru ../ORIGINAL/server/dhcp.c server/dhcp.c
--- ../ORIGINAL/server/dhcp.c 2011-07-20 00:22:49.000000000 +0200
+++ server/dhcp.c 2012-03-16 11:33:11.000000000 +0100
@@ -341,6 +343,18 @@ void dhcpdiscover (packet, ms_nulltp)
                }
        }
 #endif
+       /*
+        * Special handling to insist on new IPs whenever possible
+        */
+       if (avoid_reuse(packet, lease)) {
+          log_info ("Avoid reuse of old lease %s", piaddr (lease -> ip_addr));
+          if(lease -> ends > cur_time)
+            dissociate_lease (lease);   /* Free lease to enable reuse. */
+          lease_dereference (&lease, MDL);
+          if(lease)
+            log_error ("Lease %s can't be avoided, it's still referenced.",
+                       piaddr (lease -> ip_addr));
+       }
 
        /* If we didn't find a lease, try to allocate one... */
        if (!lease) {

Die Funktion von force-nack dagegen besteht darin, die Verarbeitung des REQUEST zu unterbinden.

diff -pbBru ../ORIGINAL/server/dhcp.c server/dhcp.c
--- ../ORIGINAL/server/dhcp.c   2011-07-20 00:22:49.000000000 +0200
+++ server/dhcp.c       2012-03-16 11:33:11.000000000 +0100
@@ -671,6 +685,12 @@ void dhcprequest (packet, ms_nulltp, ip_
                goto out;
        }
 
+       if (force_nack (packet, lease)) {
+               log_info ("%s: force disconnect.", msgbuf, piaddr (cip));
+               nak_lease (packet, &cip);
+               goto out;
+       }
+       
        /* Otherwise, send the lease to the client if we found one. */
        if (lease) {
                ack_lease (packet, lease, DHCPACK, 0, msgbuf, ms_nulltp,

Bleibt also die Hooks auch zu aktivieren. Unglücklicherweise ist das Optionshandling nicht trivial. Es gilt nicht nur die Konfiguration zum jetzigen Zeitpunkt, sondern auch die Konfiguration, die bei der letzten Verlängerung der Lease galt. Dieses ist Suche habe ich in eine separate Funktion get_lease_state ausgelagert.

Mit dieser Hilfsfunktion ist es wesentlich leichter, die Funktionalität aufzubauen.

diff -pbBru ../ORIGINAL/server/dhcp.c server/dhcp.c
--- ../ORIGINAL/server/dhcp.c   2011-07-20 00:22:49.000000000 +0200
+++ server/dhcp.c       2012-03-16 11:33:11.000000000 +0100
@@ -4472,3 +4492,129 @@ maybe_return_agent_options(struct packet
                        options->universe_count = agent_universe.index + 1;
        }
 }
+
+/*
+ * 
+ * 
+ * 
+ */
+static int get_lease_state(struct lease_state * state,
+                          struct packet * packet, struct lease * lease) {
+   int i;
+
+   state -> got_requested_address = packet -> got_requested_address;
+   shared_network_reference (&state -> shared_network,
+                            packet -> interface -> shared_network, MDL);
+   
+   /* See if we got a server identifier option. */
+   if (lookup_option (&dhcp_universe,
+                     packet -> options, DHO_DHCP_SERVER_IDENTIFIER))
+     state -> got_server_identifier = 1;
+   
+   maybe_return_agent_options(packet, state->options);
+   
+   /* Execute statements in scope starting with the subnet scope. */
+   execute_statements_in_scope ((struct binding_value **)0,
+                               packet, lease, (struct client_state *)0,
+                               packet -> options,
+                               state -> options, &lease -> scope,
+                               lease -> subnet -> group,
+                               (struct group *)0);
+   
+   /* If the lease is from a pool, run the pool scope. */
+   if (lease -> pool)
+     (execute_statements_in_scope
+      ((struct binding_value **)0, packet, lease,
+       (struct client_state *)0, packet -> options,
+       state -> options, &lease -> scope, lease -> pool -> group,
+       lease -> pool -> shared_network -> group));
+   
+   /* Execute statements from class scopes. */
+   for (i = packet -> class_count; i > 0; i--) {
+      execute_statements_in_scope
+       ((struct binding_value **)0,
+        packet, lease, (struct client_state *)0,
+        packet -> options, state -> options,
+        &lease -> scope, packet -> classes [i - 1] -> group,
+        (lease -> pool
+         ? lease -> pool -> group
+         : lease -> subnet -> group));
+   }
+}
+
+static int avoid_reuse(struct packet *packet, struct lease * lease) {
+   int avoid_this_lease = 0;
+   struct option_cache * oc;
+   struct lease_state * state;
+   int ignorep;
+
+   /* Shortcut: Nothing to do. */
+   if (!packet || !lease || (lease -> flags & STATIC_LEASE))
+     return avoid_this_lease;
+   
+   state = new_lease_state (MDL);
+   if (!state)
+     return avoid_this_lease;         /* silently ignore the error */
+   else
+     get_lease_state(state, packet, lease);
+   
+   oc = lookup_option(&server_universe, state -> options, SV_AVOID_REUSE);
+   if (oc &&
+       evaluate_boolean_option_cache(&ignorep, packet, lease,
+                                    (struct client_state *)0,
+                                    packet -> options, state -> options,
+                                    &lease -> scope, oc, MDL)) {
+      /* 120 seconds is the typical hold time for temporary allocations */
+      if(cur_time - 120 > lease -> starts)
+       avoid_this_lease = 1;          /* do not OFFER an "old" lease */
+   }
+
+   free_lease_state (state, MDL);
+   
+   return avoid_this_lease;
+}
+   
+
+static int force_nack(struct packet *packet, struct lease * lease) {
+   int force_nack = 0;
+   struct option_cache * oc;
+   struct lease_state * state;
+   struct data_string data;
+
+   /* Shortcut: Nothing to do. */
+   if (!packet || !lease || (lease -> flags & STATIC_LEASE))
+     return force_nack;
+   
+   state = new_lease_state (MDL);
+   if (!state)
+     return force_nack;               /* silently ignore the error */
+   else
+     get_lease_state(state, packet, lease);
+   
+   memset(&data, 0, sizeof(data));
+   oc = lookup_option(&server_universe, state -> options, SV_FORCE_NACK);
+   if (oc &&
+       evaluate_option_cache(&data, packet, lease,
+                            (struct client_state *)0,
+                            packet -> options, state -> options,
+                            &lease -> scope, oc, MDL)) {
+      struct tm disconnect_tm;
+      TIME disconnect;
+      
+      if(localtime_r(&cur_time, &disconnect_tm)) {
+        disconnect_tm.tm_sec  = 0;
+        disconnect_tm.tm_min  = data.data[1];
+        disconnect_tm.tm_hour = data.data[0];   
+        disconnect = mktime(&disconnect_tm);
+        if(disconnect <= cur_time &&
+           disconnect >  lease -> starts) {
+           force_nack = 1;            /* NAK the lease if it spans the disconnect time */
+        }
+      }
+      data_string_forget (&data, MDL);
+   }
+
+   free_lease_state (state, MDL);
+   
+   return force_nack;
+}

Konfiguration

Wie sieht das nun aus, wenn man diese Optionen auch benutzen will?

# short lease times to allow quick changes
shared-network Internet_Vlan {
        default-lease-time 3000;
        max-lease-time 3600;

        # static and other subnets as usual

        subnet x.y.z.0 netmask 255.255.255.0 {
                option subnet-mask 255.255.255.0;
                option routers x.y.z.1;
                option domain-name-servers a.b.c.d;
                pool {
                        allow members of "dynamic";
                        range x.y.z.10 x.y.z.254;
                        avoid-reuse on;
                        force-nack 3 0;
                }
        }

        subnet 100.64.0.0 netmask 255.255.128.0 {
                option subnet-mask 255.255.128.0;
                option routers 100.64.0.1;
                option domain-name-servers 100.64.0.4, 100.64.0.5;
                pool {
                        allow members of "CGN";
                        range 100.64.0.10 100.64.127.254;
                        avoid-reuse on;
                }
        }
}

Es gibt mehrere statische Bereiche, die wie gewohnt konfiguriert werden.

Es gibt darüberhinaus mehrere dynamische Bereiche (einer ist hier dargestellt), bei denen nachts um 3:00 eine vertraglich vereinbarte Zwangstrennung erfolgt und die IP Adressen gewechselt werden.

Darüberhinaus gibt es mehrere dynamische Carrier Grade NAT Bereiche (einer ist hier dargestellt), bei denen keine Zwangstrennung, wohl aber ein Adreßwechsel erfolgt.

Der ganze Kram ist Failover fähig und läuft hier problemlos in einem DHCP-Cluster.

Es ist nun anderthalb Jahre her, seit ich nach Failover per VLAN gesucht habe. Die entscheidende Frage war aber immer, ob es auch tatsächlich funktioniert. Ein entsprechender Test wurde damals nicht dokumentiert. Also soll es nun mit einem neuen Kernel nachgeholt werden.

Ausgangslage

Ein Server an zwei Switchen. Einige VLANs liegen auf beiden Switchen an, andere sind nur pro Switch vorhanden.

nat-server-an-zwei-switchen

Die Aufgabe besteht darin, die übergreifend vorhandenen VLANs nicht zu verlieren, auch wenn ein Switch ausfällt. Ein typisches übergreifenden VLAN ist das Management-Netz. Naturgemäß hat jeder Server eine IP in dem Netz und sollte diese Verbindung niemals verlieren.

Nicht übergreifende VLANs sind i.d.R. lokale Versorgungsnetze, die am anderen Standort keine Bedeutung haben.

Wie man das macht, ist in dem alten Artikel beschrieben.

Test

Neuer Kernel, neues Glück. Diesmal mit Doku:

$ uname -a
... FreeBSD 10.3-PRERELEASE #1 r294196M ...

Und gleich das Einrichten der VLANs

# ifconfig vlan3140 create
# ifconfig vlan3140 vlan 140 vlandev igb0 up
# ifconfig vlan4140 create
# ifconfig vlan4140 vlan 140 vlandev igb1 up
# ifconfig lagg140 create laggport vlan3140 laggport vlan4140
# ifconfig lagg140 up

Das schaut dann so aus:

# ifconfig | egrep 'igb0|140|status' | uniq
igb0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
vlan3140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
        vlan: 140 parent interface: igb0
vlan4140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
        vlan: 140 parent interface: igb1
lagg140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
        laggport: vlan4140 flags=0<>
        laggport: vlan3140 flags=5<MASTER,ACTIVE>

Also schalten wir einen Port mal aus.

# ifconfig | egrep 'igb0|140|status' | uniq
igb0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: no carrier
vlan3140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: no carrier
        vlan: 140 parent interface: igb0
vlan4140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
        vlan: 140 parent interface: igb1
lagg140: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        status: active
        laggport: vlan4140 flags=4<ACTIVE>
        laggport: vlan3140 flags=1<MASTER>

Ein tcpdump am lagg-Interface zeigt während der Umschaltung keinerlei Zuckungen. Er läuft einfach weiter.

Problem gelöst?

Es fehlt noch der Test mit dem Aussenden von Daten. Schließlich könnte sich ja spontan die MAC des Interfaces ändern.

16 bytes from fe80::204:23ff:...%lagg140, icmp_seq=26 hlim=64 time=0.240 ms
16 bytes from fe80::204:23ff:...%lagg140, icmp_seq=27 hlim=64 time=0.469 ms
16 bytes from fe80::204:23ff:...%lagg140, icmp_seq=28 hlim=64 time=0.216 ms
[... Abschalten des Ports ...]

Timeout. Der Datenfluss kommt zum Stillstand. Erst nach einigen langen Minuten geht es weiter.

Ein erneuter Test zeigt dieses Verhalten nicht mehr. Nun gehen nur noch einige Pakte des Pings verloren.

Die Umschaltzeit wird maßgeblich von der Geschwindigkeit bestimmt, mit der die Switch-Wolke die gewanderte MAC Adresse bemerken.

y 41:37.153: %LINK-5-CHANGED: Interface Gi5/41, changed state to administratively down
x 41:46.132: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:... in vlan 140 is moving from port Po32 to port Gi5/25
y 47:38.833: %LINEPROTO-5-UPDOWN: Line protocol on Interface Gi5/41, changed state to up
y 47:53.842: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:... in vlan 140 is moving from port Po32 to port Gi5/41
y 48:13.370: %LINK-5-CHANGED: Interface Gi5/41, changed state to administratively down
x 48:22.726: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:...in vlan 140 is moving from port Po32 to port Gi5/25
y 48:47.879: %LINEPROTO-5-UPDOWN: Line protocol on Interface Gi5/41, changed state to up
x 49:17.275: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:... in vlan 140 is moving from port Gi5/25 to port Po32
y 49:49.925: %LINK-5-CHANGED: Interface Gi5/41, changed state to administratively down
x 49:59.624: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:... in vlan 140 is moving from port Po32 to port Gi5/25
y 50:16.845: %LINEPROTO-5-UPDOWN: Line protocol on Interface Gi5/41, changed state to up
x 50:45.969: %C4K_EBM-4-HOSTFLAPPING: 00:1E:67:... in vlan 140 is moving from port Gi5/25 to port Po32

Man sieht schön, wie die erste Umschaltung etwas länger gebraucht hat und dass da ein zweiter Switch mit im Spiel ist.

Nachdem sich aber die Switch-Wolke an die neue MAC gewöhnt hatte, klappt die Umschaltung zügig.

Die typische Umschaltzeit liegt im Bereich einiger zig Sekunden. Das ist sehr zufrieden stellend.

Wie schon angekündigt, gibt es ein zweites Problem, das mit ARP in Zusammenhang steht. Hier stellt nicht das DSL-Netz das Problem dar, sondern eine Reihe von Carrier-Grade-NAT-Servern, die einen Großteil der Kunden mit öffentlicher Erreichbarkeit versorgen. Um die Last horizontal zu verteilen, d.h. einfach seitlich anbauen zu können, sollen sich alle Maschinen eine IP teilen.

Ausgangslage

Eine Gruppe von NAT-Servern soll sich eine IP-Adresse teilen. Die Clients sollen also alle das gleiche Default-Gateway benutzen und dann stabil mit definierten offiziellen IPs in die Welt kommunizieren.

Es ist notwendig, dass die Zuordnung zwischen Client und öffentlich sichtbarer IP weitestgehend konstant bleibt, denn nur so sind die folgenden Anforderungen an einen Internet-Zugang realisierbar:

  • Downloads und Streams laufen durch.
  • Sessions auf Webportalen bleiben nach initialer Anmeldung bestehen, man muss sich nicht während der Nutzung unvermittelt neu anmelden.
  • Längere Sitzungen (VPN, RDP oder SSH) laufen unterbrechungsfrei.
  • Verbindungen zum (IMAP)-Postfach bleiben bestehen.
  • Eingebundene Netzlaufwerke bleiben zur lokalen Arbeit verfügbar.
  • Online-Spiele laufen durch.

Für kürzere Aktionen, wie gelegentliches Webbrowsen oder Post-Abholen, sind die Anforderungen weit weniger streng. Dort reicht es, wenn für die Dauer der Verbindungen die öffentliche NAT Adresse stabil bleibt. Sie kann sogar pro Verbindung wechseln.

Beim Ausfall einer Maschine soll die Last auf die anderen Maschinen verteilt werden. Die vom Ausfall betroffenen Kunden sollen, höchstens eine kurze Unterbrechung bemerken.

Kanonische Umsetzung

Da sich alle Maschinen die gleiche IP teilen, muss die IP virtuell sein. Und da die ARP-Einträge in den Client ziemlich langlaufend sind, ist es notwendig für den Fall eines Ausfalls die MAC-Adresse umzuziehen. Es sind also auch noch virtuelle MAC-Adressen einzusetzen, die nach Bedarf zwischen den physischen Maschinen getauscht werden können.

Im einfachsten Fall gibt es zwei System: Ein aktives, dass die betreffende virtuelle IP und MAC Adresse bedient. Bei einem Ausfall übernimmt das passive Reserve-System die IP und die MAC. Das aktive System wird damit passiv. Dieses Szenario bietet aber keine Lastverteilung.

Um eine Lastverteilung hinzubekommen, müssen verschiedene Clients offenbar mit verschiedenen Servern reden. Da sie aber nur eine MAC Adresse per ARP lernen können und die Server sich nicht eine MAC Adresse teilen können (wer bekommt denn dann das betreffende Paket?), müssen zwangsweise mehrere virtuelle MAC Adressen zum Einsatz kommen. Jeder Client lernt dann eine andere virtuelle MAC Adresse und spricht dann ausschließlich mit der Maschine, die diese Adresse gerade bedient.

Bei einem Ausfall muss die MAC-Adresse des betroffenen Systems auf andere Server umziehen. Die Verteilung der anderen virtuellen MAC-Adressen ist von diesem Ausfall aber nicht betroffen. Deswegen ist es sinnvoll, pro MAC Adresse ein getrenntes Failover einzusetzen.

Aus der Cisco Welt sind diese Methoden bekannt:

  • HSRP stellt die Aktiv/Passiv-Umschaltung zwischen zwei Systemen bereit.
  • VRRP ist die nicht-proprietäre Alternative zu HSRP, die dieses auch auf mehrere Systeme ausweitete (was HSRP später übernahm).
  • GLBP ist ein Erweiterung von HSRP auf mehrere MAC-Adressen, die mehrere aktive Geräte – also Lastverteilung – ermöglicht.

Da alle diese Methoden patentbehaftet sind, hat die BSD Welt CARP entwickelt. Und dort sieht es sehr gut für mich aus:

The carp has limited abilities for load balancing the incoming connections between
hosts in Ethernet network.  For load balancing operation, one needs several CARP
interfaces that are configured to the same IP address, but to a different VHIDs.
Once an ARP request is received, the CARP protocol will use a hashing function
against the source IP address  in the ARP request to determine which VHID should
this request belong to.

Damit ist schon gleich verraten, wie es funktioniert:

  • Die IP Adresse des Clients entscheidet darüber, welche virtuelle MAC er zu sehen bekommt.
  • Da er sich dann immer mit dieser MAC verbindet, landet er immer auf dem gleichen Server.
  • Der Server kann nun sicher stellen, dass der Client für alle NAT-Verbindungen die gleiche öffentliche IP benutzt.
  • Er benötigt dafür nur lokale öffentliche IPs. Der NAT State muss nicht zwischen den Maschinen geteilt werden.

Dieses Konzept skaliert prima.

In Praxis sieht das dann so aus:

cloned_interfaces="${cloned_interfaces} carp0 carp1 carp2 carp3"
ifconfig_carp0="vhid 1 advskew 133 pass xxx 100.100.0.1/17"
ifconfig_carp1="vhid 2 advskew 166 pass yyy 100.100.0.1/17"
ifconfig_carp2="vhid 3 advskew 200 pass zzz 100.100.0.1/17"
ifconfig_carp3="vhid 4 advskew 100 pass aaa 100.100.0.1/17" 

Es gibt also vier CARP Interfaces, die sich die gleiche IP teilen. Der CARP Algorithmus sorgt dafür, dass jeder Client einer anderen VHID zugeordnet wird und damit stabil auf einem der Server landet. Es ist immer der Server aktiv, der den kleinsten advskew Wert vorweisen kann. Und die unterscheiden sich pro Maschine.

Kommt eine neue Maschine hinzu, kommt überall ein weiteres CARP Interface hinzu.

Und wir macht man das mit dem statischen NAT? Man nimmt wieder die Client IP zu Hilfe:

nat 1 config ip a.b.c.d same_ports
nat 2 config ip a.b.c.e same_ports
...
add 2001 nat   1 ipv4 from 100.64.0.0:255.192.0.63 to any
add 2002 nat   2 ipv4 from 100.64.0.1:255.192.0.63 to any
...

Das war's.

Updates

Mittlerweile sind die neueren Systeme auf 10.2-STABLE: Dort fehlt in der Man-Page der betreffende Absatz zu ARP Balance.

Die Architektur der CARP Implementierung hat sich geändert. CARP ist nun kein Pseudointerface mehr, sondern hängt direkt am physischen Interface.

Und das hat fatale Konsequenzen:

  • Es ist nicht mehr möglich, mehrere CARP Instanzen pro IP zu haben.
  • Der Code, der die virtuelle MAC anhand der Client IP klassifizierte, existiert nicht mehr.

In der Praxis sieht das dann so aus:

# ifconfig vlan500 inet 192.0.2.1/24
# ifconfig vlan500 | fgrep 192
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255

# ifconfig vlan500 inet 192.0.2.10/32 vhid 10 alias
# ifconfig vlan500 | fgrep 192
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 10

# ifconfig vlan500 inet 192.0.2.10/32 vhid 11 alias
# ifconfig vlan500 | fgrep 192
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 11

Es wurde also keine zweite CARP-Instanz zur gleichen IP hinzugefügt, sondern die Parameter der IP geändert.

Der Grund für dieses Verhalten liegt im Adressmanagement von Interfaces. Es ist nicht möglich an einem Interface zweimal die gleiche IP zu haben!

Beim alten Ansatz mit separaten Pseudointerfaces trat das Problem nicht auf: Jedes Interface hat seinen eigenen IP-Raum.

Mit verschiedenen IPs ist es allerdings kein Problem, mehrere CARP Instanzen am Interface zu halten.

# ifconfig vlan500 inet 192.0.2.10/32 vhid 10 alias
# ifconfig vlan500 inet 192.0.2.11/32 vhid 11 alias
# ifconfig vlan500 | fgrep 192
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 10
        inet 192.0.2.11 netmask 0xffffffff broadcast 192.0.2.11 vhid 11

Ein Blick in den Sourcecode offenbart, dass der neue CARP-Code jedes Instanz als alleinstehend betrachtet.

Das bedeutet, dass die folgende Fehlkonfiguration zu unerwartetem Verhalten führt:

# ifconfig vlan500 inet 192.0.2.10/32 vhid 11 alias
# ifconfig vlan500 | fgrep 192
        inet 192.0.2.1 netmask 0xffffff00 broadcast 192.0.2.255
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 11
        inet 192.0.2.11 netmask 0xffffffff broadcast 192.0.2.11 vhid 11

Der betreffende Server könnte dann mehrfach Announcements für die VHID 11 herausgeben. Diese Announcements könnten sich widersprechen und zu einem permanenten Flappen der Interfaces führen.

Tatsächlich passiert dies aber nicht, wie ein Blick in den Source und in die Ausgabe von ifconfig lehrt.

# ifconfig vlan500 | fgrep vhid
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 11
        inet 192.0.2.11 netmask 0xffffffff broadcast 192.0.2.11 vhid 11
        carp: MASTER vhid 11 advbase 1 advskew 100

# ifconfig vlan500 inet 192.0.2.10/32 vhid 10 advskew 10 alias
# ifconfig vlan500 | fgrep vhid
        inet 192.0.2.10 netmask 0xffffffff broadcast 192.0.2.10 vhid 10
        inet 192.0.2.11 netmask 0xffffffff broadcast 192.0.2.11 vhid 11
        carp: MASTER vhid 11 advbase 1 advskew 100
        carp: MASTER vhid 10 advbase 1 advskew 10

Es gibt also pro VHID eine CARP Instanz am Interface, das ein oder mehrere verschiedene IPs tragen kann.

Ratlos

Was nun? Der bisherige Ansatz ist nicht mehr möglich. Alte und neue System kooperieren nicht mehr, sie können nicht mehr im gleichen Netz betrieben werden.

Was ist eigentlich der korrekte Stand der Dinge?

  • Es gibt mehrere CARP-Instanzen, d.h. mehrere virtuelle MAC Adressen.
  • Der Automatismus der ARP-Verteilung anhand der Client-IP ist kaputt.

Wieder steht die Frage: Wie kann man korrekte ARP Antworten schicken?

Über DirectAccess hatte ich schon einiges erzählt, und natürlich auch ausprobiert. Was ich aber noch nie angesehen habe, war DirectAccess ohne IPv6 im inneren oder äußeren Netz, also total legacy. In der Schulung habe ich die Gelegenheit dazu und möchte die Ergebnisse nicht vorenthalten.

IPv4 only Lab

Nachdem der DirectAccess Client in die weite Welt verschwunden ist, und er voller Verzweiflung nach Hause telefoniert hat, bietet sich folgendes Bild.

C:\> ipconfig
Ethernet adapter Ethernet 2:
   Connection-specific DNS Suffix  . :
   Link-local IPv6 Address . . . . . : fe80::a19f:cc85:1320:8c4%5
   IPv4 Address. . . . . . . . . . . : 131.107.0.2
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
   Default Gateway . . . . . . . . . :
Tunnel adapter iphttpsinterface:
   Connection-specific DNS Suffix  . :
   IPv6 Address. . . . . . . . . . . : 2002:836b:c8:1000:1934:30a2:9e84:cdf4
   Temporary IPv6 Address. . . . . . : 2002:836b:c8:1000:f037:3b8d:cabc:bfcb
   Link-local IPv6 Address . . . . . : fe80::1934:30a2:9e84:cdf4%24
   Default Gateway . . . . . . . . . :

Es gibt also ein Interface im Internet mit öffentlichen Adressen ohne IPv6 Versorgung. Und dann gibt es den Tunnel nach Hause. Da sowohl Client als auch der DirectAccess Server öffentliche IPs haben, wurde der Tunnel mit 6to4-Adressen aufgebaut.

In den Schulungsunterlagen steht Notice the IP address for Tunnel Adapter is IPHTTPSInterface starting with 2002. This is an IP-HTTPS address. (Fehler sind so übernommen)

Nein, Microsoft! Es ist eine 6to4 Adresse. Offenbar habt Ihr wirklich keine praktische Erfahrungen mit IPv6.

Die zugehörige Routingtabelle zeigt:

c:\> route print -6
===========================================================================
Interface List
  5...00 15 5d 64 4d 4d ......Microsoft Hyper-V Network Adapter #2
  1...........................Software Loopback Interface 1
 24...00 00 00 00 00 00 00 e0 iphttpsinterface
===========================================================================
IPv6 Route Table
===========================================================================
Active Routes:
 If Metric Network Destination      Gateway
  1    331 ::1/128                  On-link
 24   4171 2002::/16                fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8::/48        fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8::/64        fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:1::/64      fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:5::/64      fe80::d92f:34d4:3add:e715
 24    331 2002:836b:c8:1000::/64   On-link
 24    331 2002:836b:c8:1000:1934:30a2:9e84:cdf4/128
                                    On-link
 24    331 2002:836b:c8:1000:f037:3b8d:cabc:bfcb/128
                                    On-link
 24    331 fd68:d6bf:56b6:7777::/96 fe80::d92f:34d4:3add:e715
  5    271 fe80::/64                On-link
 24    331 fe80::/64                On-link
 24    331 fe80::1934:30a2:9e84:cdf4/128
                                    On-link
  5    271 fe80::a19f:cc85:1320:8c4/128
                                    On-link
  1    331 ff00::/8                 On-link
  5    271 ff00::/8                 On-link
 24    331 ff00::/8                 On-link
===========================================================================
Persistent Routes:
  None

Das Routingziel ist eine Link-Local-Adresse, wie es sich für IPv6 gehört. Prima! Und die Adresse ist auch über den Tunnel erreichbar.

C:\>netsh int ipv6 sho nei 24
Internet Address                              Physical Address   Type
--------------------------------------------  -----------------  -----------
2002:836b:c8:1000:d92f:34d4:3add:e715                            Reachable (Router)
fe80::d92f:34d4:3add:e715                                        Reachable (Router)

Spielt man nun mit Browser und Explorer im Netz rum, gibt es offene Verbindungen:

C:\> netstat -n
Active Connections
  Proto  Local Address          Foreign Address        State
  TCP    131.107.0.2:49782      131.107.0.200:443      ESTABLISHED
  TCP    [2002:836b:c8:1000:f037:3b8d:cabc:bfcb]:49785  [fd68:d6bf:56b6:7777::ac10:c8]:80  ESTABLISHED
  TCP    [2002:836b:c8:1000:f037:3b8d:cabc:bfcb]:61893  [fd68:d6bf:56b6:7777::ac10:b]:80  ESTABLISHED

Die Verbindungen sind also offenbar über IPv6 zu einem ULA-Ziel (bäh!).

Das DirectAccess-Gateway macht noch NAT64, bettet also die IPv4 Adressen der LAN-Geräte ins IPv6 ein. Sieht man deutlich.

Dazu muss das Gateway offenbar auch DNS64 machen, um die DNS Antworten umzubiegen. Das sieht man leicht:

c:\>ipconfig /displaydns
Windows IP Configuration
    directaccess-webprobehost.adatum.com
    ----------------------------------------
    Record Name . . . . . : directaccess-WebProbeHost.Adatum.com
    Record Type . . . . . : 28
    Time To Live  . . . . : 213
    Data Length . . . . . : 16
    Section . . . . . . . : Answer
    AAAA Record . . . . . : fd68:d6bf:56b6:7777::ac10:c8

Aber warum macht der Client das? Der hat doch einen ganz anderen DNS Server?

C:\>netsh name show eff
DNS Effective Name Resolution Policy Table Settings

Settings for .Adatum.com
----------------------------------------------------------------------
DirectAccess (Certification Authority)  :
DirectAccess (IPsec)                    : disabled
DirectAccess (DNS Servers)              : 2002:836b:c8:3333::1
DirectAccess (Proxy Settings)           : Bypass Proxy

Settings for DirectAccess-NLS.Adatum.com
----------------------------------------------------------------------
DirectAccess (Certification Authority)  :
DirectAccess (IPsec)                    : disabled
DirectAccess (DNS Servers)              :
DirectAccess (Proxy Settings)           : Use default browser settings

Der Client hat also eine Policy für die Namensauflösung, die bei bestimmten Domains einen anderen DNS Server befragt. Genau, das Gateway.

C:\> ping lon-svr1.adatum.com
Pinging lon-svr1.adatum.com [fd68:d6bf:56b6:7777::ac10:b] with 32 bytes of data:
Reply from fd68:d6bf:56b6:7777::ac10:b: time=4ms
Reply from fd68:d6bf:56b6:7777::ac10:b: time=10ms
Reply from fd68:d6bf:56b6:7777::ac10:b: time=1ms
Ping statistics for fd68:d6bf:56b6:7777::ac10:b:
    Packets: Sent = 3, Received = 3, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 1ms, Maximum = 10ms, Average = 5ms

Allerdings funktioniert das nur, wenn diese Policy Regeln auch von der Applikation berücksichtigt werden:

C:\> nslookup lon-svr1.adatum.com
Server:  UnKnown
Address:  131.107.0.100

DNS request timed out.
    timeout was 2 seconds.
*** Request to UnKnown timed-out

Fragt man den richtigen Server, gibt es ganz andere Antworten:

C:\> nslookup lon-svr1.adatum.com 2002:836b:c8:3333::1
Server:  UnKnown
Address:  2002:836b:c8:3333::1

Non-authoritative answer:
Name:    lon-svr1.adatum.com
Addresses:  fd68:d6bf:56b6:7777::ac10:b
          172.16.0.11

All das ist mir all die Jahre verborgen geblieben, weil ich IPv6 ausgerollt hatte.

Ich vermisse nichts.

Was IPv6 ändert

Wird auf dem Außeninterface natives IPv6 eingesetzt, verschwinden sofort die 2002-er 6to4 Adressen. Dazu ist anzumerken:

  • Das im Lab die Kommunikation geklappt hatte, liegt daran, dass Microsoft das 2002:836b:c8:1000::/64 auf dem Interface betreibt.
  • Um mit 6to4 Adressen zu kommunizieren, muss man sich auf externe 6to4 Gateways verlassen. Dies kommt ins Spiel, weil der DNS Server 2002:836b:c8:3333::1 angesprochen wird.
  • Da beide Hosts über den IPv4-HTTPS-Tunnel IPv6 im externen Routing komplett umgehen, benötigen sie keine 6to4 Router im Internet.
  • Da beide Server extern öffentliche IPv4 Adressen haben, wird überhaupt 6to4 benutzt. Steht ein Gerät hinter NAT wird es auf Teredo zurück fallen.
  • Teredo funktioniert nicht mit allen NAT-Typen, speziell gibt es Probleme, wenn die öffentliche NAT-IP nicht stabil ist (z.B. bei Carrier Grade NAT oder Large Scale NAT)
  • Mit RFC 6540 ist IPv6 auf den Außeninterfaces Pflicht für die ISPs.

Wird IPv6 im LAN eingesetzt, verschwindet sofort das fc00::/7 ULA Netz. Stattdessen bekommen die Clients die gleiche IP, wie sie auch im LAN hätten. Das hat folgende Implikationen:

  • Der Client ist intern, wie extern unter der gleichen IP ansprechbar.
  • Die Fernwartung wird damit im Unternehmen unabhängig vom Aufenthaltsort des Clients.
  • Da Server und Clients IPv6 einsetzen, entfällt DNS64 und NAT64.
  • Applikationen können mit allen benötigten Ports Verbindungen in alle benötigten Richtungen ausführen. Es ist nicht notwendig im DirectAccess-Server NAT-Helper pro Protokoll zu haben.
  • Auf diese Weise funktionieren Protokolle wie Telefonie, Videokonferenzen, FTP, etc. pp. einfach auch extern.
  • Fremdapplikationen, die ihre eigene Namensauflösung fahren, funktionieren problemlos, weil sich nur das Routing zum DNS Server ändert. Es ist nicht länger notwendig, sich Domainabhängig an unterschiedliche DNS Server zu wenden.

Man sollte sich also wirklich überlegen, ob man weiter auf IPv6 verzichten will.

Manchmal überrascht einen eine Trivialität. Ich wollte das Routing einer Server-IP prüfen, aber der Trace wurde zu mir zurück reflektiert. Wie geht das?

Wunder

traceroute to 178.19.224.48 (178.19.224.48), 30 hops max, 40 byte packets
 1  switch1-9-4-1-vl52.net.iks-jena.de (217.17.197.11)  1.470 ms  1.372 ms  1.304 ms
 2  turm1-g001.net.iks-jena.de (217.17.197.49)  0.382 ms  0.438 ms  0.363 ms
 3  rudo8-t001-116.net.encoline.de (5.102.160.98)  0.613 ms  0.574 ms  0.566 ms
 4  switch3-v93.net.encoline.de (5.102.160.161)  1.465 ms  1.141 ms  1.170 ms
 5  server16-vlan58.net.encoline.de (5.102.160.132)  0.567 ms  0.538 ms  0.541 ms
 6  switch4-v94.net.encoline.de (5.102.160.178)  2.181 ms  1.469 ms  1.686 ms
 7  server16-vlan94.net.encoline.de (5.102.160.185)  0.641 ms  0.635 ms  0.575 ms
 8  switch2-v58.net.encoline.de (5.102.160.129)  9.887 ms  3.478 ms  1.380 ms
 9  rudo7-t000-123.net.encoline.de (5.102.160.90)  0.879 ms  0.740 ms  0.721 ms
10  turm1-g001-116.net.iks-jena.de (5.102.160.99)  0.991 ms  0.942 ms  1.012 ms
11  * * *
12  nat-178-19-224-48.net.encoline.de (178.19.224.48)  1.123 ms  1.052 ms  1.025 ms

Der Trace geht raus, kehrt um, kommt zurück und am Ende meint er das Ziel erreicht zu haben.

Dabei trifft er sogar bestimmte Router mehrfach. Das ist deswegen verwunderlich, weil doch die Router ihre Entscheidung allein anhand der Zieladresse fällen. Wie kann also das Paket in verschiedene Richtungen geroutet werden?

Zunächst erst einmal handelt es sich nicht um ein lokales Phänomen, sondern es funktioniert auch von anderen Quellen aus.

traceroute to 178.19.224.48 (178.19.224.48), 30 hops max, 60 byte packets
...
 4  inexio2.gw.network.manitu.net (89.238.127.62)  4.593 ms  4.594 ms  4.592 ms
 5  209-096-244-077.ip-addr.inexio.net (77.244.96.209)  7.633 ms  7.634 ms  7.642 ms
 6  DE-CIX1.de.lambdanet.net (80.81.193.74)  7.605 ms  7.358 ms  6.739 ms
 7  ae2.irt2.fra25.de.as13237.net (217.71.96.45)  7.881 ms  7.878 ms  7.584 ms
 8  ae5.irt1.han87.de.as13237.net (217.71.96.30)  13.407 ms  13.380 ms  13.390 ms
 9  ae7.irt1.ber02.de.as13237.net (217.71.96.133)  16.610 ms  16.613 ms  16.607 ms
10  bb-erf-02-loc.netz.netkom-line.net (109.73.31.198)  213.352 ms  213.364 ms  213.361 ms
11  tnk-ilm-001-loc.netz.netkom-line.net (109.73.31.195)  22.149 ms  20.946 ms  20.579 ms
12  109.73.31.141 (109.73.31.141)  147.983 ms  147.987 ms  147.974 ms
13  tnk-jen-001-loc.netz.netkom-line.net (109.73.31.193)  19.504 ms  19.453 ms  19.471 ms
14  tnk-jen-001-ipt.netz.netkom-line.net (109.73.31.194)  19.468 ms  19.468 ms  19.467 ms
15  rudo7-t010.net.encoline.de (5.102.160.105)  33.994 ms  34.003 ms  34.002 ms
16  switch2-v123.net.encoline.de (5.102.160.91)  34.350 ms  34.407 ms  34.573 ms
17  server16-vlan58.net.encoline.de (5.102.160.132)  33.875 ms  33.879 ms  33.866 ms
18  switch4-v94.net.encoline.de (5.102.160.178)  34.753 ms  35.605 ms  35.596 ms
19  server16-vlan94.net.encoline.de (5.102.160.185)  34.362 ms  34.259 ms  34.179 ms
20  switch2-v58.net.encoline.de (5.102.160.129)  34.800 ms  35.279 ms  34.905 ms
21  rudo7-t000-123.net.encoline.de (5.102.160.90)  34.440 ms  34.203 ms  34.452 ms
22  rudo5-t001-123.net.encoline.de (5.102.160.89)  34.228 ms  34.449 ms  34.435 ms
23  ar2.ber.de.colt.net (213.61.70.177)  55.706 ms  55.705 ms  55.674 ms
24  212.36.135.22 (212.36.135.22)  55.909 ms  56.438 ms  56.417 ms
25  212.36.135.22 (212.36.135.22)  55.398 ms  55.416 ms  55.389 ms
26  * * *
27  * * *
28  * * *
29  nat-178-19-224-48.net.encoline.de (178.19.224.48)  68.590 ms  68.544 ms  68.691 ms

Wow. Hin- und Rückrouting in einem Trace! Man sieht sehr schön den asymmetrischen Rouingweg.

Aber was passiert da?

Schnüffeln

Die Ursache sollte ausschließlich am angesprochenen Server zu suchen sein. Alles andere wäre eine grobe Überraschung, denn dann müßten die Geräte im Transportweg irgendeine Manipulation vorgenommen haben.

Also ein Mitschnitt auf den Außeninterfaces des betroffenen Servers aktiviert:

13:39:51.074580 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 13, length 20
13:39:51.074601 IP 5.102.160.132 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.077408 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 14, length 20
13:39:51.077414 IP 5.102.160.132 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.077939 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 15, length 20
13:39:51.077944 IP 5.102.160.132 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.078590 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 16, length 20
13:39:51.079237 IP 5.102.160.178 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.082388 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 17, length 20
13:39:51.084253 IP 5.102.160.178 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.084855 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 18, length 20
13:39:51.088146 IP 5.102.160.178 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.088802 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 19, length 20
13:39:51.088848 IP 5.102.160.185 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.092134 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 20, length 20
13:39:51.092176 IP 5.102.160.185 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.092892 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 21, length 20
13:39:51.092934 IP 5.102.160.185 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.093756 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 22, length 20
13:39:51.093774 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 22, length 20
13:39:51.097201 IP 5.102.160.129 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.100803 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 23, length 20
13:39:51.100823 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 23, length 20
13:39:51.101499 IP 5.102.160.129 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.102261 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 24, length 20
13:39:51.102280 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 24, length 20
13:39:51.102924 IP 5.102.160.129 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.103742 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 25, length 20
13:39:51.103763 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 25, length 20
13:39:51.103870 IP 5.102.160.90 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.106558 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 26, length 20
13:39:51.106577 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 26, length 20
13:39:51.106687 IP 5.102.160.90 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.107389 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 27, length 20
13:39:51.107408 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 27, length 20
13:39:51.107510 IP 5.102.160.90 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.108284 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 28, length 20
13:39:51.108302 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 28, length 20
13:39:51.108652 IP 5.102.160.99 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.111847 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 29, length 20
13:39:51.111866 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 29, length 20
13:39:51.112220 IP 5.102.160.99 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.113013 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 30, length 20
13:39:51.113032 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 30, length 20
13:39:51.113386 IP 5.102.160.99 > 217.17.192.34: ICMP time exceeded in-transit, length 36
13:39:51.114062 IP 217.17.192.34 > 178.19.224.48: ICMP echo request, id 60099, seq 31, length 20
13:39:51.114080 IP 178.19.224.48 > 217.17.192.34: ICMP echo request, id 60099, seq 31, length 20
13:39:51.115606 IP 217.17.197.43 > 217.17.192.34: ICMP time exceeded in-transit, length 36

Man sieht sehr schön, wie anfangs die Echo-Pakete mit einem Timeout beantwortet werden.

Kurz darauf (ab Sequenznummer 22) tauchen plötzlich Echo-Pakete auf, bei denen Quell- und Ziel-Adresse vertauscht sind. Diese entstehen offenbar direkt nach dem Eingang des ersten Paketes.

Nun dient die Kiste ein (Carrier Grade)-NAT. Ihr Zweck ist es also Pakete umzuschreiben und zwar so, daß möglichst wenig Pakete verloren gehen. Schließlich dient NAT ja dazu, Verbindungen zu ermöglichen! Eine Firewall oder Paketfilter würde Pakete verwerfen. NAT dagegen muß so gut wie möglich raten, wo das Paket eigentlich hin soll.

In diesem Fall geht das NAT systematisch vor:

  • Es nattet die fremde Quell-Adresse auf die eigene öffentliche IP.
  • Und es nattet die eigene öffentliche Ziel-IP zu – tja wohin, achja, da ist ja ein ganz frischer Eintrag in der NAT Tabelle …

Fertig ist der Spiegel.

Gefährlich?

Kann man das mit allen Protokollen machen? Wenn ja, könnte man trivial unter falscher Flagge segeln, oder?

Tracing the path to 178.19.224.48 on TCP port 80, 30 hops max
 1  switch1-9-4-1-vl4.net.iks-jena.de (217.17.192.125)  6.742 ms  2.125 ms  1.139 ms
 2  turm2-g001-5.net.iks-jena.de (217.17.197.50)  0.228 ms
    turm1-g001.net.iks-jena.de (217.17.197.49)  0.350 ms
    turm2-g000-3.net.iks-jena.de (217.17.197.34)  0.265 ms
 3  rudo8-t001-116.net.encoline.de (5.102.160.98)  0.449 ms  0.468 ms  0.423 ms
 4  switch3-v93.net.encoline.de (5.102.160.161)  1.414 ms  1.570 ms  2.359 ms
 5  server16-vlan58.net.encoline.de (5.102.160.132)  0.489 ms  0.431 ms  0.418 ms
 6  switch4-v94.net.encoline.de (5.102.160.178)  1.443 ms  1.166 ms  1.921 ms
 7  server16-vlan94.net.encoline.de (5.102.160.185)  0.429 ms  0.473 ms  0.444 ms
 8  switch4-v94.net.encoline.de (5.102.160.178)  14.355 ms  2.695 ms  1.462 ms
 9  server16-vlan94.net.encoline.de (5.102.160.185)  0.473 ms  0.495 ms  0.465 ms
...

Nein, man kann nicht. Und das ist gut so™.