Les tests unitaires d'un ingénieur réseau
Comment tester un moteur de topologie quand le réseau lui-même est le seul oracle ?
Michel Wijnberg
Tester un moteur de topologie a une propriété gênante : pour l’essentiel de ce qu’il fait, vous ne pouvez pas écrire la réponse attendue.
Vous pouvez tester unitairement une implémentation de Dijkstra sur un graphe que vous avez inventé. Cela prouve que votre Dijkstra est un Dijkstra. Cela ne dit rien sur le fait que le graphe construit à partir d’une vraie LSDB soit le graphe que les routeurs utilisent effectivement, ce qui est la seule chose qui compte et la seule chose qui puisse être fausse de façon intéressante.
La question devient donc : quel est l’oracle ?
Les routeurs sont l’implémentation de référence
La réponse à laquelle je me suis arrêté la coupe en deux. La RFC 2328 est la spécification : elle dit ce que le protocole est censé faire. Les routeurs du laboratoire sont l’oracle comportemental : ils disent ce qu’une implémentation fait réellement quand on l’interroge. Osprey doit satisfaire les deux, et là où ils divergent, la divergence est elle-même le résultat. Ce qui est pratique, c’est que l’oracle est juste là, prêt à être interrogé.
Cela transforme la validation en pipeline :
64 routeurs réels
↓ show ip route ospf / show ip ospf database summary
vérité de terrain : la table installée propre à chaque routeur
↓
la reconstruction d'Osprey, même instant, même portée
↓
comparer les 4 032 paires ordonnées sur quatre assertions indépendantes
Voici le côté reconstruit de cette comparaison : la table propre d’un routeur, telle qu’Osprey la détient :
428 routes, réparties par type (C connected, O intra-area, O IA inter-area,
E2 externe), chacune avec la métrique, le routeur annonceur et, ce qui compte pour
la suite, l’ensemble complet des sauts suivants, de sorte que
172.16.2.31, 172.16.2.32 apparaît comme l’ECMP à deux voies qu’il est réellement
plutôt que comme celui qui se trouvait trié en premier.
Les quatre assertions comptent plus que le décompte :
| Assertion | Pourquoi elle est distincte |
|---|---|
| Métrique exacte | Attrape un mauvais chemin qui a par hasard la bonne longueur |
Type de chemin exact (intra / inter / externe) | Attrape le bon coût atteint par la mauvaise règle |
| Ensemble complet des sauts suivants ECMP identique | Attrape le fait de choisir un membre valide et de l’appeler la réponse |
Terminaison = delivered à la destination demandée | Attrape un parcours qui s’arrête tôt et paraît plausible |
Trois d’entre elles quelconques peuvent passer pendant que le modèle est faux. Ce n’est pas hypothétique. Le bug du billet précédent produisait des chemins avec des extrémités correctes et un coût inférieur au réel, ce qu’une comparaison plus paresseuse aurait laissé passer.
Aujourd’hui les quatre passent sur 4 032 paires sur 4 032. Le nombre qui m’intéresse n’est pas 4 032. C’est quatre : le nombre de façons indépendantes par lesquelles j’ai rendu l’échec possible.
Quand il n’y a pas d’oracle, on fuzz
La vérité de terrain fonctionne quand vous pouvez interroger le réseau. Elle ne fonctionne pas pour l’autre moitié du problème : ce qui se passe quand un équipement vous envoie quelque chose de malformé, de tronqué ou simplement de bizarre.
Osprey a une règle absolue pour les analyseurs de protocole, et c’est une contrainte produit plutôt qu’une aspiration de qualité de code : journaliser l’anomalie et continuer ; ne jamais planter. Un système de supervision qui meurt sur un LSA malformé vous prive de visibilité exactement au moment où il se passe quelque chose d’intéressant.
Vous ne pouvez pas énumérer les façons dont un fournisseur vous surprendra. Alors on fuzz :
make fuzz # every target, 10s each
make fuzz FUZZTIME=60s # longer
make fuzz FUZZPKG=./internal/isis/ # one package
Le décompte actuel dans ce dépôt :
| Cibles de fuzzing | 110 |
| Fichiers de test | 419 |
| Benchmarks | 98 |
110 cibles de fuzzing n’est pas une métrique de vanité. C’est à peu près une par frontière d’analyse où un attaquant ou un agent bogué contrôle les octets : paquets OSPFv2 et OSPFv3 et chaque type de LSA, PDU et TLV IS-IS, messages BGP et attributs de chemin, en-têtes BMP, décodage des varbinds SNMP. Chacune est un endroit où la réponse honnête à « qu’est-ce que cet équipement va m’envoyer ? » est « je n’en ai aucune idée ».
Les benchmarks existent pour une raison voisine. Les analyseurs de protocole sont
sur un chemin chaud et sont écrits pour zéro allocation par paquet, avec sync.Pool
pour les tampons, et un benchmark avec b.ReportAllocs() est la seule chose qui
empêche cela de régresser discrètement la prochaine fois que quelqu’un ajoutera un
fmt.Sprintf bien pratique.
Les tests qui ont besoin d’une vraie base de données
Une troisième catégorie résiste aux deux approches. Les requêtes les plus difficiles d’Osprey sont des CTE avec agrégats et jointures multiples : « quels équipements sont actifs mais n’ont jamais été interrogés », « quelles areas sont partitionnées », « reconstruire le graphe à l’instant T ». Simuler une base pour tester cela prouve seulement que votre mock est d’accord avec vos attentes.
Ces tests tournent donc sur un vrai PostgreSQL via testcontainers, et la règle du projet est explicite : les requêtes avec CTE, agrégats ou jointures multiples exigent de vrais tests en base, couvrant les ensembles vides, les NULL, les conditions limites et celle qui attrape le plus de bugs : l’exclusivité mutuelle. Si une requête range des équipements en catégories, la somme des catégories doit égaler le total. Il est stupéfiant de voir à quelle fréquence ce n’est pas le cas, et à quel point c’est invisible sur un tableau de bord où chaque nombre paraît plausible isolément.
Les fournisseurs ne vous liront pas la RFC
La dernière catégorie est celle qu’aucune quantité de tests internes ne trouve, et c’est là que le laboratoire gagne sa place. Le vrai matériel n’implémente pas le standard que vous avez lu. Il implémente quelque chose d’adjacent.
Quelques exemples de ce laboratoire, qui ont tous coûté du temps réel :
La MIB qui n’existe pas. Les feuilles vEOS du pod de services EVPN du laboratoire ont dû être découvertes en OSPF plutôt qu’en IS-IS, pour la raison peu glorieuse que vEOS n’implémente aucun des deux arbres ISIS-MIB. Pas partiellement. Pas du tout. Aucune quantité de code SNMP correct n’y change quoi que ce soit ; la seule réponse correcte est de les découvrir par un protocole qu’elles prennent en charge, et de documenter pourquoi.
Le dialecte pré-standard. Les pseudowires MPLS L2VPN ont une MIB standard, la RFC 5601. Le vrai IOS livre à la place un dialecte Cisco pré-standard. Le collecteur implémente donc les deux et bascule automatiquement. Et parce que « nous prenons en charge les deux » est exactement le genre d’affirmation qui pourrit, les deux pilotes sont tenus à une barrière de parité pour que le chemin MIB standard ne puisse pas diverger discrètement du chemin Cisco.
La cryptographie oubliée par le temps. Les routeurs ici font tourner deux images
IOL (IOS 15.2(4)S7 sur 124 d’entre eux, 15.4(2)T4 sur les 68 autres), et chacun
n’offre que de l’échange de clés SHA-1 et des clés d’hôte ssh-rsa ; la flotte 15.2
plafonne en plus aux chiffrements CBC. Un client OpenSSH moderne standard refuse
l’échange de clés d’emblée, la connexion meurt donc avant même toute tentative
d’authentification. Le client SSH d’Osprey embarque explicitement les replis
hérités, parce que l’alternative est une fonction terminal qui marche sur les
diapositives du fournisseur et pas sur le parc réel du client.
Le serveur qui n’a jamais démarré. Une version antérieure à 64 routeurs de ce
laboratoire avait ip ssh version 2 configuré partout et aucune clé RSA jamais
générée : le serveur SSH ne tournait simplement pas, sur chaque nœud, invisiblement.
show ip ssh disait Disabled et la configuration paraissait complète.
Rien de tout cela ne se découvre en lisant une spécification. Cela se découvre en pointant son code vers du matériel et en le regardant échouer.
Ce qu’est vraiment un moteur de topologie
Quelque part au milieu de la construction du banc de validation, la forme de la chose m’est apparue plus clairement :
entrée : état réseau désordonné, partiel, contradictoire
sortie : un modèle déterministe
oracle : les routeurs eux-mêmes
C’est un compilateur, à peu de chose près. Et la conséquence utile de cette façon de voir est qu’un compilateur se juge à la conformité, pas à la plausibilité. Personne n’accepte « le code généré a l’air à peu près juste ».
Si je peux écrire une série sur un produit qui refuse de deviner, ce n’est pas parce que je serais exceptionnellement rigoureux. C’est que j’ai 64 machines qui me contrediront sur demande, et un banc qui les interroge toutes d’un coup.
Sans cela, « Osprey calcule le chemin correctement » serait une opinion. Avec, c’est 4 032 sur 4 032, sur quatre assertions, reproductible un mardi après-midi.
C’est toute la différence.
C’est le dernier billet de la série 2. La série 1, Trois IGP, une seule carte, Empreinte nulle, La vérité saut par saut et Ce qu’Osprey refuse de deviner, couvre les principes que celui-ci met à l’épreuve.