A from-scratch discrete-event packet network simulator, written in pure Python standard library. It exists to let you see protocol behaviour emerge — the TCP congestion sawtooth, fair-share convergence, bufferbloat, BBR's pacing phases, head-of-line blocking — by simulating packets one event at a time on a single virtual clock. It is a teaching and research tool, not a byte-accurate RFC implementation; every simplification is called out explicitly.
- 🎓 Course (beginner → expert, hands-on): COURSE.md
- 📖 Full engine + API manual: MANUAL.md
- 📦 Build & distribution guide: PACKAGING.md
- 📱 Android app internals: android/README.md
- ⬇️ Downloads (ready-to-run binaries): Releases
- What it is
- Download & run (no Python needed)
- Run from source
- The CLI front-end
- The web UI
- How it works (the engine in one breath)
- Repository layout
- Plots & artefacts
- The 16 milestones
- Packaging summary
- Honest simplifications
- License
Everything in netsim is an event on one clock plus a priority queue
(netsim/sim.py). Simulated time only advances when an event fires — there is
no real-time loop and no threads. Packets travel through Links (each adds a
serialization delay size/bandwidth plus a fixed propagation delay, and holds a
queue governed by a pluggable queue discipline) between Nodes (Routers
forward by a forwarding table; Hosts run applications and transport endpoints).
[Sim clock] --pops--> [event] --runs--> handler --schedules--> [more events] ...
e.g. Host.send -> Link.tx (serialize) -> propagate -> Node.recv -> ACK -> ...
From that 30-line core, the project builds up — milestone by milestone — to a surprisingly complete transport/network stack: NewReno/CUBIC/BBR/BBRv2/DCTCP congestion control, Go-Back-N and SACK reliability, DropTail/RED/CoDel/FQ-CoDel queue management, ECN, Dijkstra routing with ECMP, MPTCP multipath, QUIC-style independent streams, Gilbert–Elliott bursty loss, and Wireshark-readable pcap export. See the milestone table.
Pre-built, single-file, zero-dependency binaries are attached to every
GitHub Release. Pick
the one for your platform, mark it executable, and run it. There is nothing to
install — the Python runtime is baked into the binary by PyInstaller (desktop)
or Chaquopy (Android). The desktop binaries also bundle matplotlib + pillow,
so the plotting demos (run plots, run gif, run viz) work out of the box
with no extra setup (this is why the desktop downloads are ~80 MB rather than a
few MB).
| Platform | Asset | Notes |
|---|---|---|
| Linux x86-64 | netsim-linux-x86_64 |
ELF, glibc; built on ubuntu-latest |
| Windows x86-64 | netsim-windows-x86_64.exe |
built on windows-latest |
| macOS Apple Silicon | netsim-macos-arm64 |
M-series (arm64); built on macos-latest |
| macOS Intel | (run from source) | no binary — GitHub retired Intel-Mac CI runners; pip install -e . (the arm64 binary won't run on Intel) |
| Android (arm64 + x86-64) | netsim-v*-android-arm64-x86_64.apk |
offline app, signed (see below) |
# example for Linux; substitute the macOS asset name on a Mac
curl -L -o netsim https://github.com/diagonalciso/netsim/releases/latest/download/netsim-linux-x86_64
chmod +x netsim
./netsim list # list all demos
./netsim run m3 # TCP sawtooth
./netsim bakeoff --cc reno,cubic,bbr --rtt 80 --time 15
./netsim web # browser UI on http://127.0.0.1:8112macOS Gatekeeper will quarantine an unsigned download. Either right-click → Open the first time, or clear the quarantine flag:
xattr -d com.apple.quarantine netsim-macos-arm64.
:: download netsim-windows-x86_64.exe from the Releases page, then:
netsim-windows-x86_64.exe list
netsim-windows-x86_64.exe bakeoff --cc reno,bbr --time 10
netsim-windows-x86_64.exe webSmartScreen may warn on a freshly published unsigned .exe — choose More
info → Run anyway.
Install netsim-v*-android-arm64-x86_64.apk (enable "install from unknown
sources" for your browser/file manager). The app is fully offline: it
embeds a CPython runtime, starts the simulator's stdlib HTTP server on
127.0.0.1:8112 inside the app process, and shows the web UI in a WebView. No
network access, no Termux, no server. The APK is signed with the project's own
release key (CN=CisoDiagonal) using APK Signature Scheme v2 + v3. Verify:
apksigner verify --print-certs netsim-v1.0.1-android-arm64-x86_64.apk
# -> "Verified using v2 scheme ... true", "v3 scheme ... true", Signer DN: CN=CisoDiagonalThe desktop ELF/EXE/Mach-O binaries do not run on Android (different ABI) — use the APK (or, for the pure CLI, Termux; see PACKAGING.md).
The core is stdlib-only, so source needs nothing but Python 3.8+:
git clone https://github.com/diagonalciso/netsim
cd netsim
python -m netsim list # via the package entry point
PYTHONPATH=. python3 examples/m3_tcp_sawtooth.py # or run an example directly
for m in examples/m*.py; do echo "== $m =="; PYTHONPATH=. python3 "$m"; doneInstall it as a console command (netsim … from anywhere):
pip install -e . # core, zero deps -> `netsim` on PATH
pip install -e '.[plots]' # + matplotlib/pillow for viz/plots/gif demosThe plotting demos (viz, plots, gif) are the only things that need a
third-party package (the pre-built binaries already bundle it; this only applies
when running from source). Keep matplotlib in a venv so the core stays
dependency-free:
python3 -m venv .venv && .venv/bin/pip install matplotlib pillow
PYTHONPATH=. .venv/bin/python examples/plot_all.py # -> plots/*.png
PYTHONPATH=. .venv/bin/python examples/make_gif.py # -> plots/sawtooth.gifnetsim (module netsim.cli) is the single entry point — identical whether you
run it from source (python -m netsim), the pip console script (netsim), or a
downloaded binary (./netsim).
netsim list # every milestone demo + viz/plots/gif
netsim run m7 # run one demo (m1..m16, viz, plots, gif)
netsim run viz --static # trailing args are forwarded to the demo
netsim bakeoff --help # parametric congestion-control laboratory
netsim web # browser UI, live cwnd/queue charts (:8112)bakeoff runs the same dumbbell topology with whatever knobs you choose and
prints a comparison table (throughput, drops, queue-delay percentiles per
congestion control). It is built directly on the library, so it is the fastest
way to explore behaviour without writing a script.
| flag | meaning | default |
|---|---|---|
--cc |
comma list of reno,cubic,bbr,bbrv2,dctcp |
reno,cubic,bbr |
--bw |
bottleneck bandwidth in bytes/s | 1e6 (1 MB/s) |
--buffer |
bottleneck buffer size in packets | 100 |
--rtt |
base round-trip time in ms | 80 |
--time |
simulated seconds (capped 60) | 15 |
--flows |
concurrent flows per cc (capped 16) | 1 |
--qdisc |
droptail | red | codel |
droptail |
--ecn |
enable ECN marking | off |
--loss |
i.i.d. per-packet loss probability | 0 |
--loss-burst |
use Gilbert–Elliott bursty loss instead | off |
netsim bakeoff --cc reno,cubic,bbr,dctcp --bw 1e6 --buffer 100 \
--rtt 80 --flows 4 --qdisc codel --ecn --time 15Units gotcha: bandwidth is in bytes per second and one segment is
MSS = 1000bytes. The dumbbell path isSRC–R1–R2–DST= 3 links each way, so the per-link propagation delay isrtt/6. Throughput is reported in MB/s (delivered × MSS / time / 1e6).
netsim web serves a single self-contained HTML page from the stdlib
http.server (no Flask, no JS libraries — the charts are hand-drawn on a
<canvas>). It exposes the same knobs as bakeoff as a form, runs the
simulation server-side, and draws two charts: cwnd vs time and bottleneck
queue depth vs time, one coloured line per congestion control, plus a summary
table (throughput, drops, average/p95/max queue).
netsim web # http://127.0.0.1:8112
netsim web --host 0.0.0.0 --port 8112 # bind all interfaces (e.g. for a phone)- Default port is 8112.
- HTTP API:
GET /api/run?cc=…&bw=…&buffer=…&rtt=…&time=…&flows=…&qdisc=…&ecn=…&loss=…&loss_burst=…returns per-cc JSON{cwnd:[[t,v]…], queue:[…], thru, drops, qavg, qp95, qmax}. - Server caps:
time ≤ 60 s,flows ≤ 16, unknown cc names fall back toreno.
This exact page is what the Android app renders inside its WebView — same engine, same page, just served from a loopback socket inside the APK.
while events remain and next_time <= until:
now, fn = pop earliest event # min-heap keyed by timestamp
fn() # the handler may schedule new events
- Time only advances when an event fires. A 10-hour idle link costs nothing; a busy microsecond can cost millions of events.
- Deterministic given the RNG seed — seed
randombefore each run to compare two protocols under the identical loss pattern. - This is the same model as ns-3 / OMNeT++, stripped to its essence. The MANUAL documents the full public API of every module.
- serialization =
size / bandwidth— the link is busy, one packet at a time (this is where a queue forms). - propagation = fixed wire delay — parallel; many packets can be in flight.
netsim/ # the library (pure stdlib)
sim.py # discrete-event engine (clock + heap)
packet.py # Packet dataclass (fields reused across layers)
link.py # bandwidth + propagation + queue (+ optional loss) <- congestion lives here
node.py # Router (forward by table, incl. ECMP) + Host (endpoint w/ apps)
apps.py # UDPSource + Sink + WebClient (Poisson/Pareto web workload) <- m11
tcp.py # TCP engine: ACKs, RTT/RTO, NewReno; GoBackN/SACK; PacedSender
cc.py # congestion control: Reno / Cubic / Bbr / BbrV2 / DCTCP <- m7,m9,m12
qdisc.py # queue disciplines: DropTail / RED / CoDel / ECNThreshold (+ECN) <- m6,m10,m12
loss.py # GilbertElliott bursty (wifi-like) loss model <- m11
fairlink.py# FairLink: per-flow queues + round-robin + per-flow CoDel (FQ-CoDel) <- m12
mptcp.py # MPTCP: one connection striped across subflows (subflow seq + DSN) <- m13
quic.py # QUIC-style independent streams (per-stream delivery, no HoL) <- m14
pcap.py # PcapWriter: export packets as a Wireshark-readable .pcap <- m16
topo.py # Net builder + Dijkstra routing (+ECMP) + link failure/reroute <- m4,m5,m15
cli.py # the `netsim` front-end (list/run/bakeoff/web)
web.py # stdlib web UI server (page + /api/run)
examples/ # m1..m16 self-contained demos + plot_all.py, make_gif.py
android/ # Chaquopy Android app (embeds CPython, WebView over loopback)
netsim.spec # PyInstaller single-file build recipe
pyproject.toml # pip metadata + `netsim` console entry point
.github/workflows/build.yml # CI: builds 4 desktop binaries + signed APK on tags
Generated into plots/ (gitignored) by the venv-only demos:
| file | shows |
|---|---|
sawtooth.png |
Reno cwnd vs ssthresh — slow-start spike then AIMD ramps |
fairness.png |
two flows' cwnd converging to a fair share |
bufferbloat.png |
average queue delay DropTail/RED/CoDel (≈52/13/4 ms) |
cc_cwnd.png |
Reno/CUBIC/BBR cwnd traces |
cc_tradeoff.png |
throughput vs queue delay (BBR bottom-left = low delay) |
sack.png |
SACK vs Go-Back-N link efficiency as loss rises |
bbr_phases.png |
BBRv2 cwnd coloured by phase; PROBE_RTT dips every 10 s |
coexist.png |
two flows' bandwidth split: fair vs BBR-unfair pairings |
sawtooth.gif |
animated AIMD sawtooth + bottleneck queue (make_gif.py) |
trace.pcap |
Wireshark/tshark-readable capture (m16_pcap.py) |
Each examples/mN_*.py is self-contained and prints its result. Full details
and headline numbers are in the MANUAL.
- RTT correct: clock + serialization + propagation (m1)
- bottleneck drops + queueing (m2)
- TCP sawtooth: ACKs, RTT/RTO, cwnd, slow-start, AIMD, fast-retransmit (m3)
- two TCP flows share a bottleneck → Jain fairness ≈ 0.98 (m4)
- multi-router topology + Dijkstra routing + link-fail reroute (m5)
- queue disciplines DropTail/RED/CoDel → bufferbloat study (m6)
- congestion-control bake-off: Reno vs CUBIC vs BBR (m7)
- SACK loss recovery (RFC6675 pipe) vs Go-Back-N → efficiency under loss (m8)
- paced BBRv2 state machine: STARTUP/DRAIN/PROBE_BW/PROBE_RTT (m9)
- ECN marking + multi-flow CUBIC/BBR coexistence/fairness (m10)
- web workload (FCT), bursty wifi loss (Gilbert–Elliott), live visualizer (m11)
- DCTCP (proportional ECN), FQ-CoDel (per-flow isolation), animated GIF (m12)
- MPTCP: stripe a connection across disjoint paths (m13)
- QUIC-style independent streams (no head-of-line blocking) (m14)
- leaf-spine topology + ECMP (per-flow hash) + traffic matrix (m15)
- pcap export — Wireshark/tshark-readable trace with synth Eth/IP/TCP (m16)
Milestones 1–16 complete. Possible future work: BGP/policy routing, NAT/firewall, P4-style programmable dataplane, a real-time interactive TUI.
netsim ships in five forms; full instructions are in PACKAGING.md.
| form | command / asset | needs Python? |
|---|---|---|
| source | python -m netsim … |
yes (3.8+) |
| pip | pip install -e . → netsim |
yes (3.8+) |
| desktop binary | netsim-{linux,windows,macos-arm64}… |
no |
| Android app | netsim-…-android-arm64-x86_64.apk |
no |
| Termux (CLI on phone) | pkg install python && python -m netsim … |
yes (Termux) |
CI (.github/workflows/build.yml) builds the three desktop binaries (Linux,
Windows, macOS arm64) with PyInstaller and the signed Android APK with Chaquopy
on every v* tag, attaching them all to the GitHub Release. (No Intel-Mac
binary — GitHub retired those runners; Intel Macs run from source.)
netsim models protocol behaviour, not bytes. Deliberate scope cuts (none are bugs — the full list is in MANUAL §10):
- Everything is MSS-sized segments (no partial segments, Nagle, or delayed ACKs).
- CUBIC and BBR are teaching approximations, not the Linux implementations — the shapes and directions are representative, exact numbers are not.
- MPTCP congestion control is uncoupled (real MPTCP couples it for fairness).
- QUIC models stream multiplexing + per-stream delivery, not crypto / 0-RTT.
- Routing is static shortest-path (Dijkstra on delay) + ECMP; no BGP/OSPF.
MIT — see LICENSE. © CisoDiagonal.