
Un bac à sable réseau déterministe pour tester les règles nftables. Il utilise des espaces de noms réseau Linux éphémères (netns) et Scapy pour valider la logique de pare-feu en toute sécurité.
Pourquoi NSE ? • Fonctionnalités • Prérequis • Installation • Démarrage rapide • Fonctionnement • Structure du projet
Tester des ensembles de règles de pare-feu sur un système Linux en production présente des risques importants : des règles malformées peuvent couper les sessions SSH d'administration, laisser fuiter du trafic en clair pendant les tests, ou laisser des tables de pare-feu orphelines actives sur l'hôte.
Network Sandbox Engine (NSE) fournit un banc de test sûr et reproductible. Il construit des espaces de noms réseau Linux éphémères, câble des paires ethernet virtuelles, compile des ensembles de règles nftables et injecte des paquets synthétiques de couche 2 et couche 3 à l'aide de Scapy. Toute l'évaluation se déroule à l'intérieur de l'espace de noms bac à sable : l'état du pare-feu de l'hôte n'est jamais modifié.
Propriétés architecturales clés :
nse_<uuid>) et sont entièrement supprimés lors du démontage.nse/ représente environ 1150 instructions avec 98 % de couverture de tests.NSE crée des espaces de noms réseau, charge des ensembles de règles nftables et lit les événements de trace du noyau, il s'exécute donc en tant que root. Il n'ouvre aucun socket, aucun port ni aucun point de terminaison RPC de quelque nature que ce soit — c'est une bibliothèque et une CLI que vous invoquez, et il ne détient les privilèges que pendant la durée d'une exécution.
La version 2.1.0 a supprimé l'interface web FastAPI/Svelte que les versions précédentes
embarquaient. Cette interface s'exécutait en cours de processus en tant que root à partir de la version 2.0.0, ce qui constituait une
surface d'attaque importante pour un outil de test ; le code reste dans l'historique git au
tag v2.0.0 si vous en avez besoin.
Un test de pare-feu est une assertion négative — « ce paquet n'est pas passé » — et une assertion négative ne vaut rien si l'instrument n'est pas connu comme fonctionnel. Un moniteur de trace qui ne s'est jamais attaché au noyau et un pare-feu qui a tout bloqué produisent une sortie identique au byte près.
NSE refuse donc de rapporter un verdict dont il ne peut pas démontrer qu'il l'a mesuré :
| Garantie | Mécanisme |
|---|---|
| Le moniteur était attaché avant le premier paquet de test | Un canari de disponibilité est injecté et réinjecté jusqu'à ce que sa trace noyau soit observée. Pas d'observation, pas d'exécution. |
| Le moniteur était encore attaché après le dernier | Un canari de vivacité s'exécute après l'injection. S'il est manqué, le flux de verdicts est déclaré tronqué. |
| L'analyseur a compris ce que le noyau a dit | Les lignes de trace qu'aucun motif ne reconnaît sont comptées, et tout comptage supérieur à zéro est une erreur plutôt qu'un journal de débogage. |
| Le moniteur n'est pas mort silencieusement | La boucle de lecture enregistre pourquoi elle s'est terminée — arrêt propre, EOF inattendu, délai d'attente ou plantage — et seul un arrêt propre est acceptable. |
| Un verdict manquant n'est pas une réussite | Le lanceur CLI échoue lorsque le nombre de verdicts observés diffère du nombre attendu, dans un sens ou dans l'autre. |
Les paquets canaris sont exclus des résultats par identifiant de trace, ils n'apparaissent donc jamais dans votre flux de verdicts.
La suite de tests prouve que cela tient, plutôt que de l'affirmer : make test-blind force
l'analyseur à ne rien comprendre, et la compilation échoue à moins que le lanceur ne se termine avec un code non nul. Cette tâche s'exécute dans la CI à chaque push.
run_test_pipeline) renvoyant des modèles Pydantic structurés (TestRequest, TraceEvent).nse_<id>) câblé directement à l'hôte.nse_router_<id>) et Serveur (nse_server_<id>) pour les tests de transfert et de NAT.nse-runner). Se termine avec un code non nul en cas de verdict erroné et en cas de verdict qu'il n'a pas pu observer.mypy --strict), application des frontières architecturales (import-linter), formatage ruff, et un cliquet de couverture (make test-cov, plancher à 98 %).nft)ip)ip netns et les opérations de trace du noyau)Sur les systèmes Debian ou Ubuntu :
sudo apt update && sudo apt install -y nftables iproute2 conntrack
Installez le moteur principal avec la prise en charge de la CLI :
pip install "network-sandbox-engine[cli]"
Pour le développement local :
git clone https://github.com/onyks-os/NetworkSandboxEngine.git
cd NetworkSandboxEngine
make setup
import asyncio
from nse.core.netns_controller import NetnsController
from nse.core.pipeline import run_test_pipeline
from nse.models.test_request import TestRequest, PacketSpec
rules = """
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
tcp dport 80 accept
}
}
"""
request = TestRequest(
rules=rules,
packets=[
PacketSpec(protocol="tcp", src_ip="10.0.0.1", dst_ip="10.0.0.2", dst_port=80),
PacketSpec(protocol="tcp", src_ip="10.0.0.1", dst_ip="10.0.0.2", dst_port=22),
],
)
async def main():
controller = NetnsController()
events = await run_test_pipeline(request=request, controller=controller)
for evt in events:
if evt.verdict:
print(f"[{evt.chain}] Verdict: {evt.verdict}")
asyncio.run(main())
Créez un fichier de test firewall_test.yaml :
tests:
- name: "Allow HTTP Port 80, Drop SSH Port 22"
topology: simple
rules: |
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
tcp dport 80 accept
}
}
packets:
- protocol: tcp
src_ip: 10.0.0.1
dst_ip: 10.0.0.2
dst_port: 80
expected_verdict: ACCEPT
- protocol: tcp
src_ip: 10.0.0.1
dst_ip: 10.0.0.2
dst_port: 22
expected_verdict: DROP
expected_verdict est défini par paquet. Les clés inconnues sont rejetées plutôt que
remplacées par une valeur par défaut, ainsi une faute de frappe fait échouer la suite au lieu de devenir silencieusement une attente
que vous n'avez jamais écrite.
Exécutez la suite avec les privilèges root :
sudo nse-runner --file firewall_test.yaml
Codes de sortie : 0 tous les paquets correspondent ; 1 un verdict était erroné ou le moteur
n'a pas pu en observer un. Les erreurs d'oracle sont rapportées séparément des échecs de pare-feu,
car elles signifient que la mesure a échoué, pas l'ensemble de règles.
podman build -t nse .
podman run --rm --cap-add=NET_ADMIN --cap-add=NET_RAW \
-v "$PWD/firewall_test.yaml:/suite.yaml:ro" nse --file /suite.yaml
Utile pour figer la version de nftables sur laquelle vos règles sont testées.
NSE orchestre les sous-systèmes réseau du noyau Linux et les interfaces de trace via un pipeline d'exécution structuré en plusieurs étapes :
graph TD
subgraph Step1["1. Test Specification"]
Req["<b>TestRequest</b><br/>ruleset + packets + topology"]
end
subgraph Step2["2. Ephemeral Netns Sandbox"]
direction TB
Netns["<b>Netns Setup</b><br/>nse_<id> & veth links"]
RuleEng["<b>Rule Engine</b><br/>validate & load nftables"]
Inject["<b>Scapy Injector</b><br/>L2/L3 packet injection"]
NFT["<b>Kernel nftables</b><br/>meta nftrace set 1"]
Netns --> RuleEng
RuleEng --> Inject
Inject --> NFT
end
subgraph Step3["3. Trace Evaluation & Oracle"]
direction TB
Harvester["<b>Trace Harvester</b><br/>nft monitor trace stream"]
Oracle["<b>Deterministic Oracle</b><br/>TraceEvents & verdicts"]
Harvester --> Oracle
end
Step1 --> Step2
Step2 --> Step3RuleEngine.validate() effectue un essai à blanc de l'ensemble de règles avec nft --check -f.NetnsController crée l'espace de noms réseau isolé et configure les interfaces ethernet virtuelles (veth).meta nftrace set 1).ScapyInjector injecte des trames synthétiques à travers le lien veth.TraceHarvester capture les événements nft monitor trace et renvoie des objets TraceEvent structurés.Pour les spécifications techniques complètes, voir le Guide d'architecture technique.
NetworkSandboxEngine/
├── nse/ # Core PyPI package (network-sandbox-engine)
│ ├── core/ # Kernel primitives, pipeline, and naming rules
│ ├── models/ # Pydantic models (TestRequest, PacketSpec, TraceEvent)
│ └── cli/ # Headless YAML runner entrypoint
├── docs/ # Architecture specs and MkDocs web documentation
├── tests/ # Unit, golden file, and privileged e2e tests
│ └── fixtures/nft_trace/ # Golden `nft monitor trace` corpus
├── pyproject.toml # Build backend configuration
└── Makefile # Local automation and CI workflow
Un seul tag. git push origin vX.Y.Z compile, signe avec Sigstore, publie la
GitHub Release, téléverse vers TestPyPI, installe depuis TestPyPI et effectue un test de fumée, et
seulement ensuite téléverse vers PyPI. Répétez à blanc avec make release-dry.
Voir docs/RELEASING.md.
La documentation web interactive complète est disponible à l'adresse :
https://onyks-os.github.io/nse/
Construisez la documentation localement :
make docs
Servez la documentation avec rechargement à chaud sur http://127.0.0.1:8000 :
make docs-serve
Exécutez le linting statique et les tests unitaires :
make verify
Exécutez la vérification CI locale complète (inclut le linting, les tests unitaires, la compilation du frontend, la compilation de la documentation, le test de fumée PyPI et les tests d'intégration privilégiés) :
make ci-local
Ce projet est sous licence MIT License.