How Iroh Handles Network Changes and Path Selection: A Deep Dive into RemoteStateActor
Iroh detects network changes through the RemoteStateActor, which immediately pings all active paths to provoke loss detection and triggers hole-punching to discover fresh NAT-traversal candidates, then selects the optimal path using a configurable PathSelector trait that biases selection based on round-trip time and address type.
Iroh maintains resilient peer connections by dynamically adapting to network volatility. When underlying network conditions change—such as Wi-Fi roaming, IP address changes, or interface switches—the RemoteStateActor in iroh/src/socket/remote_map/remote_state.rs orchestrates the detection, probing, and re-selection logic to ensure uninterrupted connectivity.
Detecting Network Changes with the RemoteStateActor
When the operating system notifies Iroh of a network change, the actor receives a RemoteStateMessage::NetworkChange { is_major } message. The handler at handle_msg_network_change (lines 5253‑5270) executes a two-phase recovery process to assess path viability and discover new routes.
Immediate Path Probing
The first step forces rapid failure detection across all existing connections. The actor iterates through every active connection and pings each known path using the underlying QUIC (noq) stack:
fn handle_msg_network_change(&mut self, is_major: bool) {
// 1️⃣ Ping all the paths so loss‑detection starts ASAP.
for conn in self.connections.values() {
if let Some(noq_conn) = conn.handle.upgrade() {
for (path_id, addr) in &conn.paths {
if let Some(path) = noq_conn.path(*path_id) {
// Ping the current path
if let Err(err) = path.ping() {
warn!(%err, %path_id, ?addr, "failed to ping path");
}
}
}
}
}
// 2️⃣ On a *major* network change, force a new hole‑punch round.
if is_major {
self.trigger_holepunching();
}
}
This ping operation (lines 5253‑5270) forces the QUIC layer to notice broken links immediately rather than waiting for standard timeouts.
Triggering Hole-Punching on Major Changes
For major network changes—such as switching from Wi-Fi to cellular—the actor unconditionally invokes trigger_holepunching() (lines 5271‑5272). This initiates a NAT-traversal round via do_holepunching to discover fresh public addresses on both sides. Even minor changes may trigger hole-punching in subsequent logic to ensure the candidate address pool remains current.
The Path Selection Mechanism
After path discovery completes or when connection states change, Iroh executes the select_path method (around line 6455) to choose the optimal route from available candidates.
PathSelectionContext and the PathSelector Trait
The selection logic relies on the PathSelector trait, defined in iroh/src/socket/path_selector.rs. The core selection flow builds a context object and delegates the decision:
fn select_path(&mut self) {
let current_path = self.state.selected_path.as_ref();
let selected_addr = {
// ① Build a context describing the current situation.
let ctx = PathSelectionContext::new(current_path, &self.connections);
// ② Ask the configured PathSelector for the best address.
self.state.path_selector.select(&ctx).selected().cloned()
};
// ③ If the selector picks a different address, store it and emit an event.
if let Some(addr) = selected_addr && self.state.selected_path.as_ref() != Some(&addr) {
let prev_remote = self.state.selected_path.replace(addr.clone());
event!(target: "iroh::_events::path::selected", Level::DEBUG,
remote = %self.state.endpoint_id.fmt_short(),
network_path = %addr,
prev_network_path = %prev_remote.map(|p| format!("{p}")).unwrap_or("None".to_string()));
} else {
trace!(?current_path, "keeping current path");
}
// ④ Apply the new selection to every connection.
self.apply_selected_path();
}
The PathSelectionContext carries the current selected path and a snapshot of all connections, allowing selectors to inspect per-connection latency, path type (direct IP, relay, or custom transport), and recent history.
The BiasedRttPathSelector Default Strategy
Iroh’s default implementation, BiasedRttPathSelector (located in iroh/src/socket/biased_rtt_path_selector.rs), applies a latency-biased selection algorithm:
- Gather candidates from
RemotePathState, including addresses discovered via hole-punching, direct discovery, or address lookup. - Measure RTT for each IP-type path via QUIC statistics (
path_stats.rtt). - Apply bias: Direct IP paths with RTT ≤
GOOD_ENOUGH_LATENCY(approximately 10 ms) are preferred. If no direct path meets the threshold, the selector falls back to relay or custom transport paths.
Once the selector returns a FourTuple representing the best address, apply_selected_path() opens that path on every connection and marks redundant paths as Backup or closes them.
Implementing Custom Path Selection Logic
You can replace the default BiasedRttPathSelector with custom logic by implementing the PathSelector trait. For example, to always prefer a specific relay regardless of latency:
use iroh::socket::{PathSelector, PathSelectionContext};
struct RelayFirstSelector;
impl PathSelector for RelayFirstSelector {
fn select(&self, ctx: &PathSelectionContext) -> SelectedPath {
// Custom logic: pick the first relay address, ignore RTT.
let relay = ctx
.candidates()
.find(|addr| addr.is_relay())
.cloned()
.expect("no relay address available");
SelectedPath::new(Some(relay))
}
}
// When creating the socket:
let selector = Arc::new(RelayFirstSelector);
let socket = Socket::new(..., selector)?;
Injecting the custom selector via Arc<dyn PathSelector> ensures Iroh uses your strategy instead of the default RTT-based selection.
Summary
- Network change detection occurs via
RemoteStateMessage::NetworkChangehandled byRemoteStateActoriniroh/src/socket/remote_map/remote_state.rs. - Immediate recovery involves pinging all paths (lines 5253‑5270) to trigger rapid loss detection.
- Hole-punching is triggered on major changes via
trigger_holepunching()to discover fresh NAT-traversal candidates. - Path selection uses the
PathSelectortrait withPathSelectionContext, defaulting toBiasedRttPathSelectorwhich prefers low-latency direct paths (≤10 ms) over relays. - Application of selected paths occurs via
apply_selected_path(), which synchronizes the choice across all connections.
Frequently Asked Questions
How does Iroh detect when a network interface changes?
Iroh receives OS-level network notifications and translates them into RemoteStateMessage::NetworkChange messages sent to the RemoteStateActor. The handle_msg_network_change method then pings all existing paths to force immediate failure detection and triggers hole-punching if the change is classified as major (lines 5253‑5272).
What is the default criteria for selecting a path in Iroh?
The default BiasedRttPathSelector selects paths based on measured RTT from QUIC statistics, preferring direct IP paths with latency at or below GOOD_ENOUGH_LATENCY (approximately 10 milliseconds). If no direct path meets this threshold, it falls back to relay or custom transport paths according to the implementation in iroh/src/socket/biased_rtt_path_selector.rs.
Can I customize how Iroh chooses between direct and relay connections?
Yes, by implementing the PathSelector trait and providing your implementation when constructing the socket. The trait receives a PathSelectionContext containing all candidate addresses and connection states, allowing you to implement custom logic such as always preferring relays, prioritizing specific network interfaces, or using custom latency thresholds.
What happens to existing connections when the network changes?
When a network change occurs, the RemoteStateActor immediately pings all paths to accelerate loss detection. After hole-punching discovers new candidates, select_path() evaluates all options and apply_selected_path() migrates connections to the newly selected optimal path, marking obsolete paths as backup or closing them to prevent resource leakage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →