Describe a network, send traffic through it, measure what happens, and assert on it. In a test, in your CI, or on your laptop.
Status: alpha, release 0.3. The Kernel backend, the command line, the Python package and the pytest plugin work on Linux: you describe a network in a file or in code (two nodes, or a star, a chain, a dumbbell or two paths, over IPv4 and IPv6) with delay, jitter, loss, reordering, duplication, corruption, a rate, queues, outages and changes over time, run it in network namespaces with UDP streams, TCP and request/response workloads or your own program in the nodes, and get loss, delay percentiles, throughput, a series over time and the time to recover back, with the seed and the versions that produced them. Links can come from a library of profiles (LTE, 5G, satellite), from traces and from tables of samples. Repeat a run with derived seeds to get confidence intervals, put the checks in the file (
expect), replay a run from its directory, and make reports as Markdown, JUnit and HTML. What is not there yet is listed under "What 0.3 does and does not do". Alpha means that it is used and tested for real, and that the formats of the scenario and of the result are at version 0 and may change in a minor release (the changelog says what breaks; docs/VERSIONING.md). The feature ledger says what exists, what is next and what is only an idea. A guide with tutorials is online.
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 and routers (a star, a chain, a dumbbell, several paths, IPv4 and IPv6), delay, jitter, loss (also in bursts), reordering, duplication, corruption, a rate, queues, outages and changes over time, written in a short file or built in code, with parameters, templates and a library of profiles.
- Results are numbers with provenance. Loss, delay percentiles, throughput, the series over time and the time to recover after an event, together with the seed, the kernel and the tool versions that produced them. Several runs give intervals, and a run can be replayed.
- It fits the tools you have. A command line, a Python package, a
pytestplugin, reports for a CI (Markdown for the summary of a job, JUnit, one HTML file), and a CI that does not need a special machine. - Later in the plan: a backend that needs no root and is exact, containers as nodes, mobility, baselines and regression detection.
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, for development):
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, replay, report, 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.
A link that changes while a stream runs (scenarios/bandwidth-drop-and-outage.yaml): the rate drops, the link goes quiet for two seconds, and the limit is taken away again.
timeline:
- { at: 10s, link: access, set: { both: { rate: 2mbit } } }
- { at: 20s, link: access, outage: 2s }
- { at: 25s, link: access, set: { both: { rate: null } } }The result has the series by second in two views (what was sent in each second and what arrived in it), how late each step was, and how long each workload took to be back at its level after each event (docs/RESULTS.md; examples/timeline/).
A link that changes when the test says. The test can start a run, wait for a moment, and change a link while it goes, from Python or from asyncio (docs/PYTHON.md, examples/live-change/):
with ns.start("scenarios/lte-like.yaml", live=["access"]) as run:
run.wait_until(10) # seconds into the measured phase
run.link("access").set(loss="5%") # done between two packets, and recorded as a step
result = run.result() # a replay does the change at the same packetOne run is one sample. Run it several times with seeds derived from one seed and assert on an interval (docs/STATISTICS.md):
netscenario run scenarios/lte-like.yaml --repetitions 10 --out results/lte # ten runs of 30 s, an interval for each metric
netscenario report results/lte --format md >> "$GITHUB_STEP_SUMMARY" # the summary of a CI job
netscenario replay results/lte/run-003 # the same seed, the same lost packets?runs = ns.run("scenarios/lte-like.yaml", repetitions=10)
assert runs.metric("media.loss_ratio").ci95_high < 0.05 # the upper end of the 95 % interval of the mean
assert runs.metric("media.delay.p99").ci95_high < 0.150 # secondsYour own program in the network: a node can run a server, a client or the program that you build, started in order, waited for, supervised, with the addresses of the other nodes in its environment, and with the metrics it prints as lines of JSON in the result (docs/PROGRAMS.md; examples/echo-server/). Reports (docs/REPORTS.md) are made from a result directory: Markdown, JUnit with one test case for each workload, and one HTML file with charts and nothing to fetch (sample).
More than two nodes. A shortcut makes the usual shapes (chain, star, dumbbell, multipath), and the long form, a graph with routes of your own, is one netscenario expand away. scenarios/dumbbell.yaml:
topology:
dumbbell:
left: [alice, bob]
right: [carol, dave]
access:
both: { delay: 2ms, rate: 100mbit }
bottleneck:
both: { delay: 20ms, rate: 10mbit }The routers forward, the routes are computed (or listed), and the result says for each link how many packets entered, were dropped by the queue, by the loss and arrived, so that a loss can be put on a link. netscenario show draws the network and the path of each workload, as text or Graphviz.
TCP and request/response. Bulk transfers and closed loops over the TCP of the kernel, with the congestion control you name, measured per connection (goodput, the round trip and the window from the kernel). The classic fairness test, two flows over a dumbbell, is scenarios/tcp-fairness.yaml; the acceptance checks the sum of the goodputs, the Jain index, the formula of Mathis, the split over two paths and the queue of CoDel against what is known (docs/ACCEPTANCE.md).
Links from the library. profile: lte-good gives the delays of a published measurement; what you write next to it wins. Every number has a source (docs/PROFILES.md, netscenario profiles). Links can also follow a trace of rates, a recorded stream of packets can be replayed, and delays can come from a file of samples or from a normal, Pareto or Pareto-normal distribution.
Checks in the file. expect: holds the claims on a run or on a set of runs, with the verdict in the result, the reports and the exit code (scenarios/expect.yaml).
| Does | Does not do yet |
|---|---|
| Two nodes or a network of up to eight namespaces: star, chain, dumbbell, several paths with failover, ECMP and weights, routes of your own; IPv4, IPv6 and both; an MTU with fragments; links that go down and up | Nodes that join and leave (0.6), a network of more than eight nodes |
| Delay, jitter (uniform, normal, Pareto, Pareto-normal, tables), loss at random and in bursts, reordering, duplication, corruption, a rate, queues (tail drop, CoDel, FQ-CoDel, PIE), traces, outages, different values per direction, a library of profiles | Profiles for Wi-Fi, cable and the tiers of cellular networks that have no published source yet (docs/PROFILES.md) |
| Changes over time: steps, links down and up, outages, models that follow a pattern (a periodic one, a Markov chain), events that repeat; a live change from a test | Mobility and geometry (0.6) |
| UDP streams (constant, Poisson, on/off, bursty, replay of a recorded stream, echo), TCP bulk and request/response, measured per packet or per connection, with the series over time and the time to recover | A container as a node (0.5), MPTCP |
| Your own program as a node, with readiness, supervision, logs and metrics; packet capture of a link as pcap | Baselines and regression detection (0.7) |
Seeds, and the same lost packets for the same seed on the same kernel and tc; repetitions with confidence intervals; replay of a run; checks in the file |
Backends other than the Linux kernel (0.4 and 0.5) |
| Parameters, includes and templates in the file; results as JSON with the versions and the seed; reports as Markdown, JUnit and HTML | A cookbook (0.8) |
| A command line, a Python package, a pytest plugin, a guide |
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. Release 0.3 changes the format of the scenario file and of the result (version 0 of both, which may change in a minor release): the changelog names what breaks.
- 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, repetitions, reports, replay, errors, observing and stopping a run.
- docs/CLI.md: the commands (
run,replay,report, ...), 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.
- docs/book/: the guide, with tutorials for a tester, a network engineer and the maintainer of a CI, and a chapter for each shape of network; every scenario in it is checked and the short ones are run by the tests.
- examples/: a UDP protocol on the command line, your own program over a bad link, a pytest test with a confidence interval, and a link that changes during a run, with their output.
- docs/STATISTICS.md: repetitions and confidence intervals: the method, its coverage, and what "stable" means.
- docs/PROGRAMS.md: your own program as a node: order, readiness, supervision, logs, metrics.
- docs/REPORTS.md: Markdown, JUnit and HTML from a result directory, with a CI example and a sample.
- scenarios/: example scenarios, checked by the tests.
- docs/DOCTOR.md:
netscenario doctorandnetscenario clean: what they check, what they print, their exit codes. - docs/SCENARIO-FORMAT.md: the scenario file, version 0: every key, every unit, every error.
- docs/PRIOR-ART.md: what exists, and where netscenario differs.
- docs/PROFILES.md: the library of link profiles (3G to satellite) with the source of every number.
- docs/ACCEPTANCE.md: how each criterion of a release is proven, and what the proof does not cover.
- docs/TESTING.md: how the tests are organised, where each kind runs, how coverage and mutation tests are measured, and the rules for a new test.
- 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 (one file each in docs/decisions/).
- 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.