Problema

Los diagnósticos de red automatizados suelen fallar en entornos donde la topología, la latencia o la disponibilidad de servicios cambian de forma inesperada. Cuando una herramienta de diagnóstico (por ejemplo, un cliente CLI que muestra rutas, DNS o estado de sockets) interpreta incorrectamente una condición transitoria, el operador recibe información errónea y pierde tiempo investigando. En producción, los síntomas aparecen como “no se detecta la caída de DNS”, “se reporta una ruta válida cuando está rota” o “se ignoran restablecimientos de conectividad”. El patrón recurrente es la falta de pruebas que reproduzcan exactamente esas condiciones, lo que impide validar la lógica del diagnóstico antes de que el error llegue a los usuarios.

Causa

  1. Entornos de prueba estáticos – Las pruebas unitarias tradicionales usan sockets simulados o mocks que no reproducen la complejidad de una pila de red Linux real. Los fallos que dependen de interacciones entre namespaces, tablas de rutas y resolvers quedan sin cobertura.

  2. Inyección de fallos no determinista – Herramientas de fuzzing que generan tráfico aleatorio pueden descubrir bugs, pero la falta de una semilla reproducible dificulta la depuración y la creación de tickets consistentes.

  3. Desfase entre la verdad del simulador y la salida del diagnóstico – Cuando el simulador no expone de forma estructurada su “ground truth”, el proceso de comparación se vuelve manual y propenso a errores humanos.

  4. Automatización de tickets sin deduplicación – Un pipeline que abre un issue por cada ejecución sin verificar si el problema ya está registrado genera ruido y sobrecarga el tracker.

Solución

Implementar un pipeline de verificación determinista que combine:

  1. Generación reproducible de topologías usando ip netns y una semilla fija. Cada caso se describe en JSON (o YAML) para que cualquier operador pueda volver a crear la red idéntica.

  2. Inyección controlada de fallos mediante scripts que alteren DNS resolvers, añadan pérdida de paquetes (tc qdisc), modifiquen rutas o cierren sockets. Cada mutación lleva su propio identificador y timestamp dentro del caso.

  3. Ejecución de la herramienta de diagnóstico dentro del namespace afectado y captura de su salida estructurada (por ejemplo, JSON).

  4. Comparador de verdad vs diagnóstico que recorra la descripción del caso y la salida del diagnóstico, marcando discrepancias con un nivel de severidad (info, warning, error).

  5. Reproducción obligatoria – antes de abrir un ticket, el pipeline vuelve a lanzar el mismo caso (misma semilla, mismo número de caso) y verifica que la discrepancia persista. Si la diferencia desaparece, se descarta.

  6. Deduplicación basada en fingerprint – se calcula un hash a partir de la semilla, tipo de fallo y mensaje de error. El hash se busca en el tracker; si ya existe, se actualiza el issue en vez de crear uno nuevo.

  7. Integración con CI/CD – el flujo se ejecuta en GitHub Actions, GitLab CI o cualquier runner compatible, y publica el resultado en el repositorio mediante la API de Issues.

Este enfoque es reusable para cualquier herramienta que necesite validar su lógica de diagnóstico de red, no solo para un proyecto concreto.

Alternativas prácticas

  • Uso de containers ligeros (docker run --network=none) en vez de namespaces cuando el host no permite crear muchos namespaces.
  • Emulación de DNS con dnsmasq para controlar respuestas y tiempos de expiración sin tocar /etc/resolv.conf.
  • Persistencia de casos en una base SQLite dentro del runner para evitar recomputar la generación de topologías en cada ejecución.

Cuándo aplicar esta solución

  • Síntomas: informes de diagnóstico que no coinciden con la realidad observada, fallos intermitentes de conectividad que aparecen solo bajo carga o después de cambios de configuración.
  • Entornos: infraestructuras basadas en Linux donde se usan namespaces, containers o VMs para aislar servicios de red.
  • Escala: cuando el número de pruebas supera lo que un equipo puede ejecutar manualmente (más de 20 casos por ciclo).
  • No aplica: herramientas que solo analizan datos estáticos (logs) sin interacción directa con la pila de red, o entornos Windows sin WSL donde ip netns no está disponible.

Código

# 1. Crear namespace y veth pair
ip netns add ns1
ip link add veth0 type veth peer name veth1
ip link set veth0 netns ns1
ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth0
ip netns exec ns1 ip link set veth0 up
ip addr add 10.0.0.2/24 dev veth1
ip link set veth1 up

# 2. Configurar DNS con dnsmasq (ejemplo de outage)
mkdir -p /tmp/dns && cat > /tmp/dns/dnsmasq.conf <<EOF
port=5353
no-resolv
address=/example.com/10.0.0.2
EOF
dnsmasq --conf-dir=/tmp/dns --pid-file=/tmp/dns.pid --listen-address=10.0.0.2 --port=5353 &
DNS_PID=$!

# 3. Inyectar fallo: bloquear DNS por 5 s
ip netns exec ns1 iptables -A OUTPUT -p udp --dport 5353 -j DROP
sleep 5
ip netns exec ns1 iptables -D OUTPUT -p udp --dport 5353 -j DROP

# 4. Ejecutar la herramienta de diagnóstico dentro del namespace
ip netns exec ns1 ./netdiag --json > /tmp/diagnostic.json

# 5. Comparar con la verdad (script python o go que lea el caso JSON)
python3 compare.py --case /tmp/case.json --output /tmp/diagnostic.json

# 6. Si hay error, generar fingerprint y abrir issue vía API
FINGERPRINT=$(sha256sum /tmp/case.json /tmp/diagnostic.json | cut -d' ' -f1)
curl -X POST -H "Authorization: token $GH_TOKEN" \
  -d "{\"title\":\"Bug reproducible: $FINGERPRINT\",\"body\":\"...\"}" \
  https://api.github.com/repos/owner/repo/issues

Verificación

  1. Ejecutar el script anterior con una semilla conocida (SEED=20260102).
  2. Confirmar que el archivo /tmp/diagnostic.json contiene la información esperada (por ejemplo, ausencia de registros DNS).
  3. Verificar que compare.py devuelve un código de salida distinto de cero y muestra la discrepancia.
  4. Revisar que la llamada a la API crea un issue solo si el hash no está presente en la lista de issues abiertos (consultar GET /issues?labels=fingerprint:$FINGERPRINT).
  5. Re‑ejecutar el mismo caso sin modificar nada; el pipeline debe reproducir exactamente el mismo fallo y no crear un issue nuevo.

Notas adicionales

  • Persistencia de semillas: guarda las semillas y los hashes en un archivo cases.db para auditoría y para poder reproducir fallos meses después.
  • Límites de namespaces: algunos kernels limitan la cantidad de namespaces simultáneos; usa sysctl kernel.max_ns=... o reutiliza namespaces entre casos.
  • Tiempo de espera de DNS: la mayoría de los clientes usan un timeout de 5 s; si la simulación recupera el resolver antes, fuerza un nuevo query con dig @10.0.0.2 example.com para validar el comportamiento.
  • Seguridad: ejecuta los namespaces con capacidades limitadas (--cap-drop ALL) para evitar que un caso malicioso comprometa el host del runner.
  • Escalado: divide la matriz de casos en jobs paralelos de GitHub Actions usando la matriz include y pasa la semilla como variable de entorno.

Con este patrón, cualquier proyecto que dependa de la observación de la red puede validar su lógica de forma automática, reproducible y sin generar ruido en el tracker. La clave está en la combinación de namespaces determinísticos, inyección de fallos controlada y una capa de deduplicación basada en fingerprints.