A long-running rc.33 daemon reached OOM with roughly 22 GiB resident and 24 GiB swapped. Live inspection found 40 GiB and 10/20 GiB anonymous container mappings; the active worker stack and tracing metadata identified iroh's scheduled path-open retry loop. In iroh 1.2.0 every failed connection requeues the same path, while each retry visits every connection again. Separate control and data connections multiply the queue geometrically. Vendor the published iroh 1.2.0 source and select it with a workspace Cargo patch. Deduplicate retry admission and cap distinct pending paths at 64, evicting the oldest before inserting a new candidate. Keep the current and next retry batches bounded without combining control/data connections or changing wire protocols, identity derivation, authentication or state. Document source provenance, the live evidence and removal conditions. Upstream PR 4551 merged on October 6, but inspection of the official 1.3.0 crate still shows the unconditional enqueue, so that upgrade alone is not a fix. No version bump, release tag or installed-service change. The default workspace suite includes the exact vendored admission module. Three regressions failed with the original enqueue and pass with the fix: repeated failures across connections, duplicate admission at capacity, and 10,000 distinct candidates with bounded length/capacity and FIFO eviction. Transport integration tests continue using real endpoints and encryption. Checks on Linux in nix develop: - cargo fmt --all -- --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets, with only the inherited TSUNAGI_CONTROL_SOCKET removed from the test environment - git diff --cached --check Initial full-suite runs failed six DNS readiness checks because the inherited system socket path overrode their temporary state directories. Clearing that environment variable resolves those failures without changing application code. Root/network-dependent ignored tests and other operating systems were not run.
240 lines
7.2 KiB
Rust
240 lines
7.2 KiB
Rust
//! Uplink switch tests.
|
|
//!
|
|
//! Each test verifies that an iroh connection survives a network change on one
|
|
//! side: the switching device replugs from one router to another, and we verify
|
|
//! that a new direct path is established and data flows over it.
|
|
//!
|
|
//! We test every combination of:
|
|
//! - which side switches (client or server)
|
|
//! - which IP families are involved (v4, v6, dual-stack)
|
|
//!
|
|
//! The non-switching side is always behind a dual-stack Home NAT, so it is
|
|
//! reachable on both address families regardless of what the switcher does.
|
|
|
|
use std::time::Duration;
|
|
|
|
use iroh::{TransportAddr, endpoint::Side};
|
|
use n0_error::{Result, StackResultExt};
|
|
use n0_tracing_test::traced_test;
|
|
use patchbay::{IpSupport, RouterPreset};
|
|
use testdir::testdir;
|
|
use tracing::info;
|
|
|
|
use crate::util::{Pair, PathConnectionExt, lab_with_relay, ping_accept, ping_open};
|
|
|
|
/// Builds the lab topology and runs a single uplink switch test.
|
|
///
|
|
/// The topology has three routers:
|
|
/// - "observer": dual-stack Home NAT for the non-switching side
|
|
/// - "from": the switching side's initial router (determined by `from`)
|
|
/// - "to": the router the switching side replugs to (determined by `to`)
|
|
///
|
|
/// After both sides holepunch and exchange a ping, the switching side replugs
|
|
/// from "from" to "to". The observer waits for the selected path to change,
|
|
/// then both sides exchange another ping to confirm the new path works.
|
|
async fn run_switch_uplink(switching_side: Side, from: IpSupport, to: IpSupport) -> Result {
|
|
let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?;
|
|
let timeout = Duration::from_secs(30);
|
|
|
|
let observer_id = lab
|
|
.add_router("observer")
|
|
.preset(RouterPreset::Home)
|
|
.ip_support(IpSupport::DualStack)
|
|
.build()
|
|
.await?
|
|
.id();
|
|
|
|
let from_id = lab
|
|
.add_router("from")
|
|
.preset(router_preset(from))
|
|
.ip_support(from)
|
|
.build()
|
|
.await?
|
|
.id();
|
|
let to_id = lab
|
|
.add_router("to")
|
|
.preset(router_preset(to))
|
|
.ip_support(to)
|
|
.build()
|
|
.await?
|
|
.id();
|
|
|
|
let switcher = lab.add_device("switcher").uplink(from_id).build().await?;
|
|
let observer = lab
|
|
.add_device("observer")
|
|
.uplink(observer_id)
|
|
.build()
|
|
.await?;
|
|
|
|
info!(?switching_side, ?from, ?to, "switch uplink test start");
|
|
|
|
Pair::new(relay_map)
|
|
.left(switching_side, switcher, async move |dev, _ep, conn| {
|
|
conn.wait_ip(timeout).await.context("initial holepunch")?;
|
|
ping_accept(&conn, timeout)
|
|
.await
|
|
.context("ping_accept before switch")?;
|
|
dev.iface("eth0").unwrap().replug(to_id).await?;
|
|
ping_accept(&conn, timeout)
|
|
.await
|
|
.context("ping_accept after switch")?;
|
|
conn.closed().await;
|
|
Ok(())
|
|
})
|
|
.right(observer, async move |_dev, _ep, conn| {
|
|
conn.wait_ip(timeout).await.context("initial holepunch")?;
|
|
let previous: Vec<TransportAddr> = conn
|
|
.paths()
|
|
.iter()
|
|
.map(|p| p.remote_addr().clone())
|
|
.collect();
|
|
ping_open(&conn, timeout)
|
|
.await
|
|
.context("ping_open before switch")?;
|
|
conn.wait_selected(timeout, |p| path_switched(to, &previous, p.remote_addr()))
|
|
.await
|
|
.context("path did not switch")?;
|
|
ping_open(&conn, timeout)
|
|
.await
|
|
.context("ping_open after switch")?;
|
|
conn.close(0u32.into(), b"bye");
|
|
Ok(())
|
|
})
|
|
.run()
|
|
.await?;
|
|
|
|
guard.ok();
|
|
Ok(())
|
|
}
|
|
|
|
fn router_preset(ip: IpSupport) -> RouterPreset {
|
|
match ip {
|
|
IpSupport::V4Only => RouterPreset::Home,
|
|
IpSupport::V6Only => RouterPreset::IspV6,
|
|
IpSupport::DualStack => RouterPreset::Home,
|
|
}
|
|
}
|
|
|
|
fn path_switched(to: IpSupport, previous: &[TransportAddr], new: &TransportAddr) -> bool {
|
|
if previous.contains(new) {
|
|
return false;
|
|
}
|
|
match to {
|
|
IpSupport::V4Only => matches!(new, TransportAddr::Ip(a) if a.ip().is_ipv4()),
|
|
IpSupport::V6Only => matches!(new, TransportAddr::Ip(a) if a.ip().is_ipv6()),
|
|
IpSupport::DualStack => matches!(new, TransportAddr::Ip(_)),
|
|
}
|
|
}
|
|
|
|
// --- Client switches uplink ---
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v4_to_v4() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v4_to_v6() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v4_to_dual() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V4Only, IpSupport::DualStack).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v6_to_v4() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v6_to_v6() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_v6_to_dual() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::V6Only, IpSupport::DualStack).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_dual_to_v4() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_dual_to_v6() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_client_dual_to_dual() -> Result {
|
|
run_switch_uplink(Side::Client, IpSupport::DualStack, IpSupport::DualStack).await
|
|
}
|
|
|
|
// --- Server switches uplink ---
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v4_to_v4() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v4_to_v6() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v4_to_dual() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V4Only, IpSupport::DualStack).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v6_to_v4() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v6_to_v6() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_v6_to_dual() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::V6Only, IpSupport::DualStack).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_dual_to_v4() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::V4Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_dual_to_v6() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::V6Only).await
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[traced_test]
|
|
async fn switch_server_dual_to_dual() -> Result {
|
|
run_switch_uplink(Side::Server, IpSupport::DualStack, IpSupport::DualStack).await
|
|
}
|