{"id":49001205,"url":"https://github.com/n0-computer/patchbay","last_synced_at":"2026-04-18T18:37:29.166Z","repository":{"id":341470954,"uuid":"1164689924","full_name":"n0-computer/patchbay","owner":"n0-computer","description":"Rust library for realistic network simulation via Linux namespaces","archived":false,"fork":false,"pushed_at":"2026-04-08T22:11:19.000Z","size":1947,"stargazers_count":22,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-08T23:27:55.301Z","etag":null,"topics":["hole-punching","netlab","netns","netsim","network-simulation","networking","p2p","rust"],"latest_commit_sha":null,"homepage":"https://n0-computer.github.io/patchbay/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/n0-computer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-23T11:24:32.000Z","updated_at":"2026-04-02T19:54:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/n0-computer/patchbay","commit_stats":null,"previous_names":["n0-computer/netsim-rs","n0-computer/patchbay"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/n0-computer/patchbay","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/n0-computer%2Fpatchbay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/n0-computer%2Fpatchbay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/n0-computer%2Fpatchbay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/n0-computer%2Fpatchbay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/n0-computer","download_url":"https://codeload.github.com/n0-computer/patchbay/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/n0-computer%2Fpatchbay/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31980513,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T17:30:12.329Z","status":"ssl_error","status_checked_at":"2026-04-18T17:29:59.069Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hole-punching","netlab","netns","netsim","network-simulation","networking","p2p","rust"],"created_at":"2026-04-18T18:37:28.256Z","updated_at":"2026-04-18T18:37:29.157Z","avatar_url":"https://github.com/n0-computer.png","language":"Rust","readme":"# patchbay\n\npatchbay lets you build realistic network topologies out of Linux network\nnamespaces and run real code against them. You define routers, devices, NAT\npolicies, and link conditions through a Rust builder API, and the library\nwires everything up with veth pairs, nftables rules, and tc qdisc\nscheduling. Each node gets its own namespace with a private network stack,\nso processes running inside see what they would see on a separate machine.\nEverything runs unprivileged and cleans up when the `Lab` is dropped.\n\n## Quick example\n\nSee the [`simple.rs`](patchbay-runner/examples/simple.rs) example for the runnable version.\n\n```rust\n// Enter a user namespace before any threads are spawned.\npatchbay::init_userns().expect(\"failed to enter user namespace\");\n\n// Create a lab (async - sets up the root namespace and IX bridge).\nlet lab = Lab::new().await?;\n\n// A public router: downstream devices get globally routable IPs.\nlet dc = lab\n    .add_router(\"dc\")\n    .preset(RouterPreset::Public)\n    .build()\n    .await?;\n\n// A home router: downstream devices get private IPs behind NAT.\nlet home = lab\n    .add_router(\"home\")\n    .preset(RouterPreset::Home)\n    .build()\n    .await?;\n\n// A device behind the home router, with a lossy WiFi link.\nlet dev = lab\n    .add_device(\"laptop\")\n    .iface(\"eth0\", home.id())\n    .build()\n    .await?;\ndev.iface(\"eth0\").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;\n\n// A server in the datacenter.\nlet server = lab\n    .add_device(\"server\")\n    .iface(\"eth0\", dc.id())\n    .build()\n    .await?;\n\n// Run an OS command inside a device's network namespace.\nlet mut child = dev.spawn_command({\n    let mut cmd = tokio::process::Command::new(\"ping\");\n    cmd.args([\"-c1\", \u0026server.ip().unwrap().to_string()]);\n    cmd\n})?;\nchild.wait().await?;\n\n// Spawn an async task on the device's per-namespace tokio runtime.\nlet client_task = dev.spawn(async move |_dev| {\n    let mut stream = tokio::net::TcpStream::connect(addr).await?;\n    println!(\"local addr: {}\", stream.local_addr()?);\n    stream.write_all(b\"hello server\").await?;\n    anyhow::Ok(())\n})?;\n```\n\n\n## Requirements\n\n- Linux (bare-metal, VM, or CI container).\n- `tc` and `nft` in PATH (for link conditions and NAT rules).\n- Unprivileged user namespaces enabled (default on most distros):\n\n  ```bash\n  sysctl kernel.unprivileged_userns_clone   # check\n  sudo sysctl -w kernel.unprivileged_userns_clone=1  # enable\n  ```\n\n  On Ubuntu 24.04+ with AppArmor:\n\n  ```bash\n  sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n  ```\n\nNo `sudo` is needed at runtime. The library bootstraps into an unprivileged\nuser namespace where it has full networking capabilities.\n\n## Architecture\n\nEvery node (router or device) gets its own network namespace. A lab-scoped\nroot namespace hosts the IX bridge that interconnects all top-level routers.\nVeth pairs connect namespaces across the topology.\n\nEach namespace has a lazy async worker (single-threaded tokio runtime) and a\nlazy sync worker. `device.spawn(...)` runs async tasks on the namespace's\ntokio runtime; `device.run_sync(...)` dispatches closures to the sync\nworker. Callers never need to worry about `setns`; the workers handle\nnamespace entry.\n\n### Multi-region routing\n\nRouters can be assigned to regions, and regions can be linked with simulated\nlatency. When two routers live in different regions, traffic between them\nflows through per-region router namespaces with configurable impairment,\ngiving you realistic cross-continent delays on top of the per-link\nconditions.\n\n```rust\nlet eu = lab.add_region(\"eu\").await?;\nlet us = lab.add_region(\"us\").await?;\nlab.link_regions(\u0026eu, \u0026us, RegionLink::good(80)).await?;\n\nlet dc_eu = lab.add_router(\"dc-eu\").region(\u0026eu).build().await?;\nlet dc_us = lab.add_router(\"dc-us\").region(\u0026us).build().await?;\n// Traffic between dc-eu and dc-us now carries 80ms of added latency.\n```\n\nYou can also tear down and restore region links at runtime with\n`lab.break_region_link()` and `lab.restore_region_link()` for fault\ninjection scenarios.\n\n### Router presets\n\n`RouterPreset` configures NAT, firewall, IP support, and address pool in\none call to match real-world deployment patterns:\n\n```rust\nlet home = lab.add_router(\"home\").preset(RouterPreset::Home).build().await?;\nlet dc   = lab.add_router(\"dc\").preset(RouterPreset::Public).build().await?;\nlet corp = lab.add_router(\"corp\").preset(RouterPreset::Corporate).build().await?;\n```\n\nAvailable presets: `Home`, `Public`, `PublicV4`, `IspCgnat`, `IspV6`,\n`Corporate`, `Hotel`, `Cloud`. Individual methods called after\n`preset()` override preset values. See\n[docs/reference/ipv6.md](docs/reference/ipv6.md) for the full reference\ntable.\n\n### NAT\n\nRouters support six IPv4 NAT presets (`None`, `Home`, `Corporate`,\n`CloudNat`, `FullCone`, `Cgnat`) and four IPv6 modes (`None`, `Nptv6`,\n`Masquerade`, `Nat64`), all configured via nftables rules. You can also\nbuild custom NAT configs from mapping + filtering + timeout parameters.\n\n**NAT64** provides IPv4 access for IPv6-only devices via the well-known\nprefix `64:ff9b::/96`. A userspace SIIT translator on the router converts\nbetween IPv6 and IPv4 headers; nftables masquerade handles port mapping.\nUse `RouterPreset::IspV6` or `.nat_v6(NatV6Mode::Nat64)` directly.\nSee [docs/reference/ipv6.md](docs/reference/ipv6.md) for details.\n\n### IPv6 link-local and provisioning modes\n\nEvery IPv6-capable device/router interface exposes a link-local address\nthrough the handle snapshots:\n\n- `Device::default_iface().and_then(|i| i.ll6())`\n- `Device::interfaces().iter().filter_map(|i| i.ll6())`\n- `router.iface(\"ix\").or_else(|| router.iface(\"wan\")).and_then(|i| i.ll6())`\n- `router.interfaces().iter().filter_map(|i| i.ll6())`\n\nPatchbay also supports explicit IPv6 provisioning and DAD modes via `LabOpts`:\n\n```rust\nlet lab = Lab::with_opts(\n    LabOpts::default()\n        .ipv6_provisioning_mode(Ipv6ProvisioningMode::Static)\n        .ipv6_dad_mode(Ipv6DadMode::Enabled),\n).await?;\n```\n\n`Ipv6ProvisioningMode::Static` keeps route wiring deterministic.  \n`Ipv6ProvisioningMode::RaDriven` enables patchbay's RA/RS-driven path.  \n`Ipv6DadMode::Disabled` is the default for deterministic test setup.\n\nIn `RaDriven` mode, patchbay models Router Advertisement and Router\nSolicitation behavior through structured events and route updates. It does\nnot emit raw ICMPv6 RA or RS packets on the virtual links.\n\nFor full scope and known gaps, see [Book limitations](docs/limitations.md)\nand [IPv6 reference](docs/reference/ipv6.md).\n\n### Firewalls\n\nFirewall presets control both inbound and outbound traffic:\n`BlockInbound` (RFC 6092 CE router), `Corporate` (TCP 80,443 + UDP 53),\nand `CaptivePortal` (block non-web UDP). All presets expand to a\n`FirewallConfig` which can also be built from scratch via the builder API.\n\n### Link conditions\n\n`tc netem` and `tc tbf` provide packet loss, latency, jitter, and rate\nlimiting. Apply presets (`LinkCondition::Wifi`, `LinkCondition::Mobile4G`)\nor custom values at build time or dynamically.\n\n### Cleanup\n\nNamespace file descriptors are held in-process. When the `Lab` is dropped,\nworkers are shut down and namespaces disappear automatically.\n\n## API overview\n\n### Building a topology\n\n```rust\nlet lab = Lab::new().await?;\n\n// Regions (optional)\nlet eu = lab.add_region(\"eu\").await?;\nlet us = lab.add_region(\"us\").await?;\nlab.link_regions(\u0026eu, \u0026us, RegionLink::good(80)).await?;\n\n// Routers\nlet dc = lab.add_router(\"dc\")\n    .preset(RouterPreset::Public)\n    .region(\u0026eu)\n    .build().await?;\n\nlet home = lab.add_router(\"home\")\n    .preset(RouterPreset::Home)\n    .upstream(dc.id())           // chain behind dc\n    .nat_v6(NatV6Mode::Nptv6)\n    .build().await?;\n\n// Devices\nlet dev = lab.add_device(\"phone\")\n    .iface(\"wlan0\", home.id())\n    .iface(\"eth0\", dc.id())\n    .default_via(\"wlan0\")\n    .build().await?;\ndev.iface(\"wlan0\").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;\n```\n\n### Running code in namespaces\n\n```rust\n// Async task on the device's tokio runtime\nlet jh = dev.spawn(async move |_dev| {\n    let stream = tokio::net::TcpStream::connect(\"203.0.113.10:80\").await?;\n    Ok::\u003c_, anyhow::Error\u003e(())\n})?;\n\n// Blocking closure on the sync worker\nlet local_addr = dev.run_sync(|| {\n    let sock = std::net::UdpSocket::bind(\"0.0.0.0:0\")?;\n    Ok(sock.local_addr()?)\n})?;\n\n// Spawn an OS command (sync, returns std::process::Child)\nlet child = dev.spawn_command({\n    let mut cmd = tokio::process::Command::new(\"curl\");\n    cmd.arg(\"http://203.0.113.10\");\n    cmd\n})?;\n\n// Spawn an OS command (sync, returns std::process::Child)\nlet child = dev.spawn_command_sync({\n    let mut cmd = std::process::Command::new(\"curl\");\n    cmd.arg(\"http://203.0.113.10\");\n    cmd\n})?;\n\n// Dedicated OS thread in the namespace\nlet handle = dev.spawn_thread(|| {\n    // long-running work\n    Ok(())\n})?;\n```\n\n### Dynamic operations\n\n```rust\n// Switch a device's uplink to a different router at runtime.\ndev.iface(\"wlan0\").unwrap().replug(other_router.id()).await?;\n\n// Switch default route between interfaces.\ndev.set_default_route(\"eth0\").await?;\n\n// Link down / up.\ndev.iface(\"wlan0\").unwrap().link_down().await?;\ndev.iface(\"wlan0\").unwrap().link_up().await?;\n\n// Change link condition dynamically.\ndev.iface(\"wlan0\").unwrap().set_condition(LinkCondition::Manual(LinkLimits {\n    rate_kbit: 1000,\n    loss_pct: 5.0,\n    latency_ms: 100,\n    ..Default::default()\n}), LinkDirection::Both).await?;\n\n// Change NAT mode at runtime.\nrouter.set_nat_mode(Nat::Corporate).await?;\nrouter.flush_nat_state().await?;\n```\n\n### Handles\n\n`Device`, `Router`, and `Ix` are lightweight, cloneable handles. All three\nprovide `spawn`, `run_sync`, `spawn_thread`, `spawn_command`,\n`spawn_command_sync`, and `spawn_reflector` for running code in their\nnamespace. Handle methods return `Result` or `Option` when the underlying\nnode has been removed from the lab.\n\nFor IPv6 diagnostics, use per-interface snapshots instead of only `ip6()`:\n\n- `Iface::ip6()` for global/ULA address.\n- `Iface::ll6()` for `fe80::/10` link-local address.\n- `RouterIface::ip6()` and `RouterIface::ll6()` for router-side interface state.\n\n## TOML configuration\n\nYou can also load labs from TOML files via `Lab::load(\"lab.toml\")`:\n\n```toml\n[[router]]\nname = \"dc\"\nregion = \"eu\"\n\n[[router]]\nname = \"home\"\nnat = \"home\"\n\n[device.laptop.eth0]\ngateway = \"home\"\n\n[region.eu]\nlatencies = { us = 80 }\n```\n\n## Devtools UI\n\npatchbay includes a built-in web UI for inspecting lab runs. Set\n`PATCHBAY_OUTDIR` to write structured output (topology events, per-namespace\ntracing logs, extracted events) to disk, then serve it in the browser:\n\n```bash\n# From a cargo test\nPATCHBAY_OUTDIR=/tmp/pb cargo test my_test\npatchbay serve /tmp/pb --open\n\n# From the TOML runner (auto-serves with --open)\npatchbay run ./sims/my-sim.toml --open\n```\n\nThe UI provides five tabs:\n\n- **Topology**: interactive graph of routers, devices, and links with a\n  detail sidebar showing NAT, firewall, IPs, and counters.\n- **Events**: table of lab lifecycle events (router added, device added,\n  NAT changed, etc.) with relative/absolute timestamps.\n- **Logs**: per-namespace tracing log viewer with JSON parsing, level\n  badges, and target filtering. Supports jump-to-log from the timeline.\n- **Timeline**: grid of extracted `_events` per node over time, with\n  detail pane and jump-to-log.\n- **Perf**: throughput results table (only for TOML runner sims).\n\nEach `Lab` instance writes to a timestamped subdirectory under the outdir.\nMultiple runs accumulate in the same outdir and appear in the run selector.\n\n### Output from Rust tests\n\nTo enable devtools output from `cargo test`, pass the outdir via the\n`PATCHBAY_OUTDIR` environment variable:\n\n```rust\nlet lab = Lab::with_opts(LabOpts::default().label(\"my-test\")).await?;\n// ... run your test ...\n// Lab writes events.jsonl, state.json, *.tracing.jsonl, *.events.jsonl\n// to $PATCHBAY_OUTDIR/{timestamp}-my-test/\n```\n\nYou can also emit custom events to the timeline by using tracing targets\nwith the `_events::` convention:\n\n```rust\ntracing::info!(target: \"myapp::_events::ConnectionEstablished\", peer = %addr);\n```\n\nThese appear as timeline events in the UI, extracted automatically by the\nper-namespace tracing subscriber.\n\n## Workspace crates\n\n| Crate | Description |\n|-------|-------------|\n| `patchbay` | Core library: topology builder, namespace management, NAT, link conditions |\n| `patchbay-runner` | CLI runner for TOML-defined simulations with step sequencing |\n| `patchbay-server` | Embedded devtools HTTP server with run discovery and SSE |\n| `patchbay-vm` | QEMU VM wrapper for running simulations on macOS |\n| `patchbay-utils` | Shared utilities |\n\n## TOML simulation runner\n\nThe `patchbay` binary runs simulations defined in TOML files with a step-based\nexecution model: spawn processes, apply link conditions, wait for captures, and\nassert on outputs.\n\n```bash\ncargo install --git https://github.com/n0-computer/patchbay\n\n# Run a simulation\npatchbay run ./sims/iperf-baseline.toml\n\n# Run all sims discovered from patchbay.toml\npatchbay run\n\n# Serve a completed run directory in the browser\npatchbay serve /path/to/outdir --open\n```\n\nSee [docs/reference/toml-reference.md](docs/reference/toml-reference.md) for the full simulation file syntax.\n\n## VM mode (macOS)\n\nThe `patchbay-vm` crate wraps simulations in a QEMU Linux VM, allowing\ndevelopment on macOS:\n\n```bash\ncargo install --git https://github.com/n0-computer/patchbay patchbay-vm\npatchbay-vm run ./sims/my-sim.toml\npatchbay-vm down\n```\n\n## Documentation\n\n| Document | Description |\n|----------|-------------|\n| [docs/reference/ipv6.md](docs/reference/ipv6.md) | Real-world IPv6 deployments, NAT64, router presets reference table |\n| [docs/reference/patterns.md](docs/reference/patterns.md) | Simulating VPNs, WiFi handoff, captive portals, and other network events |\n| [docs/reference/holepunching.md](docs/reference/holepunching.md) | NAT implementation details, hole-punching mechanics, nftables fullcone map |\n| [docs/reference/toml-reference.md](docs/reference/toml-reference.md) | TOML simulation file syntax |\n\n## License\n\nCopyright 2026 N0, INC.\n\nThis project is licensed under either of\n\n * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or\n   http://www.apache.org/licenses/LICENSE-2.0)\n * MIT license ([LICENSE-MIT](LICENSE-MIT) or\n   http://opensource.org/licenses/MIT)\n\nat your option.\n\n## Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fn0-computer%2Fpatchbay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fn0-computer%2Fpatchbay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fn0-computer%2Fpatchbay/lists"}