Während der ICANN Diskussionen@AtLarge kam die Frage auf, wie man als einfacher Nutzer testen kann, ob man von dem DNSSEC Key Rollover der Root Zone betroffen ist. Das ist erstaunlich schwierig oder vollkommen trivial. Je nachdem was man will.

Das Rollover Problem

DNSSEC funktioniert im Grundsatz so, dass die Elternzone einen Verweis auf den aktiven Schlüssel der Kindzone setzt. Auf diese Weise kann man in der Kindzone der Schlüssel leicht gewechselt werden, indem man parallel den Verweis in der Elternzone aktualisiert.

Für die Root-Zone ist es nicht so einfach, weil es schlicht keine Elterzone für die Root gibt. Das Schlüsselmaterial der Root kommt stattdessen aus den (Festplatten-)Speichern der Resolver-Server. Aber wie kommt es da rein?

Zum einen kann man das Schlüsselmaterial manuell eintragen. Das hat man anfangs auch gemacht. Die Daten fanden sich in Zeitungen, auf Webseiten etc. pp. Inzwischen wird das Schlüsselmaterial von den Software- / Betriebssystemherstellern vorinstalliert mit ausgeliefert. Eine noch andere Möglichkeit besteht darin, während der Erstinstallation den aktuellen Schlüssel ungeprüft abzurufen und zu speichern.

Das Problem besteht nun darin, den Schlüssel zu wechseln. Die Abermillionen von Installationen müssen also neue Schlüsseldaten bekommen. Normalerweise eine unmögliche Aufgabe. Seit einigen Jahren gibt es aber ein automatisiertes Verfahren: RFC5011. Im Kern besagt der RFC, der Resolver soll alle Schlüssel, die er mit Hilfe der bekannten Schlüssel validieren kann, abzuspeichern hat. Damit kennt und vertraut er den neuen Schlüsseln, sobald er sie sieht.

Ob das in der Praxis funktioniert, wurde nicht wirklich getestet. Dieser Root KSK Rollover ist der erste Versuch, das zu tun. Und natürlich muss man damit rechnen, dass es schief geht. Es kann aus ganz unterschiedlichen Gründen schief gehen, z.B. könnte der Festplattenbereich durch den Resolver nicht beschreibbar sein.

Wenn es schief geht, kann der Resolver keinerlei DNS Anfragen mehr beantworten. Gar keine. Und nun stelle man sich vor, dieser betroffene Resolver steht bei einem großen Internetprovider: Es gibt einen Blackout für alle Kunden dieses Providers.

Konsequenzen

Wie viele solche Resolver es gibt, ist nicht bekannt. ICANN hat also beschlossen, den für letzten Oktober geplanten Rollover auszusetzen.

Neuere Methoden, ob ein Resolver den neuen Schlüssel schon gelernt hat, wurde in den letzten Monaten entwickelt und ausgerollt. Allerdings ist die Verbreitung dieser neuen Methoden auf die gut gepflegten Systeme beschränkt, die sowieso kein Problem mit dem Rollover haben werden. Eine Aufklärung des problematischen Dunkelfelds ist nicht zu erwarten.

Nun steht die Frage im Raum, ob und wann ein neuer Anlauf genommen wird. Neue Daten sind jedenfalls nicht zu erwarten.

Andererseits besteht das Problem, dass immer mehr Geräte auf den Markt kommen, die gar nicht in der Lage sind einen Root Schlüssel zu wechseln. Hier stehen die vielen unsupporteten IoT-Geräte im Vordergrund. Je länger man also mit dem Rollover wartet, desto mehr Anbieter können einen Rollover ignorieren. Nur ein regelmäßiger Wechsel zwingt die Industrie und die Admins zu eine korrekten Vorgehensweise.

Ängste

Das Thema ist emotionsgeladen:

  • Will ich riskieren, dass mein Internet ausfällt?
  • Wer wird mich (als Admin) verantwortlich machen, wenn es zu Ausfällen kommt?
  • Wer wird mich (als ICANN) verantwortlich machen, wenn es zu Ausfällen kommt?
  • Wie viele Leute werden einfach DNSSEC ausschalten, anstatt das Problem richtig zu beheben?

Dazu muss vorrangig die Frage beantwortet werden, ob man überhaupt betroffen ist.

Dazu gibt es eigentlich nur zwei Fälle:

  • Mein ISP (Resolver) kann DNSSEC und validiert. Dann kann es passieren, dass der KSK Rollover schief geht.
  • Mein ISP (Resolver) validiert DNSSEC nicht. Dann betrifft der Rollover mich (und ihn) gar nicht.

Um diese beiden Fälle einfach zu überprüfen, habe ich auf Anregung von Alan Greenberg eine Webseite erstellt, die den aktuellen Stand beim Nutzer ermittelt und den betreffenden Fall anzeigt.

Wie man den Stand von DNSSEC ermittelt

Natürlich kann man nicht aktiv DNS im Browser sprechen, um diese Fälle zu unterscheiden. Eine aktive Programmierung jeder Art fällt also schon mal flach.

Was man aber machen kann, ist Teile der Webseite so abzulegen, dass ein DNSSEC validierender Resolver diese nicht abrufen kann. Ich habe mich dazu entschieden eine Webseite mit unterschiedlichem CSS zu machen.

<link rel="stylesheet" href="dnssec.css">
<link rel="stylesheet" href="http://css.fail.donnerhacke.de/dnssec-fail.css">

Der zweite Teil des CSS überschreibt die vorherige Einstellung:

$ cat dnssec.css
.failed { display: none; }
.dnssec { display: block; }
$ cat dnssec-fail.css
.failed { display: block; }
.dnssec { display: none; }

Er kann aber nur abgerufen werden, wenn der Resolver keine DNSSEC Validerung macht.

Um das hin zu bekommen, muss absichtlich ein stabiler Fehler im DNSSEC-Setup erzeugt werden. Der Fehler muss so stabil sein, dass er den normalen Betrieb und automatische Korrekturmaßnahmen überlebt. Ich habe mich zu einer fehlerhaften Delegation entschieden:

$ORIGIN donnerhacke.de.
fail            NS      avalon.iks-jena.de.
                NS      broceliande.iks-jena.de.
                DS      12345 8 2 1234...

Die delegierte Zone ist dann nicht signiert, obwohl die Elternzone behauptet, sie müsse es sein.

Das schaut dann so aus:

fail.donnerhacke.de

Ein validierender Resolver kann das nicht auflösen und antwortet:

;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 10476
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 4096
;; QUESTION SECTION:
;css.fail.donnerhacke.de. IN AAAA

;; Query time: 49 msec
;; SERVER: 100.100.100.100#53

Ein nicht validierender Resolver kommt dagegen zu folgendem Ergebnis:

donnerhacke.de.         NS      avalon.iks-jena.de.
donnerhacke.de.         NS      broceliande.iks-jena.de.
;; Received 185 bytes from 2001:678:2::53#53(a.nic.de) in 25 ms

css.fail.donnerhacke.de. CNAME  pro.donnerhacke.de.
pro.donnerhacke.de.     AAAA    2001:4bd8:1:1:209:6bff:fe49:79ea
donnerhacke.de.         NS      broceliande.iks-jena.de.
donnerhacke.de.         NS      avalon.iks-jena.de.
;; Received 219 bytes from 2001:4bd8:52:1:20a:e4ff:fe80:bec8#53(broceliande.iks-jena.de) in 1 ms

Die abgerufene Webseite wird also, in Abhängigkeit von den DNSSEC-Fähigkeiten des Resolvers, unterschiedlich aussehen.

Und nun gehe hin und teste Deinen Resolver: *click*

Die ASAs von Cisco können ja Portchannels, d.h. mehrere Leitungen zu Einer bündeln. Außerdem können Sie Failover, ein Gerät kann für ein anderes einspringen. Beides zusammen ist allerdings trickreich, da es im ungünstigen Fall zu Verbindungsabbrüchen führt.

Aufbau

Zwei ASAs im Active/Standby-Failover sind an getrennte Switche angeschlossen. Zur Erhöhung der Bandbreite wird ein Interface als LACP-Bündel zum übergeben. Ist die primäre ASA aktiv, funktioniert es prächtig.

asa# sh port-channel summary 
Group  Port-channel  Protocol  Span-cluster  Ports
------+-------------+---------+------------+------------------------------------
1      Po1(U)            LACP          No     Gi0/0(P)   Gi0/1(P)   
asa# failover exec mate show port-channel summary
Group  Port-channel  Protocol  Span-cluster  Ports
------+-------------+---------+------------+------------------------------------
1      Po1(U)            LACP          No     Gi0/0(P)   Gi0/1(P)   

Die Interfaces haben folgende Eigenschaften:

asa# sh interface | in Interface|MAC
Interface GigabitEthernet0/0 "", is up, line protocol is up
        MAC address 00.01.01, MTU 1500
Interface GigabitEthernet0/1 "", is up, line protocol is up
        MAC address 00.01.02, MTU 1500
Interface Port-channel1 "outside", is up, line protocol is up
        MAC address 00.01.01, MTU 1500

und

asa# fail exec mate sh interface | in Interface|MAC
Interface GigabitEthernet0/0 "", is up, line protocol is up
        MAC address 00.02.01, MTU 1500
Interface GigabitEthernet0/1 "", is up, line protocol is up
        MAC address 00.02.02, MTU 1500
Interface Port-channel1 "outside", is up, line protocol is up
        MAC address 00.02.01, MTU 1500

Wie man sieht übernimmt der Port-Channel die MAC Adresse des ersten Interfaces. Soweit so dokumentiert.

asa-portchannel-active

Wie man sieht, tauchen die beiden MAC Adressen der Interfaces am jeweiligen Switch auf. Die ASA antwortet im ARP mit der MAC Adresse des Port-Channels und alles ist gut.

Failover

Bei Umbauten kam es vor, dass die ASAs umschalteten. Das ist beabsichtigt und stellt kein Problem dar.

Die aktive ASA ist einfach das andere Gerät, der Status incl. der IPs und MAC Adressen wechselt zwischen den beiden ASAs und es funktioniert.

asa-portchannel-failover

Man sieht sehr schön, wie die Haupt-MAC des Portchannel wechselt und nun zur aktiven ASA zeigt.

Ein Blick in die ASA selbst bestätigt den Wechsel:

asa# sh interface | in Interface|MAC
Interface GigabitEthernet0/0 "", is up, line protocol is up
        MAC address 00.02.01, MTU 1500
Interface GigabitEthernet0/1 "", is up, line protocol is up
        MAC address 00.02.02, MTU 1500
Interface Port-channel1 "outside", is up, line protocol is up
        MAC address 00.01.01, MTU 1500

und

asa# fail exec mate sh interface | in Interface|MAC
Interface GigabitEthernet0/0 "", is up, line protocol is up
        MAC address 00.02.01, MTU 1500
Interface GigabitEthernet0/1 "", is up, line protocol is up
        MAC address 00.02.02, MTU 1500
Interface Port-channel1 "outside", is up, line protocol is up
        MAC address 00.01.01, MTU 1500

Achtung! Da die Funktion der ASAs gewechselt hat ist jetzt die aktive ASA auf der Hardware des zweiten Geräts. Deswegen haben sich die Hardware-MAC der Interfaces nicht verschoben, wohl aber die Funktion (mate ist jetzt das, was vorher aktiv war).

Probleme

Denkt man genauer über das nach, was da zu sehen ist, bleiben Fragen:

  • Warum wechseln die MAC Adressen der realen Interfaces nicht? Die MACs aller anderen Interfaces wechseln nämlich.
  • Warum erscheinen die MAC Adressen der Interfaces beim Switch? Eigentlich sollte doch nur die MAC des Port-Channels auftauchen.

Tatsächlich gibt es zeitweise Ausfälle, bei denen keine Außenkommunikation mehr möglich ist. Und da sieht es real so aus:

asa-portchannel-failover-fail

Die MAC Adresse des aktiven Port-Channels zeigt wieder auf die alte ASA, die aktuell passiv ist. Und die verwirft natürlich alle Pakete.

Wenn kurze Zeit später die aktive ASA ein Paket aussendet, lernen die Switche wieder um und es funktioniert wieder alles. Bis zum nächsten Vorfall.

Lösung

Ganz offensichtlich sendet eine ASA neben dem Port-Channel auch Daten über die Interfaces selbst aus. Und dabei benutzt sie die nicht wechselnden MAC-Adressen der Interfaces!

Man kann darüber diskutieren, ob es sich um einen Bug handelt. Man kann auch anfangen zu untersuchen, welche Art von Paketen die ASA auf den individuellen Ports eines LACP-Bundles aussendet. Das ist alles spannend, aber nicht zielführend.

Man muss die MAC des Port-Channels von der MAC der Interfaces trennen, und das geht!

asa(conf)# int Po1
asa(config-if)# mac-address 00.01.00 standby 00.02.00

Und das schaut dann so aus:

asa-portchannel-failover-ok1

Cool nicht?!

Die neue MAC taucht separat von den Interface-MACs auf und damit sollte ein Umschalten auch klappen:

asa-portchannel-failover-ok2

Tatsächlich springt nur die virtuelle, manuell gesetzte MAC des Port-Channels um.

Und die Probleme sind weg.

Seit einiger Zeit wird eine Webseite mit offenbar unliebsamen Inhalt, die wir hosten, angegriffen. Nach einem lockeren Anfang kam die inzwischen obligatorische Traffic-Bombe. Und die lies sich erstaunlich leicht entschärfen.

Entwicklung eines DDoS

Dieser Distributed Denial of Service ist eine Protestaktion. Das Ziel ist bekannt, die mögliche Ursache auch, der Initiator des Angriffs allerdings nicht. Also müssen wir mit den Folgen leben.

Zuerst fing es mit einem dauerhaften Abruf der Webseite (F5-Bombe) einiger weniger Quellen an. Da die betroffene Seite statisch ist, war die Last des Webservers zwar hoch, aber nicht bedrohlich.

Als nächstes schwenkte der Angriff auf HTTPS um, was den Server wegen der TLS-Bearbeitung stärker belastete.

Kurz darauf kamen die Anfragen aus aller Welt. Offenbar hatte der Angreifer ein Botnet von übernommen PCs und Webservern zugeschaltet. Nun war es nervig.

Als nächstes kam es zu einer verbesserten Form von Syn-Floods: Der TLS Verbindungsaufbau wurde komplett durchgeführt, aber keine Webseite abgefragt. Das führte zu einem ersten Ausfall der Verfügbarkeit, da nun die Maximalanzahl der Webserver-Prozesse/Threads in der Warteschleife fest saßen.

Die Gegenmaßname war eine Limitierung von TCP-Syns pro Host und Zeiteinheit mit dem recent-Modul von iptables. Zusätzlich wurden die entsprechenden Hosts, die zu viele 408-Einträge im Webserverlog hinterließen, per Script und iptables geblockt (und bei Inaktivität automatisch freigegeben). Selbstverständlich gibt auch der Abruf der angegriffenen Webseite selbst schon Strafpunkte.

Jeden Werktag gegen 16 Uhr kommt eine Verfügbarkeitsabfrage über einen der vielen Ist-Mein-Server-Erreichbar-Dienste. Kurz danach beginnt eine neue Variante des Angriffs. Meist sucht sich der Angreifer eine andere Webseite auf diesem Shared-Hosting-Webserver aus und bearbeitet diese.

Dabei kann der Angreifer auch mal eine Seite erwischen, die PHP und datenbanklastig ist. Das stört sehr, wird aber von dem inzwischen installierten Block-Skript kurzfristig eingefangen.

Und dann knallte es rein: Unmengen von UDP/GRE und anderen verbindungslosen Protokollen schlugen auf die IP ein. Es wurde ernst.

Umgang mit der Traffic-Welle

Glücklicherweise hält die Traffic-Welle immer nur kurze Zeit an. Es genügt aber, im Netz der Uplink-Carrieren Störungen zu verursachen.

Mit dem Einsatz von Paketfiltern (für z.B. UDP) gegen die betroffene IP möglichst weit außen, konnten die Auswirkungen gedämpft werden. Die kurzen Störungen zu Beginn der täglichen Welle blieben aber im Netz der Uplinks.

Zur leichteren Separierung des Traffics bekam die Webseite eine neue IP. Aber welche?

Der Massentraffic kommt i.d.R. von IoT-Geräten: Schlecht gewarteten und vor allem schlecht entwickelten Kleinstgeräten mit Internet-Anschluss. Könnte das ein Ansatzpunkt der Verteidigung sein?

Die meisten IP-Stacks sind inzwischen (1993) classless, d.h. sie müssen zu jeder IP die Netzmaske kennen, damit sie die Pakete routen können. Früher war das anders: Da ergab sich aus der IP die Netzmaske von allein.

Darüber hinaus werden beim Anschluss von IP-Netzen an einer Broadcast-Netztopologie (z.B. Ethernet) bestimmte IP-Adressen reserviert: Die letzte IP im Netz (Hostteil besteht nur aus 1-Bits) ist die Broadcast-Adresse.

Des weiteren gilt die erste Adresse im Netz (Hostteil  besteht nur ais 0-Bits) als Netzadresse. Diese soll nicht verwendet werden, weil einige IP-Stacks die Adressen intern invertiert ablegten und somit Netz- und Broadcast-Adressen nicht auseinander halten konnten. Um mit diesen Hosts weiter arbeiten zu können, wird bis heute generell nicht an die Netzadresse gesendet.

Es war also nahe liegend, als neue IP Adresse die a.b.c.0 aus einem (ehemaligen) Class-C Netz zu verwenden.

Gesagt, getan. Die Traffic-Spitzen sind weg.

Fazit

Es ist gelungen, Angriffe durch kaputte System an der Quelle zu unterbinden. Eben weil die Systeme auch noch anders kaputt sind.

Inzwischen hat sich der TCP-Angriff auch auf IPv6 ausgebreitet, alles vorher war immer nur IPv4.

Warten wir's ab.

Hinweis für Manager: Es nützt nichts zu fragen, ob der Angriff vorbei ist. Die Frage ist sinnlos.

Die Commerzbank verlangt für beleghafte Überweisungen Geld. Das kann ich akzeptieren. Ich habe schon jahrelang am Automaten die Aufträge selbst erfasst. Onlinenbanking hat jahrelang nicht für mich funktoiniert. Jetzt tut's. Und jetzt soll das auch noch kostenpflichtig werden.

Rückblick

Mit der Dresdner Bank war ich als Kunde immer zufrieden, nicht nur, weil ich meine feste Ansprechpartnerin dort kannte.

Kurz vor der Jahrtausendwende kam Online-Banking (in Form von HBCI) auf und ich wollte das auch mal ausprobieren. Also hab ich mich an die Bank gewendet und einen Smartcard-Leser mit seriellem Anschluss per Post bekommen. Als ich den zwei Monate nicht einmal benutzt hatte, rief jemand schüchtern aus dem Rechenzentrum in Frankfurt an: Herr Donnerhacke, ich kenn' Sie ja aus dem Usenet. Sie haben unser Gerät bekommen und nie benutzt. Haben Sie Sicherheitsbedenken oder gar etwas schlimmes gefunden?

Das hatte ich natürlich nicht. Mein Problem war, dass das Teil an einer NeXTstation nicht lief. Aber da konnte er mir nicht helfen. So blieb ich bei beleghafter Kontoführung und dann die Benutzung der Automaten. Da hin zu gehen ist nicht aufwendig, wenn es in der Nähe der Arbeitsstelle ist.

Später ging die Bank in der Commerzbank auf und ich bin mit gegangen. Man ist ja bequem. Einen feste Ansprechpartner brauchte ich nicht mehr, freute mich aber die paar Mal, die ich dort war, immer an die gleiche und kompetente Person zu gelangen.

Irgendwann mochte ich den doch etwas weiteren Fußweg nicht mehr und beantragte wieder mal Online-Banking. Es stellte sich heraus, dass bis auf einen extra PIN/TAN-Brief nichts weiter geschah, als dass man die gleiche Oberfläche wie am Automaten nun auch zu Hause hatte. Intern ist beides Online-Banking nur halt von verschiedenen Zonen aus. Das ist clever und gestattete mir, Online-Banking von außerhalb Deutschlands zu untersagen.

Etwas später wurde TAN durch iTAN und mTAN abgelöst. ChipTAN, wie es die Sparkasse anbietet, war leider nicht im Angebot. Noch etwas später kam PhotoTAN.

Diskriminierung

Und heute lese ich in der Bank-Mitteilung:

Die Commerzbank empfiehlt ihren Kunden im Onlinebanking die Nutzung der photoTAN.
Sie beruht auf modernsten Sicherheitsstandards, ist kostenlos und es 
gilt die Commerzbank Sicherheitsgarantie. 
Mit der photoTAN ist Banking mit dem Smartphone ohne Zusatzgeräte möglich.
Die mobileTAN als bisherige Alternative zur TAN-Generierung kann von Kunden
weiterhin genutzt werden, ist künftig jedoch nicht mehr kostenlos. 
Wechseln Sie jetzt und testen Sie unsere PhotoTAN-App auf www.commerzbank.de

Ab 1.05.2018 werden wir den Versand einer (angeforderten, tatsächlich genutzten)
mobileTAN per SMS mit 0,09 EUR bepreisen.
Sollten Sie mit dieser Änderung nicht einverstanden sein, sprechen Sie uns gern
bis zum 30.04.2018 an. Bis zu diesem Termin haben Sie die Möglichkeit zu widersprechen,
oder das Konto fristlos und kostenfrei zu kündigen. 
Widersprechen oder kündigen Sie nicht, gilt Ihre Zustimmung zu der Vertragsänderung
als erteilt. Das geänderte Preis- und Leistungsverzeichnis kann in unseren 
Geschäftsräumen eingesehen werden und wird auf Wunsch ausgehändigt oder zugesandt.

Hallo? Ich muss ein Smartphone haben und eine Software von Euch installieren, damit ich Euch Kosten spare?

Wenigstens erkennt Ihr, dass Eure Anwendung nicht auf allen Smartphones laufen wird. Ihr schreibt selbst:

Wenn Sie kein Mobiltelefon besitzen, auf dem Sie die Commerzbank photoTAN-App
installieren können, haben Sie die Möglichkeit, bei Anmeldung zur photoTAN
ein preisgünstiges Lesegerät zu erwerben.

Bevor ich jetzt schaue, ob es für mein Handy überhaupt Eure Software gibt (Nein, das ist kein NeXTphone), lese ich nochmal nach, dass ihr bei mTAN die gleichen Sicherheit garantiert wie bei PhotoTAN.

Das ist deswegen witzig, weil ihr bei PhotoTAN schreibt, dass Eure Software das Hauptsicherheitsrisiko implementiert hat:

Auf Ihrem Smartphone können Sie Überweisungen noch schneller und einfacher durchführen
als am Computer. Nutzen Sie dafür die kostenlose Banking-App der Commerzbank.
In Verbindung mit der photoTAN-App entfällt das Scannen der Grafik. Durch eine
ausgeklügelte Technik sind die Apps direkt miteinander gekoppelt.

Selbst Wikipedia weiß:

Werden allerdings beim mobilen photoTAN-Banking sowohl die Banking- wie auch die photoTAN-Funktion via App auf einem einzigen Smartphone zusammengeführt, sodass die photoTAN-App und die Banking-App direkt miteinander kommunizieren, lassen sich die Überweisungen wieder manipulieren. Solche Apps werden zum Beispiel von der Deutschen Bank, der Commerzbank und der Norisbank bereitgestellt.

Und selbstverständlich gibt es die App nur für Android und iOS. Nicht für Windows Phone, Symbian, etc.

So und nun müssen wir reden. Zumindest müsst Ihr auf meinen Widerspruch, der bei Euch eingehen wird, reagieren. Sonst bleibt alles beim Alten. Klar?

Haskell ist derartig faul, dass die gängigen Methoden zur Abschätzung der Programmlaufzeit scheitern. Nach der bahnbrechenden Arbeit von Okasaki bestand aber Hoffnung, dieses Thema in den Griff zu bekommen. Einige der Annahmen dieser Arbeit sind nicht anwendbar, wenn Haskell selbst zum Aufräumen von Zwischenergebnissen zu faul ist. Um selbst einige Ideen auszuprobieren, brauche ich reale Messwerte. Und die will ich jetzt erzeugen.

Profiling

Der klassische Weg mit Performance Problemen umzugehen, ist Profiling. Als Beispiel muss wieder die Fibonacci-Folge herhalten. Und das besonders ineffizient:

fib :: Int -> Int
fib 0 = 1
fib 1 = 1
fib n = a + b
 where
  a = fib (n-1)
  b = fib (n-2)

Ich habe die Zwischen-Terme a und b extra benannt, damit sie im Profiling dann auch auftauchen.

Ich möchte die Zeit messen, die Haskell zur Abarbeitung braucht, dazu lasse ich mir die verstrichene CPU Zeit vor und nach der Abarbeitung geben. Um das Programm mit verschiedenen Werten zu testen, lese ich das Argument der Funktion von der Kommando-Zeile ein.

import System.Environment
import System.CPUTime

main = do
  [arg] <- getArgs
  limit <- readIO arg
  print limit
  start <- getCPUTime
  let result = fib limit
  finish <- getCPUTime
  print (finish - start) -- picoseconds

Nun noch kompilieren und probieren:

$ ghc -O3 -prof -fprof-auto -rtsopts fib.hs
[1 of 1] Compiling Main             ( fib.hs, fib.o )
Linking fib ...
$ fib 20
20
14000000
$ fib 200
200
14000000

Huch? Warum sollte das Programm die gleiche Zeit brauchen, wenn es doch so unterschiedliche Berechnungen zu vollführen hat? Die angegebene Zeit ist in Picosekunden, also stehen da real 14µs. Etwas wenig.

Schauen wir mal in das Profiling:

$ fib 200 +RTS -p
200
5000000
$ cat fib.prof
                                                  individual      inherited
COST CENTRE MODULE             no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                54          0    0.0    1.1     0.0  100.0
 CAF        Main               107          0    0.0    0.1     0.0    2.2
  main      Main               108          1    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding     95          0    0.0    5.5     0.0    5.5
 CAF        GHC.IO.Handle.FD    92          0    0.0   56.5     0.0   56.5
 CAF        GHC.IO.Handle.Text  91          0    0.0    0.1     0.0    0.1
 CAF        Text.Read.Lex       88          0    0.0    1.1     0.0    1.1
 CAF        GHC.IO.Encoding.Ic  83          0    0.0    0.4     0.0    0.4
 CAF        GHC.Conc.Signal     73          0    0.0    1.0     0.0    1.0
 CAF        GHC.Read            68          0    0.0    1.5     0.0    1.5
 main       Main               109          0    0.0   30.5     0.0   30.5

Man sieht sehr schön. dass die übergebene Zahl vom Lexer bearbeitet, Daten ausgegeben wurden. Was aber komplett fehlt, ist jedoch die Abarbeitung der fib-Funktion?

Haskell ist schlicht zu faul, die Berechnung überhaupt auszuführen, wenn sowieso niemand das Ergebnis braucht! Um dem abzuhelfen, muss man das Ergebnis also auch noch ausgeben.

main = do
  [arg] <- getArgs
  limit <- readIO arg
  print limit
  start <- getCPUTime
  let result = fib limit
  finish <- getCPUTime
  print (finish - start)
  print result

Einfach am Ende noch das Ergebnis ausgeben. Sollte tun, oder?

$ fib 35
35
9000000
[ ... Pause ...]
14930352

Das Ergebnis erscheint mit deutlicher Verzögerung nach der Ausgabe. Insofern sind  die angegebenen 9µs definitiv unpassend. Was ist passiert?

Haskell war zu faul, das Ergebnis sofort zu berechnen. Erst als es durch die Ausgabe gezwungen wurde, hat es die Berechnung nachgeholt. Da war die Zeitmessung allerdings schon lange vorbei.

Die Verwendung des Ergebnisses muss also vor dem Ende der Zeitmessung erfolgen!

main = do
  [arg] <- getArgs
  limit <- readIO arg
  print limit
  start <- getCPUTime
  let result = fib limit
  print result
  finish <- getCPUTime
  print (finish - start)

Und das gibt:

$ fib 35 +RTS -p
35
14930352
1354134000000

Jetzt zeigt die Zeitmessung auch passende 1,3 Sekunden. Und was sagt das  Profilinging?

                                                  individual      inherited
COST CENTRE   MODULE                   entries  %time %alloc   %time %alloc

MAIN          MAIN                          0    0.0    1.1   100.0  100.0
 CAF          Main                          0    0.0    0.0     0.0    2.1
  main        Main                          1    0.0    2.0     0.0    2.0
 CAF          GHC.IO.Encoding               0    0.0    5.4     0.0    5.4
 CAF          GHC.IO.Handle.FD              0    0.0   54.6     0.0   54.6
 CAF          GHC.IO.Handle.Text            0    0.0    0.1     0.0    0.1
 CAF          Text.Read.Lex                 0    0.0    1.1     0.0    1.1
 CAF          GHC.IO.Encoding.Iconv         0    0.1    0.4     0.1    0.4
 CAF          GHC.Conc.Signal               0    0.0    1.0     0.0    1.0
 CAF          GHC.Read                      0    0.0    1.5     0.0    1.5
 main         Main                          0    0.0   32.8    99.9   32.8
  main.result Main                          1    0.0    0.0    99.9    0.0
   fib        Main                   29860703   28.3    0.0    99.9    0.0
    fib.a     Main                   14930351   40.8    0.0    40.8    0.0
    fib.b     Main                   14930351   30.8    0.0    30.8    0.0

Die eine Zahl Differenz zwischen a und b macht allein 10% der benötigten Rechenleistung aus. Das ist beachtlich.

Zeitverhalten

Zurück zum urspünglichen Problem: Wie schnell sind Algorithmen in Haskell? Die Frage bezieht sich normalerweise auf die Entwicklung des Zeitverhaltens in Abhängigkeit von der zu verarbeitenden Datenmenge.

Grundsätzlich könnte man das bisherige Programm für jeden Zahlenwert einzeln aufrufen und die Zeiten aufschreiben. Aber das ist aufwändig und kann von Haskell selbst gemacht werden.

Zunächst einmal ist zuverlässig sicher zu stellen, dass der Versuch der Berechnung nicht über alle Zeiten hinauswächst. Die Berechnung muss notfalls abgebrochen werden. Das löst die Funktion System.Timeout.timeout: Sie lässt die Berechnung in parallelen Thread laufen und bricht den notfalls ab. Das ganze Signalisierungsverhalten ist zwar prinzipell einfach, aber die Bibliotheksfunktion löst auch die kleinen Dreckeffekte, über die man selbst noch nicht gestolpert ist.

Anderseits ist der bisherige Rückgabewerte von CPUTime schwer verständlich. Besser wäre ein klarer Wert ins Sekunden. Deswegen wechsle ich hier auf Data.Time.getCurrentTime.

Der dritte Punkt, der zu beachten ist, behandelt die vollständige Evaluierung der Eingabedaten vor der Ausführung der zu testenden Funktion, sowie das Erzwingen der notwendigen Berechnung. Dazu verlange ich zwei Hilfsfunktionen: pre dient dazu, aus dem numerischen Wert der Datenmenge auch brauchbare Ausgangsdaten zu erzeugen, z.B. eine Baumstruktur, über die der Algorithmus dann laufen soll. post dagegen extrahiert aus dem Ergebnis des Algorithmus eine Zahl. Diese Extraktion muss nicht zwingend alle Teile des Ergebnisses auswerten, es genügt z.B. die Länge eines Rückgabestrings zu ermitteln, wenn es sich um einen Algorithmus zur Stringverarbeitung handelt, schließlich sind die konkreten Inhalte des Strings dabei nicht relevant.

Die Eingabedaten werden komplett angezeigt, um alle Details zu evaluieren. Zum Erzwingen der Auswertung an der richtigen Stelle werden die Zahlenwerte auf positive Wertebereiche per Control.Monad.guard geprüft.

import Data.Time
import Control.Monad

profile :: (Show a) => (a -> b) -> (Int -> a) -> (b -> Int) -> Int
        -> IO (Maybe NominalDiffTime)
profile test pre post n = do
  let input = pre n
  guard $ 0 <= length (show input) -- fully evaluate before measuring
  timeout 5000000 (do
      start <- getCurrentTime
      let output = test input
      guard $ 0 <= post output -- evaluate the relevant part
      finish <- getCurrentTime
      return $ finish `diffUTCTime` start
    )

Nochmal in Worten:

  • Die Funktion pre macht aus einem Zahlenwert die Datenstruktur a, auf der der Algorithmus arbeiten soll.
  • Die Funktoin test führt den Algorithmus aus, in dem sie als Argument eine Datenstruktur a übergeben bekommt und eine Datenstruktur b zurück liefert.
  • Die Funktion post presst aus der Ergebnisstruktur b einen positiven Zahlenwert heraus, wobei sie die Ausführung des Algorithmus erzwingt.
  • Klappt alles, erhalten wir die Zeit, die die Funktionen test und post benötigt haben.
  • Die Berechnung ist mit einem Timeout von 5 Sekunden umschlossen: Endet die Berechnung nicht rechtzeitig gibt es Nothing.

Mit einer solche Mess-Funktion kann man nun eine Mess-Reihe automatisch erzeugen:

mkProfile :: (Show a) => (a -> b) -> (Int -> a) -> (b -> Int) -> IO ()
mkProfile test pre post = worker (profile test pre post) testpoints
 where
  testpoints = [ 1 .. 9 ]
            ++ [ x*10^c | c <- [0 .. ], x <- [10 .. 99] ]
  worker f (x:xs) = do
    r <- f x
    case r of
      (Just y) -> do putStrLn . shows x . showChar ',' . init $ show y
                     when (y < 5) $ worker f xs
      _        -> putStrLn "# Timed out"

Für eine Reihe von Testpunkten, die immer größere Abstände annehmen, wird die Laufzeit ermittelt. Da die Darstellung der Laufzeit ein finales "s" für Sekunden am Ende hat, wird es per init abgeschnitten. Der Rest der Ausgabe ist ein CSV.

Die Messung wird fortgeführt, wenn überhaupt ein Ergebnis ermittelt werden konnte (kein Timeout) und die Laufzeit die 5 Sekunden nicht überschreitet. Die Timeout-Funktion garantiert nur, dass der Abbruch nicht vor 5 Sekunden stattfindet. Länger kann es durchaus dauern, da die typische Haskell-Runtime kooperatives Multitasking betreibt.

Und was sagt das zu unserer Fibonacci-Folge?

main = mkProfile fib id id

Die Hauptfunktion ist denkbar einfach: Erstelle ein Profil der fib-Funktion, wobei die Eingabemenge aus der Zahl besteht, die übergeben wird. Das Ergebnis ist ebenfalls eine Zahl, die so übernommen wird wie sie ist.

image005

Das ist klar expotentielles Wachstum.

Damit steht nun ein Handwerkszeug zur Verfügung, um andere - interessantere Algorithmen - zu untersuchen.

Nachbetrachtung

Natürlich würde man die Fibonacci-Folge in Haskell niemals so schreiben, sondern eher so:

fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
fib1 x = fibs !! x

main = mkProfile fib1 id abs

Die Fibonacci-Folge besteht aus den Werten 1, 1 und dann der Summe der Fibonacci-Werte beginnend am Anfang und um ein Element versetzt. Die Liste ist unendlich lang, aber dank des faulen Haskells besteht da kein Problem: Denn nur der x-te Wert wird dann heraus genommen.

Da wir mit Int rechnen, gibt es Überläufe während der Rechnung. Anstatt auf den korrekten Typ Integer auszuweichen, nehme ich hier einfach den Absolutwert als Ergebnis. Es geht ja nicht um die Berechnung der Werte an sich, sondern um die Art der Ermittlung (also den Algorithmus).

Und das ergibt dann?

image007

Das ist spannend! Mit einigen Ausreißern geht die Berechnung fast linear durch und zwar in die zig Millionen, statt bis 42.

Beobachtet man allerdings den RAM-Verbrauch während der Abarbeitung, so steigt dieser bis zu 4 GB an (mehr gebe ich dem Prozess hier nicht). Dann bricht die Abarbeitung ab. Die Stellen, bei denen die Laufzeit so stark ansteigt, entspricht den Stellen, wo das System massiv nach RAM verlangt.

Was ist passiert?

Die Definition von fibs ist global, d.h. der Wert steht immer zur Verfügung. Anders gesagt, verwirft Haskell die ermittelten Werte des Feldes nie wieder. Deswegen steigt der RAM-Verbrauch immer weiter an. Am Ende sind über 40 Mio. Werte berechnet worden. Incl. Overhead für die verkettete Liste sind das also ungefähr 80 Byte pro Eintrag.

Andererseits führt das zu den niedrigen Laufzeiten, da ja alle Werte schon berechnet wurden.

Alle Werte? Nein, die Schrittweite steigt ja immer um eine Größenordung. Damit müssen auch immer mehr Zwischenwerte berechnet werden, um auf das nächste Ergebnis zu kommen. Dies erklärt die sprunghaft steigende Abarbeitungszeit, die mit der größeren Schrittweite korreliert.

Was wäre nun, wenn man korrekterweise nicht mit den übrig gebliebenen Berechnungen des vorherigen Versuchen zu betrügen versucht?

fib2 x = fib' !! x
  where fib' = 1 : 1 : zipWith (+) fib' (tail fib')

Hier werden die Zwischenergebnisse in einer lokalen Variablen gehalten, die außerhalb des Aufrufs keine Bedeutung hat. Es besteht sogar die Hoffnung, dass die nicht benötigten Zwischenergebnisse direkt verworfen werden, und so der RAM-Bedarf nicht exorbitant steigt.

image007

Zwei Dinge sind auffällig:

  • Der RAM-Bedarf steigt wieder so an, das Programm hält aber länger durch, wenn die Grenze erreicht ist.
  • Das Laufzeitverhalten ist ähnlich, erreicht aber deutlich höhere Werte.

Was ist passiert?

Haskell erkennt durchaus, dass die Definition von fib' unabhängig vom Aufrufparameter ist und benutzt diese Tatsache zu dem Memorization-Trick, der bei fibs noch explizit aufgeschrieben wurde. Das führt zu dem ähnlichen Verhalten bis zu 40 Mio. Danach muss (und kann) Haskell die Zwischenergebnisse immer wieder verwerfen, um weiter arbeiten zu können.

Kann man Haskell den Algorithmus expliziter vermitteln? Schließlich ist es ja wenig elegant, eine Liste von uninteressanten Werten jedesmal von Anfang an zu durchlaufen (was schonmal minimal lineare Laufzeit generiert)

fib3 x = head $ drop x fib'
 where fib' = 1 : 1 : zipWith (+) fib' (tail fib') 

Der einzige Unterschied ist hier, dass statt dem (!!) Operator, der eine Liste bis zum x-ten Element durchläuft, der Anfang der Liste weggeworfen wird, um dann das erste Element zu nehmen.

image007

Das gleiche Bild, allerdings sind die Laufzeiten kürzer und die Abarbeitung erfolgt glatter. Es ist nicht so erratisch. Das exorbitante RAM-Bedarf ist allerdings noh da.

Also muss auf die Liste als solche verzichtet werden.

fib4 x = fib' x (1,1)
 where
  fib' 0 (a,_) = a
  fib' n (a,b) = fib' (n-1) (b,a+b) 

Dieses Programm nimmt den Kern der zipWith (+) Funktion explizit her und zählt parallel rückwärts. Es sammelt die Werte gar nicht erst auf, sondern verwirft sie sofort.

image007

Der RAM-Bedarf ist diesmal konstant. Das ist allerdings erwartungskonform.

Eine echte Überraschung ist allerdings die Laufzeit dieses Algorithmus:

  • Die Laufzeit ist praktisch linear mit 2 Sekunden je 10 Mio.
  • Allerdings sind die anderen Algorithmen durch die Bank deutlich schneller als die direkte Berechnung der Werte.
Laufzeit im Vergleich
Mio fib1 fib2 fib3 fib4
10 53ms 53ms 52ms 1750ms
20 192ms 189ms 189ms 3714ms
30 349ms 264ms 238ms  
40 321ms 361ms 298ms  
50   839ms 339ms  
60   366ms 365ms  

Fazit

Die von Haskell angewendeten Methoden der Memorization gestatten es, perfomante Algorithmen zu schreiben, die im Gegensatz zu den klassischen Algorithmen geschickter betrügen. Man muss bei Performance Messungen also höllisch aufpassen.

Wir haben eine Reihe von ASAs im Einsatz, die auf Kundenwunsch gepflegt werden. Wie immer betreffen Änderungswünsche nur die Einrichtung von Freischaltungen, aber nie das Wegräumen von Altlasten. Für diesen Fall gibt es zwei Tools, die ich vorstellen möchte.

Information des Kunden

Zunächst ist es wichtig, den Kunden regelmäßig über die aktuelle Konfiguration zu informieren. Trivialerweise würde man ihm die Konfiguration zugänglich machen.

Aber das ist eine schlechte Idee, da die ASAs einiges an Konfiguration enthalten, die man nicht ständig durch die Gegend reichen möchte. Es gibt auch genug Fälle, wo eine ASA mehrere Kunden bedient (z.B. mehrere Hosting-VLANs). Dann darf natürlich keine Information über die Konfiguration für andere Kunden leaken.

Es ist also notwendig, sich auf die wesentlichen und interessanten Teile zu beschränken. In diesem Fall interessiert den Kunden, welche ACLs und welche NAT Regeln für ihn aktiv sind. Was den Kunden interessiert, hängt von den Interfaces ab, die ihm zugeordnet sind.

Das Script mkacl-from-asaconfig.pl liest auf Standardeingabe eine komplette abgespeicherte ASA-Konfig ein, erwartet als Argument die Liste der interessanten Interfaces (nameif) und gibt aus:

  • die den Interfaces zugeordneten access-groupKommandos
  • die den access-group zugeordneten access-list Einträge
  • die den Interfaces zugeordneten nat Kommandos
  • die dabei referenzierten object-group Definitionen
  • die dabei referenzierten object Definitionen

Ganz praktisch sieht das dann so aus:

$ zcat /archiv/cf.asa.20180111.gz | mkacl-from-asaconfig.pl iks-lab
: Written by enable_15 at 11:13:55.766 CET Thu Jan 11 2018
!
!!!!!!
! active ACLs at interfaces
!!!!!
access-group to_lab out interface iks-lab
!!!!!
! ACLs
!!!!!
access-list to_lab extended permit ip object-group iks any 
access-list to_lab extended permit tcp any object obj-lab1 object-group http_s 
access-list to_lab extended permit tcp any object obj-lab2 object-group http_s 
!
!!!!!!
! extra NATs
!!!!!
!!!!!
! Object groups
!!!!!
object-group network iks
 network-object object o4-iks
 network-object object o6-iks
!
object-group service http_s tcp
 port-object eq www
 port-object eq https
!
!!!!!
! Objects
!!!!!
object network obj-lab1
 host 192.168.91.10
 nat (iks-lab,outside) static o4-lab1-public
!
object network obj-lab2
 host 192.168.91.11
 nat (iks-lab,outside) static o4-lab2-public
!
...
!
!!!!!
! Generated at Thu, 11 Jan 2018 13:35:03 +0100

Diese Auszüge können dem Kunden auf definiertem Wege (z.B. Kundenportal) zur Verfügung gestellt werden.

Es gestattet dem Kunden, sich selbst die aktuelle Konfiguration anzusehen und entlastet die Hotline bei uns.

Inkonsistenzen

Ein ganz anderer Teil des Problem sind Inkonsistenzen und Altlasten. Die Definition eines Objektes, das nicht verwendet wird, ist eine Inkonsistenz. Die Verwendung einer IP Adresse, die nicht mehr in Benutzung ist, ist eine Altlast. Beides soll gefunden werden.

Wie eine Konfiguration auszusehen hat, ist selbstverständlich Ansichtssache. Bei uns hat es sich als günstig herausgestellt, sämtliche IP Adressen in Objekten und Objektgruppen zu sammeln, damit diese schon allein anhand ihrer Benennung oder description auch noch Jahre später zugeordnet werden können. Nur mit klarer Zuordnung ist ein späterer Rückbau überhaupt möglich.

Das Script clean-asa-config.pl liest von der Standardeingabe eine komplette Konfigurationsdatei und listet auf, was unstimmig erscheint:

  • Nicht mehr benutzte access-list, object, object-group. name, tunnel-group, class-map, transform-set, crypto map, ...
  • Elemente, die verwendet werden, aber nicht definiert wurden, werden aufgelistet.
  • Es wird das Anlegen von object oder name Definitionen empfohlen, wenn IPs direkt in der Konfiguration stehen.
  • Ist bei Objekten ein DNS-Name zur IP hinterlegt, so wird geprüft, ob der Name noch zu dieser IP auflöst. Es ist nicht empfehlenswert, direkt mit fqdn statt host zu arbeiten, wenn man feststellen will, ob die intendierte Eintragung noch korrekt ist. fqdn sollte man nur bei dynamischen IPs benutzen.
  • Bei name Definitionen wird geprüft, oben IP und Name noch per DNS zusammen passen.
  • Da name Definitionen agnostisch gegenüber der Protokollfamilie sind, wird für IPv4 ein trailing dot am Namen empfohlen, bei IPv6 nicht.
  • Ausgelaufene Zertifikate führen zur Empfehlung, den trust-point zu löschen.
  • Ebenso soll man unvollständige crypto map Einträge löschen.

Die Ausgabe enthält:

  • die Empfehlung incl, Begründung
  • die direkt ausführbaren Kommandos, um die Empfehlung umzusetzen, ohne den Betrieb zu gefährden
  • alle anderen Verwendungen des Namens oder der IP in der Konfiguration

Wenn Euch etwas auffällt, was man noch testen sollte oder könnte ...

VPNs dienen der geschützten Kommunikation, entsprechend einfach sollte ihre Einrichtung sein. Unglücklicherweise ist aber schon die Festlegung, was genau schützenswert ist, Ergebnis einer komplexen Abstimmung. Deswegen werden zunehmend VPNs global aufgemacht und nur bei Bedarf genutzt. Ein striktes Gerät, wie die ASA, mag das gar nicht. Ein Endkundengerät lieb es …

IPSec klassisch

Der klassische Ansatz ist es, für jedes Paar von geschützten Netzen einen Tunnel anzulegen, der je nach Bedarf aufgebaut und abgebaut werden kann. Diese Tunnel nennt man Security Association, sie definieren sich über die IP-Adressen der beteiligten Netze.

vpn-policy

Durch die dynamischen Tunnel muss jedes beteiligte Gerät wissen, welche Paare von Netzen zu schützen sind und notfalls einen Tunnelaufbau initiieren. Es ist unzulässig, Pakete unverschlüsselt zu übertragen, wenn die Policy Verschlüsselung vorschreibt.

Man kann eine solche Policy also auch so verstehen, das es eine ACL gibt, die festlegt, welcher Datenverkehr verschlüsselt ist oder nicht. Konsequenterweise enthalten die Tunnel dann auch Protokoll und Portnummern, wenn nicht rein auf IP Ebene selektiert wird.

Umgekehrt wird die Policy-ACL auch bei eingehendem Verkehr angewendet. Was laut Policy verschlüsselt zu sein hat, muss verschlüsselt ankommen. Pakete, die am VPN vorbei eintrudeln, werden kommentarlos verworfen.

Das offenkundige Problem mit dem Ansatz ist der Koordinierungsaufwand:

  • Beide Seiten müssen sich über die exakten Policy-ACLs klar sein.
  • Änderungen sind nur beidseitig und gleichzeitig möglich.

IPSec simplified

Die offenkundige Verbesserung besteht darin, dem Tunnel nicht mehr mit den IP-Adressen zu verknüpfen. Stattdessen wird ein generischer Tunnel aufgebaut, der beliebige Datenpakete verschlüsselt transportieren kann.

Jede Seite legt für sich fest, welche Ziele durch den Tunnel und welche unverschlüsselt übertragen werden sollen.

vpn-routing

Da man die Entwicklung eines neuen Protokolls scheute, wird ein klassischer IPSec Tunnel genutzt, der jedoch maximal breit arbeitet: Beliebige Quell- und Ziel-Adressen passen in den Tunnel.

Selbstverständlich kann man nun nicht mehr mit (Policy-)ACLs arbeiten, denn dann müsste sämtlicher Datenverkehr zwangsweise in den Tunnel geschoben werden. Stattdessen verzichtet man komplett auf die ACLs und routet die Ziel-Adressen entweder in den Tunnel oder ins Internet.

Auf diese Weise können Tunnel nach Bedarf auf jeder Seite einzeln konfiguriert werden: Es gibt einen existenten Tunnel und da wirft man rein, was wichtig scheint. Die Gegenseite muss nicht mehr informiert werden.

Eine Konsequenz aus dem Verzicht auf die ACLs, ja auf den Verzicht auf Abstimmung, ist, dass das empfangende Gerät nicht mehr feststellen kann, ob das ankommende Paket verschlüsselt hätte sein sollen oder nicht. Bei Fehlkonfigurationen kann ein Teil des Traffics unverschlüsselt laufen.

vpn-routing-fail

Die Vorteile liegen auf der Hand:

  • Einfaches Setup (nur Gegenstelle und Schlüssel notwendig)
  • Verschlüsselung nach Bedarf (des Senders) beliebig erweiterbar
  • Bei Fehlkonfiguration bleibt die Funktion bestehen, nur die Sicherheit ist weg.

Mischbetrieb

Der vereinfachte Ansatz ist gerade im Endkundenbereich beliebt, weil sich der Kunde so schnell seine Lan-Kopplung zusammen klicken kann. Der striktere Ansatz ist eher im Firmenumfeld zu finden, wo man traditionell nur mit abgestimmten Konfigurationen zu tun hat.

In meinem Fall habe ich nun ein Endkundengerät (CPE), das ausschließlich nach der vereinfachten Methode arbeitet, und eine ASA, die ausschließlich nach der traditionellen Methode konfiguriert werden will.

vpn-mixed-fail

Aufgrund der Policy-ACL würde eine ASA keinen unverschlüsselten Traffic annehmen, wenn der durch den Tunnel kommen müsste. Das will ich nicht aufgeben.

An der CPE sind die Wechselmöglichkeiten zu beschränkt, um überhaupt einen anderen Typ von Verbindung hinzubekommen. Also konzentriere ich mich auf das eigentliche Problem: Den VPN Tunnel und die dazu passende Policy-ACL.

vpn-mixed

Da die CPE den Tunnel vorgibt, muss er offensichtlich ein 0.0.0.0/0.0.0.0 Tunnel werden. Das führt zur sofortigen Funktionsunfähigkeit der ASA.

Schaut man genauer auf die ACLs, so stellt man fest, dass diese mehrfach benutzt werden:

  • Jedes Paket, dass von der ACL erlaubt wird, kommt in den Tunnel oder muss daraus kommen.
  • Jedes Paket, dass von der ACL abgelehnt wird, darf nicht in den Tunnel oder daraus kommen.
  • Die Tunnel-Konfiguration ergibt sich aus den Permit-Regeln der ACL.

Der Trick besteht also darin, allen Traffic abzulehnen, der nicht in den Tunnel soll, um anschließend alles zuzulassen.

access-list vpn deny ip object-group og-not-local any 
access-list vpn deny ip any object-group og-not-remote
access-list vpn permit ip any any 

Die Objektgruppen, die den Kehrwert der eigentlich zulässigen Netze bilden, sind am einfachsten aus zwei Bereichen zu bauen, die bis an das gewünschte Netz ran gehen.

object-group network og-not-local
 network-object object o-not-local-higher
 network-object object o-not-local-lower

object network o-not-local-lower
 range 0.0.0.0 198.51.99.255
object network o-not-local-higher
 range 198.51.101.0 255.255.255.255

In gleicher Weise werden die inversen Bereiche für das Remote-Netz definiert.

Es kann nun noch vorkommen, dass die ASA sich zickig hat beim annehmen des Verbindungsaufbaus. Dann genügt es die Basisadressen der Netze zu erlauben.

access-list vpn line 1 perm ip host 0.0.0.0 host 0.0.0.0

Tut.

Eine Appliance, die in einer fremden Umgebung Daten einsammelt, soll von extern angesprochen werden. Zum einen muss der Hersteller Fernwartung machen und zum anderen gibt es eine Gruppe von Stellen, die auf die gesammelten Daten zugreifen sollen. Völlig unerwartet wird das geschützte Netzwerk dieser Stellen ebenfalls mit öffentlichen IP Adressen betrieben, so dass für beide Zwecke eine Default Route benötigt wird. Das Ganze soll trotzdem nur mit einer ASA funktionieren. Eine Aufsplittung in mehrere Kontexte (ASA-Virtualisierung) scheitert am fehlenden Support für Remote-Access. Es muss also anders gehen.

Aufgabenstellung

Die Appliance arbeitet in einem anderen, größeren Netzwerk und benutzt selbst nur private IP Adressen (hier 192.0.2.x). Um alle Messstellen erreichen zu können, zeigt die Default Route dieses Gerätes in das fremde Netzwerk (hier grau).

Der Remote-Access Teil (hier grün) ist ein einfaches Setup. Um die vorab unbekannten Clients erreichen zu können, muss die Default Route ins Internet zeigen. Dieser Uplink der ASA benutzt öffentliche IPs (PA-space, hier 189.51.100.x). Die Remote-Access Clients bekommen aus einem Adresspool des lokalen Netzes IPs, so dass sie ohne weiteres Routing auf die Appliance zugreifen können. Von Seiten der Appliance aus erscheint der Client im LAN.

Die ursprüngliche Annahme war, dass die abfragenden Stellen, die über ein extra gesichertes Netzwerk zugeführt werden, ebenfalls in koordinierter Weise mit nicht-öffentlichen Adressen arbeiten würden. In diesem Fall hätte eine spezifische Route zu all diesen Netzen genügt. Allerdings haben die Planer dieses Netzes den Koordinationsaufwand gescheut und verlangen stattdessen, öffentliche IP-Adressen aus dem lokalen PA-Space zu benutzen, der auch normal über Internet erreichbar ist.

ASA multiple default routes

Die Gegenstellen hinter dem geschützten Netzwerk (hier rot) sind vorab ebenso wenig bekannt, wie die Fernwartungs-Clients (grün). In dem hier skizzierten Bild, soll eine solche Gegenstelle eine ganz andere öffentliche IP Adresse haben (hier 203.0.113.133). Um solche Adressen zu erreichen, müsste eine Default Route ins rote Netz gesetzt werden.

Angefasst werden darf nur die ASA, alle anderen Geräte unterliegen fremder Verfügungsgewalt.

Remote-Access

Die Konfiguration für das Remote-Access VPN gemäß Lehrbuch:

interface Ethernet0/0
 nameif green
 security-level 0
 ip address 198.51.100.25 255.255.255.192
!
interface Ethernet0/1
 nameif gray
 security-level 100
 ip address 192.0.2.5 255.255.255.0
!
route green 0.0.0.0 0.0.0.0 198.51.100.1 1
!
ip local pool vpnippool 192.0.2.101-192.0.2.102 mask 255.255.255.0
!
crypto map vpn_outside 999 ipsec-isakmp dynamic vpn_dyn
crypto map vpn_outside interface green
...

Erwartungsgemäß tut das seinen Dienst:

  • Der entfernte Supportmitarbeiter kann per Default Route erreicht werden.
  • Der entfernte Supportmitarbeiter bekommt über das VPN eine IP aus dem inside-LAN.
  • Die Appliance redet mit ihm über das lokale Netzwerk. Extra Routen werden nicht benötigt.

Einmal hin ...

Um die Stellen, die mit öffentlichen IP Adressen aus dem geschützten Netz kommen, versorgen zu können, sind einige Überlegungen notwendig.

Zuerst fällt auf, dass der Verbindungsaufbau bei unbekannten Gegenstellen nur von extern kommen kann. Die Clients werden ausschließlich eine öffentliche IP au dem Netz 198.51.100.248/29 zugreifen können. Von dem privaten Netz hinter der ASA wissen sie nichts.

Damit die Appliance nichts von den fremden Adressen merkt, braucht es eine Doppel-NAT:

interface Ethernet0/2
 nameif red
 security-level 0
 ip address 198.51.100.254 255.255.255.248
!
object network o-server
 host 192.0.2.4
!
nat (red,green) source dynamic any interface destination static interface o-server

Eingehende Pakete über das rote Interface von beliebiger Quelle an die Interface-IP werden so genanntet, dass die Quell-IP zur grünen Interface-Adresse und die Zieladresse, die der Appliance wird. Damit sieht die Appliance nur Anfragen aus ihrem LAN.

Um zu testen, dass diese Idee auch tut, wird ein Testaufbau erstellt:

ASA multiple default routes Test

Anstelle des roten Netzwerkes, steht ein Linux-Rechner, den man passend konfiguriert:

# ip addr add 198.51.100.254/29 dev eth_rot
# ip addr show eth_rot
1: eth_rot: <BROADCAST,MULTICAST,UP,10000>
    inet 198.51.100.254/29 scope global eth_rot
       valid_lft forever preferred_lft forever 

Zusätzlich bekommt er die fremde IP Adresse. Damit er diese auch als Quelladresse benutzt, gibt es eine spezielle extra Route:

# ip addr add 203.0.113.133/32 dev lo
# ip route add 198.51.100.249/32 src 203.0.113.133 dev eth_rot

Wenn man testweise versucht eine Verbindung zur Appliance herzustellen, scheint es erst einmal zu tun.

# ssh 198.51.100.249
15:42:02  IP 203.0.113.133.35158 > 198.51.100.249.22: S 3034099253:3034099253(0)
15:42:03  IP 203.0.113.133.35158 > 198.51.100.249.22: S 3034099253:3034099253(0)
15:42:05  IP 203.0.113.133.35158 > 198.51.100.249.22: S 3034099253:3034099253(0)

Wie man sieht, geht die Anfrage mit der richtigen Absende-IP raus.

Allerdings kommt keine Antwort zurück. Aber warum?

... und zurück

Ein Blick auf die ASA zeigt, das die Datenpakete korrekt nattet und sogar Antworten bekommt.

 16: 15:42:02   192.0.2.5.35158 > 192.0.2.4.22: S 3034099253:3034099253(0) win 14600
 17: 15:42:02   192.0.2.4.22 > 192.0.2.5.35158: S 2009198346:2009198346(0) ack 3034099253 win 14480
 18: 15:42:03   192.0.2.5.35158 > 192.0.2.4.22: S 3034099253:3034099253(0) win 14600
 19: 15:42:03   192.0.2.4.22 > 192.0.2.5.35158: S 2009198346:2009198346(0) ack 3034099253 win 14480
 20: 15:42:05   192.0.2.5.35158 > 192.0.2.4.22: S 3034099253:3034099253(0) win 14600
 21: 15:42:05   192.0.2.4.22 > 192.0.2.5.35158: S 2009198346:2009198346(0) ack 3034099253 win 14480

Warum kommen die Datenpakete nicht wieder zurück? Weil die Default Route fehlt!

Glücklicherweise ist die ASA kein Router. Genauer gesagt die Routingentscheidung wird in zwei Schritten vorgenommen:

  • Steht das ausgehende Interface nicht fest, so wird die generische Routingtabelle befragt, um das Interface zu ermitteln.
  • Ist allerdings klar, auf welchem Bein ausgesendet werden muss, so wird nur der Teil der Routingtabelle betrachtet, der diesem Interface zugeordnet ist.

Eine der Konsequenzen aus diesem Ansatz erklärt die seltsame Anforderung, bei jeder statische konfigurierten Route immer das Interface mit angeben zu müssen: Um so eine schräge Logik umsetzen zu können, muss man erst mal die Angaben dafür haben.

In diesem Fall genügt es also eine weitere Default Route einzutragen, die eine höhere Metrik hat. Dies stellt sicher, dass die ASA keine zwei gleichberechtigten Routen erhält, mit denen sie nicht umgehen kann. Die Situation für die ASA bleibt also eindeutig.

(conf)# route red 0.0.0.0 0.0.0.0 198.51.100.254 2

An der Standard-Routingtabelle ändert sich damit ja nichts:

C    198.51.100.248 255.255.255.248 is directly connected, red
C    198.51.100.0 255.255.255.192 is directly connected, green
C    192.0.2.0 255.255.255.0 is directly connected, gray
S*   0.0.0.0 0.0.0.0 [1/0] via 198.51.100.1, green

Ganz anders ist nun aber die Situation für die Pakete dar, die aus dem (reverse)-NAT heraus fallen.

  • Der NAT Eintrag enthält die Quell- und Ziel-Interfaces, sowie die IP-Adressen und Ports.
  • Pakete bestehender Flows werden also nicht normal behandelt, sondern anhand des vorhandenen NAT-Eintrags.
  • Damit umgehen sie sämtliches konfiguriertes Routing, ACLs etc. insbesondere die Standard-Routingtabelle.
  • Da das Zielinterface fest steht, greift nur noch die Routingtabelle dieses Interfaces, also die niedrig priorisierte Default Route.

Und siehe an:

# ssh  198.51.100.249 
16:38:20  IP 203.0.113.133.51310 > 198.51.100.249.22: S 3979383937:3979383937(0)
16:38:20  IP 198.51.100.249.22 > 203.0.113.133.51310: S 3186400021:3186400021(0) ack 3979383938
16:38:20  IP 203.0.113.133.51310 > 198.51.100.249.22: . ack 1 win 115
16:38:20  IP 198.51.100.249.22 > 203.0.113.133.51310: P 1:22(21) ack 1 win 114
16:38:20  IP 203.0.113.133.51310 > 198.51.100.249.22: . ack 22 win 115
16:38:20  IP 203.0.113.133.51310 > 198.51.100.249.22: P 1:21(20) ack 22 win 115
16:38:20  IP 198.51.100.249.22 > 203.0.113.133.51310: . ack 21 win 114
 136: 16:38:20    192.0.2.5.51310 > 192.0.2.4.22: S 1381971035:1381971035(0) win 14600
 137: 16:38:20    arp who-has 192.0.2.5 tell 192.0.2.4 
 138: 16:38:20    arp reply 192.0.2.5 is-at e8:b7:48:fd:89:81 
 139: 16:38:20    192.0.2.4.22 > 192.0.2.5.51310: S 2009198346:2009198346(0) ack 1381971036 win 14480
 140: 16:38:20    192.0.2.5.51310 > 192.0.2.4.22: . ack 2009198347 win 115
 141: 16:38:20    192.0.2.4.22 > 192.0.2.5.51310: P 2009198347:2009198368(21) ack 1381971036 win 114
 142: 16:38:20    192.0.2.5.51310 > 192.0.2.4.22: . ack 2009198368 win 115
 143: 16:38:20    192.0.2.5.51310 > 192.0.2.4.22: P 1381971036:1381971056(20) ack 2009198368 win 115
 144: 16:38:20    192.0.2.4.22 > 192.0.2.5.51310: . ack 1381971056 win 114
 145: 16:38:20    192.0.2.5.51310 > 192.0.2.4.22: P 1381971056:1381971848(792) ack
 146: 16:38:20    192.0.2.4.22 > 192.0.2.5.51310: . ack 1381971848 win 126

Hurra!

Nachdem Google mit dem Public-DNS Dienst unter 8.8.8.8 zum Standard in allen Fragen der DNS-Problembehebung geworden ist, springen nun immer mehr Firmen auf. Aktuell treibt 9.9.9.9 die Sau durch Dorf. Dabei generiert diese Idee massive Probleme, wettbewerbsrechtlich und technisch.

Wettbewerb

Zunächst gibt es eine massive Beschränkung von leicht merkbaren IP-Adressen. Der IPv4 Adressraum läßt nur 220 Adressen der Form x.x.x.x zu.

Davon benutzen schon einige die IPs für DNS Dienste:

  • 8.8.8.8 PTR google-public-dns-a.google.com.
  • 9.9.9.9 PTR dns.quad9.net.
  • 75.75.75.75 PTR cdns01.comcast.net.
  • 79.79.79.79 PTR public-dns-a.as9105.net.
  • 99.99.99.99 PTR dnsr4.sbcglobal.net.
  • 108.108.108.108 PTR 108-108-108-108.pools.spcsdns.net.
  • 114.114.114.114 PTR public1.114dns.com.

Einige benutzen die IPs für ihre eigenen Nameserver:

  • 77.77.77.77 PTR ns3.hiweb.ir.
  • 93.93.93.93 PTR ns.ngenix.net.
  • 199.199.199.199 PTR NS1.Shane.co.
  • 208.208.208.208 PTR ns1648.ztomy.com.

Andere haben eigene Ideen:

  • 16.16.16.16 PTR ldtools.gre.hp.com.
  • 23.23.23.23 PTR select.zone.
  • 76.76.76.76 PTR lo0-rtc-svw.nco.riseb.net.
  • 145.145.145.145 PTR Multicast-RendezvousPoint.surf.net.
  • 192.192.192.192 PTR medmgmt-192.tajen.edu.tw.

Der Rest macht sich gar keine Gedanken und gibt die IPs an Kunden raus. 69 davon haben einen entsprechenden Reverse-DNS Eintrag.

Nehmen wir einfach mal an, es liese sich mit diesen Adressen ein DNS-Geschäft machen. Dann kommt die Frage auf, ob diese extrem knappe Ressource gerecht verteilt ist und zukünftige Marktteilnehmer mit ihren innovativen Idee auch die Chance auf Teilnahme bekommen können.

Die Antwort der zuständigen (nationalen) Wettbewerbsbehörde liegt auf der Hand: Natürlich nicht.

Damit bedürfen diese Adressen der Regulierung bzw. der Versteigerung wie bei Funkfrequenzen.

Eigenbedarf

Die offenkundigste Schlussfolgerung des Theaters um 9.9.9.9 ist, selbst weiter zu machen. Ich habe kurzerhand die IP 10.10.10.10 als DNS-Anycast konfiguriert und in Betrieb genommen.

$ dig AAAA lutz.donnerhacke.de @10.10.10.10 +dnssec +multiline
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 727
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 4, AUTHORITY: 3, ADDITIONAL: 1

;; ANSWER SECTION:
lutz.donnerhacke.de.    CNAME   pro.donnerhacke.de.
lutz.donnerhacke.de.    RRSIG   CNAME 5 3 57600 ...
pro.donnerhacke.de.     AAAA    2001:4bd8:1:1:209:6bff:fe49:79ea
pro.donnerhacke.de.     RRSIG   AAAA 5 3 57600 ...

;; AUTHORITY SECTION:
donnerhacke.de.         NS      avalon.iks-jena.de.
donnerhacke.de.         NS      broceliande.iks-jena.de.
donnerhacke.de.         RRSIG   NS 5 2 57600 ...

;; Query time: 11 msec
;; SERVER: 10.10.10.10#53(10.10.10.10)
;; WHEN: Fri Nov 17 21:04:34 CET 2017
;; MSG SIZE  rcvd: 672

Die Antwort kommt mit dem AD-Flag, das eine erfolgreich mit DNSSEC validierte Auskunft bekundet. Damit ist schon mal Sicherheit abgehakt.

Da 10.0.0.0/8 ein privater IP-Bereich ist, der nicht öffentlich erreicht werden kann, ist ausgeschlossen, dass der Kunde einen Nameserver irgendwo im Internet befragt. Stattdessen befragt er einen nächstgelegenen Server im Zuständigkeitsbereich seines ISP. Er hat damit die Gewährleistung, dass niemand die IP per Routingtricks im Internet entführt hat. In Konsequenz hat er damit sichergestellt ausschließlich mit seinem Vertragspartner zu kommunizieren und diesen haftbar machen zu können.

Die Verwendung eines lokalen Servers gestattet dem DNS Betreiber, auf die Weitergabe von Kundeninformationen zu verzichten. Ein angenehmer Nebeneffekt davon ist, dass alle Antworten für alle Anfragenden passen und somit effektiv gecached werden können. Zusammen mit der kurzen Laufzeit zwischen Server und Kunde kann so eine DNS Anfrage deutlich schneller beantwortet werden.

Da auf diese Weise die lokal günstig erreichbaren CDNs bevorzugt werden, wir das Surferlebnis insgesamt fluffiger. Regionale Datenquellen entlasten auch die Außenleitungen des ISP und erlauben so auch anderen Nutzern ein besseres Internet.

Was noch zu tun ist, ist der Hotline beizubringen, bei DNS Problemen nicht mehr auf die 8.8.8.8 zu verweisen, sondern stattdessen die 10.10.10.10 zu empfehlen. Das wird Zeit brauchen. Ebenso wird es dauern, bis die Techniker vor Ort nicht beim ersten Problem die DNS Einstellungen verändern, und wenn, dann auch die 10.10.10.10.

Es ist abzusehen, dass Kunden die 10er IPs auch intern verwenden. Für diese Kunden wird es dann die 100.100.100.100 geben. Ich bin ja nicht so.

Und wenn Du kein Kunde von uns bist? Dann tritt Deinen ISP, dass er auch einen solchen Service aufbaut! Es wäre Best Current Practice, diese IPs überall mit den o.g. Garantien erreichen zu können.

Das FreeBSD Handbuch für Portierungen ist eigentlich ziemlich klar. Allerdings funktionieren die Aktionen nur als root. Entwickeln mit solchen Rechnten mag ich nicht. Im IRC und den Mailinglisten heißt es, das sei halt so. Man könne ja mit pourdriere testen. Allerdings benötigt das ebenfalls root-Rechte.

Folgt man dem Handbuch, so ist das alles sehr einfach: Directory anlegen, Makefile schreiben und pkg-descr ausfüllen.

Der nächste Schritt ist das Herunterladen der Quellen. Schon da hatte ich erste Probleme, denn eigentlich wollte ich die Software nicht nochmal separat veröffentlichen. Aber die Idee des Ports ist halt, dass es um die Einbindung von Fremdsoftware geht. Und die liegt halt an anderer Stelle. Also lege ich eine extra Veröffentlichung an.

Damit der Port weiß, dass er die richtigen Dateien läd, werden Prüfsummen erstellt.

$ make makesum
=> parpd-1.1.tgz doesn't seem to exist in /usr/ports/distfiles/.
=> /usr/ports/distfiles/ is not writable by you; cannot fetch.
*** Error code 1

Logisch. Ich bin ja nicht root auf dem System. Und in das Standardverzeichnis darf ich nicht schreiben. Was nun?

In /usr/ports/Mk/bsd.port.mk steht:

# DISTDIR               - Where to search for and store copies of original sources
#                                 Default: ${PORTSDIR}/distfiles

Das kann ich ja umstellen, vielleicht geht's dann?

$ export DISTDIR=/tmp/myport
$ make makesum
=> parpd-1.1.tgz doesn't seem to exist in /tmp/myport/.
=> Attempting to fetch ftp://ftp.iks-jena.de/pub/mitarb/lutz/parpd/parpd-1.1.tgz
parpd-1.1.tgz                                 100% of   16 kB   10 MBps 00m00s
$ cat distinfo
TIMESTAMP = 1510327772
SHA256 (parpd-1.1.tgz) = 95318905767c1123eab87efa4fa664a57e5ed8f697802c6b7d5d0799ad8ea6e6
SIZE (parpd-1.1.tgz) = 17197

Na also! Geht doch.

Alle weiteren Schritte funktionieren dann als Nutzer ohne weitere Probleme.

Nunja, ein Problem gibt es doch noch. Hat man nämlich DEVELOPER=yes in dem make Optionen gesetzt, so erscheint bei jeden Aufruf von make

/!\ parpd-1.1: Makefile warnings, please consider fixing /!\

Not validating first entry in CATEGORIES due to being outside of PORTSDIR.
Please ensure this is proper when committing.

Ist ja auch klar. Natürlich bin ich als normaler Nutzer nicht im originalen PORTSDIR tätig. Die Meldung kann man getrost ignorieren.