Release Notes & Changelog
All notable changes to the Network Sandbox Engine (NSE) project are documented in this file.
The format follows Keep a Changelog. This project adheres to Semantic Versioning.
[Unreleased]
Added
publish-docs.yml— the documentation site is rebuilt and pushed toonyks-os.github.io/nsewhen a release is published, from the released tag. Publication was a manual step: 2.1.1 shipped on 2026-09-09 and the published site still did not mention it two days later.
Fixed
- The
S607migration guard was linting a package that does not exist here. It ranruff check --select S607 ttp/— TTP's package name, copied across with the test.ruff checkon a missing path warns on stderr, then prints "All checks passed!" and exits 0, so the guard reported success while reading no source at all. It now lintsnse/, asserts the directory exists, and treats a lint that could not open its target as a failure. - The guard has a positive control. "All checks passed!" is worth nothing
unless the rule would have said otherwise, so a file with a bare-binary call is
fed to ruff and
S607must be reported. - CI could not find ruff. The test invoked a bare
ruff, which resolves through$PATH;make setupinstalls it into the virtualenv, which CI does not put on$PATH. A missing bare name raisesFileNotFoundErrorrather than returning 127, so the "ruff is not available" skip never ran and all four unit jobs errored. Ruff is now invoked assys.executable -m ruff.
[2.1.1] - 2026-09-10
Security: the engine no longer resolves a root-executed binary through $PATH.
Security
- PATH hijacking closed. NSE creates network namespaces and reads kernel
trace events, so it runs as root - and it invoked
ip,nftandnsenterby bare name, leaving the kernel to resolve them through$PATH. Anyone able to influence the environment of thesudoinvocation could place their ownipearlier in the search order and have it executed with full privileges.
This is the same defect that TTP closed in its 0.4.8 cycle, and it mattered more here than it looks: NSE is the instrument TTP's zero-leak claim rests on. A verification engine with a privilege-escalation path is a strange thing to trust about a firewall.
New nse/core/paths.py resolves every binary against a fixed list of
root-owned system directories, never $PATH, and refuses one that is group-
or world-writable, or that sits in a writable directory - write access there
is enough to replace the file by rename. BinaryNotFoundError is a
FileNotFoundError, so callers that already handled a missing tool keep
working unchanged.
66 call sites migrated. The deliverable is
test_a_hostile_nft_on_path_is_not_executed and
test_the_engine_really_invokes_the_trusted_binary, which plant a hostile
binary first on $PATH and assert the argv handed to subprocess still names
the trusted absolute path - the second catches a call site the migration
missed, which asserting on resolve() alone would not.
Changed
- Test coverage 98.45% → 98.09% across a larger surface: 239 → 259 tests.
[2.1.0] - 2026-09-08
Trustworthy oracle, archived web interface, and a test suite that can fail.
The theme of this release is one defect repeated in several places: a check that could not fail. The CLI runner reported success when it observed nothing, the readiness probe signalled readiness before the kernel was listening, an unparsed trace line vanished at DEBUG level, and the parser's only test disappeared if its fixture directory were emptied. Each of those made a green result compatible with a blind instrument.
⚠️ Breaking Changes
- Archived the web interface (
gui/): the FastAPI + Svelte application, its Uvicorn server, theguiextra and thense.serviceunit are removed. Since 2.0.0 that server ran in-process as root, which is a large attack surface for a testing tool with no external users. The code remains in git history at tagv2.0.0. NSE is now a library and a CLI: no socket, no port, no RPC. - Stricter suite-file schema: unknown keys in a test case or a packet entry
are now errors instead of being silently defaulted. A misspelled
expect_verdictused to become an implicit expectation ofACCEPT. The README's own YAML example was invalid under the real schema and has been corrected. REJECTnormalises toDROPin expectations: the trace stream cannot distinguish them, so an expectation ofREJECTis compared asDROPrather than never matching.ScapyInjector.inject()takeshost_netns: the namespace owning the sending interface is now passed by the caller. It used to be derived from the interface name asnse_router_<suffix>, a namespace nothing has ever created, so gateway-topology MAC lookups could only fail.matchevents report upper-cased verdicts, consistent withverdictevents. The same verdict previously had two spellings depending on the line it came from.
Fixed
- The runner could report PASSED having observed nothing (
nse/cli/runner.py). The count-mismatch branch printed[FAIL] Oracle Errorwithout setting the failure flag, andzip(..., strict=False)truncated the comparison in both directions. A run with zero observed verdicts printed=> SUCCESSand exited 0 — including in this project's own CI, where the YAML runner is the end-to-end gate. - The readiness probe proved nothing (
nse/core/trace_harvester.py).wait_ready()fired when the read loop was scheduled, not whennft monitor tracehad subscribed to the kernel, so packets injected in that window were lost. It is retained for diagnostics and documented as insufficient. - The trace deadline could expire mid-run (
nse/core/pipeline.py). A fixed 5-second budget, set when the loop started, truncated the verdict stream at roughly 33 packets. The deadline is now extended after every injection, and the read loop polls it so an extension actually takes effect. - A crashed read loop was indistinguishable from a clean one. Both pushed the
same
Nonesentinel. The harvester now records a terminal state. - Unparsed trace lines vanished silently. They are counted; any line that looks like trace output but matches no pattern is an oracle error.
- The parser could not read valid nftables identifiers. Table and chain names
were matched with
\w+, so any name containing-,.or/, or any quoted name, failed to parse — silently. Quoted names containing spaces now parse too. - Deleting the parser's fixtures deleted its tests.
parametrizeover an empty glob collects zero tests and reports success. PacketSpecvalidation errors escaped as tracebacks instead of being reported as a failed test case.
Added
- Canary probes as a permanent positive control (
nse/core/pipeline.py). A probe packet is injected before the test packets and again after them; the run is reported only if both were observed in the kernel trace. Canaries are excluded from results by trace id. This is what makes a "no leak" result evidence rather than a hope. HarvestStateandTraceHarvester.health_errors(): the read loop's outcome (clean stop, unexpected EOF, timeout, crash) is explicit, and the pipeline turns anything unhealthy into anerrorevent the runner fails on.NSE_FORCE_BLINDandmake test-blind: a test hook that makes the parser understand nothing, plus a CI job asserting the suite then fails. This is the meta-test that guards the guard.- Oracle errors are reported separately from firewall failures in the runner summary: a broken measurement and a broken ruleset are different problems.
- Golden corpus of six
nft monitor tracefixtures covering IPv6, gateway forwarding and NAT, NSE's own scaffolding table, hyphenated and quoted identifiers, and the quoted/unquotediifvariants — plusscripts/capture_trace_fixture.shfor adding captures from new kernels. test_parser_understands_every_line_of_a_real_trace: asserts on the actual kernel under test that zero trace lines were unparsed. CI runs it onubuntu-22.04andubuntu-24.04, so a format change breaks a build instead of blinding the oracle.- Coverage ratchet:
make test-covenforces a floor (currently 98%), withpytest-cova declared dev dependency rather than something you happen to have. - Runner logic is unit-testable:
reduce_verdicts,build_case,evaluate_caseandload_suiteare pure functions.nse/cli/runner.pywent from 0% to 98% coverage. nse.core.mock_listener.main(): the CLI entry point is a function, so it can be tested.- Container runner image: the Dockerfile now builds a CLI image for running a suite against a pinned nftables version.
Changed
- Test coverage: 51% → 98% across
nse/, 21 tests → 232. The three modules that carry correctness were the three least covered:runner.py0% → 98%,pipeline.py30% → 99%,trace_harvester.py35% → 98%. - Import contracts rewritten now that
gui/is gone:nse.coreandnse.modelsmay not importnse.cli, andnse.modelsmay not import the engine. --strict-markers: a typo in a pytest marker silently deselected the test it was meant to tag.- CI gained coverage, multi-image integration and blindness jobs, and lost the Node 24 frontend job.
[2.0.0] - 2026-08-13
Single-Process In-Process Architecture, Pydantic Hard Dependency, Deterministic Verdict Oracle, and Strict Static Typing.
⚠️ Breaking Changes
- Elimination of
rootdDaemon: Removed socket daemongui/rootd.py,gui/api/rootd_client.py, andgui/api/deps.py. FastAPI web application andNetnsControllernow run directly in-process with root privileges. - Mandatory Pydantic Dependency:
pydanticpromoted to a mandatory core dependency ofnse/; eliminated all stdlib fallback dataclasses and stubs. - Consolidated Model Hierarchy: Deleted
nse/models/base.py.TopologyTypeandPacketSpecnow collapse to a single source of truth innse.models.test_request. - Target Orchestrator Signature: Rewrote
run_test_pipelinesignature to target standard request-driven API returninglist[TraceEvent]. - Purged Controller State: Removed
enqueue_test,release_test,has_test,get_status,get_event_queue, and_teststracking methods fromNetnsController.
Added
- Trace Harvester Readiness Probe: Added
wait_ready()inTraceHarvesterusingasyncio.Eventreadiness signal, eliminating hardcoded warm-up delay sleeps. - Uniform Naming & Startup Sweep: Created
nse.core.naminghelper withderive_names()and automated orphan netns/veth startup sweep inNetnsController. - Teardown Retry & Subprocess Timeouts: Implemented retry backoff in
destroy_netnsand enforcedtimeout=parameters across allsubprocess.runcalls. - Strict Static Typing & Import Boundaries: Enforced
mypy --strictacross all 22 source files and configuredimport-lintercontract preventingnse/from importinggui/. - MkDocs Web Documentation: Created comprehensive web documentation site powered by MkDocs Material and
mkdocstrings. - Local CI Automation: Added
make ci-localandmake docsMakefile targets.
1.1.1 - 2026-08-05
Kernel tracing initialization fix, background noise filtering in YAML test runner, and Makefile dynamic binary resolution.
Added
- Automatic Kernel Tracing Prepending: Automatically inject a high-priority
table inet nse_traceprerouting chain (meta nftrace set 1) inRuleEngine.load()to guaranteenft monitor tracecaptures trace events for all test packets.
Fixed
- YAML Runner Trace Noise Filtering: Filtered out background setup noise (IPv6 NDP, DAD, MLD, and socket init packets) in
nse.cli.runnerto accurately match verdicts (ACCEPT/DROP) to injected test packets. nft monitor traceRegex Parser: Updated_PACKET_REingui/daemon/trace_harvester.pyto handle both quoted and unquotediifstrings in trace lines across different Linux kernel andnftablesversions.- Dynamic Executable Resolution in Makefile: Updated
Makefileto dynamically detectPYTHON,RUFF,TWINE, andPYTESTin.venvwith automatic fallbacks toPATHsystem binaries. - Ruff Code Quality Compliance: Resolved all Ruff static analysis errors (
ASYNC221,I001,BLE001,UP037,TRY401,PIE790) acrossnse/,gui/, andtests/.
1.1.0 - 2026-06-19
Introducing native container environments support and a Zero-Trust Privilege Separation architecture for the web server.
Added
- Native Container Support (
nsenterfallback): Added robust container detection innse/core/utils.py(checkingcontainerenv,/.dockerenv,/proc/1/environ, andcgroupformat). Dynamic fallback fromip netns exectonsenter --netnamespace switching prevents remount errors in container runtimes. - Zero-Trust Privilege Separation:
nse-rootdUNIX domain socket server running as root and managing network namespaces, Scapy injection, and trace harvesting. Secure/var/run/nse-core.socksocket is automatically chowned toSUDO_UID/SUDO_GIDwhen run viasudo.RootdClientclient proxy allowing unprivileged web server instances (nse-web/gui/server.py) to delegate low-level sandbox execution without running as root.- Dedicated RPC Unit Tests: Added asynchronous mocking test
test_rootd_rpc_communicationto verify JSON-RPC protocol between client and daemon.
Changed
- Makefile and dev-setup: Restructured commands (
make run-rootd,make run-web,make backend, andmake dev) and updated startup instructions to reflect the decoupled daemon architecture.
1.0.0 - 2026-06-18
First stable release of the Network Sandbox Engine.
Summary
NSE v1.0.0 is published as a headless Python library (network-sandbox-engine on PyPI) with an optional GUI layer that lives in-repository. The core engine depends only on scapy; CLI tooling requires pydantic and pyyaml via the [cli] extra. The GUI daemon (FastAPI and Svelte) is excluded from the wheel by design and is run from a repository clone.
Added
Core Headless Engine (nse/)
NetnsController: async context manager for ephemeral Linux network namespace lifecycle (create, configure, teardown). Supportssimple(host to sandbox) andgateway(host to router to server) topologies.PCAPAsserter: wraps ScapyAsyncSnifferto arm BPF-filtered captures on veth interfaces and assert captured packet counts in integration tests.RuleEngine: validates and loadsnftablesrulesets usingnft --check -fandnft -f. Parses line-level error messages into structuredRuleValidationErrorexceptions. Automatically arms kernel tracing viameta nftrace set 1.ScapyInjector: forges and injects IPv4/IPv6 TCP, UDP, ICMP, and ICMPv6 packets at Layer 2/3. Uses in-processsendp()for host-originating packets andip netns execsubprocess for egress from inside a namespace.run_test_pipeline(): top-level orchestrator that chains validation, topology setup, mock listener spawning, rule loading, trace harvesting, sequential packet injection, conntrack polling, and namespace teardown.parse_conntrack_line(): parser for/proc/net/nf_conntrackentries. Extractsproto,state,src,dst,sport, anddportfor both IPv4 and IPv6 flows.- Stateful traffic and conntrack integration:
/proc/net/nf_conntrackis polled after each packet injection and connection states (SYN_SENT,ESTABLISHED,TIME_WAIT) are streamed to consumers. - Dual-stack IPv4/IPv6: all veth links are configured with both address families. DAD is disabled globally inside namespaces (
accept_dad=0) for instant address availability.
Data Models (nse/models/)
PacketSpec: Pydantic model (lazy import) defining protocol, IPs, ports, TCP flags, and packet size. Validates IPv4/IPv6 addresses and allowed flag values.TestRequest: Pydantic model withrules,packets: list[PacketSpec], andtopology: TopologyType.TopologyType: string enum with valuessimpleandgateway.TraceEvent: Pydantic model for kernel trace output events of typehook,match, orverdict.base.py: pure stdlib dataclasses for use without Pydantic.
CLI Runner (nse/cli/runner.py)
nse-runner --file <yaml>CLI entrypoint registered inpyproject.toml.- Reads YAML test suites, invokes
run_test_pipeline(), evaluates expected verdicts, and prints formatted results. - Silent drops (no matching
TraceEvent) are treated asDROP. - Exits with
0on full pass,1on any failure. - Displays a clear install hint if
pydanticorpyyamlis missing.
GUI Daemon (gui/ - not on PyPI)
TraceHarvester: async subprocess spawningnft monitor traceinside the evaluation namespace. Parsed events are pushed into anasyncio.Queue.MockListener: background TCP/UDP echo daemon spawner usingip netns exec. Enables complete TCP handshakes and valid conntrack state generation.- FastAPI REST API:
POST /api/testandGET /api/status/{test_id}. - WebSocket streaming:
WS /ws/{test_id}streamsTraceEventJSON messages to the frontend.
Frontend (gui/gui_svelte/ - not on PyPI)
- Rule editor for
nftablesruleset authoring. - Multi-packet sequence crafter with topology selector.
- Real-time animated pipeline visualizer: hook, rule match, and verdict events.
- Conntrack table: live tabular view of active connection states.
- Offline documentation view at
#/docs.
Packaging and Release
pyproject.tomlat repository root. Build backend:setuptools. Targets onlynse/viapackages.find.include. Hard dependency:scapy>=2.5.0. Optional extra[cli]:pydantic>=2.0.0andpyyaml>=6.0.make release: runslint + test, builds the wheel and source distribution, copies deployment assets, generatesSHA256SUMS, and signs it with GPG. The signing key is auto-detected from the keyring and can be overridden withGPG_KEY_ID=<id>.Dockerfile: multi-stage production image for containerized daemon deployment.scripts/nse.service: systemd unit template binding to/run/nse.sockin production.tests/test_netns.py: unified test suite with 20 unit tests and 2 root-only integration tests.conftest.py: rootsys.pathinjection allowing pytest to discover bothnse/andgui/packages.
Makefile Targets
| Target | Description |
|---|---|
make setup |
Bootstrap venv and run npm install |
make test |
Run unit tests |
make integration-test |
Run root-level integration tests |
make lint |
Ruff static analysis |
make format |
Ruff auto-format |
make verify |
Run lint and test |
make release |
Build and sign all release artifacts |
make publish-test |
Upload to TestPyPI via Twine |
make publish |
Upload to PyPI via Twine |
Changed
- Repository restructured from a monolithic
backend/nse/layout to a root-level separation: nse/is the headless PyPI package (replacesbackend/nse/).gui/is the GUI daemon (replaces GUI modules formerly inbackend/nse/and thefrontend/directory).tests/is the unified test suite (replacesbackend/tests/).TraceHarvesterandMockListenermoved togui/daemon/. These components are part of the GUI layer and are not included in the wheel.- Pydantic imports in
nse/models are wrapped intry/except ImportErrorto allow the core to be imported with onlyscapyinstalled. TestRequest.packetchanged toTestRequest.packets: list[PacketSpec]to support packet sequences. This is a breaking API change.pyproject.tomlmigrated frombackend/pyproject.tomlto the repository root. Build backend changed from Poetry to setuptools.Makefileupdated with root-level paths.
Fixed
- GPG signing in
make releasefailed with "no default secret key" in non-interactive shells. Fixed by adding--local-user $(GPG_KEY_ID)with automatic key detection from the keyring. - Interface name assertion in
test_create_namespace_lifecycle_mockedwas off by one character for 8-character namespace names. - Svelte compilation crash caused by raw curly braces in code blocks. Fixed by escaping them as
{and}.