Erweiterte Suche


In einer Etage ist per Medienkonverter TX/SX ein LAN versorgt, da der zugehörige Router im Rechenzentrum steht. Im Rechenzentrum soll nun ein neuere Server aufgestellt werden. Nichts leichter als das, denn der Medienkonverter im Rechenzentrum hat einen eingebauten 4-Port Hub, an dem neben den Router noch der Server gesteckt wird.

Anfangs funktioniert alles bestens, aber nach einer guten halben Stunde beschwert sich der Kunde, daß an einem Arbeitsplatz DHCP nicht mehr ginge, an einem anderen Platz geht dagegen alles problemlos. IPv6 über Autoconfig teilt aber an beiden Plätzen die Adressen zu.

Es stellte sich heraus, daß der 4-Port Hub des Konverters nach einer Weile aufhört Broadcast Pakete korrekt weiterzuleiten. Er transportiert Broadcasts nur noch von den Kupferports zum Glasport aber nicht mehr vom Glas zum Kupfer. Wenn man den Server abzieht, funktioniert alles wieder prächtig. Unicast ist nicht betroffen, das tut in alle gewünschten Richtungen.

Das beobachtete Phänomen ist nun leicht erklärt:

  • Der Router hat seine Multicasts für Autoconfig, die wie Broadcasts verteilt werden, in Richtung des Glasinterfaces durchbekommen. Also bekamen die Rechner IPv6 Adressen. Allerdings konnten sie damit nicht arbeiten, weil Neighbor Discovery (Multicast!) die MAC-Adresse des Routers nicht ermitteln konnte.
  • Der Rechner, der über DHCP keine Adresse bekam, war per "ipconfig /release" gesäubert und sendete also Broadcasts, die nicht ankamen.
  • Der Rechner, bei dem DHCP funktionierte, war bereits eine Lease zugeteilt und die Erneuerung der Lease erfolgt per Unicast, die problemlos ankommen.

Fazit: Diese Medienkonverter sind dafür gedacht, Clients anzuschließen, nicht auf der Server- oder Routerseite zu stehen.

Am 4. Oktober findet in Berlin eine öffentliche Anhörung zum Thema Netzneutralität statt, auf wir als Sachverständige (seitens der SPD Fraktion) geladen sind. Das Diskussionthema ist spannend, denn es geht im Endeffekt um die Frage, wofür man als Provider eigentlich bei einem Internetzugang kassiert.

Da das Thema Netzneutralität aktuell heiß diskutiert wird, findet gleich am darauffolgenden Samstag eine offene Diskussionveranstaltung in Berlin statt. Der Schwerpunkt liegt hier mehr auf der Frage, wofür man als Verbraucher bei einem Internetzugang eigentlich bezahlt.

An beiden Veranstaltungen kann man kostenfrei teilnehmen, von der Anhörung gibt es einen Live-Stream unter www.bundestag.de oder später die Aufzeichung.

Wer mit IPv6 Adressen zu tun hat, muß halt auch sich mit anderen über die benutzten Adressen unterhalten. Leider gibt es dazu aktuell keine gemeinsame Sprachreglung für die Adressbestandteile. Dies führt leicht zu Mißverständnissen und im schlimmsten Fall zu Fehlkonfigurationen.

IPv6 Adresse
2001 4bd8 0 666 248 54ff fe12 ee3f
RIPE IKS Kunde VLAN MAC Adresse der Netzkarte

Es eine Sammlung von Namensvorschlägen, die zu einem einheitlichen, verbindlichen Namen für die Teile der IP Adresse führen soll. Aktuell läuft eine Umfrage zur Beliebheit der Namensvorschläge. Es kristallisieren sich zwei Namen heraus:

  • Hextet als Bezeichner für 16 Bits
  • Quibble als Bezeicher für 4 Nibbles (Gruppe von 4 Bits)
ipv6-address-part-names

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.

Wie schon in DNSSEC Amplification Attack erklärt, mißbraucht man meine Technik für DDoS Angriffe. Unglücklicherweise bieten weder das Protokoll noch die Software Schutz. Im Gegenteil: Beide versuchen die bestmögliche (Angriffs-)Performance hinzulegen. Dies ist der Versuch einer intelligenteren Abwehr.

Die Ausgangslage ist wohlbekannt. Man sendet einem DNS-Server Anfragen mit gefälschten Absendeadressen, welcher dieser beantwortet und deutlich größere Pakete an das Opfer sendet. Man kann so auf triviale Weise mehrere hundert Mbps zusammenbekommen.

Bekannte Lösungen

Als Serverbetreiber steht man hilflos da. Genaugenommen hat man folgende Möglichkeiten:

  • Man kann den DNS Betrieb ganz einstellen. Das bedeutet, daß man keine eigenen DNS Server für eigene Zonen mehr betreiben kann.
  • Man kann die Bandbreite des DNS Servers beschränken. Dann wird der Hauptteil der DNS Pakete immer noch der Angriff sein und man verwirft hauptsächlich die notwendigen Antworten für den eigenen Wirkbetrieb. Die Paketverluste der Drosselung treffen alle Anfragen mit gleicher Wahrscheinlichkeit, den Angreifer stört es aber nicht.
  • Man kann pro Client ein Ratelimit einführen. Diese Lösung behagt mir nicht, weil sie weiterhin einen konstanten Angriffstrom liefert. DDoS funktioniert auch mit niedrigen Bandbreiten, wenn man nur genügend Server findet (was leicht ist). Darüber hinaus ist die Lösung ressourcenintensiv.
  • Man kann auf die häufigsten Anfragen (ANY via UDP) mit REFUSED oder TRUNCATED antworten. Die Antwortpakete sind dann kleiner als die Anfragen, so daß sich der Angriff für den Angreifer nicht lohnt. Unglücklicherweise interessiert es den Angreifer nicht und die Restbandbreiten der Angriffe sind, wie im Punkt zuvor, immer noch ausreichend.

Alle diese Möglichkeiten haben mich nicht praktisch überzeugt.

Tägliche Praxis

Um ein Gefühl für die Größenordnung des Problem zu bekommen, hier einige reale Beispielbilder. Zuerst die Bandbreitenbeschränkung auf 100Mbps. Die Leitung geht zum Switch hin in die Sättigung, obwohl nur acht Mbps an Angriffsdaten vorliegen. Der Verstärkungseffekt ist unverkennbar. So geht das also nicht.

dnssec-attack-bandwith

Nun die Verkleinerung der Antwortdaten (in meinem Fall durch REFUSED). Der Effekt tritt unmittelbar nach Wechsel der Software ein und ist sehr beruhigend. Allerdings beschweren sich die Opfer weiterhin völlig zu Recht, weil allein dieser Server immer noch eine typische Geschäftskundenleitung mit 2Mbps in die Sättigung treibt. Darüber hinaus fallen auch einige Datenpakete auf, die den Angriff nicht mit der klassischen ANY Anfrage durchführen. Ein Wettrennen, das man nur verlieren kann, hat schon längst begonnen.

dnssec-attack-refused

Meine Überlegung war nun, einen selbstlernenden Prozeß zu haben, der auf "böse" Aktionen mit vollständiger Ignoranz reagiert. Allerdings soll er auch selbstheilend sein. Das Ganze System muß auf Autopilot laufen können, ohne Schaden anzurichten.

Dampening in der Theorie

Als Netzwerker kenne ich BGP Route Dampening, das konzeptionell auf die Problemstellung paßt. Falsche Defaults hatten diese Lösung in Verruf gebracht. Ich hatte vorsichtig zu sein und der Praxis das letzte Wort zu überlassen.

Die Idee bestand also darin, pro anfragender IP (oder Netz) Strafpunkte je nach Art der Anfrage und Größe der Antwort zu vergeben. Diese Strafpunkte sollten nach einer gewissen Halbwertzeit verfallen. Im Sinne einer Hysterese sollte das Dampening bei hohen Werten aktiviert werden und erst bei einer deutlich niedrigeren Schwelle wieder aus gehen. Während des Dampenings sollten keine Anfragen beliebigen Typs beantwortet werden, insbesondere auch keine Fehlermeldungen verschickt werden.

An der Cisco Implementation gefiel mir instantan nicht, daß in regelmäßigen Abständen ein Wartungsdurchlauf notwendig war, um die Werte in der Tabelle altern zu lassen. Ich wollte in konstanter, oder wenigstens logarithmischer Zeit fertig werden. Das Altern Tabelleneinträge mußte irgendwie nebenbei mit abfallen.

Darüber hinaus wollte ich eine möglichst schnelle Bearbeitung im durchschnittlichen Fall erreichen. Dieser wird aber von den Angriffen dominiert. Eine Reihenfolge, in der die Angriffs-IPs vorn stehen, würde sich hier anbieten.

Als Datenmodell wählte ich einen Heap. In Arraydarstellung benötigt der Heap keinerlei Verwaltungsinformation, denn die Baumstruktur ergibt sich über die Indexarithmetik. In dieser Darstellung stehen die "höchstwertigen" Einträge vorn, so daß die lineare Suche die Adresse in ungefährer Reihenfolge der Häufigkeit durchsuchen wird.

Anstatt den Heap regelmäßig zu altern, wollte ich nur das aktuell bearbeitete Element und das erste Element anfassen. Dazu war es notwendig, nicht nur den aktuellen Punktestand, sondern auch den Zeitpunkt der letzen Änderung abzuspeichern. So läßt sich nachträglich der jetztige Punktstand berechnen. Damit sich die Berechnung auch lohnt, kann man sie einige Sekunden bedenkenlos aussetzen und mit veralteten Werten rechnen.

Ein wesentliches Designkriterium stabiler Software ist der deterministische Umgang mit Ressourcen. Der Heap sollte also nur eine feste Größe haben. Einträge, die unter einen bestimmten Grenzwert fallen, können einfach gelöscht werden. Ist der Heap trotzdem voll, so überschreiben neue Werte den allerletzten Eintrag im Heap.

Theoretisch sollte so der Heap alle Elemente regelmäßig altern lassen. Da das erste Element jedesmal gealtert wird, fällt es so unter die Punktzahl anderer Einträge. Da es dabei seinen Spitzenplatz verliert, kommen alle Einträge mal zum Zuge.

Aus der Sicherheitstechnik kannte ich auch die Gefahr von Integer-Overflows. Die Strafpunkte werden also auf einen Maximalwert begrenzt und nicht endlos aufaddiert.

Tückische Praxis

Mangels implementierter Konfigurationsmöglichkeiten habe mit einigen konservativen Default begonnen:

  • 1 Strafpunkt pro Anfrage
  • Dampening aktiv bei 40000 Strafpunkten
  • Dampening inaktiv bei 1000 Strafpunkten
  • Rauswurf aus der Tabelle bei 100 Strafpunkten

Der allererste Versuch war ernüchternd: Kommentarloser Absturz nach wenigen Sekunden. Grund war schlicht, daß der Heap nicht alloziert war. Anfängerfehler. Also habe ich gleich noch die Funktionsaufrufe mit ausführlichen asserts versehen.

Aber schon die nächsten Tests waren vielversprechend.

dnssec-attack-dampening1

Bis 02:00 lief die alte Software, die REFUSED Pakete ausliefert (mit nahezu konstanter Datenrate). Beim Wechsel der Software fällt diese aus und es gibt einen Einbruch, dem ein drastisches Ansteigen folgt, weil das Dampening lernt. Bereits nach 19 Sekunden hat das Dampening die ersten Angreifer gefangen. Das waren 40000 Queries!

02:10:30.212 general: notice: running
02:10:49.136 client: warning: 99.71.68.170 enters dampening.
02:11:07.477 client: warning: 108.248.25.44 enters dampening.
02:11:27.321 client: warning: 173.218.224.224 enters dampening.
02:11:58.131 client: warning: 217.23.3.214 enters dampening.

Aber wenige Minuten später war der nächste Absturz da:

02:15:51.732 critical: dampening.c:282: fatal error:
02:15:51.732 critical: RUNTIME_CHECK(b > 0 && b <= dampening_last) failed
02:15:51.732 critical: exiting (due to fatal error in library)

Dieser Fehler steht direkt in den Heap-Manipulationsroutinen. Also wurde die Funktion "heap_down" logisch aufgeräumt (mit Dank an Knuth, TAoCP) und verlor dabei 80% des Codes.

Erst 13:30 ging es wieder los und prompt in die Hose: Der Traffic stieg unaufhaltsam. Grund war, daß sich die IPs um den letzten Eintrag in der Tabelle prügelten. Die Einträge warfen sich gegenseitig raus, so daß keiner die notwendige Punktzahl aufsammeln konnte.

13:22:31.985 client: info: 61.247.0.2 added to dampening with 1 (replacing 212.49.98.14 with 31)

Die Lösung war schlicht, nicht zu überschreiben, wenn der vorhandene Eintrag bereits eine höhere Punktzahl aufweist. Statt dessen wird eine Fehlermeldung geworfen, wenn der letzte Eintrag zu hohe Strafpunkte hat. Dann ist die Tabelle offenbar nur mit Angreifern gefüllt.

Mit diesen Änderungen hielt sich das System richtig gut. Nach einer Anlernphase um 14:30 drückte es den Ausgangstraffic herunter auf ca. 2 Mbps. Dann begann es wieder Angriffe durchzulassen.

15:29:59.321 client: error: Dampening table to small! Entry 213.186.33.20 with 12175 at the end.

WTF? So viele Angreifer hatte ich im mitlaufenden Sniffing nicht beobachtet. Stattdessen gab es ab und zu mal einen Spike mit einer neuen Adresse. Der Fehler muß woanders liegen. Meine Annahme, die Tabelle würde irgendwie automatisch durchaltern schien sich nicht erfüllt zu haben. Wie auch? Die vorderen Einträge werden durch den weiter bestehenden Angriff immer weiter angeheizt und liegen permanent am oberen Anschlag. Die Strafpunkte verfallen also langsamer als neue Punkte dazukommen.

Es war keine schlaue Idee, jeden Tabellenüberlauf zu loggen, ohne etwas zu tun. Das GB Logfiles hätte ich mir sparen können. Es ist aber auch keine Lösung, bei Tabellenüberlauf das Programm zu beenden. Denn alles was man dann tun kann, ist das Programm neu zu starten. Es genügt also, die Tabelle zu leeren und neu zu lernen.

Um die komplette Tabelle altern zu lassen werden jetzt drei Werte angefaßt: Der erste, der letzte und ein Wert an einer zufälligen Stelle. Der Zufall muß nicht perfekt sein, nur ab und zu mal hier oder dort reinschlagen. Kurz nach 17:00 ging auch diese Änderung live. Kurz nach 18:00 ist allerdings schon wieder unterwartet Schluß:

17:55:51.045 general: critical: dampening.c:283: fatal error:
17:55:51.045 general: critical: RUNTIME_CHECK(b > 0 && b <= dampening_last) failed
17:55:51.045 general: critical: exiting (due to fatal error in library

An dieser Stelle hätte dieser Fehler aber nie auftreten können. Was ist nur geschehen?

Die einzige Erklärung ist, daß jemand an dem Heap gedreht hat, während er bearbeitet wurde. Bind läuft aber in einem Prozeß, also Singletask. Oder? Eben nicht. Es ist multithreaded. Ein Quickcheck mit einer statischen Variable, die beim API Aufruf hoch zählt und dann wieder abfällt ergibt instantan:

general: critical: RUNTIME_CHECK(duplicatecheck == 0) failed

Na Prima! Der Code wurde nun heftig aufgeräumt, um ein zentrales LOCK um die Datenstruktur zu erlauben. Und siehe da! Es geht.

dnssec-attack-dampening3

Weiterhin wurde die Anlernphase verkürzt, indem folgende Strafpunkttabelle aktiv ist:

  • 10 Strafpunkte pro erste Query
  • 100 Strafpunkte für eine ANY Anfrage, sonst 1 Strafpunkt
  • Wird eine Anfrage mit gleicher ID wiederholt, so gibt es 100 Strafpunkte mal die schon vorhandenen Wiederholungen. D.h. für die erste Wiederholung einer Query gibt es 100 Punkte, für die zweite Wiederholung 200. Das summiert sich schnell auf.
  • Je nach Antwortgröße gibt es zwischen 1 und 5 Punkten bis 500 Byte, 10 und 50 Punkten bis 2500 Byte und dann 100 oder 200 Punkte für größere Antworten.

Mit diesen Einstellungen werden Angriffe im Schnitt nach 40 Paketen erkannt und ignoriert. Im Klartext die Zahlen noch mal nach zwei Stunden Betrieb:

  5 minute  input rate  381000 bits/sec,   23 packets/sec
  5 minute output rate 7275000 bits/sec, 9479 packets/sec 

Von 10000 Paketen werden 30 als sinnvoll anerkannt und bearbeitet.

Der Patch

Dieser Patch steht hier nur noch aus historischen Gründen. Eine benutzbare Version findet sich hier.

Achtung Update
diff -rbTuN bind-9.9.1-P3/bin/named/client.c bind-9.9.1-P3-dampening/bin/named/client.c
--- bind-9.9.1-P3/bin/named/client.c    2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/client.c  2012-09-23 22:05:38.000000000 +0200
@@ -55,6 +55,8 @@
        #include <named/server.h>
        #include <named/update.h>

+       #include "dampening.h"
+
        /***
         *** Client
         ***/
@@ -877,6 +879,8 @@
                        goto done;
                }

+               dampening_by_size(client, mr->length);
+
                result = client_allocsendbuf(client, &buffer, NULL, mr->length,
                                             sendbuf, &data);
                if (result != ISC_R_SUCCESS)
diff -rbTuN bind-9.9.1-P3/bin/named/dampening.c bind-9.9.1-P3-dampening/bin/named/dampening.c
--- bind-9.9.1-P3/bin/named/dampening.c 1970-01-01 01:00:00.000000000 +0100
+++ bind-9.9.1-P3-dampening/bin/named/dampening.c       2012-09-23 22:34:37.000000000 +0200
@@ -0,0 +1,415 @@
+       /*
+        * Dampening
+        * =========
+        *
+        * Purpose of dampening is to sort out misbehaving clients in order to
+        * serve the legitemate clients and protect the innocent vitims of a
+        * DNS(SEC) amplification attack caused by spoofed UDP queries.
+        *
+        * Overview
+        * ~~~~~~~~
+        *
+        * Each querier IP gets a additive penalty depening on the type of the
+        * query, the size of the answer, and other parameters.
+        *
+        * If the penalty reaches a limit, the state of the IP switches to a
+        * "dampening". In this state the server drops every query from this IP
+        * without further processing (besides adding penalty points).
+        *
+        * The penalty is decreased expotentially over time. So if no further points
+        * are added fast enough, the total penalty will drop below a secondary limit.
+        * Then the state of the IP is changed back to "normal".
+        *
+        * Implementation
+        * ~~~~~~~~~~~~~~
+        *
+        * DNS servers to not track their clients. So adding state to querier IPs
+        * requires a new storage model. Because access to this storage is needed
+        * by processing each query, the storage must be in memory only. In order
+        * to prevent ressouce exhaustion, the used memory is fixed.
+        *
+        * The assumed access pattern is that high penalty clients will be cause
+        * most of the queries. The used data structure is therefore a heap.
+        *
+        * The following operations are implemented:
+        *
+        *  a) Searching an IP by travering the heap array from front (high values)
+        *     to the end (beware of a partially filled heap).
+        *
+        *  b) Adding a new IP (if not found) by overwriting the very last entry
+        *     of the heap, if the heap is full. This will cause one of the low
+        *     penality entries to be lost.
+        *
+        *  c) If a searched IP was found, the last updated timestamp is used to
+        *     recalculate the penality value (expotential decrease plus new
+        *     points), update the state, and rebalanced the heap for this modified
+        *     entry only. If the penality value falls below a certain limit, the
+        *     entry is removed from the heap.
+        *
+        *  d) The top, last, and a random node are recalculated and rebalanced
+        *     after each update or insertion, if the nodes were not updated
+        *     recently.
+        *
+        * Using this approach, the operations on the heap will be O(log n) on
+        * each access. There is no need for a regular maintainence activity.
+        *
+        * TODO
+        * ~~~~
+        *
+        *  - All hardcoded parameters need to be (re)configurable.
+        *  - Allow ACL dependant penalty parameters.
+        *  - Check long time stability.
+        *  - Check, if the assumptions are really true.
+        *
+        */
+
+       #include "dampening.h"
+       #include <isc/util.h>
+       #include <stdlib.h>
+       #include <math.h>
+       #include <named/globals.h>
+       #include <named/log.h>
+
+       #include <isc/netaddr.h>
+       #include <isc/stdtime.h>
+
+       typedef unsigned int dampening_penalty_t;
+       typedef size_t dampening_position_t;
+       #define DAMPENING_NOTHING 0
+
+       struct {
+          struct {
+             dampening_penalty_t top, damp, norm, drop;
+          } limit;
+          size_t heapsize;
+          int updatedelay;
+          int halflife;
+       } dampening_configuration = {
+            { 60000, 40000, 1000, 100 },      /* limits */
+            5000,                             /* entries in the heap */
+            5,                                /* delay expire by seconds */
+            600                               /* half-live of penalty in seconds */
+       };
+
+       typedef struct dampening_entry {
+          isc_netaddr_t netaddr;
+          isc_stdtime_t last_updated;
+          dns_messageid_t last_id;
+          size_t last_id_count;
+          unsigned int penalty;
+          dampening_state_t state;
+       } dampening_entry_t;
+
+       static dampening_entry_t * dampening_heap = NULL;
+       static dampening_position_t dampening_last = 0;
+       static isc_mutex_t dampening_lock;
+
+       /*
+        * Helper functions
+        */
+       void dampening_init(void);
+       dampening_position_t dampening_search(const isc_netaddr_t * netaddr);
+       void dampening_add(const isc_netaddr_t * netaddr, dampening_penalty_t points, isc_stdtime_t now);
+       void dampening_update(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now);
+       void dampening_update1(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now);
+       dampening_state_t dampening_get_state(dampening_position_t entry);
+       void dampening_expire(isc_stdtime_t now);
+       void dampening_heap_up(dampening_position_t entry);
+       void dampening_heap_down(dampening_position_t entry);
+       void dampening_heap_swap(dampening_position_t a, dampening_position_t b);
+       int dampening_below_top(dampening_position_t entry);
+
+       /*
+        * Main API function
+        */
+       dampening_state_t dampening_query(ns_client_t * client) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_state_t state;
+          dampening_position_t entry;
+
+          RUNTIME_CHECK(client != NULL);
+          LOCK(&dampening_lock);
+
+          if(dampening_heap == NULL)
+            dampening_init();                 /* Autoinit */
+
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry == DAMPENING_NOTHING) {
+             dampening_add(&addr, 10, now);
+             state = DAMPENING_NORMAL;
+          } else {
+             state = dampening_get_state(entry);
+          }
+
+          UNLOCK(&dampening_lock);
+          return state;
+       }
+
+       void dampening_by_qtype(ns_client_t * client, dns_rdatatype_t qtype) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_position_t entry;
+          dampening_penalty_t points;
+
+          RUNTIME_CHECK(client != NULL);
+          INSIST(dampening_heap != NULL);
+
+          LOCK(&dampening_lock);
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry != DAMPENING_NOTHING) {
+             switch(qtype) {
+              case dns_rdatatype_any: points = 100; break;
+              default               : points =   1; break;
+             }
+
+             if(dampening_heap[entry].last_id == client->message->id) {
+                points += (dampening_heap[entry].last_id_count++)*100;
+             } else {
+                dampening_heap[entry].last_id = client->message->id;
+                dampening_heap[entry].last_id_count = 1;
+             }
+
+             dampening_update(entry, points, now);
+          }
+
+          UNLOCK(&dampening_lock);
+       }
+
+       void dampening_by_size(ns_client_t * client, size_t length) {
+          isc_stdtime_t now;
+          isc_netaddr_t addr;
+          dampening_position_t entry;
+          dampening_penalty_t points;
+
+          RUNTIME_CHECK(client != NULL);
+          INSIST(dampening_heap != NULL);
+
+          LOCK(&dampening_lock);
+          isc_stdtime_get(&now);
+          isc_netaddr_fromsockaddr(&addr, &client->peeraddr);
+
+          entry = dampening_search(&addr);
+          if(entry != DAMPENING_NOTHING) {
+             if(length <= 100)  points =   1; else
+             if(length <= 200)  points =   2; else
+             if(length <= 300)  points =   3; else
+             if(length <= 400)  points =   4; else
+             if(length <= 500)  points =   5; else
+             if(length <= 700)  points =  10; else
+             if(length <= 1000) points =  20; else
+             if(length <= 1500) points =  30; else
+             if(length <= 2000) points =  40; else
+             if(length <= 2500) points =  50; else
+             if(length <= 5000) points = 100; else
+                                points = 200;
+             dampening_update(entry, points, now);
+          }
+
+          UNLOCK(&dampening_lock);
+       }
+
+       /*
+        * Set up the heap: allocate the memory range. Change of size is possibe,
+        * so let dampening_last untouched.
+        */
+       void dampening_init(void) {
+          dampening_heap = realloc(dampening_heap, (1+dampening_configuration.heapsize) * sizeof(dampening_entry_t));
+          if(dampening_heap == NULL)
+            UNEXPECTED_ERROR(__FILE__, __LINE__, "realloc() failed");
+       }
+
+       /*
+        * Return the current state.
+        */
+       dampening_state_t dampening_get_state(dampening_position_t entry) {
+          REQUIRE(dampening_heap != NULL);
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          return dampening_heap[entry].state;
+       }
+
+       /*
+        * Simple search though the heap, exploiting the feature, that the heap
+        * is mapped to a continiously filled, linear array. High penalty values
+        * occur first, so they are searched first.
+        * Returns DAMPENING_NOTHING if no match.
+        */
+       dampening_position_t dampening_search(const isc_netaddr_t * netaddr) {
+          dampening_position_t i;
+
+          REQUIRE(dampening_heap != NULL);
+          for(i = 1; i <= dampening_last; i++) {
+             if(ISC_TRUE == isc_netaddr_equal(&dampening_heap[i].netaddr, netaddr))
+               return i;
+          }
+          return DAMPENING_NOTHING;
+       }
+
+
+       /*
+        * Add an element to the end (eventually overwrite the last one)
+        */
+       void dampening_add(const isc_netaddr_t * netaddr, dampening_penalty_t points, isc_stdtime_t now) {
+          REQUIRE(dampening_heap != NULL);
+
+          if(dampening_last < dampening_configuration.heapsize) {
+             dampening_last++;
+          } else {
+             if(dampening_heap[dampening_last].penalty > points) {
+                if(dampening_heap[dampening_last].penalty > dampening_configuration.limit.drop)
+                  isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_ERROR,
+                                "Dampening table overflow! Restart dampening.");
+                dampening_last = 1;
+                return;
+             }
+          }
+
+          memcpy(&dampening_heap[dampening_last].netaddr, netaddr, sizeof(isc_netaddr_t));
+          dampening_heap[dampening_last].last_updated = now;
+          dampening_heap[dampening_last].penalty = 0;
+          dampening_heap[dampening_last].last_id = 0;
+          dampening_heap[dampening_last].last_id_count = 0;
+
+          dampening_update(dampening_last, points, now);
+       }
+
+       /*
+        * Update an existing entry with new points
+        */
+       void dampening_update(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now) {
+          dampening_update1(entry, points, now);
+          dampening_expire(now);
+       }
+
+       void dampening_update1(dampening_position_t entry, dampening_penalty_t points, isc_stdtime_t now) {
+          int timediff;
+          char onbuf[ISC_NETADDR_FORMATSIZE];
+
+          REQUIRE(dampening_heap != NULL);
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          timediff = now - dampening_heap[entry].last_updated;
+          if(timediff > dampening_configuration.updatedelay) {
+             float penalty = dampening_heap[entry].penalty;
+             penalty *= exp(-(0.693*timediff)/dampening_configuration.halflife);
+             dampening_heap[entry].penalty = penalty;
+             dampening_heap[entry].last_updated = now;
+          }
+          if(dampening_heap[entry].penalty >= dampening_configuration.limit.top - points)
+            dampening_heap[entry].penalty = dampening_configuration.limit.top;
+          else
+            dampening_heap[entry].penalty += points;
+
+          if(dampening_heap[entry].state == DAMPENING_SUPPRESS &&
+             dampening_heap[entry].penalty < dampening_configuration.limit.norm) {
+
+             isc_netaddr_format(&dampening_heap[entry].netaddr, onbuf, sizeof(onbuf));
+             isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
+                           "%s dampening removed.", onbuf);
+             dampening_heap[entry].state = DAMPENING_NORMAL;
+          }
+
+          if(dampening_heap[entry].state == DAMPENING_NORMAL &&
+             dampening_heap[entry].penalty > dampening_configuration.limit.damp) {
+
+             isc_netaddr_format(&dampening_heap[entry].netaddr, onbuf, sizeof(onbuf));
+             isc_log_write(ns_g_lctx, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
+                           "%s dampening activated.", onbuf);
+             dampening_heap[entry].state = DAMPENING_SUPPRESS;
+          }
+
+          if(dampening_heap[entry].penalty < dampening_configuration.limit.drop &&
+             timediff > dampening_configuration.updatedelay) {
+             if(entry < dampening_last) {
+                dampening_heap_swap(entry, dampening_last);
+             } else {
+                entry = 0;
+             }
+             dampening_last--;
+          }
+
+          if(entry > 0) {
+             if(dampening_below_top(entry))
+               dampening_heap_down(entry);
+             else
+               dampening_heap_up(entry);
+          }
+       }
+
+       /*
+        * Expire penalties
+        */
+       void dampening_expire(isc_stdtime_t now) {
+          dampening_penalty_t poi[3] = { 1, dampening_last, 1+(random()%dampening_last) };
+          unsigned int i;
+
+          for(i=0; i<sizeof(poi)/sizeof(*poi); i++) {
+
+             if(0 < poi[i] && poi[i] <= dampening_last &&
+                now > dampening_heap[poi[i]].last_updated + dampening_configuration.updatedelay)
+               dampening_update1(poi[i], 0, now);
+          }
+       }
+
+       /*
+        * Heap operations
+        */
+       void dampening_heap_up(dampening_position_t entry) {
+          dampening_position_t e;
+
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          for(e = entry/2;
+              e > 0;
+              entry = e, e = entry/2) {
+             if(dampening_heap[entry].penalty <= dampening_heap[e].penalty)
+               break;
+             dampening_heap_swap(entry, e);
+          }
+       }
+
+       void dampening_heap_down(dampening_position_t entry) {
+          dampening_position_t e;
+
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+
+          for(e = 2*entry;
+              e <= dampening_last;
+              entry = e, e = 2*entry) {
+
+             if(e+1 <= dampening_last &&
+                dampening_heap[e].penalty <= dampening_heap[e+1].penalty)
+               e = e+1;
+
+             if(dampening_heap[e].penalty <= dampening_heap[entry].penalty)
+               break;
+
+             dampening_heap_swap(entry, e);
+          }
+       }
+
+       void dampening_heap_swap(dampening_position_t a, dampening_position_t b) {
+          dampening_entry_t temp;
+
+          RUNTIME_CHECK(a > 0 && a <= dampening_last);
+          RUNTIME_CHECK(b > 0 && b <= dampening_last);
+          RUNTIME_CHECK(a != b);
+
+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[b], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));
+       }
+
+       int dampening_below_top(dampening_position_t entry) {
+          RUNTIME_CHECK(entry > 0 && entry <= dampening_last);
+          return entry <= 1 ||
+            (dampening_heap[entry].penalty <= dampening_heap[entry/2].penalty);
+       }
+
diff -rbTuN bind-9.9.1-P3/bin/named/dampening.h bind-9.9.1-P3-dampening/bin/named/dampening.h
--- bind-9.9.1-P3/bin/named/dampening.h 1970-01-01 01:00:00.000000000 +0100
+++ bind-9.9.1-P3-dampening/bin/named/dampening.h       2012-09-23 21:37:44.000000000 +0200
@@ -0,0 +1,14 @@
+       #ifndef _DAMPENING_H
+       #define _DAMPENING_H
+
+       #include <named/client.h>
+
+       typedef enum dampenting_state {
+          DAMPENING_NORMAL, DAMPENING_SUPPRESS
+       } dampening_state_t;
+
+       dampening_state_t dampening_query(ns_client_t * client);
+       void dampening_by_qtype(ns_client_t * client, dns_rdatatype_t qtype);
+       void dampening_by_size(ns_client_t * client, size_t length);
+
+       #endif /* _DAMPENING_H */
diff -rbTuN bind-9.9.1-P3/bin/named/Makefile.in bind-9.9.1-P3-dampening/bin/named/Makefile.in
--- bind-9.9.1-P3/bin/named/Makefile.in 2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/Makefile.in       2012-09-19 15:55:39.000000000 +0200
@@ -87,6 +87,7 @@
                        zoneconf.@O@ \
                        lwaddr.@O@ lwresd.@O@ lwdclient.@O@ lwderror.@O@ lwdgabn.@O@ \
                        lwdgnba.@O@ lwdgrbn.@O@ lwdnoop.@O@ lwsearch.@O@ \
+                       dampening.@O@ \
                        ${DLZDRIVER_OBJS} ${DBDRIVER_OBJS}

        UOBJS =         unix/os.@O@ unix/dlz_dlopen_driver.@O@
@@ -101,6 +102,7 @@
                        zoneconf.c \
                        lwaddr.c lwresd.c lwdclient.c lwderror.c lwdgabn.c \
                        lwdgnba.c lwdgrbn.c lwdnoop.c lwsearch.c \
+                       dampening.c \
                        ${DLZDRIVER_SRCS} ${DBDRIVER_SRCS}

        MANPAGES =      named.8 lwresd.8 named.conf.5
diff -rbTuN bind-9.9.1-P3/bin/named/query.c bind-9.9.1-P3-dampening/bin/named/query.c
--- bind-9.9.1-P3/bin/named/query.c     2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/query.c   2012-09-23 21:50:22.000000000 +0200
@@ -61,6 +61,8 @@
        #include <named/sortlist.h>
        #include <named/xfrout.h>

+       #include "dampening.h"
+
        #if 0
        /*
         * It has been recommended that DNS64 be changed to return excluded
@@ -7282,6 +7284,14 @@
                }

                /*
+                * Update the penalty and report the current state
+                */
+               if (dampening_query(client) == DAMPENING_SUPPRESS) {
+                       query_next(client, DNS_R_DROP);
+                       return;
+               }
+
+               /*
                 * Get the question name.
                 */
                result = dns_message_firstname(message, DNS_SECTION_QUESTION);
@@ -7323,6 +7333,7 @@
                INSIST(rdataset != NULL);
                qtype = rdataset->type;
                dns_rdatatypestats_increment(ns_g_server->rcvquerystats, qtype);
+               dampening_by_qtype(client, qtype);
                if (dns_rdatatype_ismeta(qtype)) {
                        switch (qtype) {
                        case dns_rdatatype_any:

Einen richtigen Patch zum Download gibt es, wenn die Konfigurierbarkeit gegeben ist.

Peinlich, peinlich

Da muß man erst den Quellcode veröffentlichen, um einen besonders abstrusen Fehler gezeigt zu bekommen. Ja merkt denn keiner die Copy und Waste Orgien? Hier werden effektiv Daten vernichtet und der Heap wird völlig zerstört!

+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));

Nochmal zum Mitschreiben: t = a; a = a; b = t; Wie soll das funktionieren?

+          memcpy(&temp,              &dampening_heap[a], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[a], &dampening_heap[b], sizeof(dampening_entry_t));
+          memcpy(&dampening_heap[b], &temp,              sizeof(dampening_entry_t));  

So ist es natürlich richtig! Und der Bug ist oben im Patch schon gefixt. Danke an den Suse Mitarbeiter, der das bemerkt hat.

Und noch so ein Hammerfehler: Die Berechnung des Punkteverfalls enthielt nicht das notwendige Minuszeichen. Die Strafpunkte wurden mit der Zeit immer mehr. Ist im Patch behoben, wie ich das gefunden habe, kommt in einem extra Beitrag.

Software für DNS Dampening zu entwickeln, ist ein iterativer Prozeß. Ob die Annahmen dann auch im laufenden Code die erwarteten Ergebnisse liefern, ist nur durch Testen unter realen Bedingungen zu ermitteln.

Release often, release early

Die ersten überraschenden Auswirkungungen meiner Patchveröffentlichung waren:

  • Ich wurde frühzeitig auf konzeptionelle Fehler hingewiesen. Im Grundsatz stimme ich den Ausführungen ja zu, bin aber noch beim Sammeln eigener Erfahrungen. Habt also Geduld mit meinem Starrsinn.
  • Man zeigte mir einen katastrophalen Programmierfehler in der Heapverwaltung, den ich sicher nur nach Tagen selbst gefunden hätte.
  • Es gab Zuspruch und Hinweise, was jetzt (in diesem frühen Stadium) schon besser zu machen geht. Das hilft mir sehr.
  • Ich habe viel über interne Prozesse bei uns hier gelernt. Mehr als ich wissen wollte.

Danke dafür.

Hätte ich später veröffentlicht, hätte ich vermutlich andere Wege eingeschlagen und mein Code wäre mir zu schade zum Wegwerfen gewesen. Wahrscheinlich hätte ich aufgegeben.

Erste Beobachtungen

Nach dem Rollout auf der betroffenen Maschine, schaute alles erstmal prima aus. Die Last ging runter, ich war zufrieden.

Es gab allerdings einige Eigentümlichkeiten, so wanderten die IPv6 fähigen Resolver von DTAG, HE und SiXXS ins Dampening. Genauere Informationen fehlten noch. Also wurde der Patch um ein Debugging erweitert.

--- bind-9.9.1-P3/bin/named/query.c     2012-08-24 06:43:09.000000000 +0200
+++ bind-9.9.1-P3-dampening/bin/named/query.c   2012-09-25 22:37:26.000000000 +0
@@ -7146,7 +7148,7 @@
        }

        static inline void
-       log_query(ns_client_t *client, unsigned int flags, unsigned int extflags) {
+       log_query(ns_client_t *client, unsigned int flags, unsigned int extflags, unsigned int penalty) {
                char namebuf[DNS_NAME_FORMATSIZE];
                char typename[DNS_RDATATYPE_FORMATSIZE];
                char classname[DNS_RDATACLASS_FORMATSIZE];
@@ -7165,7 +7167,7 @@
                isc_netaddr_format(&client->destaddr, onbuf, sizeof(onbuf));

                ns_client_log(client, NS_LOGCATEGORY_QUERIES, NS_LOGMODULE_QUERY,
-                             level, "query: %s %s %s %s%s%s%s%s%s (%s)", namebuf,
+                             level, "query: %s %s %s %s%s%s%s%s%s (%s) %u", namebuf,
                              classname, typename, WANTRECURSION(client) ? "+" : "-",
                              (client->signer != NULL) ? "S": "",
                              (client->opt != NULL) ? "E" : "",
@@ -7173,7 +7175,7 @@
                                         "T" : "",
                              ((extflags & DNS_MESSAGEEXTFLAG_DO) != 0) ? "D" : "",
                              ((flags & DNS_MESSAGEFLAG_CD) != 0) ? "C" : "",
-                             onbuf);
+                             onbuf, penalty);
        }

        static inline void
@@ -7228,6 +7230,7 @@
                unsigned int saved_extflags = client->extflags;
                unsigned int saved_flags = client->message->flags;
                isc_boolean_t want_ad;
+               unsigned int penalty;

                CTRACE("ns_query_start");

@@ -7282,6 +7285,14 @@
                }

                /*
+                * Update the penalty and report the current state
+                */
+               if (dampening_query(client, &penalty) == DAMPENING_SUPPRESS) {
+                       query_next(client, DNS_R_DROP);
+                       return;
+               }
+
+               /*
                 * Get the question name.
                 */
                result = dns_message_firstname(message, DNS_SECTION_QUESTION);
@@ -7306,7 +7317,7 @@
                }

                if (ns_g_server->log_queries)
-                       log_query(client, saved_flags, saved_extflags);
+                       log_query(client, saved_flags, saved_extflags, penalty);

                /*
                 * Check for multiple question queries, since edns1 is dead.

In dampening_query den Wert zu speichern, ist dann trivial.

Auffällig war, daß das Arbeiten am System insgesamt zäh wurde. Die Bash fragt immer den Hostname für den Prompt ab, also mal strace -f hostname -f aufgerufen. Es gibt erhebliche Timeouts bei der Namensauflösung über Localhost. Also mal auf die Schnelle etwas der Art Strafpunkte über Zeit hingerotzt, sorry.

penalty3

Zuerst einmal die auffälligen Spikes am späten Abend:

  • Gegen 22:57 hatte ich ein tcpdump explizit mit Namensauflösung laufen lassen, um zu verifizieren, daß die Namensauflösung überhaupt noch funktioniert.
  • Die breiten Balken gegen 22:05 und 23:43 sind Brute Force Angriffe via SSH. Die SSH prüft die Konsistenz von PTR und A(AAA) vor dem Anmeldeversuch.
  • Um Mitternacht rennt das System ungebremst ins Dampening.

Zuerst die Balken etwas breiter aufgelöst (nur das Zeitfenster zwischen 23:02:50 und 23:07:00):

penalty3-1

Es ist sehr schön zu sehen, wie die Strafpunkte sich aufsummieren und wieder genullt werden. Dampening funktioniert, wenn auch irgendwie invers. Möglicherweise fällt der Eintrag immer wieder aus der Tabelle heraus. Das geht so.

Ein Blick auf den Vorfall um Mitternacht offenbart, daß Testscripte die interne Konsistenz der gehosteten Zonen prüfen: Stimmen Vor- und Rückwärtseinträge, ist der Nameserver auch in der Parentzone gelistet? Ja, es kommen binnen weniger Minuten tatsächlich 40000 Anfragen zusammen. Das geht so nicht.

Die offenkundige Lösung besteht darin, die lokal erreichbaren Netze vom Dampening auszunehmen. Denn wer Spoofing für diese Adressen annimmt, ist nicht besser als die Angreifer, die anderswo die Verletzung von BCP38 ausnutzen.

Ein anderer Fall war ein Client, der es mit einigen wenigen Anfragen in 20 Minuten auf über 1000 Strafpunkte schaffte. Den Graphen mußte ich lange ansehen, bis ich die beiden Linien einzeichnen und mich an den Kopf fassen konnte:

penalty4

Anstatt die Punkte verfallen zu lassen (wie die grüne Linie zeigt) stiegen die Punktzahlen expotentiell. Und tatsächlich war mir im Code das notwendige Minuszeichen im Exponenten durch die Lappen gegangen. Was für ein grober Programmierfehler!

Allerdings war der Fehler auch sehr nützlich: So konnte ich testen, wie sich das System verhält, wenn alle IPs als Angriff zu betrachten ist. Glücklicherweise hatte ich mich schon auf feste Ressourcennutzung festgelegt. Andernfalls wäre die Kiste geplatzt.

Bugfix in Produktion

Nach diesen ersten Fehlerbehebungen fühlt sich das System gut an und die Statistiken sind vielversprechend:

penalty5

Nach einer Anlernphase verfallen die Punkte in Wellen und die Clients werden wieder freigegeben. Da die Angriffsraten pro Ziel nicht gleichbleibend sind, verlaufen sich die Wellen nach und nach.

Obwohl die Punkte aktuell pro IP gesammelt werden, werden sie doch pro Anfragetyp unterschiedlich behandelt. Die Frage ist also, ob normale Anfragen durch die Angriffe mit ANY Pakteten in Mitleidernschaft gezogen wurden. Dazu mal die Strafpunkt über die Zeit pro Querytype:

penalty6-1

Keine der normalen Anfragen wird von einer IP gestellt, die ins Dampening gerutscht ist! Der Normalbetrieb ist ungestört. Im Vergleich dazu die Strafpunkt unter Berücksichtigung der Angriffe:

penalty6

Angriffe werden weiterhin binnen 40 Pakete erkannt und geblockt. Man sieht sehr schön die Stufen, die durch die Wiederholungen eines ANY-Paketes mit gleicher ID und großer Antwort.  Die Datenrate gestattet wieder einen normalen Betrieb.

Abgefallen ist eine Statistik der Anfragetypen des DNS im praktischen Betrieb.

 165976 A
  56926 ANY
  46890 AAAA
  36527 PTR
  11336 MX
   8025 SOA
   4958 DNSKEY
   2974 NS
   2786 SRV
   2293 TXT
    587 A6
    568 SPF
    543 DLV
    170 DS
    105 CNAME
      4 HINFO
      4 AXFR
      3 TLSA
      2 NAPTR
      2 RRSIG
      1 NSEC

Erfreulich ist die Verbreitung von TLSA. Weniger erfreulich sind die Anfragen nach A6. Die DLV Anfragen sind normal, weil ich eine solche betreibe. Aber auch das Verhältnis von IPv6 uind IPv4 Anfragen ist Grund zur Freude.

Offene Baustellen

Das System ist erstmal grundsätzlich einsatzbereit. Aber es gibt genug zu tun:

  • Ist der eingesetzte Algorithmus optimal? Sollte man nicht besser mit Ringpuffer für die Alterung und Hashtable für die Suche arbeiten?
  • Es fehlen die Konfigurierbarkeit, ACLs etc.
  • Was passiert, wenn ein Angriff mit der IP eines Resolvers eines großen ISP stattfindet? Sind dann die Zonen für die Kunden des ISP unerreichbar?
  • Wie verhält sich der Ansatz im Vergleich zu anderen Ansätzen? Wo liegen die Vor- und Nachteile der einzelnen Lösungen?

Die Anzeige der IP-Adresse des Besuchers ist eigentlich eine Trivialität. Schließlich besteht während des Seitenabrufs eine Verbindung mit genau der IP. Interessanter ist es, parallel auch legacy IP Adressen und DNSSEC Eigenschaften zu ermitteln. Und dann soll das Ganze noch aus CMS generierten Seiten, d.h. gecachtem Content heraus funktionieren.

Die Abfrage der aktuell abfragenden IP Adresse ist mit serverseitigen Scriptsprachen eine Standardaufgabe.

<div id='your-ip'>
  <?= htmlspecialchars($_SERVER['<span class="term">REMOTE_ADDR</span>']) ?>
</div>

Schwierig wird es, wenn die Webseiten nicht ständig serverseitig bearbeitet werden, sondern aus einem Cache kommen. Dies kann der Cache eines CMS oder der eines Proxies sein. Dann ist die Scriptbearbeitung schon lange abgeschlossen, die IP steht als normaler Text drin und ist somit in der Regel falsch. Es ist unerwünscht, nur wegen dieser Anzeige, die sinnvollen Cache-Funktionen zu deaktivieren.

Stattdessen lagert man den Code der IP Ermittlung aus in ein minimales serverseitiges Script, daß nur die IP Adresse ausgibt. Und dieses Script muß vom Webbrowser clientseitig nachgeladen werden. Dazu dient Javascript. Das Nachladen von Inhalten ist unter dem Schlagwort Ajax bekannt.

<div id='your-ip'>
  <script type='text/javascript' src='http://lutz.donnerhacke.de/check-ipv6-dnssec.js'>
  </script>
</div>

Worauf kann nun dieses Script selbst zurückgreifen? Normalerweise würde man für Ajax ein Framework wie jQuery nehmen. Dies ist aber hier nicht möglich. Das einbettende System kann unbekannt sein (bei Fremdeinbindung) oder ungeeignete Frameworks schon benutzen, mit denen sich das eigene Framework beissen könnte.

Es bleibt nur, Ajax von Hand zu machen. Glücklicherweise brauche ich hier keine Rücksicht auf seltsame und alte Browser zu nehmen, ich kann mich an den Standard halten. Wenn also der Browser diesen Standard unterstützt, wird das HTML Stück mit der IP-Adresse nachgeladen:

if (window.XMLHttpRequest) {
  var xmlhttp = new XMLHttpRequest();

  xmlhttp.onreadystatechange = function() {
    if(xmlhttp.readyState == 4) {
      var mydiv = document.getElementById("your-ip");
      mydiv.innerHTML = xmlhttp.responseText;
    }
  };
  xmlhttp.open("GET","/cgi-bin/check-ipv6-dnssec.pl",true);
  xmlhttp.send(null);
}

Der serverseitige Code muß mit einem relativen Pfad geladen werden, dies verlangen die Sicherheitseinstellungen der gängigen Browser, um Cross-Site Angriffe zu vermindern. In diesem Fall wird serverseitig ein Perl-Script ausgeführt.

#! /usr/bin/perl

print q{Content-Type: text/html; charset=latin1

<html>
<body id="inhalt">};

print $ENV{'REMOTE_ADDR'};

print "</body></html>";

Damit ist das Cache-Problem gelöst.

IPv6 und IPv4 zusammen

Wenn der Client per IPv6 Adresse ankommt, dann soll auch noch seine IPv4 Adresse ermittelt werden. Dazu wird der Code leicht abgewandet. Es wird ein Marker zurückgeliefert, der dem Javascript zeigt, das noch etwas fehlt.

...
my $addr = $ENV{'REMOTE_ADDR'};
print "$addr<br/>";

if($addr =~ /:/) {
    print "<span id='your-legacy-ip'></span><br/>\n";
}
...

Und das Javascript selbst wird entsprechend erweitert.

...
      mydiv.innerHTML = xmlhttp.responseText;

      var legacy = document.getElementById("your-legacy-ip");
      if(legacy) {
        var fill_legacy  = document.createElement("script");
        fill_legacy.type = "text/javascript";
        fill_legacy.src  = "http://v4.lutz.donnerhacke.de/cgi-bin/check-v4.js"
        legacy.appendChild(fill_legacy);
      }
    }
...

Der große Trick besteht darin, dass Scripte von anderen Servern nachgeladen werden dürfen. In diesem Fall von einem Server, der kein AAAA Record im DNS anbietet. Damit ist der Client gezwungen, auf das veraltete IPv4 Protokoll zurückzuspringen. Genausogut kann man einen IPv6-only Server befragen, um das aktuelle IP Protokoll anzufordern. Dieses Javascript darf nicht mit "document.write" arbeiten, sondern muß in den bestehenden DOM des Dokuments eingreifen.

#! /usr/bin/perl

print <<END
Content-Type: text/javascript

document.getElementById("your-legacy-ip").innerHTML = "$ENV{'REMOTE_ADDR'}";
END

Mehrwerte hinzufügen

Alle weiteren Sachen sind problemlos. Das den Haupttext aufgerufene Perl-Script kann wesentlich mehr tun. Es kann auf Übergang-IPv6 Adressen testen ($addr =~ /^2001:0*:/ || $addr =~ /^2002:/) und validierende Resolver nach DNSSEC fragen dig +dnssec PTR.

Bleibt noch die Mehrsprachigkeit. Dazu muß das Script erfahren, in welchem Sprachkontext es aufgerufen wurde. Am einfachsten geht das mit einem URL Parameter beim Script-Aufruf. Aber wer soll diesen auswerten? Witzigerweise ist es auch clientseitig im Browser möglich, die Parameter von einer URL an die anderen zu hängen. Damit ergibt sich das folgende vollständige Script.

if (window.XMLHttpRequest) {
  var xmlhttp = new XMLHttpRequest();
  var urlargs = document.currentScript.src.match(/(\?.*)/);
  var args = urlargs ? urlargs[0] : "";

  xmlhttp.onreadystatechange = function() {
    if(xmlhttp.readyState == 4) {
      var mydiv = document.getElementById("your-ip");
      mydiv.innerHTML = xmlhttp.responseText;

      var legacy = document.getElementById("your-legacy-ip");
      if(legacy) {
        var fill_legacy  = document.createElement("script");
        fill_legacy.type = "text/javascript";
        fill_legacy.src  = "http://v4.lutz.donnerhacke.de/cgi-bin/check-v4.js"+args
        legacy.appendChild(fill_legacy);
      }
    }
  };
  xmlhttp.open("GET","/cgi-bin/check-ipv6-dnssec.pl"+args,true);
  xmlhttp.send(null);
}

Wer es nutzen will, soll es so einbinden, wie am Anfang geschrieben. Der relevante URL Parameter lautet locale={ger,eng}.

Erzwingen von IPv6

Wie bei RIPE festgestellt wurde, funktioniert die Erzwingung von IPv6 Verbindungen nicht, wenn als Übergangstechnik nur Teredo zum Einsatz kommt. Der Grund liegt darin, dass die aktuellen Windows-Systeme keine AAAA Anfragen stellen, wenn sie keine halbwegs brauchbaren IPv6 Adressen lokal vorliegen haben.

Das Nachschlagen von v6.lutz.donnerhacke.de ist also sinnfrei. Trägt man dagegen die IP Adresse ein, klappt es problemlos.

Viel Spaß!