Advanced search


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 …

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.

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™.