How iroh Handles Network Path Migration Between Relay and Direct Connections
iroh leverages QUIC native address migration to seamlessly switch connections between relayed and direct paths without dropping the logical connection, using a latency-biased algorithm to automatically select the optimal route.
The n0-computer/iroh networking layer enables robust peer-to-peer connectivity by dynamically migrating active connections between relay servers and direct UDP paths. Built on QUIC via the noq crate, the implementation allows a single logical connection to survive underlying network path changes without application-level reconnection logic.
Enabling QUIC Address Migration
At the transport layer, iroh explicitly enables QUIC address migration to allow source address changes after the initial handshake. In iroh/src/socket.rs, the transport configuration sets server_handshake_migration(true):
// iroh/src/socket.rs – enabling migration for the client side
transport_config.server_handshake_migration(true);
This configuration appears around line 2620 and instructs the QUIC stack to accept packets from new source addresses belonging to the same connection. Without this flag, the connection would terminate when the underlying path changes, forcing a full reconnection.
Biased RTT Path Selection Algorithm
iroh uses the BiasedRttPathSelector to continuously evaluate available network paths. Located in iroh/src/socket/biased_rtt_path_selector.rs, this selector measures round-trip time (RTT) across all candidate paths and prefers the route with the lowest latency.
The selector is instantiated in iroh/src/socket.rs around line 998:
// iroh/src/socket.rs – default path selector
path_selector: Arc::new(BiasedRttPathSelector::default()),
The algorithm maintains fallback paths while actively using the lowest-latency option. If the current direct path exhibits high RTT or packet loss, the selector triggers a migration to an alternate address—such as a relay stream—without interrupting the application-level connection.
Managing Multiple Network Paths in RemoteMap
The RemoteMap structure, defined in iroh/src/socket/remote_map.rs, tracks every reachable address for a peer. This includes direct IP addresses discovered through NAT hole-punching and relay-server addresses. The endpoint aggregates these into MappedAddrs, exposing the active path through methods like active_addr().
When multiple paths exist, iroh keeps them warm as potential migration targets. The connection maintains state across all viable paths, allowing instantaneous switching when the path selector determines a better route is available.
Automatic Failover and Path Recovery
When network conditions degrade, iroh automatically fails over to alternate paths. If packets stop arriving on the current path or RTT increases significantly, the BiasedRttPathSelector removes that path from the active set. The QUIC stack then initiates migration to the next best address, re-binding the connection to a new socket without tearing down the logical connection.
Conversely, when a direct path becomes viable—for example, after successful NAT traversal—the selector detects the improved RTT and migrates the connection back from the relay to the direct address. This bi-directional migration ensures optimal performance while maintaining connectivity.
Implementation Examples
Configure an endpoint with migration enabled and the biased RTT selector:
use iroh::endpoint::{Endpoint, Options};
use iroh::socket::BiasedRttPathSelector;
use std::sync::Arc;
// Build the endpoint with migration enabled
let mut transport = iroh::socket::TransportConfig::default();
transport.server_handshake_migration(true); // Allow address migration
let opts = Options {
transports: vec![transport],
path_selector: Arc::new(BiasedRttPathSelector::default()), // Select best path
..Default::default()
};
let endpoint = Endpoint::bind(opts).await?;
Inspect the currently active path to determine if traffic flows directly or through a relay:
let conn = endpoint.connect(peer_id, None).await?;
// MappedAddrs trait exposes the active transport address
println!("Active address: {}", conn.active_addr());
For testing purposes, you can force a network change to trigger migration logic:
// Simulate network change to trigger QUIC migration to next best path
conn.force_network_change(true).await;
Summary
- QUIC address migration is enabled via
server_handshake_migration(true)iniroh/src/socket.rs, allowing connections to survive IP address changes after the handshake. - Biased RTT Path Selection continuously measures latency across available routes via
BiasedRttPathSelector, preferring direct connections when viable while maintaining relay fallbacks. - RemoteMap in
iroh/src/socket/remote_map.rsstores all discovered addresses for a peer, enabling rapid switching between direct and relayed paths. - Automatic failover occurs when path quality degrades, seamlessly migrating to alternative routes without application interruption.
- The implementation spans
iroh/src/socket.rs,iroh/src/socket/biased_rtt_path_selector.rs, andiroh/src/socket/transports.rs.
Frequently Asked Questions
How does iroh decide when to switch from relay to direct connections?
The BiasedRttPathSelector continuously monitors round-trip time on all available paths. When a direct path demonstrates lower latency than the current relay connection, the selector triggers a QUIC migration to the direct address. This typically occurs after successful NAT hole-punching establishes a viable direct route.
Can iroh maintain connections when switching between Wi-Fi and cellular networks?
Yes. Because iroh enables QUIC address migration via server_handshake_migration(true), the connection can re-bind to new local addresses—such as when switching from Wi-Fi to cellular—without terminating the logical connection. The path selector treats these as standard path migrations and selects the optimal route based on RTT metrics.
What happens if the direct path fails after migration?
If the active direct path fails or RTT degrades significantly, the BiasedRttPathSelector automatically removes that path from the active set and migrates the connection back to a relayed path or alternative direct address. This failover occurs at the QUIC layer without requiring application-level reconnection logic, as maintained in iroh/src/socket/remote_map.rs.
Where is the path selection logic implemented in the source code?
The core path selection algorithm resides in iroh/src/socket/biased_rtt_path_selector.rs. The integration with the socket layer occurs in iroh/src/socket.rs around line 998, where the selector is instantiated. Transport configuration enabling migration appears in iroh/src/socket/transports.rs and is applied in iroh/src/socket.rs near line 2620.
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 →