Network lab engine for Linux — create isolated, reproducible topologies using namespaces. NLL DSL with loops, imports, cross-references, subnet pools, topology patterns. 28 CLI commands. Inline impairments, nftables, VRF, WireGuard, VXLAN. Millisecond startup, no Docker required.
  • Rust 98%
  • JavaScript 1%
  • Tree-sitter Query 0.8%
  • Just 0.2%
Find a file
Marc Pardo 8a6701dd17 release: 0.7.0 — the Plan 160 / nlink 0.25 arc
Adopts nlink 0.21 → 0.25 and turns the new declarative/ergonomic APIs
into three long-standing closeouts, plus two observability wins and the
free upstream shaping/firewall correctness fixes.

### Workspace dep bump

- **`nlink = "0.21" → "0.25"`.** Four minor versions of upstream
  correctness work, adopted clean — no nlink-lab-side compile change
  (the sole NamespaceGuard is on a synchronous capture thread so 0.25's
  !Send guard is a non-issue; Chain::new Result, LinkStats accessors,
  and the #[non_exhaustive] diff/nl80211/sockdiag changes touch no call
  site). nlink-lab feeds nlink only typed Rate/Duration/Percent, so
  0.25's silent tc-string unit changes cannot reach it.

### Adoptions

- **WireGuard fully declarative.** The imperative device pre-create loop
  (deploy step 6c + its apply_diff mirror) is gone;
  WireguardConfig::ensure_devices bootstraps the link inside
  apply_stack_for_node before the NetworkConfig apply so tunnel
  addresses land on an existing link. Closes feedback item #3 vs 0.19.
- **RateLimiter::reconcile.** Deploy step 15 and the live diff path
  reconcile idempotently instead of tearing down and rebuilding the HTB
  tree — zero kernel calls when unchanged, no packet-drop window. Closes
  Plan 158g.
- **del_*_if_exists teardown.** clear_impairment, mgmt veth/bridge
  teardown, and apply_diff link/route removal use the typed helpers
  instead of QdiscNotFound matching or `let _ = del_*`.
- **Stale-marker self-heal.** The pre-create guard uses
  namespace::is_namespace and clears a stale netns marker left by an
  unclean shutdown, so a crashed deploy re-deploys without a manual
  destroy --orphans.

### Observability

- **watch nftables rule counters.** NewRule drift lines show the typed
  (packets, bytes) counter via RuleInfo::counter().
- **Per-process TCP bandwidth.** The backend collector dumps each node's
  TCP sockets (Connection<SockDiag>), tracks goodput with a per-node
  SocketRateTracker, and attributes flows via SocketOwnerMap. Top flows
  ride on the new NodeMetrics.sockets field (serde(default) for
  wire-compat) and render in the metrics stream.

### Free upstream fixes now in effect

- Traffic shaping is now correct (psched/HTB/TBF tick fixes, #191–#218;
  every RateLimiter / PerPeerImpairer shape was previously 15–125× off).
- Firewall rules install in declared order (#195); reject really rejects
  (#205); impairment flower filters match IPv6/ARP/VLAN (#201).

### Breaking: apply --check --json schema v3

The v1 `diff` / `layered_summary` / `layered_summary_deprecated` fields
(deprecated in 0.6.0 for a one-release window) are removed;
schema_version is now 3. Branch on it and read the typed network /
nftables maps. The human-readable apply --check output is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 10:45:48 +02:00
.github/workflows fix(deploy,test,ci): close 3 regressions surfaced by the new CI matrix 2026-06-01 21:21:51 +02:00
bins release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
crates release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
docs feat(diff,cli): Plan 159d — serde derive on LayeredDiff + JSON schema v2 2026-06-01 10:20:40 +02:00
editors fix: Zed extension config for grammar compilation and block comments 2026-04-02 10:12:27 +02:00
examples docs(cookbook): multi-tenant VRF+WG+nftables WAN — Plan 151 Example B 2026-04-28 13:42:18 +02:00
.gitattributes add .gitattributes to exclude tree-sitter generated C from language stats 2026-04-08 14:28:11 +02:00
.gitignore feat: add shell-style run syntax (Plan 116) 2026-03-31 11:21:11 +02:00
Cargo.lock release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
Cargo.toml release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
CHANGELOG.md release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
CLAUDE.md release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00
justfile fix: install with SUID root and proper capabilities 2026-04-04 17:47:40 +02:00
LICENSE-APACHE chore: add license files, changelog, and crate metadata (plan 086) 2026-03-28 14:21:17 +01:00
LICENSE-MIT chore: add license files, changelog, and crate metadata (plan 086) 2026-03-28 14:21:17 +01:00
nlink-0.19-feedback.md docs(feedback): 2026-06-08 update — all 10 items still open in 0.21 2026-06-08 13:40:45 +02:00
nlink-0.19-realignment.md docs(plans): 159 arc — nlink 0.19 adoption roadmap 2026-06-01 10:15:06 +02:00
nlink-feedback.md docs(plans): 159 arc — nlink 0.19 adoption roadmap 2026-06-01 10:15:06 +02:00
nlink-upstream-asks.md feat(deps): nlink 0.18 — adopt upstream wins, simplify Plan 158 2026-05-29 12:53:30 +02:00
README.md release: 0.7.0 — the Plan 160 / nlink 0.25 arc 2026-07-15 10:45:48 +02:00

nlink-lab

Reproducible Linux network labs in 100ms, scriptable from Rust, with deeper TC, nftables, VRF, WireGuard, and macvlan/ipvlan control than any container-based alternative.

network radio {
  members [hq, alpha, bravo]
  subnet 172.100.3.0/24

  impair hq    -- alpha { delay 15ms loss 1% }
  impair hq    -- bravo { delay 40ms loss 5% rate-cap 10mbit }
  impair alpha -- bravo { delay 60ms loss 8% }
}

That's a 3-node satellite mesh with distance-dependent impairment. There is no way to express this in a single containerlab or Docker-Compose topology — Docker's bridge network gives every container the same egress queue, with no per-destination netem on a shared L2. nlink-lab uses the kernel's HTB+netem+flower TC primitives directly via netlink. Three lines of NLL.

For the full version — 12 satellites in an Iridium-style ring, generated by a for loop with modulo arithmetic, deployed in well under a second — see the satellite-mesh cookbook recipe.

Quickstart

git clone https://github.com/p13marc/nlink-lab && cd nlink-lab
cargo build --release
sudo ./target/release/nlink-lab deploy examples/simple.nll
sudo ./target/release/nlink-lab exec simple host -- ping -c 3 router
sudo ./target/release/nlink-lab destroy simple

Or use the #[lab_test] macro from a cargo test integration suite — no Docker daemon, no compose file, just cargo test:

use nlink_lab::lab_test;

#[lab_test("examples/simple.nll")]
async fn test_connectivity(lab: RunningLab) {
    let out = lab.exec("host", "ping", &["-c1", "10.0.0.1"]).unwrap();
    assert_eq!(out.exit_code, 0);
    // Auto-destroy on drop.
}
  • Deep Linux networking, no Docker required. TC (per-pair netem, HTB, flower, u32, matchall), nftables, VRF, WireGuard (with auto-key generation), macvlan, ipvlan, VXLAN, bonds, bridges with VLAN filtering — all via netlink directly. Runs in any CI runner with CAP_NET_ADMIN + CAP_SYS_ADMIN. No daemon. Sub-second deploy.
  • A real DSL, not stringly-typed YAML. for loops, let variables, parametric import, glob patterns, IP arithmetic (subnet(), host()), conditional blocks. Errors come with miette source spans. Type-checked at parse time.
  • Library-first. use nlink_lab::Topology and deploy from #[tokio::test]. The CLI is a thin wrapper around the library.
  • Reconcile, don't redeploy. nlink-lab apply converges live state to NLL with zero packet loss for unchanged links.

What it isn't

  • A vendor NOS lab tool. If you need cEOS, SR Linux, vMX, vJunos, Palo Alto, or Fortinet images, use containerlab. nlink-lab targets pure-Linux topologies.
  • A multi-host orchestrator. Single-host only.
  • A GUI. CLI + library only.

How it compares to containerlab

The two tools target different jobs. Use containerlab when you need to run a vendor NOS image, want a web UI, or need multi-host clustering. Use nlink-lab when you need depth in Linux networking primitives, library-first Rust testing, or a CI environment without a Docker daemon.

containerlab nlink-lab
Vendor NOS support 80+ images pure Linux only
Per-pair netem on shared L2 structural first-class
TC depth (HTB / flower / u32) ⚠️ raw exec: first-class
VRF + WireGuard composition ⚠️ raw exec: first-class
Multi-host
Web UI
Deploy speed seconds sub-second
CI footprint Docker daemon namespace + caps
Library API Go (undocumented) Rust (first-class)
DSL: loops / imports / arithmetic static YAML

A full feature-by-feature comparison — including capability matrix, side-by-side examples, and migration notes — lives at docs/COMPARISON.md.

Documentation

  • User guide — install, first lab, CLI tour, container management, CI/CD, packet capture.
  • Harness guide — patterns for building Rust test harnesses on top of nlink-lab (spawn ordering, capture endpoint selection, failure-mode debugging, parallel-lab concurrency).
  • NLL language spec — every keyword, with examples.
  • Testing guide#[lab_test] macro and integration test patterns.
  • Troubleshooting — permission errors, MTU mismatches, namespace cleanup.
  • Examples — 40 NLL files covering loops, imports, VRF, WireGuard, VXLAN, containers, scenarios, benchmarks, Wi-Fi.
  • Architecture / design — the why and how.
  • Architecture for contributors — code map, the 18-step deploy sequence, how to add an NLL feature end-to-end.
  • Comparison vs containerlab — capability matrix + side-by-side examples.
  • Active plans — what's coming next (docs overhaul, killer examples, full reconcile, lab archives).
  • Changelog — per-release notes (Keep-a-Changelog format). The README's ## Status section pins the current minor.

Status

Beta. NLL syntax and Rust API stable across patch releases; breaking changes flagged in CHANGELOG with migration notes. Built on nlink 0.25 as of nlink-lab 0.7.0.

Current release: 0.7.0 (2026-07-15) — the "Plan 160 / nlink 0.25" arc. WireGuard is fully declarative (device bootstrap via WireguardConfig::ensure_devices), rate limits reconcile instead of rebuild (RateLimiter::reconcile), teardown uses the typed del_*_if_exists helpers, and deploy self-heals a stale namespace marker. watch shows per-rule nftables counters, and live metrics gain per-process TCP bandwidth (sockdiag goodput + attribution). The nlink bump also makes traffic shaping correct (psched-tick fixes) and installs firewall rules in declared order. apply --check --json is now schema v3 — the deprecated v1 diff / layered_summary / layered_summary_deprecated fields are removed; read the typed network / nftables maps. See CHANGELOG.md for the full per-commit record + migration notes.

Requirements

  • Linux kernel 4.19+ (5.x recommended)
  • One of: root, SUID install, or CAP_NET_ADMIN + CAP_SYS_ADMIN capabilities. Some features need extra caps: CAP_DAC_OVERRIDE for DNS injection, CAP_SYS_MODULE for Wi-Fi (mac80211_hwsim auto-load).
  • Rust 1.85+ (edition 2024)

Editor support

NLL files (.nll) have grammars for VS Code, Neovim (tree-sitter), Helix, and Zed under editors/. Each editor's directory has its own README with install instructions.

License

MIT OR Apache-2.0