Le jour où ma topologie m'a menti
Un post-mortem technique sur mon propre produit, et le laboratoire qui l'a démasqué
Michel Wijnberg
J’ai construit Osprey pour qu’il me dise la vérité sur les réseaux. Puis j’ai découvert qu’il me mentait. Avec assurance, par écrit, sur un canvas que je présentais à des gens.
Voici ce qui s’est passé, comment je l’ai attrapé, et ce que j’ai changé. C’est le billet le plus utile de la série, parce que tout ce que j’ai écrit d’autre sur l’honnêteté ne coûte rien tant que la chose auditée n’est pas votre propre travail.
Ce que je croyais
Le moteur de chemin d’Osprey faisait ce que fait presque tout outil de chemin. Étant donné une source et une destination, il faisait tourner un Dijkstra conscient des areas depuis la source sur la topologie OSPF et dessinait la route la moins chère.
J’avais de bonnes raisons de lui faire confiance. Il respectait les règles de la RFC 2328 auxquelles je tenais : intra-area préféré à inter-area, inter-area transitant par le backbone, E1 avant E2, ECMP énuméré plutôt qu’écrasé. Il avait des tests. Il correspondait à mon modèle mental du laboratoire. Quand je cliquais deux routeurs, un chemin sensé s’allumait.
Il décrivait aussi, une requête inter-area sur huit, une route qu’aucun paquet ne prendrait jamais.
Comment je l’ai découvert
Pas par un rapport de bug. Par une corvée.
Je construisais un banc de validation pour tout autre chose, et je voulais un jeu de
vérité de terrain pour le contrôler. Le laboratoire a 64 routeurs dans l’AS 200,
tous joignables en SSH, tous prêts à imprimer leur propre table de routage. J’ai
donc fait la chose bête et évidente : je me suis connecté aux 64 et j’ai extrait
show ip route ospf de chacun.
Puis, au lieu de survoler le résultat, j’ai comparé la vraie table de chaque routeur à ce que le moteur de chemin d’Osprey affirmait être le chemin, pour les 4 032 paires source/destination ordonnées.
360 d’entre elles étaient en désaccord.
Pas « le coût est légèrement décalé ». Le chemin dessiné contenait un routeur que le paquet ne visite manifestement jamais.
Le cas canonique
En voici un, chaque ligne mesurée sur l’équipement plutôt que modélisée :
| Routeur | Dit de la destination | Achemine vers |
|---|---|---|
dfw1-gw2 | metric 20031, type inter area | dfw1-cr1 |
dfw1-cr1 | metric 30001, type intra area | ord1-cr1 ← pas dfw2-cr1 |
ord1-cr1 | metric 20001, type intra area | jfk1-cr1 |
jfk1-cr1 | metric 10001, type intra area | lhr1-cr1 |
lhr1-cr1 | metric 1, connected | livré |
Osprey dessinait :
dfw1-gw2 → dfw1-cr1 → dfw2-cr1 → iad2-cr1 → ams1-cr1 → lhr1-cr1
Le paquet n’atteint jamais dfw2-cr1. Le chemin d’Osprey était moins cher. Ce
n’était simplement pas un chemin qu’un routeur construirait.
Deux règles indépendantes de la RFC 2328 font que dfw1-cr1 refuse le couloir bon
marché, et je n’avais implémenté ni l’une ni l’autre au niveau d’un routeur
intermédiaire individuel :
§16 étape (3) / §16.2 ¶1 : la porte du backbone. Un routeur rattaché à plusieurs
areas n’examine que les summary-LSA du backbone. Le summary moins cher de
dfw2-cr1 à 20011 est bel et bien dans la base de l’area 4.6.23.0 de dfw1-cr1
(je l’ai confirmé avec show ip ospf database summary, parce que je n’y croyais pas
non plus), mais il n’est pas candidat, parce que dfw1-cr1 est un ABR et que ce
summary n’est pas arrivé par l’area 0. La RFC 3509 §2.1/§2.2, le comportement ABR de
Cisco qui tourne réellement sur ces équipements, conditionne cela à une connexion
backbone active, que dfw1-cr1 possède.
§16.2 étape (6) : intra bat inter, point final. “If the paths present in the
table are intra-area paths, do nothing with the LSA.” dfw1-cr1 détient déjà un
chemin intra-area à la métrique 30001. Le summary à 20011 perd face à lui. La
métrique n’entre jamais dans la comparaison. C’est une règle sur le type de
chemin.
Chacune des deux règles à elle seule tue le couloir. Mon moteur ne pouvait voir ni
l’une ni l’autre, parce qu’il ne demandait rien à dfw1-cr1. Il interrogeait la
vision du monde du routeur source puis traçait une ligne à travers huit autres
routeurs comme s’ils la partageaient.
La forme de l’erreur
Cela mérite d’être nommé précisément, parce que le détail OSPF est moins intéressant que la catégorie.
J’avais modélisé un chemin là où le réseau met en œuvre une suite de décisions indépendantes. Ces deux choses coïncident exactement tant que chaque routeur sur la route applique la même règle de décision au même jeu d’entrées. À l’intérieur d’une seule area OSPF, c’est le cas (même LSDB, même algorithme, même résultat), et c’est pourquoi le nombre de défauts pour les paires intra-area était de :
0 sur 1 193.
Dès qu’un ABR est impliqué, le jeu d’entrées cesse d’être partagé : un ABR regarde délibérément un ensemble différent de LSA de celui des routeurs qui l’entourent. Mon modèle n’avait aucun endroit où loger ce fait, parce qu’il avait un graphe et un Dijkstra, pas soixante-quatre routeurs ayant chacun un avis.
| Paires | Avec un saut qu’aucun routeur ne prendrait | |
|---|---|---|
| Intra-area | 1 193 | 0 |
| Inter-area | 2 839 | 360 (12,68 %) |
| Toutes | 4 032 | 360 (8,93 %) |
Le correctif, et comment je sais qu’il marche
Le moteur parcourt désormais la chaîne. À chaque saut il évalue la route propre de ce routeur vers l’adresse de destination, en utilisant sa propre LSDB, ses appartenances d’area, son statut d’ABR et son mélange de distances administratives, puis suit le saut suivant qu’il sélectionne réellement, et recommence.
Puis j’ai refait tourner les mêmes 4 032 paires, face à la même vérité de terrain :
| Contrôle | Résultat |
|---|---|
| Métrique propre du routeur reproduite exactement | 4 032 / 4 032 |
| Type de chemin (intra / inter / externe) exact | 4 032 / 4 032 |
| Ensemble complet des sauts suivants ECMP installés identique | 4 032 / 4 032 |
La chaîne se termine par delivered à la destination demandée | 4 032 / 4 032 |
Pas « amélioré ». Identique sur chaque paire, y compris l’ensemble complet des sauts suivants à coût égal plutôt qu’un membre choisi arbitrairement.
C’est la même requête qui inventait un saut, montrant maintenant chaque routeur qui
prend réellement une décision, l’area dans laquelle il la prend, et les ports
physiques des deux côtés. Le total, 30061, est la métrique installée par la source
elle-même : littéralement le nombre que show ip route imprime sur lax1-gw1.
Et là où la chaîne n’est pas calculable (OSPFv3, IS-IS, EIGRP, une portée
partielle, une area portant un lien virtuel), le moteur ne retombe pas
discrètement sur l’ancienne image. Il renvoie la vue enracinée à la source avec un
model_note nommant la raison, affiché comme une étape d’explication à part
entière. Ce refus est le sujet du billet quatre,
et il existe à cause de ce bug.
Un plus petit, pour l’équilibre
Toutes les blessures auto-infligées ne sont pas des algorithmes. En voici une de deux lignes qui m’a coûté un après-midi.
L’enregistreur GRE d’Osprey forme une vraie adjacence OSPF, il doit donc émettre un Router-LSA. Au début, il annonçait le sous-réseau interne du tunnel comme réseau stub : d’apparence correcte, conforme aux RFC, et parfaitement raisonnable.
Chaque routeur de l’area a alors installé une route vers le préfixe du tunnel. Cette route supplantait le chemin de transport sur lequel le tunnel GRE lui-même circulait. Le transport externe s’est cassé. L’adjacence est tombée. L’enregistreur s’est reconnecté, a réémis le LSA, et a recommencé.
Le correctif est maintenant dans le commentaire du code, pour que personne ne le retire par accident :
// Only the P2P link is advertised. NO stub network for the tunnel
// inner subnet. Advertising the tunnel subnet (even at max cost) causes SPF
// on other routers to install a route for the tunnel prefix, which can
// disrupt the GRE outer path and kill the adjacency.
Un outil de supervision qui met à terre la chose qu’il supervise est un genre de honte bien particulier, et je ne l’aurais jamais trouvé sur un schéma.
À quoi sert réellement le laboratoire
Je décrivais autrefois le laboratoire comme l’endroit où je fais la démonstration d’Osprey. C’était la mauvaise fiche de poste, et la corriger a changé ma façon de construire.
Le laboratoire n’est pas là pour faire bien paraître Osprey. Il est là pour prouver qu’Osprey a tort.
Ce qui veut dire que le laboratoire ne vaut que dans la mesure où il peut me contredire : il est donc volontairement inconfortable. Trois opérateurs qui ne partagent aucun protocole. Quinze areas OSPF là où trois se démontreraient mieux. Un domaine IS-IS avec dix-sept areas de niveau 1. EIGRP, qui n’a aucune base à état de liens et brise toutes les hypothèses du code OSPF. Des liens à double pile dont les coûts v4 et v6 diffèrent d’un facteur 10 sur les 124.
Rien de tout cela ne fait une plus jolie capture. Tout cela a attrapé quelque chose.
Les 360 couloirs fictifs n’ont pas été trouvés par une lecture attentive, ni par un test que j’aurais été assez malin pour écrire à l’avance. Ils ont été trouvés parce que j’avais 64 vrais routeurs à qui on pouvait demander ce qu’ils pensaient réellement, et que je les ai enfin tous interrogés d’un coup au lieu de me fier aux deux ou trois que j’avais vérifiés par sondage.
Ce qui soulève la question évidente, et c’est le sujet du billet suivant : si le réseau est la seule chose qui peut vous dire si votre modèle est juste, comment en faire une suite de tests ?
Ensuite : Les tests unitaires d’un ingénieur réseau.