How-To: Embedding NSE in Python
NSE is a library first. This guide covers calling the engine directly from your own test suite, which is how you assert things about a ruleset that a YAML file cannot express.
Running one test
import asyncio
from nse.core.netns_controller import NetnsController
from nse.core.pipeline import run_test_pipeline
from nse.models.test_request import PacketSpec, TestRequest
RULES = """
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
tcp dport 80 accept
}
}
"""
async def main() -> None:
request = TestRequest(
rules=RULES,
packets=[
PacketSpec(protocol="tcp", dst_port=80),
PacketSpec(protocol="tcp", dst_port=22),
],
)
events = await run_test_pipeline(request=request, controller=NetnsController())
# ALWAYS check for oracle errors before reading verdicts.
errors = [e.raw_message for e in events if e.type == "error"]
if errors:
raise AssertionError(f"the measurement failed, not the firewall: {errors}")
for evt in events:
if evt.verdict and evt.table != "nse_trace":
print(f"[{evt.chain}] {evt.verdict}")
asyncio.run(main())
Check for error events first
run_test_pipeline returns an error event when its canary probes were not
observed, i.e. when the kernel trace was not actually being read. If you skip
that check and go straight to counting verdicts, an empty list looks exactly
like "the firewall dropped everything" — which is the bug this whole design
exists to prevent. Reuse nse.cli.runner.collect_oracle_errors if you like.
Reducing a trace stream to verdicts
Rather than filtering events by hand, reuse the runner's logic:
from nse.cli.runner import collect_oracle_errors, reduce_verdicts
errors = collect_oracle_errors(events)
assert not errors, errors
assert reduce_verdicts(events) == ["ACCEPT", "DROP"]
reduce_verdicts groups events by trace_id, drops NSE's own nse_trace
scaffolding, and resolves each packet to a single verdict with DROP/REJECT
taking precedence over ACCEPT.
In pytest
import os
import pytest
from nse.cli.runner import collect_oracle_errors, reduce_verdicts
from nse.core.netns_controller import NetnsController
from nse.core.pipeline import run_test_pipeline
from nse.models.test_request import PacketSpec, TestRequest
pytestmark = [
pytest.mark.asyncio,
pytest.mark.skipif(os.geteuid() != 0, reason="NSE needs root for netns and kernel tracing"),
]
async def test_ssh_is_blocked(my_ruleset: str) -> None:
request = TestRequest(
rules=my_ruleset,
packets=[PacketSpec(protocol="tcp", dst_port=22)],
)
events = await run_test_pipeline(request=request, controller=NetnsController())
assert not collect_oracle_errors(events)
assert reduce_verdicts(events) == ["DROP"]
Streaming events as they arrive
Pass a queue to consume events live — useful for progress output on a long suite:
queue: asyncio.Queue = asyncio.Queue()
task = asyncio.create_task(run_test_pipeline(request=request, queue=queue))
while True:
evt = await queue.get()
if evt is None: # sentinel: monitoring ended
break
print(evt.type, evt.trace_id, evt.verdict)
events = await task
The queue receives canary events too, since it is a raw feed; the list returned by the coroutine is the filtered one.
Tuning
Every timing knob is an environment variable, so you rarely need to touch code. See CLI reference. The two that matter most for slow machines and CI runners:
| Variable | Effect |
|---|---|
NSE_CANARY_ATTEMPTS |
Raise it if readiness canaries fail on a loaded runner. |
NSE_TRACE_IDLE_TIMEOUT |
Raise it if traces arrive slowly under heavy load. |
Do not work around a failing canary by ignoring the error. A canary that never arrives means the engine could not see the kernel; whatever it would have reported about your ruleset would have been fiction.