
Un sandbox de red determinista para probar reglas de nftables. Utiliza espacios de nombres de red de Linux efímeros (netns) y Scapy para validar la lógica del firewall de forma segura.
¿Por qué NSE? • Contrato del oráculo • Aserciones en el cable • Características • Requisitos • Instalación • Inicio rápido • Cómo funciona • Estructura del proyecto
Probar conjuntos de reglas de firewall en un sistema Linux en vivo plantea riesgos significativos: las reglas malformadas pueden cortar sesiones de gestión SSH, filtrar tráfico en texto claro durante las pruebas o dejar tablas de firewall huérfanas activas en el host.
Network Sandbox Engine (NSE) proporciona un arnés de pruebas seguro y reproducible. Construye espacios de nombres de red Linux efímeros, conecta pares de ethernet virtuales, compila conjuntos de reglas nftables e inyecta paquetes sintéticos de Capa 2 y Capa 3 usando Scapy. Toda la evaluación ocurre dentro del espacio de nombres del sandbox: el estado del firewall del host nunca se altera.
Propiedades arquitectónicas clave:
nse_<uuid>) y se eliminan por completo durante el desmontaje.nse/ tiene ~1200 sentencias con 98% de cobertura de pruebas.NSE crea espacios de nombres de red, carga conjuntos de reglas nftables y lee eventos de traza del kernel, por lo que se ejecuta como root. No abre un socket, un puerto ni un endpoint RPC de ningún tipo — es una biblioteca y una CLI que tú invocas, y mantiene privilegios solo durante la duración de una ejecución.
La versión 2.1.0 eliminó la interfaz web FastAPI/Svelte que incluían las versiones anteriores.
Esa interfaz se ejecutaba en proceso como root desde la 2.0.0 en adelante, lo que constituía una
gran superficie de ataque para una herramienta de pruebas; el código permanece en el historial de git en
la etiqueta v2.0.0 si lo necesitas.
Una prueba de firewall es una aserción negativa — "este paquete no pasó" — y una aserción negativa no vale nada a menos que se sepa que el instrumento está funcionando. Un monitor de trazas que nunca se conectó al kernel y un firewall que bloqueó todo producen una salida idéntica byte a byte.
Por lo tanto, NSE se niega a informar un veredicto que no pueda demostrar que midió:
| Garantía | Mecanismo |
|---|---|
| El monitor se adjuntó antes del primer paquete de prueba | Se inyecta un canario de disponibilidad y se vuelve a inyectar hasta que se observa su traza del kernel. Sin observación, no hay ejecución. |
| El monitor seguía adjunto después del último | Se ejecuta un canario de vitalidad tras la inyección. Si se pierde, el flujo de veredictos se declara truncado. |
| El analizador entendió lo que dijo el kernel | Se cuentan las líneas de traza que ningún patrón coincide, y cualquier recuento superior a cero es un error en lugar de un registro de depuración. |
| El monitor no murió silenciosamente | El bucle de lectura registra por qué terminó — parada limpia, EOF inesperado, tiempo de espera agotado o fallo — y solo una parada limpia es aceptable. |
| Un veredicto ausente no es una aprobación | El ejecutor de la CLI falla cuando el número de veredictos observados difiere del número esperado, en cualquier dirección. |
Los paquetes canario se excluyen de los resultados por id de traza, por lo que nunca aparecen en tu flujo de veredictos.
La suite demuestra que esto se cumple, en lugar de afirmarlo: make test-blind fuerza
al analizador a no entender nada, y la compilación falla a menos que el ejecutor salga
con un código distinto de cero. Ese trabajo se ejecuta en CI en cada push.
nft monitor trace informa lo que el conjunto de reglas decidió. No puede informar lo que
salió de una interfaz en la que el conjunto de reglas nunca coincidió, que es la pregunta que
realmente plantea una afirmación de cero fugas. PCAPAsserter cubre eso: un envoltorio de AsyncSniffer
que captura en una interfaz con nombre y devuelve las tramas, para que una prueba
pueda afirmar que un estímulo produjo ninguna.
from nse import PCAPAsserter
asserter = PCAPAsserter(iface="veth-host")
await asserter.start()
# ... send the stimulus ...
leaked = await asserter.stop()
assert not leaked, leaked
Aplica la misma regla que el oráculo de trazas, y es responsabilidad del llamador hacerla cumplir: un sniffer que nunca se conectó y un firewall que bloqueó todo devuelven ambos una lista vacía. Capture el estímulo una vez con el conjunto de reglas vaciado y exija que la trama sea vista antes de afirmar su ausencia.
El filtro predeterminado suprime el ruido de enlace local, y nada más.
DEFAULT_FILTER excluye ARP y los tipos 133-137 de ICMPv6 - Neighbour Discovery, que transporta un límite de saltos de 255 y no puede alcanzar a un observador remoto. Todo lo demás se captura, incluido el eco ICMPv6 hacia una dirección enrutable globalmente. Un argumento filter se combina con ese valor predeterminado; replace_default_filter=True usa solo el suyo. La vía de escape existe porque el filtro se compila en BPF y se evalúa en el kernel: un paquete que excluye nunca llega al espacio de usuario, por lo que ningún consumidor puede recuperarlo aguas abajo. Hasta la 2.1.2 el valor predeterminado decía not arp and not icmp6, lo que descartaba también ICMPv6 enrutable - véase
issue #14.