← Todos los artículos

Las pruebas unitarias de un ingeniero de redes

¿Cómo se prueba un motor de topología cuando la propia red es el único oráculo?

Michel Wijnberg

Probar un motor de topología tiene una propiedad incómoda: para la mayor parte de lo que hace, no se puede escribir de antemano la respuesta esperada.

Puede hacer pruebas unitarias de una implementación de Dijkstra contra un grafo que se haya inventado. Eso demuestra que su Dijkstra es un Dijkstra. No dice nada sobre si el grafo que construyó a partir de una LSDB real es el grafo que los routers están usando de verdad, que es lo único que importa y lo único que puede estar mal de una forma interesante.

Así que la pregunta pasa a ser: ¿cuál es el oráculo?


Los routers son la implementación de referencia

La respuesta con la que me quedé lo parte en dos. El RFC 2328 es la especificación: dice qué se supone que hace el protocolo. Los routers del laboratorio son el oráculo de comportamiento: dicen qué hace realmente una implementación cuando se le pregunta. Osprey tiene que satisfacer a ambos, y donde divergen, la divergencia es en sí misma el hallazgo. Lo útil es que el oráculo está ahí mismo, dispuesto a que le pregunten.

Eso convierte la validación en una tubería:

64 routers reales
      ↓  show ip route ospf  /  show ip ospf database summary
verdad de referencia: la tabla instalada propia de cada router

la reconstrucción de Osprey, mismo instante, mismo ámbito

comparar los 4.032 pares ordenados con cuatro aserciones independientes

Este es el lado reconstruido de esa comparación: la tabla propia de un router, tal como la tiene Osprey:

La tabla de rutas de lax1-gw1: 428 rutas con distintivos de tipo, métricas y pares de siguientes saltos ECMP
La tabla de rutas de lax1-gw1: 428 rutas con distintivos de tipo, métricas y pares de siguientes saltos ECMP

428 rutas, separadas por tipo (C conectada, O intraárea, O IA interárea, E2 externa), cada una con la métrica, el router anunciante y, algo importante para lo que viene, el conjunto completo de siguientes saltos, de modo que 172.16.2.31, 172.16.2.32 aparece como el ECMP de dos vías que realmente es, y no como el que resultara ordenarse primero.

Las cuatro aserciones importan más que el recuento:

AserciónPor qué está separada
Métrica exactaPilla un camino erróneo que casualmente tiene la longitud correcta
Tipo de camino exacto (intra / inter / external)Pilla el coste correcto alcanzado por la regla equivocada
Conjunto completo de siguientes saltos ECMP idénticoPilla que se elija un miembro válido y se le llame la respuesta
Terminación = delivered en el destino solicitadoPilla un recorrido que se para antes de tiempo y parece plausible

Tres cualesquiera de ellas pueden pasar mientras el modelo está mal. Eso no es hipotético. El fallo de la entrada anterior producía caminos con extremos correctos y coste más barato que el real, que es exactamente lo que una comparación más perezosa habría dejado pasar.

Hoy las cuatro pasan en 4.032 de 4.032 pares. El número que me importa no es 4.032. Es cuatro: la cantidad de formas independientes en que hice posible fallar.


Cuando no hay oráculo, haga fuzzing

La verdad de referencia funciona cuando se le puede preguntar a la red. No funciona para la otra mitad del problema: qué pasa cuando un dispositivo le envía algo malformado, truncado o simplemente raro.

Osprey tiene una regla absoluta para los analizadores de protocolo, y es una restricción de producto más que una aspiración de calidad de código: registre la anomalía y siga; nunca se caiga. Un sistema de monitorización que muere ante un LSA malformado le quita la visibilidad justo en el momento en que está pasando algo interesante.

No se pueden enumerar las formas en que un fabricante le va a sorprender. Así que se hace fuzzing:

make fuzz                          # every target, 10s each
make fuzz FUZZTIME=60s             # longer
make fuzz FUZZPKG=./internal/isis/ # one package

El recuento actual en este repositorio:

Objetivos de fuzzing110
Archivos de prueba419
Benchmarks98

110 objetivos de fuzzing no es una métrica de vanidad. Es aproximadamente uno por cada frontera de análisis donde un atacante o un agente defectuoso controla los bytes: paquetes OSPFv2 y OSPFv3 y todos los tipos de LSA, PDU y TLV de IS-IS, mensajes y atributos de camino de BGP, cabeceras BMP, decodificación de varbind de SNMP. Cada uno de ellos es un sitio donde la respuesta honesta a “¿qué me va a enviar este dispositivo?” es “no tengo ni idea”.

Los benchmarks existen por un motivo relacionado. Los analizadores de protocolo están en un camino caliente y están escritos para cero asignaciones por paquete, con sync.Pool para los búferes, y un benchmark con b.ReportAllocs() es lo único que impide que eso regrese en silencio la próxima vez que alguien añada un cómodo fmt.Sprintf.


Las pruebas que necesitan una base de datos real

Una tercera categoría se resiste a ambos enfoques. Las consultas más duras de Osprey son CTE con agregados y múltiples uniones: “qué dispositivos están activos pero nunca se han sondeado”, “qué áreas están particionadas”, “reconstruye el grafo a fecha T”. Simular una base de datos para probar eso demuestra solamente que su simulación coincide con sus expectativas.

Así que esas se ejecutan contra un PostgreSQL real vía testcontainers, y la regla del proyecto es explícita: las consultas con CTE, agregados o múltiples uniones exigen pruebas contra una BD real, cubriendo conjuntos vacíos, NULL, condiciones de contorno y la que caza más fallos: la exclusividad mutua. Si una consulta reparte dispositivos en categorías, la suma de las categorías debe ser igual al total. Es asombroso lo a menudo que no lo es, y lo invisible que resulta en un panel donde cada número parece plausible por su cuenta.


Los fabricantes no le van a leer el RFC

La última categoría es la que ninguna cantidad de pruebas internas encuentra, y es donde el laboratorio se gana el sueldo. El equipamiento real no implementa el estándar que usted leyó. Implementa algo adyacente.

Unos cuantos de este laboratorio, todos ellos con un coste real en tiempo:

La MIB que no está. Las hojas vEOS del pod de servicios EVPN del laboratorio tuvieron que descubrirse por OSPF y no por IS-IS, por la nada glamurosa razón de que vEOS no implementa ninguno de los dos árboles de la ISIS-MIB. No parcialmente. En absoluto. Ninguna cantidad de código SNMP correcto arregla eso; la única respuesta correcta es descubrirlas por un protocolo que sí admitan, y documentar por qué.

El dialecto preestándar. Los pseudowires de MPLS L2VPN tienen una MIB estándar, el RFC 5601. El IOS real trae en su lugar un dialecto Cisco preestándar. Así que el sondeador implementa ambos y recurre al otro automáticamente. Y como “admitimos los dos” es exactamente el tipo de afirmación que se pudre, los dos controladores se someten a una barrera de paridad para que el camino de la MIB estándar no pueda divergir en silencio del de Cisco.

La criptografía que el tiempo olvidó. Los routers de aquí ejecutan dos imágenes IOL (IOS 15.2(4)S7 en 124 de ellos, 15.4(2)T4 en los otros 68), y todos y cada uno ofrecen únicamente intercambio de claves SHA-1 y claves de host ssh-rsa; la flota 15.2, además, no pasa de cifrados CBC. Un cliente OpenSSH moderno de serie rechaza el intercambio de claves de plano, así que la conexión muere antes siquiera de intentar la autenticación. El cliente SSH de Osprey lleva los recursos heredados de forma explícita, porque la alternativa es una funcionalidad de terminal que funciona en las diapositivas del fabricante y no en el parque real del cliente.

El servidor que nunca se arrancó. Una construcción anterior de 64 routers de este laboratorio tenía ip ssh version 2 configurado en todas partes y ninguna clave RSA generada jamás, así que el servidor SSH sencillamente no estaba corriendo, en todos los nodos, de forma invisible. show ip ssh decía Disabled y la configuración parecía completa.

Ninguna de estas cosas se descubre leyendo una especificación. Se descubren apuntando tu código a equipamiento real y viéndolo fallar.


Qué es realmente un motor de topología

En algún punto de la construcción del banco de validación, la forma del asunto se me hizo más clara:

entrada: estado de red desordenado, parcial, contradictorio
salida:  un modelo determinista
oráculo: los propios routers

Eso es un compilador, más o menos. Y la consecuencia útil de pensarlo así es que a un compilador se le juzga por conformidad, no por plausibilidad. Nadie acepta “el código generado parece más o menos correcto”.

La razón por la que puedo escribir una serie sobre un producto que se niega a adivinar no es que yo tenga principios fuera de lo común. Es que tengo 64 máquinas que me contradicen cuando se lo pido, y un banco que les pregunta a todas a la vez.

Sin eso, “Osprey calcula el camino correctamente” sería una opinión. Con eso, son 4.032 de 4.032, sobre cuatro aserciones, reproducible un martes por la tarde.

Esa es toda la diferencia.


Esta es la última entrada de la Serie 2. La Serie 1, Tres IGP, un solo mapa, Huella cero, La verdad salto a salto y Lo que Osprey se niega a adivinar, cubre los principios que esta pone a prueba.