Describe a network, run your program in it, measure what happens, and assert on it. In a test, in your CI, or on your laptop.
Status: pre-alpha, release 0.1 is being prepared. The Kernel backend, the command line, the Python package and the pytest plugin work on Linux: you describe a point-to-point link in a file, run it in network namespaces, and get loss, delay percentiles and throughput back, with the seed and the versions that produced them. What is not there yet is listed under "What 0.1 does not do". The feature ledger says what exists, what is next and what is only an idea.
Code that talks over a network behaves differently on a good link and on a bad one: a video call over a mobile network that loses bursts of packets, a game on a link with 150 ms of delay, a protocol on a satellite hop, a service when a link goes down for two seconds. Testing that today usually means a shell script with tc netem, run by hand, with results read by eye, not repeatable on the next machine and not part of the tests.
netscenario is meant to make this a normal, repeatable test:
- A scenario is data. Nodes, links, delay, jitter, loss, rate, written in a short file or built in code. (Bursty loss, queues and changes over time come in the next releases.)
- Results are numbers with provenance. Loss, delay percentiles, throughput, together with the seed, the kernel and the tool versions that produced them.
- It fits the tools you have. A command line, a Python package, a
pytestplugin, and a CI that does not need a special machine. - Later in the plan: your own program or container as a node, assertions that understand noise (repetitions, confidence intervals, baselines), and reports as JUnit and HTML.
Release 0.1.0 is not published yet. Until it is, use "From source" below. The commands in the first two blocks are what will work after the release.
Linux, with the tools ip, tc, unshare, nsenter and prlimit (packages iproute2 and util-linux) and the right to create network namespaces. Debian and Ubuntu 22.04 give a user that right; Ubuntu 24.04 needs one setting; a container needs more (docs/PERMISSIONS.md).
pip install netscenario pytest-netscenario # wheels for Linux, x86_64 and aarch64, Python 3.11 or newercargo install netscenario-cli netscenario-agent # the programs `netscenario` and `netscenario-agent`, Rust 1.89 or newerThe wheel of netscenario carries the two programs, so the Python package needs nothing else. The command line needs both programs side by side, which is why both crates are installed.
From source (a checkout of this repository):
cargo build --release # target/release/netscenario and netscenario-agent
uv sync # the Python packages, if you want themAsk the machine what it can do, then run an example. Nothing needs root on a machine that allows user namespaces.
netscenario doctor
netscenario run scenarios/lte-like.yaml --seed 42running lte-like for 30s ...
netscenario run: lte-like
backend kernel (user namespace, iproute2 6.15.0)
seed 42
workload media client -> server 2 mbit/s, 1200 byte packets
sent 6250 received 6203 lost 47 (0.75 %) reordered 1760
delay min 21.0 ms median 41.1 ms p95 48.9 ms p99 52.4 ms max 57.6 ms
throughput 1.98 mbit/s on average over 30 s (31 bins of one second in result.json)
result: netscenario-results/lte-like-20261030T202546Z/result.json
The scenario is scenarios/lte-like.yaml: two nodes and a link with 40 ms of delay, a jitter of 5 ms, 1 % loss and different rates in the two directions, and one stream of 2 mbit/s. The same seed gives the same lost packets on the same kernel and tc; the delays vary a little from run to run, as they do on a real link.
From Python (docs/PYTHON.md):
import netscenario as ns
result = ns.run(ns.Scenario.load("scenarios/lte-like.yaml"))
assert result["media"].delay.p99 < 0.150 # secondsAs a pytest test (docs/PYTEST.md):
def test_media_survives_the_lte_link(netscenario):
media = netscenario.run("scenarios/lte-like.yaml")["media"]
assert media.loss_ratio < 0.02
assert media.delay.p99 < 0.150 # secondsOn a machine that cannot create namespaces the test is skipped with the reason (and fails on a CI, where a test that never ran proves nothing). Two complete examples with their output are in examples/.
A scenario file that is wrong is refused with the line, the column and a hint:
error: `both` cannot be combined with `forward` or `reverse`
--> scenarios/invalid/both-and-forward.yaml:9:13
|
8 | between: [client, server]
9 | both: { delay: 10ms }
| ^
= path: topology.links[0].both
= help: use `both` for the same values in both directions, or `forward` and `reverse` for different ones
The commands are run, validate, schema, doctor and clean (docs/CLI.md); the result is described field by field in docs/RESULTS.md. If something does not work, docs/TROUBLESHOOTING.md starts from the message.
| Does | Does not do yet |
|---|---|
| Two nodes and one link, IPv4 | More than two nodes (a star, a chain, a dumbbell: 0.3), IPv6 (0.3) |
| Delay, jitter (uniform, normal), independent loss, a rate, different values per direction | Bursty loss, reordering, duplication, outages (0.2), queue disciplines (0.3) |
| Constant-rate UDP streams, measured per packet: loss, one-way delay percentiles, throughput | Your own program or container as a node (0.2) |
Seeds, and the same lost packets for the same seed on the same kernel and tc |
Changes over time: a rate that drops, an outage (0.2) |
| Results as JSON with the versions and the seed that made them | Reports as JUnit and HTML, repetitions and confidence intervals (0.2) |
| A command line, a Python package, a pytest plugin | Backends other than the Linux kernel (0.4 and 0.5) |
A scenario that asks for something that is not in the left column is refused with an error that names the release in which it is planned; it is never approximated silently.
A sketch of release 0.2, not implemented: a timeline, a bursty loss model, and assertions with repetitions. The real format of release 0.1 is described in docs/SCENARIO-FORMAT.md.
# scenarios/lte-burst-loss.yaml
version: 0
name: lte-burst-loss
seed: 42
duration: 30s
topology:
nodes: [client, server]
links:
- id: access
between: [client, server]
forward:
delay: 40ms
jitter: 5ms
rate: 10mbit
loss: { model: gilbert-elliott, p: 0.02, r: 0.3 }
reverse: { delay: 40ms, rate: 20mbit }
timeline:
- { at: 10s, link: access, set: { forward.rate: 2mbit } }
workloads:
- { id: media, kind: udp-cbr, from: client, to: server, rate: 2mbit, packet-size: 1200 }# test_media.py (sketch)
def test_media_survives_burst_loss(netscenario):
run = netscenario.run("scenarios/lte-burst-loss.yaml", repetitions=10)
assert run.metric("media.delivered_ratio").ci95_low > 0.97
assert run.metric("media.delay").p99 < 0.150 # seconds- Correct before convenient. Every backend says what it reproduces faithfully and what it does not. A scenario that asks for something a backend cannot do correctly fails with an error. It is never quietly approximated. See docs/ARCHITECTURE.md.
- The kernel is the reference. The Linux network stack (network namespaces and
netem) is the ground truth. Other backends are checked against it. - Reproducible by default. Seeds everywhere they are possible, and every result records how it was made.
- Nothing happens to the host. All emulation lives in namespaces that netscenario creates and removes. It never changes the network of your machine.
- Complete in the end, released in steps. The feature ledger and the coverage matrix list everything that is planned, and every release has acceptance criteria.
A Rust core (scenario model, scheduler, backends, traffic generators, probes, statistics) with a command line, and a Python package with a pytest plugin on top. Backends: the Linux kernel (namespaces plus netem), later a packet-level emulator in userspace that runs without root, and a portable proxy mode for UDP. The core is also usable from Rust tests. Details: docs/ARCHITECTURE.md.
- FEATURES.md: every planned feature, its priority, its release and its status.
- docs/COVERAGE.md: what is faithful on which backend.
- docs/ARCHITECTURE.md: the parts and how they fit.
- docs/PYTEST.md: the pytest plugin: the fixture, the marker, the guard for machines that cannot run scenarios, the report.
- docs/PYTHON.md: the Python package: scenarios, builder, results in SI units, errors, observing and stopping a run.
- docs/CLI.md: the commands, their options, the exit codes and the JSON they print.
- docs/FIDELITY.md: how close the Kernel backend is to what a scenario says, measured, and what it does that you may not expect.
- docs/RESULTS.md: the result of a run, field by field.
- docs/AGENT.md: the agent in the nodes: the packet, the record file, the command line.
- docs/PERMISSIONS.md: the ways to get the right to create namespaces, on a laptop, in a container and in a CI.
- docs/TROUBLESHOOTING.md: from a message or a symptom to the cause and the fix.
- examples/: a UDP protocol on the command line, and a pytest test, with their output.
- docs/DOCTOR.md:
netscenario doctorandnetscenario clean: what they check, what they print, their exit codes. - docs/SCENARIO-FORMAT.md: the scenario file, draft.
- docs/PRIOR-ART.md: what exists, and where netscenario differs.
- docs/VERSIONING.md: what a version number promises, and what a release may break.
- docs/RELEASING.md: how a release is made, and the one-time setup of the registries.
- docs/DECISIONS.md: the decisions made so far, with reasons.
- spikes/: experiments that check what the Linux kernel allows, to run before building.
Linux for the real emulation, on x86_64 and aarch64; the tests ran on kernels 6.8, 6.12 and 7.0 and older kernels are untested. Python 3.11 or newer; Rust 1.89 or newer to build from source. Other systems are targeted by the portable backends later.
See CONTRIBUTING.md, SECURITY.md and the code of conduct.
Licensed under either of Apache License, Version 2.0 or MIT license, at your option. Unless you say otherwise, any contribution you submit for inclusion is licensed as above, without additional terms or conditions.