Files
Aleksandr Bogomiakov 2b0c938487 Bound iroh path retries to prevent multi-connection OOM
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.
2026-10-14 23:28:53 +01:00

175 lines
4.7 KiB
Rust

//! NAT traversal matrix tests.
//!
//! Tests holepunching across combinations of the upstream [`patchbay::Nat`]
//! behavior tiers:
//!
//! - `None`: no NAT, publicly routable.
//! - `Open`: endpoint-independent mapping (EIM), endpoint-independent
//! filtering (EIF); RFC 3489 full cone. Typical of routers with UPnP or
//! static port forwarding.
//! - `Moderate`: EIM, address-and-port-dependent filtering (APDF); RFC 3489
//! port-restricted cone. The typical home router.
//! - `Strict`: endpoint-dependent mapping (EDM) with random ports, APDF; RFC
//! 3489 symmetric. Holepunching between two `Strict` NATs requires a relay.
//!
//! Every test expects a direct path to be established. Tests where holepunching
//! is not yet working are marked `#[ignore]`.
use std::time::Duration;
use n0_error::{Result, StackResultExt};
use n0_tracing_test::traced_test;
use patchbay::Nat;
use testdir::testdir;
use tracing::info;
use super::util::{Pair, PathConnectionExt, lab_with_relay};
use crate::util::{is_relayed, ping_accept, ping_open};
async fn run_nat_holepunch(nat_server: Nat, nat_client: Nat) -> Result {
let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?;
let router_server = lab.add_router("nat_server").nat(nat_server).build().await?;
let router_client = lab.add_router("nat_client").nat(nat_client).build().await?;
let server = lab
.add_device("server")
.uplink(router_server.id())
.build()
.await?;
let client = lab
.add_device("client")
.uplink(router_client.id())
.build()
.await?;
let timeout = Duration::from_secs(15);
Pair::new(relay_map)
.server(server, async move |_dev, _ep, conn| {
assert!(is_relayed(&conn), "connection started relayed");
conn.wait_ip(timeout).await.context("holepunch to direct")?;
info!("connection became direct");
ping_accept(&conn, timeout).await?;
conn.closed().await;
Ok(())
})
.client(client, async move |_dev, _ep, conn| {
assert!(is_relayed(&conn), "connection started relayed");
conn.wait_ip(timeout).await.context("holepunch to direct")?;
info!("connection became direct");
ping_open(&conn, timeout).await?;
conn.close(0u32.into(), b"bye");
Ok(())
})
.run()
.await?;
guard.ok();
Ok(())
}
// None x *
#[tokio::test]
#[traced_test]
async fn nat_none_x_none() -> Result {
run_nat_holepunch(Nat::None, Nat::None).await
}
#[tokio::test]
#[traced_test]
async fn nat_none_x_open() -> Result {
run_nat_holepunch(Nat::None, Nat::Open).await
}
#[tokio::test]
#[traced_test]
async fn nat_none_x_moderate() -> Result {
run_nat_holepunch(Nat::None, Nat::Moderate).await
}
#[tokio::test]
#[traced_test]
async fn nat_none_x_strict() -> Result {
run_nat_holepunch(Nat::None, Nat::Strict).await
}
// Open x *
#[tokio::test]
#[traced_test]
async fn nat_open_x_none() -> Result {
run_nat_holepunch(Nat::Open, Nat::None).await
}
#[tokio::test]
#[traced_test]
async fn nat_open_x_open() -> Result {
run_nat_holepunch(Nat::Open, Nat::Open).await
}
#[tokio::test]
#[traced_test]
async fn nat_open_x_moderate() -> Result {
run_nat_holepunch(Nat::Open, Nat::Moderate).await
}
#[tokio::test]
#[traced_test]
async fn nat_open_x_strict() -> Result {
run_nat_holepunch(Nat::Open, Nat::Strict).await
}
// Moderate x *
#[tokio::test]
#[traced_test]
async fn nat_moderate_x_none() -> Result {
run_nat_holepunch(Nat::Moderate, Nat::None).await
}
#[tokio::test]
#[traced_test]
async fn nat_moderate_x_open() -> Result {
run_nat_holepunch(Nat::Moderate, Nat::Open).await
}
#[tokio::test]
#[traced_test]
async fn nat_moderate_x_moderate() -> Result {
run_nat_holepunch(Nat::Moderate, Nat::Moderate).await
}
#[tokio::test]
#[traced_test]
#[ignore = "not yet passing (and likely can't without port guessing)"]
async fn nat_moderate_x_strict() -> Result {
run_nat_holepunch(Nat::Moderate, Nat::Strict).await
}
// Strict x *
#[tokio::test]
#[traced_test]
async fn nat_strict_x_none() -> Result {
run_nat_holepunch(Nat::Strict, Nat::None).await
}
#[tokio::test]
#[traced_test]
async fn nat_strict_x_open() -> Result {
run_nat_holepunch(Nat::Strict, Nat::Open).await
}
#[tokio::test]
#[traced_test]
#[ignore = "not yet passing (and likely can't without port guessing)"]
async fn nat_strict_x_moderate() -> Result {
run_nat_holepunch(Nat::Strict, Nat::Moderate).await
}
#[tokio::test]
#[traced_test]
#[ignore = "not yet passing (and likely can't without port guessing)"]
async fn nat_strict_x_strict() -> Result {
run_nat_holepunch(Nat::Strict, Nat::Strict).await
}