Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ let dev = lab
.iface("eth0", home.id())
.build()
.await?;
dev.iface("eth0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
dev.iface("eth0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;

// A server in the datacenter.
let server = lab
Expand Down Expand Up @@ -191,7 +191,7 @@ and `CaptivePortal` (block non-web UDP). All presets expand to a
### Link conditions

`tc netem` and `tc tbf` provide packet loss, latency, jitter, and rate
limiting. Apply presets (`LinkCondition::Wifi`, `LinkCondition::Mobile4G`)
limiting. Apply presets (`LinkCondition::wifi()`, `LinkCondition::mobile_4g()`)
or custom values at build time or dynamically.

### Cleanup
Expand Down Expand Up @@ -229,7 +229,7 @@ let dev = lab.add_device("phone")
.iface("eth0", dc.id())
.default_via("wlan0")
.build().await?;
dev.iface("wlan0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
dev.iface("wlan0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;
```

### Running code in namespaces
Expand Down Expand Up @@ -282,12 +282,10 @@ dev.iface("wlan0").unwrap().link_down().await?;
dev.iface("wlan0").unwrap().link_up().await?;

// Change link condition dynamically.
dev.iface("wlan0").unwrap().set_condition(LinkCondition::Manual(LinkLimits {
rate_kbit: 1000,
loss_pct: 5.0,
latency_ms: 100,
..Default::default()
}), LinkDirection::Both).await?;
dev.iface("wlan0").unwrap().set_condition(
LinkCondition::new().rate_kbit(1000).loss_pct(5.0).latency_ms(100),
LinkDirection::Both,
).await?;

// Change NAT mode at runtime.
router.set_nat_mode(Nat::Corporate).await?;
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ let laptop = lab
.iface("eth0", home.id())
.build()
.await?;
laptop.iface("eth0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
laptop.iface("eth0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;
```

At this point you have five network namespaces — the IX root, two
Expand Down
14 changes: 6 additions & 8 deletions docs/guide/running-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,16 @@ Modify link impairment on the fly to simulate degrading or improving
network quality:

```rust
use patchbay::{LinkCondition, LinkDirection, LinkLimits};
use patchbay::{LinkCondition, LinkDirection};

// Switch to a 3G-like link.
dev.iface("wlan0").unwrap().set_condition(LinkCondition::Mobile3G, LinkDirection::Both).await?;
dev.iface("wlan0").unwrap().set_condition(LinkCondition::mobile_3g(), LinkDirection::Both).await?;

// Apply custom impairment.
dev.iface("wlan0").unwrap().set_condition(LinkCondition::Manual(LinkLimits {
rate_kbit: 500,
loss_pct: 15.0,
latency_ms: 200,
..Default::default()
}), LinkDirection::Both).await?;
dev.iface("wlan0").unwrap().set_condition(
LinkCondition::new().rate_kbit(500).loss_pct(15.0).latency_ms(200),
LinkDirection::Both,
).await?;

// Remove all impairment and return to a clean link.
dev.iface("wlan0").unwrap().clear_condition(LinkDirection::Both).await?;
Expand Down
44 changes: 26 additions & 18 deletions docs/guide/topology.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ let phone = lab
.default_via("wlan0")
.build()
.await?;
phone.iface("wlan0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
phone.iface("cell0").unwrap().set_condition(LinkCondition::Mobile4G, LinkDirection::Both).await?;
phone.iface("wlan0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;
phone.iface("cell0").unwrap().set_condition(LinkCondition::mobile_4g(), LinkDirection::Both).await?;
```

The `.default_via("wlan0")` call sets which interface carries the default
Expand All @@ -166,42 +166,50 @@ The built-in presets model common access technologies:

| Preset | Loss | Latency | Jitter | Rate |
|--------|------|---------|--------|------|
| `Wifi` | 2% | 5 ms | 1 ms | 54 Mbit/s |
| `Mobile4G` | 1% | 30 ms | 10 ms | 50 Mbit/s |
| `Mobile3G` | 3% | 100 ms | 30 ms | 2 Mbit/s |
| `Satellite` | 0.5% | 600 ms | 50 ms | 10 Mbit/s |
| `wifi()` | 2% | 5 ms | 1 ms | 54 Mbit/s |
| `mobile_4g()` | 1% | 30 ms | 10 ms | 50 Mbit/s |
| `mobile_3g()` | 3% | 100 ms | 30 ms | 2 Mbit/s |
| `satellite()` | 0.5% | 600 ms | 50 ms | 10 Mbit/s |

Apply a preset after building the device:

```rust
let dev = lab.add_device("laptop")
.iface("eth0", home.id())
.build().await?;
dev.iface("eth0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
dev.iface("eth0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;
```

### Custom parameters

When the presets do not match your scenario, build a `LinkLimits` struct
directly:
When the presets do not match your scenario, chain setters onto
`LinkCondition::new()`. For a link defined by bandwidth and round-trip
time, `rate_mbit` and `rtt_ms` cover the common case directly:

```rust
use patchbay::{LinkCondition, LinkLimits};
use patchbay::{LinkCondition, LinkDirection};

let degraded = LinkCondition::Manual(LinkLimits {
rate_kbit: 1000, // 1 Mbit/s
loss_pct: 10.0, // 10% packet loss
latency_ms: 50, // 50 ms one-way delay
jitter_ms: 20, // 20 ms jitter
..Default::default()
});
let degraded = LinkCondition::new().rate_mbit(1).rtt_ms(100).loss_pct(10.0);

let dev = lab.add_device("laptop")
.iface("eth0", home.id())
.build().await?;
dev.iface("eth0").unwrap().set_condition(degraded, LinkDirection::Both).await?;
```

`rtt_ms` sets the one-way latency to half the given value and, if you have
not set a buffer explicitly, sizes the rate-limiter buffer to hold a full
RTT of data. When you need to control latency and jitter independently
instead, set the fields directly:

```rust
let degraded = LinkCondition::new()
.rate_kbit(1000) // 1 Mbit/s
.loss_pct(10.0) // 10% packet loss
.latency_ms(50) // 50 ms one-way delay
.jitter_ms(20); // 20 ms jitter
```

### Runtime changes

You can change or remove link conditions at any point after the topology
Expand All @@ -210,7 +218,7 @@ for example switching from WiFi to a congested 3G link and verifying that
your application adapts:

```rust
dev.iface("eth0").unwrap().set_condition(LinkCondition::Mobile3G, LinkDirection::Both).await?;
dev.iface("eth0").unwrap().set_condition(LinkCondition::mobile_3g(), LinkDirection::Both).await?;

// Later, restore a clean link.
dev.iface("eth0").unwrap().clear_condition(LinkDirection::Both).await?;
Expand Down
2 changes: 1 addition & 1 deletion docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ settings that may need adjustment.
patchbay models link effects with `tc` parameters: latency, jitter,
loss, and rate limits. It does not model WiFi or cellular PHY/MAC
behavior such as radio scheduling, channel contention, or handover
signaling. The link condition presets (`Wifi`, `Mobile4G`, etc.) apply
signaling. The link condition presets (`wifi()`, `mobile_4g()`, etc.) apply
realistic impairment at the IP layer, which is sufficient for transport
and application resilience testing but not for radio-layer research.

Expand Down
21 changes: 9 additions & 12 deletions docs/reference/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,13 @@ let cell_router = lab.add_router("cell").nat(Nat::Cgnat).build().await?;
let device = lab.add_device("phone")
.iface("eth0", wifi_router.id())
.build().await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;

// Simulate handoff with connectivity gap
device.iface("eth0").unwrap().link_down().await?;
tokio::time::sleep(Duration::from_millis(500)).await;
device.iface("eth0").unwrap().replug(cell_router.id()).await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::Mobile4G, LinkDirection::Both).await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::mobile_4g(), LinkDirection::Both).await?;
device.iface("eth0").unwrap().link_up().await?;

// Assert: application reconnects within X seconds
Expand Down Expand Up @@ -236,17 +236,14 @@ calls, each direction is limited by the sender's upload.
// 20 Mbps down, 2 Mbps up (10:1 ratio)
let router = lab.add_router("isp")
.nat(Nat::Home)
.downlink_condition(LinkCondition::Manual(LinkLimits {
rate_kbit: 20_000,
..Default::default()
}))
.downlink_condition(LinkCondition::new().rate_mbit(20))
.build().await?;

let device = lab.add_device("client").uplink(router.id()).build().await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::Manual(LinkLimits {
rate_kbit: 2_000,
..Default::default()
}), LinkDirection::Both).await?;
device.iface("eth0").unwrap().set_condition(
LinkCondition::new().rate_mbit(2),
LinkDirection::Both,
).await?;
```

---
Expand Down Expand Up @@ -337,9 +334,9 @@ Network conditions worsen over time (moving away from WiFi AP, entering tunnel
on cellular, weather affecting satellite).

```rust
device.iface("eth0").unwrap().set_condition(LinkCondition::Wifi, LinkDirection::Both).await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::wifi(), LinkDirection::Both).await?;
tokio::time::sleep(Duration::from_secs(5)).await;
device.iface("eth0").unwrap().set_condition(LinkCondition::WifiBad, LinkDirection::Both).await?;
device.iface("eth0").unwrap().set_condition(LinkCondition::wifi_bad(), LinkDirection::Both).await?;
tokio::time::sleep(Duration::from_secs(5)).await;
device.iface("eth0").unwrap().clear_condition(LinkDirection::Both).await?; // remove impairment
```
Expand Down
2 changes: 1 addition & 1 deletion patchbay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ iroh-metrics = ["dep:iroh-metrics"]
iroh-metrics = { version = "1.0.0", optional = true }
anyhow = "1"
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
derive_more = { version = "2.1.1", features = ["debug", "display"] }
derive_more = { version = "2.1.1", features = ["debug", "display", "eq"] }
futures = "0.3"
ipnet = { version = "2.11", features = ["serde"] }
libc = "0.2"
Expand Down
138 changes: 8 additions & 130 deletions patchbay/src/lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use serde::Deserialize;
use tokio_util::sync::CancellationToken;
use tracing::{debug, debug_span};

pub use crate::qdisc::LinkLimits;
pub use crate::qdisc::LinkCondition;
use crate::{
config::LabConfig,
core::{
Expand Down Expand Up @@ -83,124 +83,6 @@ pub enum LinkDirection {
Ingress,
}

/// Link-layer impairment profile applied via `tc netem`.
///
/// Named presets model common last-mile conditions. Use [`LinkCondition::Manual`]
/// with [`LinkLimits`] for full control over all `tc netem` parameters.
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum LinkCondition {
/// Wired LAN (1G Ethernet). No impairment.
///
/// Use for datacenter-local, same-rack communication.
Lan,
/// Good WiFi — 5 GHz band, close to AP, low contention.
///
/// 5 ms one-way delay, 2 ms jitter, 0.1 % loss.
Wifi,
/// Congested WiFi — 2.4 GHz, far from AP, interference.
///
/// 40 ms one-way delay, 15 ms jitter, 2 % loss, 20 Mbit.
WifiBad,
/// 4G/LTE good signal.
///
/// 25 ms one-way delay, 8 ms jitter, 0.5 % loss.
Mobile4G,
/// 3G or degraded 4G.
///
/// 100 ms one-way delay, 30 ms jitter, 2 % loss, 2 Mbit.
Mobile3G,
/// LEO satellite (Starlink-class).
///
/// 40 ms one-way delay, 7 ms jitter, 1 % loss.
Satellite,
/// GEO satellite (HughesNet/Viasat).
///
/// 300 ms one-way delay, 20 ms jitter, 0.5 % loss, 25 Mbit.
SatelliteGeo,
/// Fully custom impairment parameters.
Manual(LinkLimits),
}

impl<'de> Deserialize<'de> for LinkCondition {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Preset(String),
Manual(LinkLimits),
}

match Repr::deserialize(deserializer)? {
Repr::Preset(s) => match s.as_str() {
"lan" => Ok(LinkCondition::Lan),
"wifi" => Ok(LinkCondition::Wifi),
"wifi-bad" => Ok(LinkCondition::WifiBad),
"mobile-4g" | "mobile" => Ok(LinkCondition::Mobile4G),
"mobile-3g" => Ok(LinkCondition::Mobile3G),
"satellite" => Ok(LinkCondition::Satellite),
"satellite-geo" => Ok(LinkCondition::SatelliteGeo),
_ => Err(serde::de::Error::custom(format!(
"unknown link condition preset '{s}'"
))),
},
Repr::Manual(limits) => Ok(LinkCondition::Manual(limits)),
}
}
}

impl LinkCondition {
/// Converts this preset (or manual config) into concrete [`LinkLimits`].
pub fn to_limits(self) -> LinkLimits {
match self {
LinkCondition::Lan => LinkLimits::default(),
LinkCondition::Wifi => LinkLimits {
latency_ms: 5,
jitter_ms: 2,
loss_pct: 0.1,
..Default::default()
},
LinkCondition::WifiBad => LinkLimits {
latency_ms: 40,
jitter_ms: 15,
loss_pct: 2.0,
rate_kbit: 20_000,
..Default::default()
},
LinkCondition::Mobile4G => LinkLimits {
latency_ms: 25,
jitter_ms: 8,
loss_pct: 0.5,
..Default::default()
},
LinkCondition::Mobile3G => LinkLimits {
latency_ms: 100,
jitter_ms: 30,
loss_pct: 2.0,
rate_kbit: 2_000,
..Default::default()
},
LinkCondition::Satellite => LinkLimits {
latency_ms: 40,
jitter_ms: 7,
loss_pct: 1.0,
..Default::default()
},
LinkCondition::SatelliteGeo => LinkLimits {
latency_ms: 300,
jitter_ms: 20,
loss_pct: 0.5,
rate_kbit: 25_000,
..Default::default()
},
LinkCondition::Manual(limits) => limits,
}
}
}

// ─────────────────────────────────────────────
// Region
// ─────────────────────────────────────────────
Expand Down Expand Up @@ -1450,17 +1332,13 @@ impl Lab {

// Apply netem impairment on both veth ends.
if link.latency_ms > 0 || link.jitter_ms > 0 || link.loss_pct > 0.0 {
let limits = LinkLimits {
latency_ms: link.latency_ms,
jitter_ms: link.jitter_ms,
loss_pct: link.loss_pct as f32,
rate_kbit: if link.rate_mbit > 0 {
link.rate_mbit * 1000
} else {
0
},
..Default::default()
};
let mut limits = LinkCondition::new()
.latency_ms(link.latency_ms)
.jitter_ms(link.jitter_ms)
.loss_pct(link.loss_pct as f32);
if link.rate_mbit > 0 {
limits = limits.rate_mbit(link.rate_mbit);
}
let veth_a4 = veth_a.clone();
let limits_a = limits;
let rt_a = netns.rt_handle_for(&s.a.ns)?;
Expand Down
Loading
Loading