Advanced search


Nun bin ich schuld(tm). Ich habe fremde Systeme angegriffen. Dabei habe ich einfach nur meine Arbeit getan.

Stellt man einem DNS Server eine Anfrage, so beantwortet er diese. Das ist sein Job. Und DNS ist ein Dienst, der manchmal unter sehr hoher Last steht. Die Serversoftware ist also darauf optimiert, möglichst viele Anfragen beantworten zu können. DNS Anfragen und Antworten werden per UDP ausgetauscht, einem statuslosen Protokoll. Auf ein einzelenes Anfragpaket hin gibt es ein einzelnes Antwortpaket und dann ist für beide Seiten die Sache vergessen. Das geht rasend schnell.

Allerdings kann niemand prüfen, ob die Anfragen auch vom richtigen System kamen. Das einkommende Paket wird schlicht beantwortet und die Antwort geht an die (vorgebliche) Absender-IP-Adresse. Und die kann man fälschen (spoofen). Damit gehen die Antworten an eine anderes System.

Normalerweise ist eine solche Adressfälschung ohne größere Probleme, weil der Angreifer genauso viele Pakete aussenden muß, wie auch ankommen. Problematisch wird es, wenn die Antwort deutlich größer ist als die Anfrage. Dann gewinnt der Angreifer Bandbreite. Mit DNSSEC sieht das Zahlenverhältnis ungefähr so aus: Aus einer 43 Byte großen Anfrage enstand eine 2796 Byte große Antwort. Eine Versechzigfachung!

IP 217.17.207.133.53378 > 217.17.192.66.53:  51078+ [1au] ANY? donnerhacke.de. (43)
IP 217.17.192.66.53 > 217.17.207.133.53378:  51078* 14/0/11 NS avalon.iks-jena.de.,
 NS broceliande.iks-jena.de., MX mailv4.donnerhacke.de. 6, MX mailv6.donnerhacke.de. 4,
 SOA, Type46, Type46, Type46, Type47, Type46, Type46, Type46, Type48[|domain] (2796)

Der Effekt tritt auch bei anderen Diensten auf und ist wohlbekannt. Allerdings gibt es bisher keine gute Lösung. Jeder Ansatz zerstört das DNS irgendwie. Trotzdem mußte jetzt schnell eine Lösung her, denn es liegt eine Beschwerde aus Schweden vor, der Server hier würde die Schulen dort als Teil eine dDoS-Netzwerkes angreifen. Die paar Mbps waren mir gar nicht aufgefallen.

Zuerst habe ich mir die gerade aktuelle Version von BIND gezogen, und ganz normal in Betrieb genommen. Es gibt selbstverständlich erstmal keine Änderung am Verhalten. Des kommen weiterhin Tausende von "ANY"-Anfragen über UDP an die gehosten Zonen.

Glücklicherweise ist der Sourcecode an dieser Stelle für einen Quick-Hack ganz übersichtlich:

--- bin/named/query.c.ORIG      2012-09-10 18:34:44.000000000 +0200
+++ bin/named/query.c   2012-09-10 17:43:23.000000000 +0200
@@ -7326,6 +7326,10 @@
        if (dns_rdatatype_ismeta(qtype)) {
                switch (qtype) {
                case dns_rdatatype_any:
+                       if ((client->attributes & NS_CLIENTATTR_TCP) == 0) {
+                               query_error(client, DNS_R_REFUSED, __LINE__);
+                               return;
+                       }
                        break; /* Let query_find handle it. */
                case dns_rdatatype_ixfr:
                case dns_rdatatype_axfr:

Wenn also jemand nach "ANY" fragt und die Anfrage per UDP reinkommt, dann wird die Anfrage schlicht abgelehnt. Damit ist die Antwort kleiner als die Anfrage und der Angriff lohnt sich nicht mehr. Bis das aber die Angreifer merken, kann es dauern. Es stört nur etwas, daß überhaupt Antwortpakete rausgehen. Zum kommentarlosen Wegwerfen kann ich mich aktuell nicht überwinden.

Andererseits ist schon bekannt, welche Systeme damit Probleme haben werden. Die Software qmail stellt standardmäßig "ANY" Anfragen, um die Mailsserver zu bestimmen. Das wird nun nicht mehr klappen.

Eine korrekte Lösung kann nur sein, ein Dampening einzuführen. Anhand von Strafpunkten für jede Art von Anfrage und Antwort kann dann einfach das Antworten komplett eingestellt werden. Da Strafpunkte mit der Zeit verfallen, wird es dann automatisch irgendwann wieder gehen. Das wird eine spannende Aufgabe.

IPv6 bietet mit der dynamischen Adresszuteilung per SLAAC einen eleganten Weg, feste und temporäre IP Adressen an Hostsysteme zuzuweisen. Das hat Vor- und Nachteile. Gerade bei Umbauten im Netzwerk müssen spontan völlig unbeteiligte Systeme angefaßt werden. Aber wie minimiert man diese Aufwände?

Zunächst einmal verringert SLAAC, also StateLess Address AutoConfig, den Verwaltungsaufwand für den Netzwerker erheblich: Der Router verteilt ein Prefix, aus dem sich die Hosts im versorgten Netzwerk automatisch bedienen, solange das Prefix gültig ist. Soweit so einfach. Das ganze skaliert auch bestens, weil man mehrere Prefixe pro Router oder auch mehrere Router reinstellen kann und alles gleichzeitig funktioniert. Die Hosts lernen dann halt mehrere IP Adressen mit den dazugehörigen Routern. SLAAC ist damit der Mechanismus, mit dem man leicht und im laufenden Betrieb von einem Provider zum anderen wechseln kann, indem man in einer Übergangszeit beide gleichzeitig anschließt: Renumbering is easy.

Hosts leiten mit SLAAC mehrere IP Adressen aus dem Prefix ab: Zum einen eine statische IP, die i.d.R. aus der MAC Adresse oder einer anderen eindeutigen und dauerhaften Nummer gebildet wird, sowie einen Satz zufällig generierter IPs, die nur zeitweise genutzt werden (Privacy extensions).

Interessant für den Dauerbetrieb sind die festen IPs, die auf anderen Systemen hinterlegt werden: Im DNS und in Konfigurationdateien, Zugriffsbeschränkungen, Firewalls, Datenbanken etc. pp. Ändern sich diese Adressen ungeplant, ist der manuelle und zeitliche Aufwand bis zum reibungslosen Betrieb erheblich.

Linux und Window 2003 leiten die feste IPs von der MAC der jeweiligen Netzwerkkarte nach dem EUI-64 Schema ab. Ist zum Tausch von Technik gezwungen (z.B. durch Hardwareausfall), so bekommen diese Server neue IPs. Zur parallelen Inbetriebnahme eines neues Servers ist das ungemein nützlich: Die Technik wird einfach angesteckt und angeschaltet, schon ist das Gerät unter einer vorhersehbaren IP in einem beliebigen Netzsegment direkt erreichbar.

Erfolgt aber der Funktionsübergang von der alten zur neuen Hardware, ergibt sich ein Problem: Überall wo die alte Maschine mit ihrer IP bekannt war, muß die neue IP verbreitet werden. Eine Änderung im DNS scheint einfach, jedoch gibt es immer noch zuviel Software, die das Ergebnis einer DNS Auflösung nicht regelmäßig (TTL!) überprüfen. Firewalls wie iptables, Netzwerksoftware wie Sendmail und NTP, Datenbanken wie PostgreSQL müssen neu gestartet werden, um die Änderungen mitzubekommen – von den manuell eingetragenen IPs in verschiedenen Geräten und Scripten ganz zu schweigen. Wohl dem, der seine Konfigurationen zentral in Textform abgelegt hat: zgrep -Ri "alteip" /archiv/configs/ liefert zumindest Anhaltspunkte für die manuell anzufassenden Systeme. Aber auch zentrales Deployment und Konfigurationsmanagement generiert nach der Änderung der IP einen heftigen Schluckauf über die vielen betroffenen Systeme.

Um gegen Hardwaretausch besser gefeit zu sein, generiert Window 2008 die feste IP beim Einstellen in ein neues Netzwerk zufällig und speichert diese ab. Nun kann man am Server die Netzkarten auswechseln, ohne daß auf anderen Systemen etwas angefaßt werden muß: Der Hardwaretausch bleibt ein lokales Phänomen. Window 2008 macht die Netzwerkumgebung an der Adresse des versorgenden Routers fest: Ändert sich die default Route nicht, so behält Window auch für andere Prefixe den gleichen Suffix bei: Renumbering ist easy. Ändert sich aber die default Route, ändert Windows die festen IPs. Es genügt also, den Router zu tauschen, um spontanes Chaos für alle versorgten Windows Netze zu generieren. Zwar korrigieren sich die DNS Einträge von allein (wenn man das nicht verbietet), aber die anderen betroffenen Systeme sind ebenfalls überrascht.

Die Anforderungen an "feste IPv6 Adressen" sind also vielfältig und widersprüchlich:

  • Hardwaretausch soll nur lokale Aktionen erfordern.
  • Neue Hardware soll ohne großen Verwaltungsaufwand versorgt werden.
  • Neue Netze sollen ohne großen Verwaltungsaufwand einrichtbar sein.

Bei uns hat sich folgendes Schema als praktisch herausgestellt:

  • Router bekommen feste link-lokal Adressen: fe80::<gerät>:<interface>, wobei die Gerätenummer zentral in 0.8.e.f.ip6.arpa. als Wildcard verwaltet wird.
  • Da die Routingtabellen praktisch nur Link-Local Adressen enthalten (z.B. bei SLAAC und OSPF), entfällt die Notwendigkeit von offizellen IPv6 Adressen auf Transfernetzen und die Existenz der Reverse-DNS-Zone erweist sich als äußerst nützlich.
  • Bei den meisten Routern und Firewalls wird der Link-Local Teil auch für die offiziellen Adressen verwendet, womit ein konsistentes Erscheinungsbild beibehalten wird.
  • Für Server ist die Verwaltung in einer zentralen Geräteliste umständlich und unnötig. Da praktisch alle Systeme dual-stack laufen, existiert bereits eine IPv4 Adresse (öffentlich oder privat). Diese übernehmen wir in den lokalen Teil der IPv6 Adresse. Aus 192.168.240.12 wird 2001:db8:x:y:192:168:240:12. Die EUI-64 Beschränkungen für die höchstwertigen Bits existieren nicht, wenn der kennzeichnende "ff:fe" Teil fehlt.
  • Ein Windows System kann nur voll dynamisch oder voll statisch konfiguriert werden. Die IPv6 Adresse ergibt sich also aus der IPv4 und dem gültigen Netzprefix, während das default Gateway die schon bekannte Form fe80::<gerät>:<interface> hat.
  • Linux Systeme gestatten eine Mischkonfiguration. In /proc/sys/net/ipv6/conf/ kann man mit "echo 0 > autoconfig" die Adressgenerierung abschalten, während die Routen gelernt werden. Mit "echo 0 > accept_ra_defrtr" kann man das Lernen der default Route untersagen, aber die Adressbildung zulassen.

Beispielkonfig eines Linux Systems:

( cd /proc/sys/net/ipv6/conf
  # all future interfaces and all current interfaces (incl. the predefined ones)
  for i in default all eth*; do
    echo 0 > $i/accept_ra
    echo 0 > $i/forwarding
  done
)
... loading modules, creating bonding, vlans, etc. ...
( cd /proc/sys/net/ipv6/conf
  # automatic address in the managment network, but no routing
  echo 0 > mgmt/accept_ra_defrtr
  echo 1 > mgmt/accept_ra
  # fixed address in the public network, learn routers
  echo 0 > public/autoconf
  echo 1 > public/accept_ra
)
# ip -6 addr automatic dev mgmt
ip -6 addr add 2001:db8:42:23:203:0:113:131/64 dev public
# ip -6 route default via automatic dev public
ip -4 addr add 203.0.113.131/24 dev public

ip link set mgmt up
ip link set public up 
ip -4 route add default via 203.0.113.1

Zuerst natürlich die Defaults umstellen und dann die Interfaces gleichartig konfigurieren, anderenfalls handelt man sich eine schwer zu debuggende Racecondition ein. Unglücklicherweise überschreibt "all" nicht zwingend die Einstellung existierender Interfaces, deswegen werden diese extra benannt. Die Einrichtung neuer Interface im laufenden Betrieb generiert dank dieser Defaults keine Routingstörungen mehr.

Nachdem dann alle Interfaces existieren, werden nur die Ausgewählten mit IPv6 versorgt werden. Externe vorgegebene Angaben sind dabei nur auskommentiert angegeben. In dieser Konfiguration kann der Host in ein beliebiges Managment-Netz gestellt werden und hat dort immer eine durch die Hardware vorhersagbare Adresse aus dem jeweiligen Management-Netz. Die MAC Adresse der Hardware kann man im Notfall am Switchport ablesen, um sich einen Management-Zugriff zu verschaffen.

Die Routingtabelle schaut dann so aus:

2001:db8:42:23::/64 dev public  proto kernel  metric 256
2001:db8:42:f000::/64 dev mgmt  proto kernel  metric 256  expires 2592292sec
fe80::/64 dev public  proto kernel  metric 256
fe80::/64 dev mgmt  proto kernel  metric 256
ff00::/8 dev public  metric 256
ff00::/8 dev mgmt  metric 256
default via fe80::52:3167 dev public  proto kernel  metric 1024  expires 1647sec
unreachable default dev lo  proto kernel  metric -1  error -101 metric 10 255

Beispielkonfig eines Cisco Routers:

interface Loopback0
 ip address 192.0.2.56 255.255.255.255
 ipv6 address 2001:db8:42::56:0/128
!
interface FastEthernet1/0
 description Management IPv6 only
 ipv6 address FE80::56:1000 link-local
 ipv6 address 2001:db8:42:f000::56:1000/64
!
interface GigabitEthernet2/0.75
 description Core
 encapsulation dot1Q 75
 ip address 198.51.100.28 255.255.255.248
 ipv6 address FE80::56:2075 link-local
 ipv6 unnumbered Loopback0
 ipv6 ospf 1 area 0
!
router ospf 1
  network 198.51.100.24 255.255.255.248 area 0
!
ipv6 router ospf 1
!

Interessant ist am Cisco Beispiel der Verzicht auf Transfernetze im Core und die deutliche Vereinfachung der OSPF Konfiguration gegenüber der legacy Version.

Ein Kunde beschwert sich darüber, daß an einer Dose im Raum die Webseiten zwei Wochen älter wären als an der anderen Dose. Beide Dosen sind identisch beschaltet und liegen im gleichen VLAN. Was geht da vor?

Der erste Verdacht, jemand habe sich an den Patchungen auf der Etage zu schaffen gemacht, ist unzutreffend.

An den betroffenen Systemen springt mir ein nslookup mit Server 192.168.0.1 ins Auge. Diese IP gibt es in diesem Kundennetz nicht. Jedenfalls nicht von mir. Auf die Frage nach Fremdtechnik zeigt man mir einen selbst beschafften WLAN Router, den man an eine der beiden Dosen anschlossen hat. Warum auch nicht?

World_IPv6_launch_logo_128

Schon ahnend, was geschehen ist, bitte ich die Konfig des WLAN Routers so zu verändern, daß er die Finger von IP und DHCP läßt. Anschließend möge man das Kabel von der Wanddose an den Router doch in die blauen LAN Dosem stecken, nicht in die gelbe WAN Dose. Damit ist es eine WLAN Bridge, die sich nicht in Dinge einmischt, von denen sie nichts versteht (vor allem eben IPv6).

Die Webseite selbst zeigt tatsächlich je nach Anschluß (am LAN-Port des WLAN-Routers oder an der Wanddose) unterschiedliche Webseiteninhalte. Da kann man Rechner und Browser wechseln wie man will. Selbst Cache Löschen und Rebooten bringen keinerlei Effekt.

Ein DNS Lookup liefert zwei IPs: IPv4 und IPv6. Die Nagelprobe zeigt, was los ist:

$ telnet -4 www.***.de 80
Connected to ***.servers.jiffybox.net.
GET / HTTP/1.0
Host: www.***.de

 

HTTP/1.1 200 OK
Date: Fri, 14 Sep 2012 11:29:52 GMT
Server: Mongrel 1.1.5
Status: 200 OK
ETag: "8e...ff"
X-Runtime: 250ms
Content-Type: text/html; charset=utf-8
Content-Length: 924
Cache-Control: private, max-age=0, must-revalidate
Set-Cookie: _newapp_session=B...0; path=/; HttpOnly
Vary: Accept-Encoding
Connection: close
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    <head>

Und die andere IP:

$ telnet -6 www.***.de 80
Connected to ***.webpack.hosteurope.de.
GET / HTTP/1.0
Host: www.***.de
 
HTTP/1.1 200 OK
Date: Fri, 14 Sep 2012 11:23:03 GMT
Server: Apache
Last-Modified: Mon, 15 Jun 2009 07:31:26 GMT
ETag: "8f...80"
Accept-Ranges: bytes
Content-Length: 3711
Connection: close
Content-Type: text/html
 
<html>
  <head>

Da keiner der Beteiligen von IPv6 wußte, liegt der Verdacht nahe, daß im Zuge des World IPv6 Launch Days der AAAA Record hinzugekommen ist. Als man dann die Webpräsens auf einen anderen Server verschob, hat man nur den A Record geändert.

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

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

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

Sowie

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

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

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

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

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

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

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

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

Dokumentation ist das Stiefkind der IT. Aktuelle Doku is jedoch das Rückrat erfolgreicher Projekte. Wie hält man Dokumentation aktuell? In dem man sie freiwillig benutzt, weil sie einen Mehrwert abwirft. Netzplänen sollten also nicht nur das "Was ist" dokumentieren, sondern auch das "Was geschieht".

Admin kontra Managment

Typischerweise benutzt man zur Beantwortung der Frage, was denn da im Netz geschieht Trafficgraphen und für den groben Überblick eine Weathermap. Installation und Aufbereitung solcher Systeme ist ziemlich aufwendig. Wenn man sie schon hat, will man möglichst komplette Informationen an einer Stelle abrufen können.

Darüberhinaus benutzt man für die Projektdokumentation gern vereinfachende AdHoc Darstellungen, die schnell und auf Zuruf z.B. mit Visio erstellt werden. Diese verstauben dann in den Projektunterlagen, bis irgend ein Manager ein seit Monaten veraltetes Dokument in den Händen hält, um damit neue Planungen vorzunehmen und Entscheidungen zu fällen.

Obwohl Visio mächtig genug ist, die Dokumentation auch mit externen Datenquellen zu animieren, weigert sich der Netzwerkadmin, diesem Tool direkten Zugriffe auf die Netzwerkknoten zu erlauben. Außerdem müßte der Zugriff dann auch beim Kunden oder auf Präsentationen funktionieren.

Andererseits ist das Netzwerkmonitoring der Admins für den Manager zu komplex und zu mächtig. Endkunden will man gar nicht alle Details des realen Netzes zeigen. Schon gar nicht will man einen externen Zugriff des Endkunden auf die Managementsysteme selbst.

Einfach erstellt, einfach genutzt

Was liegt also näher, die Visio-Dokumente, die für den Endkunden erstellt wurden, auf eine Supportwebseite zu legen und dieser Webseite zu erlauben, die Graphik zu animieren? Zum einen sind die Dokumente nicht webtauglich und andererseits will man auf den Webseiten keine Office-Software aktiv laufen lassen.

Glücklicherweise kann Visio nach SVG exportieren. Und dieser Output ist hervorragend maschinell nachbearbeitbar. Mittlerweile ist SVG auch browserseitig ausreichend unterstützt, um dort bedenkenlos den Einsatz freizugeben. Die Anfangszeiten, bei denen extra Plugins benötigt wurden, sind glücklicherweise vorbei.

<g id="shape69-50" v:mID="69" v:groupContext="shape" v:layerMember="2"
     transform="translate(276.378,-523.305)">
 <title>Dynamic connector.69</title>
 <desc>G0/2</desc>
 <v:textBlock v:margins="rect(4,4,4,4)" v:tabSpace="42.5197"/>
 <v:textRect cx="21.292" cy="602.833" width="40" height="17.6036"/>
 <path d="M0 601.45 L336.41 603.27" class="st1" />
 <rect v:rectContext="textBkgnd" x="12.2454" y="598.034"
         width="17.3436" height="9.59985" class="st5"/>
 <text x="12.62" y="605.23" class="st6" v:langID="1031">
   <v:paragraph v:horizAlign="1"/>
   <v:tabList/>
   G0/2
  </text>
</g>

Um aus einem Visio-SVG eine Weathermap zu machen, müssen die manuell angelegten Verbinder eingefärbt werden. Je nach Auslastung der Leitung ändern sich die Farben von einem leichten Grün bis zu einem kräftigen Rot. (Hier mit einer anderen Linie.)

 <path d="M-6.07 595.28 L-8.11 806.72" class="st1" style='stroke:red'/>
netzplan-linie1

Da Leitungen in beide Richtungen Daten transportieren können, müssen die Verbinderlinien doppelt eingefärbt werden. Weathermaps lösen dies, in dem sie zwei Pfeile statt einer Verbindung einzeichnen und diese Pfeile separate einfärben. Schöner wäre es aber, wenn der Verbinder selbst seine Farbe ändern könnte. Glücklicherweise beherscht SVG Gradienten, um lineare Farbverläufe vorzugeben.

<linearGradient id="gruen-rot">
 <stop offset="0" style="stop-color:#00ff00;stop-opacity:1"/>
 <stop offset="1" style="stop-color:#ff0000;stop-opacity:1"/>
</linearGradient>
[...]
<path d="M-6.07 595.28 L-8.11 806.72" class="st1" style='stroke:url(#gruen-rot)'/>

Unglücklicherweise ist der Farbverlauf streng horizontal, so daß diese steile Linie den Farbwechsel nur in ihrer Breite durchmacht. Der Farbverlauf hätte eigentlich der Linie folgen sollen. Alle Versuchen mit Transformationen den Farbverlauf zu beeinflussen, waren vergeblich. Denn der Farbverlauf kann und muß separat gedreht werden.

<linearGradient id="gruen-rot" gradientTransform="rotate(90)">
 <stop offset="0" style="stop-color:#00ff00;stop-opacity:1"/>
 <stop offset="1" style="stop-color:#ff0000;stop-opacity:1"/>
</linearGradient>

Jetzt verlaufen die Farben wie gewünscht der Linie entlang.

Für jede Linie ist nun ein extra Gradient anzulegen. Dabei ist darauf zu achten, daß der Verlauf der Definition der Linie folgt, damit man später automatisiert die Leitungsauslastungen als Farbwerte an den Endstellen einsetzen kann. Die Zeichenrichtung im Visio hat wenig mit der Zeichenrichtung im SVG zu tun. Dies kann also komplexere Transformationen erfordern, denn eine Rotation um 180° dreht den Farbverlauf komplett aus dem Liniensegment heraus. Mit einer Matixoperation kann man den Verlauf gleich wieder an die richtige Stelle schieben.

Darüberhinaus empfiehlt es sich, kleine Ansätze an den Verbindern stehen zu lassen, um keinen so abrupten Farbwechsel am Gerät zu bekommen. Man setzt dazu seine Start- und Endwerte erst bei 10% vorm Ende. Da die Farbwerte später ersetzt werden sollen, muß bereits hier vermerkt werden, welches Interface an welchem Gerät zum Einsatz kommen soll.

<!-- device:interface max_bandwith -->
<linearGradient id="line13" gradientTransform="matrix(-1,0,0,-1,1,1)">
 <stop offset="0" style="stop-color:#000000;stop-opacity:1"/>
 <stop offset=".1" style="stop-color:#00ff00;stop-opacity:1"/>
 <stop offset=".9" style="stop-color:#ff0000;stop-opacity:1"/>
 <stop offset="1" style="stop-color:#000000;stop-opacity:1"/>
</linearGradient>

Es kann aber passieren, daß die Umfärbung völlig scheitert: Die Linie verschwindet ganz. Dies passiert, wenn eine Linie exakt horizontal oder vertikal liegt. Dann ist die Höhe oder Breite der Linie exakt Null und die Gradientenoperation bricht mit einer Division durch Null ab. In dem Fall muß man einfach einen Endpunkt der Linie um ein winziges Stück verschieben. (Wieder eine andere Linie.)

<path d="M0 588.19 L222.79 588.20" class="st1" style='stroke:url(#line3)'/>

Alles zusammen sollte sich dann ein Bild ergeben, bei dem klar ersichtlich ist, welche Meßstelle die Daten liefern wird. Diese wurde hier beschriftet und muß das grüne Ende des Verbinders erhalten. Damit sind die Voraussetzungen für eine maschinelle Überarbeitung geschaffen.

netzplan-animiert

Besonders hübsch ist, wie der Farbverlauf auch den gebogenen Pfaden folgt.

Diese manuellen Anpassungen des Visio-Exportes sind in einer guten halben Stunde zu erledigen. Mit etwas Übung geht dauert es nur einige Minuten. Komplexere Netzpläne oder ewig wiederkehrende Exports kann man automatisieren.

Die SVG Graphik kann nur auf einem beliebigen Webserver als statische Datei abgelegt werden (Mime-Type: image/svg+xml). Die URL kann zur Abnahme herumgeschickt werden.

Leben einhauchen

Zur Weathermap mutiert dieses Bild dann, indem es von einem Perl/PHP/Whatever-Script überarbeitet wird. Dieses holt sich bei Bedarf die notwendigen Daten aus einer Datenbank (MRTG, etc.) oder live von den Geräten. Der Farbwert wechselt gemäß rgb(255*load, 255*(1-load), 0) für die Hin- und Rückrichtung des Links.

Es spricht überhaupt nichts dagegen, weitere Angaben mit der Graphik zu verknüpfen. So kann das Symbol der aktiven Technik zwischen grau und rot umgefärbt werden, um die CPU-Last zu visualisieren. Ebenso bietet es sich an, die Speicherauslastung als "Füllhöhe" der Füllfarbe anzuzeigen. Wer noch Ideen hat, möge sie in die Kommentare schreiben oder einfach selbst umsetzen.

Die Implementation auf dem Webserver kann als PHP Datei die Daten, z.B. per SSH von einem zu SNMP berechtigten Host, holen und die SVG Datei vor der Ausliefung überarbeiten. Der SNMP-Host liefert, z.B. per command="/usr/local/sbin/obtain-line-rates.pl customer", die aktuellen Input- und Outputraten ausschließlich für die hinterlegten Geräte und Interfaces.

#! /usr/bin/perl

use strict;
use warnings;

my %points = ();

if($ARGV[0] eq 'customer') {
    %points = (
        'S1' => ['Port-channel32', 'Port-channel1', 'GigabitEthernet4/3', 'GigabitEthernet1/3', 'GigabitEthernet1/2'],
        'R1' => ['GigabitEthernet0/0', 'GigabitEthernet0/1', 'GigabitEthernet0/3'],
        'R2' => ['GigabitEthernet0/0', 'GigabitEthernet0/1', 'GigabitEthernet0/2'],
        'R3' => ['GigabitEthernet0/0', 'GigabitEthernet0/1', 'GigabitEthernet0/2'],
    );
} else {
    die "Usage: $0 name\n";
}

my %conf = ();
# Fill the config with the SNMP communities

# Read in the interface Name-ID mapping
my %ids = ();
foreach my $host (keys %conf) {
    open(S, "snmpbulkwalk -v2c -c '$conf{$host}' -t 2 -r 2 -L o: $host IF-MIB::ifDescr |") || next;
    while(<S>) {
        next unless my ($id,$val) = /\.([^\s.]+) = \S+: (.*\S)/;
        next unless grep {$_ eq $val} @{$points{$host}};
        $ids{$host}{$id} = $val;
    }
    close(S);
}

# Read the line rates and print them
while(my($host,$hh) = each %ids) {
    my @m = ((map {"1.3.6.1.4.1.9.2.2.1.1.6.$_"}  keys %$hh),
             (map {"1.3.6.1.4.1.9.2.2.1.1.8.$_"} keys %$hh));
    my $mc = $#m + 1;
    my $ms = join(" ", sort @m);

    my %c = ();

    open(S, "snmpget -v2c -c '$conf{$host}' -t 2 -r 2 -L o: $host $ms |") || next
    while(<S>) {
        next unless my ($dir,$id,$val) = /(\d+)\.([^\s.]+) = \S+: (.*\S)/;
        next unless defined $hh->{$id};
        $c{$hh->{$id}}{$dir} = $val;
    }
    close(S);

    while(my($name,$vals) = each %c) {
        print "$host:$name ".$vals->{6}," ".$vals->{8}."\n";
    }
}

Mein PHP Script zur Überarbeitung der Graphik ist dann ebenfalls ziemlich einfach:

<?
$unknown_color = '909090'; // Make links grey if no data is available
$svg_file = 'path/to/plan.svg';

// Read the current in/out bandwith values
$max_bandwidth = 1;
$max_bandwidth = max($max_bandwidth, $match[2], $match[3]);
$traf = popen("ssh -i path/to/sshkey user@snmp-host true", "r");
while($line = fgets($traf)) {
    if(preg_match('/^(\S+:\S+) (\d+) (\d+)$/', $line, $match)) {
        $inrate [$match[1]] = $match[2];
        $outrate[$match[1]] = $match[3];
        $max_bandwidth = max($max_bandwidth, $match[2], $match[3]);
    }
}
pclose($traf);

header("Content-Type: image/svg+xml");

$svg = fopen($svg_file, "r");
while($line = fgets($svg)) {
    if(preg_match('/<!-- (\S+:\S+) (\d+) -->/', $line, $match)) {
        $current_interface = $match[1];
        $current_speed = $match[2];
        if(is_numeric($_REQUEST['maxscale']))
          $current_speed *= $_REQUEST['maxscale'];
        elseif($_REQUEST['maxscale'] == 'max')
          $current_speed = min($max_bandwidth, $current_speed);
    }
    if(preg_match('/<linearGradient id="(\S+)"/', $line, $match)) {
        $current_gradient = $match[1];
    }
    if($current_speed > 0 &&
       preg_match('/^(\s+<stop offset=".(\d)" style="stop-color:#)\S{6}(;.*)/', $line, $match)) {
        if(isset($inrate[$current_interface])) {
            $util = (1.0*$inrate[$current_interface])/$current_speed;
            $incolor = sprintf("%02x%02x00", 255*$util, 255*(1-$util));
            $inline[$current_gradient] = sprintf("%02d%% %.2fMbps", $util*100, $inrate[$current_interface]/1000000.0);
        } else {
            $incolor = $unknown_color;
            $inline[$current_gradient] = "n/a";
        }
        if(isset($outrate[$current_interface])) {
            $util = (1.0*$outrate[$current_interface])/$current_speed;
            $outcolor = sprintf("%02x%02x00", 255*$util, 255*(1-$util));
            $outline[$current_gradient] = sprintf("%02d%% %0.2fMbps", $util*100, $outrate[$current_interface]/1000000.0);
        } else {
            $outcolor = $unknown_color;
            $outline[$current_gradient] = "n/a";
        }
        switch($match[2]) {
         case  1: print "$match[1]$outcolor$match[3]\n"; break;
         case  9: print "$match[1]$incolor$match[3]\n"; break;
         default: print $line; break;
        }
    } elseif(preg_match('/^\s*<title>Dynamic connector[.\d]*<\/title>\s*$/', $line)) {
        // Skip output of title
    } elseif(preg_match('/style=.stroke:url\(#(\S+)\)./', $line, $match) && isset($inline[$match[1]])) {
        print $line;
        print "\t\t\t<title>In: {$inline[$match[1]]}\nOut: {$outline[$match[1]]}</title>\n";
    } else {
        print $line;
    }
}
fclose($svg);
?>

Fazit

Kundenspezifische Views wurden mit dem Monitoringsystem verknüpft, so daß alle Beteiligte einen Vorteil davon haben:

  • Der Kunde kann seinen Teil des Netzes anhand der für ihn erstellen Dokumentation live einsehen.
  • Der Admin bekommt einen Einblick der Kundensicht und kann bei Wartungen die Auswirkungen auf den Kunden überprüfen.
  • Der Admin kann - wenn live verknüpft - eine aktuelle Weathermap eines beschränkten Bereiches während einer Umbaus offen halten. Die Dokumentation wurde beispielsweise adhoc für diesen Umbau erstellt und enthält so nur die interessanten Teile.
  • Der Support kann auf einen für den Kunden relevanten Teil der Netzstruktur schauen, während er mit dem Kunden telefoniert.
  • Da die Dokumentation auf der Supportwebseite liegt, kann sie nicht in veralteter Form im Mailpostfach eines Vertrieblers oder in den "Eigenen Dokumenten" eines Managers versauern.

Es gibt also durchaus Gründe, Netze zu dokumentieren und diese Dokumentation aktuell zu halten: Sie aktiv zu nutzen.

In order to terminate a large number of broadband customers on an LNS, MPD on FreeBSD was recommended to me. There seems to exists a group of Russian operators using this software for really large deployments. Alternativly OpenL2TP could be used. However  OpenL2TP does handle all the PPP sessions in user space, while MPD relies on NetGraph.

First contact

The setup is simple, even through the documentation of MPD lacks expressivness. What a command does, is documented at a single point at most. If you do not know where to look, you're stuck.

Additionaly MPD strictly distinguishes between the layers of operation. So you can only configure something at the place (and find documentation) in which this setting is relevant. I.e. vanJacobson compression occurs in the IPCP part of (PPP) bundles. Protocol field compression as part of the PPP session is negotiated in the link layer, which is responsible for setting up PPP. Therefore, all such settings are on this layer—even the authentication protocols to be used by LCP.

The configuration file is strictly linear. Each command can change the current layer. Thus the following commands might be executed in a different context. I can recommend to create the configuration manually (online) and enable "log +console".

Disillusionment

First experiments turned into pure horror.

lasttest-mpd-patch1

After a quite funny start "no more threads" and "Fatal message queue overflow!" messages were logged. The MPD crashed. But it crashed controlled, so finished all sessions before terminating itself.

The event handling for radius requests caused the problems. MPD implemented event queuing by reading and writing an internal pipe. This pipe correlated to a ring buffer containing the real event information. In order to prevent blocking writes to the pipe, no more bytes (dummy values for "there's an event") should be written beside those that fit into the kernel buffer.

The obvious patch is to increase the pipe size and extend the ring buffer. But I did not make any success: it got a segfault.

Since each event on each layer generates several further events, a event flood can happen. But the single thread for event processing must not block! If any potentional blocking may occur, a new thread is spawned and handled asynchonly. So RADIUS authenication and accounting exceeded the limit of 5000 threads per process. On the RADIUS server, the load looked similar.

A carrier experienced during my tests, how badly RADIUS servers may react. The commercial LAC queries the RADIUS on every login attempt to determine the appropriate LNS for the realm. During a simultanious drop of all sessions, he was overwhelmed by so many requests that his radius servers—specifically the database backend—gave up and the entire DSL infrastructure quit the service. Meanwhile, the realm to LNS mapping is a static configuration.

Slowly, more slowly

It is always better to be kind to its suppliers, particularly to the radius servers. So I droppedthe entire event handling of the MPD and wrote it from scratch using mesg_queue. The required library was already used by MPD.

A distinction is made for "sequential" and "parallel" events. Sequential events run under the global lock the MPD. They keep serialized in the same order in which they were generated. Thus, the layers do not get confused, and event processing can rely on the previous actions. Parallel processing is used for external communication. They are handeled by a fixed number of active worker threads.

The length of both queues determine MPD's overload (Load = 2×queue_len(serial) + 10×queue_len(parallel)). An overloaded MPD does refuse new connections.

Well equipped, I was full of hope and suddenly disappointed. The kernel paniced: deep, deep inside the NetGraph code.

bsd-lasttest-panic-1

And not just once, but also at a completely different place.

bsd-lasttest-panic-2

Is the netgraph code broken? Does the kernel has problems with rapid creation and deletion of interfaces? Do I work too fast?

Even RADIUS accounting created problems, because a test equipment managed to connect, drop, and reconnect three times within one second. My SQL unique constraints refused to accept such data. The MPD is definitely too fast! I added a pause of 20ms (configurable) between the processing of two serial events.

My thread for the serial processing runs separately from the main thread, which was not the case in the original code. I put a separate lock to each NetGraph call. This prevents concurrent access on NetGraph sockets.

So the system started to behave stable.

Stress testing

I choosed an OpenL2TP on Linux for generating lots of sessions:

  • Open a random number of L2TP tunnels.
  • Create up to 9000 PPP sessions (round robin over the L2TP tunnels) as fast as possible.
  • Create new PPP sessions at a limited rate up to 9000 sessions are established.
  • Drop and recreate a random selection of 300 sesssion to simulate a living broadband environment.
  • Stay a while and finally drop the sessions one after the other.

A typical test result is given below: "Sessions" are generated by OpenL2TP/pppd, each of them make MPD "Links" within the tunnels, and negotiate the PPP-"Bundles". Queue lengths are plotted too.

mpd-logins-13040106

The initial length of the parallel queue is large, then the overload functionality throttles the connection rate. The plots for sessions, links and bundles are practically identical. There are no differences, because only successful sessions are count.

It is also nice to see how high the serial queue jumps when interface disappear: The original MPD is definitely susceptible to queue overrun. If this effect occurs, the original MPD drops all connections and stops. An unacceptable situation.

The FreeBSD box with MPD runs at a maximum load of 3 and requires 280 MB of RAM. The Linux machine generates a load of 20 during connection setup and a load of 50 on connection release. The memory required on the Linux side is about 2 GB to hold the many pppd instances.

Touch the limits

Next test simulates a interconnection breakdown: Instead of terminating pppd instances, let's drop the L2TP tunnels.

mpd-logins-13040211

Clearly bundles, as the highest layer, are removed first. Then the associated L2TP links go down. The load on BSD box rises up to 6.

On Linux side a disaster starts: All pppd instances compete for the scheduler in order to stop working. The load rises to over 700, and the machine will not calm down. A courageous "killall -9 pppd" redeemed the machine after half an hour.

But how far can we go? More than 10000? More than 20000? Let's try it out:

mpd-logins-13040213

The setup rises rapidly to 11,500 sessions and stopped spontaneously. On the Linux side OpenL2TP reports erroneous "RPC" parameters: It reached the limits of its implementation. The machine gets tangled and needs to be rebooted. The BSD box is fine.

And now several times in succession:

mpd-logins-130401

Up and down, again and again. Sometimes a crash by my own stupidity, but it does!

Going live

In the production environment, it works surprisingly quiet. Nevertheless, the system keeps crashing:

bsd-lasttest-panic-3

All crashes are now only located in the libalias module. Although the code was moved to svn-HEAD … Read more.

But there are further changes in the MPD code:

  • In order to replace the code in production quickly, the MPD quit itself when there are no more sessions open. The configuration option is called "delayed-one-shot". So MPD needs now to be called in a loop (/usr/local/etc/rc.d/mpd5 contains 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 &
  • The RADIUS accounting reports only a limited set of termination cause codes, but I do need the complete error message:
ATTRIBUTE      mpd-term-cause  23      string
  • Relevant system logging (interfaces come and go, users log on and off) is now activated permanently, not only for debugging. Similarly, errors are always relevant. Otherwise, the server would run blind.

Oh, and the patch: Here you are. Please feel free to ask for commercial Support.

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 …

Nachdem der erste Fehler in CGN eingegrenzt und behoben werden konnte, treten weiter Abstürze auf. Natürlich in anderen Ausprägungen. Und so geht die Fehlersuche weiter.

Debugging ist wie Zwiebeln schälen: Man entfernt vorsichtig Schicht um Schicht, um am Ende mit leeren Händen dazustehen.

unknown

Hyperoptimiert

Diese neuen Abstürze sind eher seltsam. So meldete ein Server einen Panic in einer Funktion laut Stacktrace, die diesen Code gar nicht enthält.

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff80012f3000) != lnk->la (0xffffff8001327000)

panic() at panic+0x187
HouseKeeping() at HouseKeeping+0x161
LibAliasOutLocked() at LibAliasOutLocked+0x62
LibAliasOut() at LibAliasOut+0x54

Es handelt sich um eine Compiler-Optimierung, die Code verschiebt und umsortiert. Diese wird aber nur bei höheren Optimierungsstufen aktiviert. Doch nicht im Kernel, oder? Schließlich ist eine höhere Optimierung nicht empfohlen.

# $FreeBSD: releng/8.3/sys/conf/kern.pre.mk

. if ${MACHINE_ARCH} == "amd64"
COPTFLAGS?=-O2 -frename-registers -pipe
. else
COPTFLAGS?=${_MINUS_O} -pipe
. endif

Also doch! Aber das müßte den gesamten Kernel betreffen.

Die Suche fördert die Abstürze zu Tage, die immer nur dann auftreten, während Streit um ein Mutex herrscht. Und das Mutex muß nicht in libalias liegen:

/var/crash# tar xOf textdump.tar.15 | fgrep -2 address_hook
db:0:kdb.enter.default>  bt
Tracing pid 2437 tid 100255 td 0xffffff001df85460
ng_address_hook() at ng_address_hook+0x8e
ng_ppp_link_xmit() at ng_ppp_link_xmit+0x108
ng_apply_item() at ng_apply_item+0x220
--

Tracing command mpd5 pid 2437 tid 100255 td 0xffffff001df85460
ng_address_hook() at ng_address_hook+0x8e
ng_ppp_link_xmit() at ng_ppp_link_xmit+0x108
ng_apply_item() at ng_apply_item+0x220
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xb0
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_ksocket_incoming2() at ng_ksocket_incoming2+0x18e
ng_apply_item() at ng_apply_item+0x320
--
_mtx_lock_sleep() at _mtx_lock_sleep+0xd7
_mtx_lock_flags() at _mtx_lock_flags+0x43
ng_address_hook() at ng_address_hook+0x42
ng_iface_send() at ng_iface_send+0xc5
ng_iface_output() at ng_iface_output+0x1b3

Ich habe probeweise auf eingen Servern die Optimierung für den libalias-Code deaktiviert und so ein neues Modul geschaffen.

Damit kann ein Fehler, der durch den optimierenden Compiler verursacht wurde, ausgeschlossen werden. Er könnte die Mutexe unter Last, auf verschiedenen CPU, ggf. im Interrupt-Handler, durch Instruction-Scheduling einer Racecondition ausgesetzt haben.

Um zumindest den Interrupt-Anteil zu minimieren, habe ich net.isr.direct=0 gesetzt. So wird NAT nicht mehr direkt im Interface-Interrupt ausgeführt. Den CPUs tut es gut. Die Last verteilt sich mehr.

Um Schwierigkeiten mit Mutexen und CPU-Switching zu vermeiden, ist zusätzlich net.isr.bindthreads=1 gesetzt.

Aber da ist ja auch noch der Compiler-Optimierer einzubremsen, und zwar im kompletten Kernel. Bisher war ja nur das Modul ohne Optimierung kompiliert.

$ env  CFLAGS='-O -pipe' COPTFLAGS='-O -pipe' \
          MAKEOBJDIRPREFIX=~/mykernel NO_KERNELCLEAN=yes \
          KERNCONF=LNS make buildkernel

Wenn man Glück hat, kracht es nun an anderen Stellen. Dies bedeutet dann, daß man ein weitere Schicht der Zwiebel abgetragen hat.

Fatal trap 12: page fault while in kernel mode
cpuid = 5; apic id = 13
fault virtual address   = 0x58
fault code              = supervisor read data, page not present
instruction pointer     = 0x20:0xffffffff8069c912
stack pointer           = 0x28:0xffffff835f8c3800
frame pointer           = 0x28:0xffffff835f8c3820
code segment            = base 0x0, limit 0xfffff, type 0x1b
                        = DPL 0, pres 1, long 1, def32 0, gran 1
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = 12913 (mpd5)

rtfree() at rtfree+0x72
in_ifadownkill() at in_ifadownkill+0xb0
rn_walktree() at rn_walktree+0x7c
in_ifadown() at in_ifadown+0x8b
in_control() at in_control+0x956
ifioctl() at ifioctl+0x75e
kern_ioctl() at kern_ioctl+0xfe
ioctl() at ioctl+0xfd
amd64_syscall() at amd64_syscall+0xf9
Xfast_syscall() at Xfast_syscall+0xfc

<4>if_rtdel: error 3
<6>in_scrubprefix: err=65, prefix delete failed

Out of area Einsätze

Aber es sind auch alte Bekannte dabei:

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup:
 la (0xffffff8001288000) != lnk->la (0x2000000008)
KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup:
 la (0xffffff80014aa000) != lnk->la (0x2000000008) 

Dieser Fehler ist wirklich interessant. Er deutet darauf hin, daß der Kernel – anderwo – den Speicher überschreibt.

Darüberhinaus ist es bedenkenswert, in diesem Fall aufzuräumen anstatt abzustürzen. Die Gefahr, ein Memory-Leak zu produzieren, ist groß, allerdings tritt der Fehler nur alle paar Tage einmal auf. Beim Absturz gegen die aktuellen NAT-Tabellen sowieso verloren, vom Wegwerfen wird der Schaden also nicht größer.

Es besteht natürlich die Gefahr, daß kein kleiner Fehler vorliegt, sondern ganze RAM-Bereiche vernichtet worden. In diesem Fall wäre der erste Fehler nur der Anfang einer heftigen Kaskade. Es ist also sinnvoll, den Fehler nur begrenzt oft zu ignorieren.

#define VALID_PTR_MASK 0xff00000000000000lu
#define VALID_PTR(p) ((((uint64_t)p)&VALID_PTR_MASK) == VALID_PTR_MASK)

#define CHECK_LNK(la, lnk, i, abortcmd) do {                                   \
   if(!VALID_PTR(lnk)) {                                                       \
      _broken_lnk(la, lnk, i, "lnk", __FILE__, __LINE__, __FUNCTION__);        \
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   if(!VALID_PTR(lnk->la)) {                                                   \
      _broken_lnk(la, lnk->la, i, "lnk->la", __FILE__, __LINE__, __FUNCTION__);\
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p) @%d\n",          \
                           __FILE__, __LINE__, __FUNCTION__, la, lnk->la, i)); \
} while(0)

static void _broken_lnk(struct libalias *la, void * p, int i, char const * prt, char const * file, int line, char const * func);

static int _broken_retry_count = 0;
static void _broken_lnk(struct libalias *la, void * p, int i, char const * ptr, char const * file, int line, char const * func) {
   if(_broken_retry_count >= 10)        /* No mercy */
     panic("%s:%d %s: %s=%p is invalid @%d\n", file, line, func, ptr, p, i);

   printf("%s:%d %s: %s=%p is invalid @%d\n", file, line, func, ptr, p, i);
   printf("%d. retry to throw away broken NAT tables (will leak memory)\n", ++_broken_retry_count);
   LIBALIAS_LOCK_ASSERT(la);
   for (i = 0; i < LINK_TABLE_OUT_SIZE; i++)
     LIST_INIT(&la->linkTableOut[i]);
   for (i = 0; i < LINK_TABLE_IN_SIZE; i++)
     LIST_INIT(&la->linkTableIn[i]);
}

Und dann im Code überall die struct alias_link Zeiger vor jeder Verwendung an CHECK_LNK verfüttern.

Ja, der Code ist häßlich. Besonders die Prüfung mit einer Maskierung. Aber alle Abstürze in _FindLinkOut haben inzwischen Adressen, die nicht mehr dem Testmuster entsprechen. Man müßte eigentlich mit mincore den Zeiger prüfen, aber diese Funktion ist ein richtiger Performancekiller.

Es dauert nun wieder einige Tage, bis diese neue Version durch die zufälligen Reboots ausgerollt ist. Erst wenn diese Versionen wieder abstürzen, gibt es weitere Ergebnisse.

KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff800133e000) != lnk->la (0x4800000008) @3545
KDB: enter: panic
libalias/alias_db.c:850 IncrementalCleanup: la (0xffffff80013da000) != lnk->la (0x200000002) @918

Oh, damit ist eine weitere Vermutung widerlegt: Die Hashfunktion ist in Ordnung. Es bestand eine gewisse Chance, daß ein negativer Wert als Ergebnis der Hashberechnung entsteht. Ebenfalls möglich wäre ein Off-by-one Error an den Grenzen des Wertebreichs gewesen. Aber dies liegt ganz offensichtlich nicht vor.

Einige Tage später hat die Methode nun tatsächlich Wirkung gezeigt: Zumindest ein Absturz wurde verhindert.

Plötzlich (fast) stabil

Nachdem ein Kunde vom Netz genommen wurde, der selbst CGN machte, treten die Abstürze in der libalias nicht mehr auf. Aktuell ist es nur eine Korrelation, aber man darf hoffen.

Es ist gut möglich, daß die Fehler von einer überproportional hohen Anzahl von Verbindungen mit dem gleichen Quellport und der gleichen Quelladresse verursacht wurden. Die Libalias könnte also in der Hinsicht nicht streßfest sein.

Weitersuchen

Achja, um das auch auszuschließen: Die Systeme haben ECC-RAM. Speicherfehler als Ursache waren eigentlich schon dadurch praktisch unmöglich, weil immer an der gleichen Stelle auf unterschiedlichsten Systemen das Problem auftritt. Es hat nur nach wie vor keine erkennbare Ursache. Andererseits hat die detailierte und geduldige Untersuchung schon einen Fehler in den Firewallregeln aufgedeckt. Es ist also nicht hoffnungslos.

Die ganz gemeine Methode ist radikal upzudaten. Das ist aber ein anderes Thema. Dennoch ist die Überlegung interessant, da es unter den FreeBSD Entwicklern Hinweise darauf gibt, daß es beim schnellen Löschen von Interfaces zum use-after-free im Kernelspeicher kommen kann.

Da ich nicht einfach upgraden kann, variiere ich die Geschwindigkeiten von 20ms bis 120ms in gleichmäßigen Schritten. Jeder LNS hat nun eine andere Verarbeitungsgeschwindigkeit für Ereignisse. Mal sehen, wie sich das auswirkt, wenn nachts die Massen wieder "die Zwansgtrennung vorziehen.

Die Ergebnisse sind sparsam: Die Kiste mit 20ms hat zwei Abstürze mehr als die Kiste mit 120ms. Nun laufen alle Kiste auf 100ms Delay.

Auffällig ist, daß inzwischen immer der gleiche Wert als falscher Pointer auftritt. Möglicherweise läßt sich der überschreibende Prozeß anhand des überschriebenden Umfeldes eingrenzen. Dazu wird der Code abgeändert, um einen Hexdump der struct alias_link zu erhalten.

#define CHECK_LNK(la, lnk, i, abortcmd) do {                                   \
   if(!VALID_PTR(lnk)) {                                                       \
      _broken_lnk(la, lnk, i, "lnk", __FILE__, __LINE__, __FUNCTION__);        \
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   if(!VALID_PTR(lnk->la)) {                                                   \
      hexdump(lnk, sizeof(*lnk), "lnk", 0);                                    \
      _broken_lnk(la, lnk->la, i, "lnk->la", __FILE__, __LINE__, __FUNCTION__);\
      lnk = NULL;                                                              \
      abortcmd;                                                                \
   }                                                                           \
   KASSERT(la == lnk->la, ("%s:%d %s: la (%p) != lnk->la (%p) @%d\n",          \
                           __FILE__, __LINE__, __FUNCTION__, la, lnk->la, i)); \
} while(0)

Und das gibt tatsächlich Ergebnisse. Ein Absturz der mit querverlinkten Einträgen:

[... kein Dump ...]
libalias/alias_db.c:884 IncrementalCleanup: la (0xffffff80014d6000) != lnk->la (0xffffff800136a000) @2061

Ein Absturz der mit falschen Zeiger und Aufräumcode, der aber nicht hilft:

lnk0000   01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0010   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0030   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0050   00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00  |................|
lnk0060   00 00 00 00 00 00 00 00                          |........        |
alias_db.c:886 IncrementalCleanup: lnk->la=0x100000001 is invalid @3064
1. retry to throw away broken NAT tables (will leak memory)

Fatal trap 12: page fault while in kernel mode
cpuid = 4; apic id = 12
fault virtual address   = 0x34
fault code              = supervisor read data, page not present
instruction pointer     = 0x20:0xffffffff80f6e0d8
stack pointer           = 0x28:0xffffff835f81f360
frame pointer           = 0x28:0xffffff835f81f3c0
code segment            = base 0x0, limit 0xfffff, type 0x1b
                        = DPL 0, pres 1, long 1, def32 0, gran 1
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = 12 (swi1: netisr 4)

Ein Absturz der mit falschen Zeiger und Aufräumcode, der diesmal hilft:

lnk0000   08 00 00 00 20 00 00 00 04 00 00 00 01 00 00 00  |.... ...........|
lnk0010   ec ca 4a 01 03 f6 14 38 63 6d 64 34 00 00 00 00  |..J....8cmd4....|
lnk0020   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0030   00 00 00 00 00 00 00 00 6f 01 00 00 fb 24 00 00  |........o....$..|
lnk0040   6a 01 00 00 dc 24 00 00 00 00 00 00 00 00 00 00  |j....$..........|
lnk0050   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
lnk0060   00 00 00 00 00 00 00 00                          |........        |
alias_db.c:1260 _FindLinkIn: lnk->la=0x2000000008 is invalid @2910
1. retry to throw away broken NAT tables (will leak memory)
[... läuft noch stundenlang weiter ...]

Insgesamt wird beim Aufräumen tatsächlich die Betriebsfähigkeit des Systems erhalten. Drei bis vier Aufräumversuche überlebt eine Maschine, bevor sie entgültig rebootet. Und die Vorfälle liegen immer noch bei ca. ein Vorfall pro Tag bei einem LNS statt der gesamten Plattform.

Die Fehler treten vorzugsweise an Maschinen auf, die nicht nur CGN und PPP/ADSL und dafür machen, sondern auch CGN für DHCP/VDSL. Es ist also möglich, daß ein Fehler mit hohen Verbindungsanzahlen pro Souce-IP und Port bedingt wird.

Welches CMD?

Mittlerweile ist der letzte Hexdump das absolut vorherrschende Phänomen der noch korrigierbaren Fehler geworden.

Aber was zu Hölle sucht ein "cmd" Text mitten in einem NAT-Eintrag? Welcher Prozess konnte es sein, der diesen String hinterlies? Welcher Prozess könnte einen Datensatz generiert, der diesen Text enthält? Könnte das geschehen nachdem ein Interface nicht mehr existiert, so daß ein "use-after-free" tatsächlich zur Wirkung kommt?

Die Suche im Kernelcode findet diesen String ausschließlich im Parser von NetGraph Nachrichten. Und NetGraph Nachrichten werden vom MPD generiert. Und die sollen zu spät kommen? Ich hatte doch die Eventverarbeitung extra verlangsamt, um die Abstürze in den Griff zu bekommen! Na dann setze ich die doch mal wieder ganz runter: Von 0 bis 10ms.

Das Beschleunigen der Eventverarbeitung hat das Problem nur verschärft: Es sind wieder 100ms aktiv. Das war's also auch nicht.

Aber der Memorydump stammt tatsächlich von einer NetGraph Nachricht:

struct ng_mesg {
 struct  ng_msghdr {
  u_char          version;                /* 08 ... NGM_VERSION */
  u_char          spare;                  /* 00 ... pad */
  u_int16_t       spare2;                 /* 00 00 ... pad */
  u_int32_t       arglen;                 /* 0x00000020 ... length of data */
  u_int32_t       cmd;                    /* 0x00000004 ... NGM_NAME */
  u_int32_t       flags;                  /* 0x00000001 ... NGF_RESP */
  u_int32_t       token;                  /* ec ca 4a 01 ... Token für den MPD */
  u_int32_t       typecookie;             /* 03 f6 14 38 ... NGM_PPP_COOKIE */
  u_char          cmdstr[NG_CMDSTRSIZ];   /* "com4" */
 } header;
 char    data[];                 /* 6f 01 00 ... */
};

Speicher, mein Speicher

Es ist für die Stabilität des Systems zwingend notwendig, die Abstürze durch Memory-Corruption zu verhindern. Das ist aber ein anderes Thema.

Nachtrag

Mit dem Verzicht auf das Löschen von Interface-Knoten konnten die Fehler bisher vollständig beseitigt werden.

Wird der MPD als LNS unter Hochlast gesetzt, können die L2TP Verbindungen wegbrechen. Die Gründe dafür sind manigfaltig, exemplarisch werden hier einige betrachtet.

Schadensmaximierung

Im Code des MPD sind verschiedenste Asserts verteilt. Wenn davon einer zuschlägt, beendet sich der MPD kontrolliert. Und wenn sich der MPD beendet, fliegen alle Nutzer raus. Ein typischer Assert ist:

ASSERT "new == PHASE_DEAD || new == PHASE_TERMINATE || new == PHASE_AUTHENTICATE"
 failed: file "lcp.c", line 418

Auslöser der Bedingung ist eine Nutzeranmeldung, die nicht protokollkonform abläuft. Das kann immer mal passieren.

Abstürzen darf eine Software deswegen aber nicht. Was könnte man stattdessen tun?

Man sollte diese eine Anmeldung abbrechen, den einzelnen Kanal herunterfahren und einfach weiter machen.

Interface dicht

Wenn es richtig brummt, geht viel Datenvolumen in Richtung der Endgeräte über den L2TP Tunnel. Auf der Datenleitung mischen sich L2TP-Datenpakete und L2TP-Steuerbefehle.

L2TP: Control connection terminated: 6 (No buffer space available)

Wenn ein Interface ausgehend unter Volllast steht, kann es nicht mehr die Daten von den einliefernden Prozessen abnehmen. Die Unmöglichkeit Daten abzunehmen, signalisiert das Unix mit ENOBUFS.

Der MPD beendet daraufhin sofort den L2TP Kanal und wirft alle Nutzer raus. Eine viel bessere Lösung gibt es vermutlich nicht: Man könnte etwas warten und die Aussendung erneut probieren. Für den L2TP Kanal sollte es einem die Mühe wert sein.

Andererseits kann man die Netzwerkkarte effektiver konfigurieren. Denn die hat wirklich ein Problem.

# sysctl dev.igb | fgrep no_desc_avail
dev.igb.1.queue0.no_desc_avail: 19
dev.igb.1.queue1.no_desc_avail: 0
dev.igb.1.queue2.no_desc_avail: 9
dev.igb.1.queue3.no_desc_avail: 0 

Die Defaulteinstellung von hw.igb.txd und hw.igb.rxd kann und sollte man von 1024 auf 4096 erhöhen. Das entschärft das Problem.

Darüberhinaus ist besonders eine Karte mit mehreren Queues anfällig für dieses Problem: Der MPD handelt alle L2TP Verbindungen in einem Thread ab und so landen diese alle in der gleichen Sendequeue. Da vier Sendequeues vorhanden sind, werden diese nur mit einem Viertel der möglichen Geschwindigkeit geleert.

Mehrere LNS laufen unter igb, andere unter em-Treibern. Der em-Treiber hat nur eine ausgehende Warteschlange und diese Maschinen sind deutlich unauffälliger gegen diese Abbrüche. Also wechseln alle LNS von igb auf em.

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.