Die Unit-Tests eines Netzwerkingenieurs
Wie testet man eine Topologie-Engine, wenn das Netzwerk selbst das einzige Orakel ist?
Michel Wijnberg
Eine Topologie-Engine zu testen hat eine unbequeme Eigenschaft: Für das meiste, was sie tut, lässt sich die erwartete Antwort nicht aufschreiben.
Sie können eine Dijkstra-Implementierung gegen einen selbst erfundenen Graphen testen. Das beweist, dass Ihr Dijkstra ein Dijkstra ist. Es sagt nichts darüber aus, ob der Graph, den Sie aus einer echten LSDB gebaut haben, der Graph ist, den die Router tatsächlich benutzen, und das ist das Einzige, worauf es ankommt, und das Einzige, was auf interessante Weise falsch sein kann.
Die Frage lautet also: Was ist das Orakel?
Die Router sind die Referenzimplementierung
Die Antwort, bei der ich gelandet bin, teilt es in zwei Teile. RFC 2328 ist die Spezifikation: Sie sagt, was das Protokoll tun soll. Die Router im Labor sind das Verhaltensorakel: Sie sagen, was eine Implementierung tatsächlich tut, wenn man sie fragt. Osprey muss beides erfüllen, und wo die beiden auseinandergehen, ist genau das der Befund. Das Praktische daran ist, dass das Orakel direkt daneben steht und sich fragen lässt.
Daraus wird eine Pipeline:
64 echte Router
↓ show ip route ospf / show ip ospf database summary
gesicherte Wahrheit: die eigene installierte Tabelle jedes Routers
↓
Ospreys Rekonstruktion, derselbe Augenblick, derselbe Bereich
↓
alle 4.032 geordneten Paare auf vier unabhängige Zusicherungen vergleichen
Das hier ist die rekonstruierte Seite dieses Vergleichs: die eigene Tabelle eines Routers, so wie Osprey sie hält:
428 Routen, aufgeteilt nach Typ (C connected, O intra-area, O IA
inter-area, E2 extern), jede mit der Metrik, dem ankündigenden Router und, was
für das Folgende wichtig ist, der vollständigen Next-Hop-Menge, sodass
172.16.2.31, 172.16.2.32 als das Zweiwege-ECMP erscheint, das es tatsächlich
ist, und nicht als das, was zufällig zuerst sortiert wurde.
Die vier Zusicherungen wiegen schwerer als die Anzahl:
| Zusicherung | Warum sie eigenständig ist |
|---|---|
| Metrik exakt | Fängt einen falschen Pfad ab, der zufällig die richtige Länge hat |
Pfadtyp exakt (intra / inter / extern) | Fängt die richtigen Kosten ab, die über die falsche Regel zustande kamen |
| Vollständige ECMP-Next-Hop-Menge identisch | Fängt ab, dass ein gültiges Mitglied herausgegriffen und zur Antwort erklärt wird |
Abschluss = delivered am angefragten Ziel | Fängt einen Lauf ab, der zu früh endet und plausibel aussieht |
Drei beliebige davon können bestehen, während das Modell falsch ist. Das ist nicht hypothetisch. Der Fehler aus dem vorherigen Beitrag erzeugte Pfade mit richtigen Endpunkten und niedrigeren Kosten als in Wirklichkeit, und genau das hätte ein bequemerer Vergleich durchgewunken.
Heute bestehen alle vier bei 4.032 von 4.032 Paaren. Die Zahl, die mich interessiert, ist nicht 4.032. Es ist vier: die Anzahl unabhängiger Arten, auf die ich ein Scheitern überhaupt möglich gemacht habe.
Wo es kein Orakel gibt, wird gefuzzt
Gesicherte Wahrheit funktioniert, solange Sie das Netzwerk fragen können. Für die andere Hälfte des Problems funktioniert sie nicht: was passiert, wenn ein Gerät Ihnen etwas Fehlerhaftes, Abgeschnittenes oder schlicht Seltsames schickt.
Osprey hat eine absolute Regel für Protokoll-Parser, und sie ist eine Produkteigenschaft und kein Streben nach Codequalität: Die Auffälligkeit protokollieren und weitermachen; niemals abstürzen. Ein Monitoring-System, das an einem fehlerhaften LSA stirbt, nimmt Ihnen die Sicht genau in dem Moment, in dem etwas Interessantes passiert.
Sie können nicht aufzählen, womit ein Hersteller Sie überraschen wird. Also fuzzen Sie:
make fuzz # every target, 10s each
make fuzz FUZZTIME=60s # longer
make fuzz FUZZPKG=./internal/isis/ # one package
Der aktuelle Stand in diesem Repository:
| Fuzz-Ziele | 110 |
| Testdateien | 419 |
| Benchmarks | 98 |
110 Fuzz-Ziele sind keine Eitelkeitskennzahl. Es ist ungefähr eines je Parse-Grenze, an der ein Angreifer oder ein fehlerhafter Agent die Bytes kontrolliert: OSPFv2- und OSPFv3-Pakete und jeder LSA-Typ, IS-IS-PDUs und -TLVs, BGP-Nachrichten und Pfadattribute, BMP-Header, das Dekodieren von SNMP-Varbinds. Jede einzelne davon ist eine Stelle, an der die ehrliche Antwort auf “was wird mir dieses Gerät schicken?” lautet: “Ich habe keine Ahnung.”
Die Benchmarks gibt es aus einem verwandten Grund. Die Protokoll-Parser liegen auf
einem heißen Pfad und sind auf null Allokationen je Paket geschrieben, mit
sync.Pool für Puffer, und ein Benchmark mit b.ReportAllocs() ist das Einzige,
was verhindert, dass das beim nächsten Mal still zurückfällt, wenn jemand ein
bequemes fmt.Sprintf einbaut.
Die Tests, die eine echte Datenbank brauchen
Eine dritte Kategorie sperrt sich gegen beide Ansätze. Ospreys schwierigere Abfragen sind CTEs mit Aggregaten und Mehrfach-Joins: “welche Geräte sind aktiv, wurden aber nie abgefragt”, “welche Areas sind partitioniert”, “rekonstruiere den Graphen zum Zeitpunkt T”. Eine Datenbank nachzubilden, um so etwas zu testen, beweist nur, dass Ihr Mock mit Ihren Erwartungen übereinstimmt.
Also laufen diese Tests über testcontainers gegen ein echtes PostgreSQL, und die Projektregel ist ausdrücklich: CTE-, Aggregat- und Mehrfach-Join-Abfragen brauchen echte DB-Tests, die leere Mengen, NULL-Werte, Randbedingungen und das abdecken, was die meisten Fehler fängt: wechselseitige Ausschließlichkeit. Wenn eine Abfrage Geräte in Kategorien einsortiert, muss die Summe der Kategorien der Gesamtzahl entsprechen. Es ist erstaunlich, wie oft sie das nicht tut, und wie unsichtbar das auf einem Dashboard ist, auf dem jede Zahl für sich genommen plausibel aussieht.
Die Hersteller lesen Ihnen die RFC nicht vor
Die letzte Kategorie ist die, die kein Umfang an internem Testen findet, und hier verdient sich das Labor seinen Unterhalt. Echte Geräte implementieren nicht den Standard, den Sie gelesen haben. Sie implementieren etwas daneben.
Ein paar Beispiele aus diesem Labor, die alle echte Zeit gekostet haben:
Die MIB, die es nicht gibt. Die vEOS-Leaves im EVPN-Services-Pod des Labors mussten über OSPF statt über IS-IS erkannt werden, aus dem wenig glamourösen Grund, dass vEOS keinen der beiden ISIS-MIB-Bäume implementiert. Nicht teilweise. Überhaupt nicht. Kein Maß an korrektem SNMP-Code behebt das; die einzig richtige Antwort ist, sie über ein Protokoll zu erkennen, das sie unterstützen, und zu dokumentieren, warum.
Der vorstandardisierte Dialekt. MPLS-L2VPN-Pseudowires haben eine Standard-MIB, RFC 5601. Echtes IOS liefert stattdessen einen vorstandardisierten Cisco-Dialekt aus. Also implementiert der Poller beides und fällt automatisch zurück. Und weil “wir unterstützen beides” genau die Art Behauptung ist, die verrottet, werden die beiden Treiber an einer Paritätsschranke gemessen, damit der Standard-MIB-Pfad nicht still vom Cisco-Pfad abweichen kann.
Die Kryptografie, die die Zeit vergessen hat. Die Router hier fahren zwei
IOL-Images (IOS 15.2(4)S7 auf 124 von ihnen, 15.4(2)T4 auf den übrigen 68), und
jeder einzelne bietet nur SHA-1-Schlüsselaustausch und ssh-rsa-Hostschlüssel;
die 15.2-Flotte kommt obendrein über CBC-Chiffren nicht hinaus. Ein moderner
OpenSSH-Client lehnt den Schlüsselaustausch rundheraus ab, die Verbindung stirbt
also, bevor überhaupt eine Authentifizierung versucht wird. Ospreys SSH-Client
trägt die Rückfallebenen für Altgeräte ausdrücklich mit sich, denn die Alternative
ist eine Terminal-Funktion, die auf den Folien des Herstellers funktioniert und
im tatsächlichen Bestand des Kunden nicht.
Der Server, der nie gestartet wurde. Ein früherer 64-Router-Aufbau dieses
Labors hatte überall ip ssh version 2 konfiguriert und nie einen RSA-Schlüssel
erzeugt, der SSH-Server lief also schlicht nicht, auf jedem Knoten, unsichtbar.
show ip ssh sagte Disabled, und die Konfiguration sah vollständig aus.
Nichts davon findet man, indem man eine Spezifikation liest. Man findet es, indem man seinen Code auf echte Geräte richtet und ihm beim Scheitern zusieht.
Was eine Topologie-Engine wirklich ist
Irgendwann mitten beim Bau des Validierungsgerüsts wurde mir die Form der Sache klarer:
Eingabe: unordentlicher, unvollständiger, widersprüchlicher Netzwerkzustand
Ausgabe: ein deterministisches Modell
Orakel: die Router selbst
Das ist beinahe ein Compiler. Und die nützliche Folge dieser Sichtweise ist, dass ein Compiler an Konformität gemessen wird und nicht an Plausibilität. Niemand akzeptiert “der erzeugte Code sieht ungefähr richtig aus”.
Der Grund, warum ich eine Reihe über ein Produkt schreiben kann, das nicht raten will, ist nicht, dass ich ungewöhnlich prinzipienfest wäre. Es ist, dass ich 64 Maschinen habe, die mir auf Zuruf widersprechen, und ein Gerüst, das sie alle gleichzeitig fragt.
Ohne das wäre “Osprey berechnet den Pfad korrekt” eine Meinung. Damit sind es 4.032 von 4.032, auf vier Zusicherungen, an einem Dienstagnachmittag reproduzierbar.
Das ist der ganze Unterschied.
Das ist der letzte Beitrag der Serie 2. Serie 1, Drei IGPs, eine Karte, Kein Fußabdruck, Hop-für-Hop-Wahrheit und Was Osprey nicht raten will, behandelt die Grundsätze, die dieser Beitrag auf die Probe stellt.