Skip to content

diagonalciso/netsim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

netsim

python deps milestones platforms license status

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.


Table of contents

  1. What it is
  2. Download & run (no Python needed)
  3. Run from source
  4. The CLI front-end
  5. The web UI
  6. How it works (the engine in one breath)
  7. Repository layout
  8. Plots & artefacts
  9. The 16 milestones
  10. Packaging summary
  11. Honest simplifications
  12. License

What it is

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.


Download & run (no Python needed)

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)

Linux / macOS

# 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:8112

macOS 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.

Windows

:: 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 web

SmartScreen may warn on a freshly published unsigned .exe — choose More info → Run anyway.

Android

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=CisoDiagonal

The 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).


Run from source

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"; done

Install 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 demos

The 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.gif

The CLI front-end

netsim (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 — the parametric lab

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 15

Units gotcha: bandwidth is in bytes per second and one segment is MSS = 1000 bytes. The dumbbell path is SRC–R1–R2–DST = 3 links each way, so the per-link propagation delay is rtt/6. Throughput is reported in MB/s (delivered × MSS / time / 1e6).


The web UI

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 to reno.

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.


How it works (the engine in one breath)

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 random before 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.

Two delays — don't mix them up

  • 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.

Repository layout

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

Plots & artefacts

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)

The 16 milestones

Each examples/mN_*.py is self-contained and prints its result. Full details and headline numbers are in the MANUAL.

  1. RTT correct: clock + serialization + propagation (m1)
  2. bottleneck drops + queueing (m2)
  3. TCP sawtooth: ACKs, RTT/RTO, cwnd, slow-start, AIMD, fast-retransmit (m3)
  4. two TCP flows share a bottleneck → Jain fairness ≈ 0.98 (m4)
  5. multi-router topology + Dijkstra routing + link-fail reroute (m5)
  6. queue disciplines DropTail/RED/CoDel → bufferbloat study (m6)
  7. congestion-control bake-off: Reno vs CUBIC vs BBR (m7)
  8. SACK loss recovery (RFC6675 pipe) vs Go-Back-N → efficiency under loss (m8)
  9. paced BBRv2 state machine: STARTUP/DRAIN/PROBE_BW/PROBE_RTT (m9)
  10. ECN marking + multi-flow CUBIC/BBR coexistence/fairness (m10)
  11. web workload (FCT), bursty wifi loss (Gilbert–Elliott), live visualizer (m11)
  12. DCTCP (proportional ECN), FQ-CoDel (per-flow isolation), animated GIF (m12)
  13. MPTCP: stripe a connection across disjoint paths (m13)
  14. QUIC-style independent streams (no head-of-line blocking) (m14)
  15. leaf-spine topology + ECMP (per-flow hash) + traffic matrix (m15)
  16. 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.


Packaging summary

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.)


Honest simplifications

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.

License

MIT — see LICENSE. © CisoDiagonal.

About

From-scratch discrete-event packet network simulator (TCP/CUBIC/BBR/DCTCP, SACK, AQM, ECMP, MPTCP, QUIC, pcap) — pure-Python stdlib, for fun & research

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors