How iroh Handles Connection Migration from Relay to Direct Connections
iroh leverages QUIC's native address migration alongside a latency-biased path selector to seamlessly transition connections between relay servers and direct peer-to-peer paths without interrupting the logical connection or dropping application data.
iroh is a peer-to-peer networking library built on QUIC (via the quinn crate) that enables resilient connections across NATs and firewalls. Understanding how iroh handles connection migration from relay to direct connections is essential for building distributed applications that automatically adapt to changing network topologies and optimize for latency.
Enabling QUIC Address Migration
At the transport layer, iroh explicitly enables QUIC's connection migration feature to allow source addresses to change after the initial handshake. This is configured in iroh/src/socket.rs by setting server_handshake_migration(true) on the transport configuration.
// iroh/src/socket.rs – enabling migration for the endpoint
let mut transport_config = TransportConfig::default();
transport_config.server_handshake_migration(true); // Allow address changes post-handshake
This single configuration change tells the underlying QUIC stack to accept packets from new source addresses for existing connection IDs, preventing connection teardown when the network path changes.
Multi-Path Management with RemoteMap
iroh maintains awareness of all potential network paths through the RemoteMap structure (defined in iroh/src/socket/remote_map.rs). This component tracks every reachable address for a peer, including:
- Direct UDP addresses discovered via NAT hole-punching
- Relay server addresses allocated for the connection
- Historic addresses from previous successful connections
The RemoteMap exposes these addresses through the MappedAddrs trait, allowing the system to simultaneously maintain multiple candidate paths for a single logical connection.
Latency-Biased Path Selection
Path selection is handled by BiasedRttPathSelector (implemented in iroh/src/socket/biased_rtt_path_selector.rs), which continuously measures round-trip time (RTT) across all available paths. The selector prefers the path with the lowest RTT while keeping alternative paths as hot standbys.
// iroh/src/socket.rs – configuring the default path selector
use std::sync::Arc;
use iroh::socket::BiasedRttPathSelector;
let path_selector = Arc::new(BiasedRttPathSelector::default());
When BiasedRttPathSelector detects that the current direct path has significantly lower latency than the relay path (typically after successful NAT hole-punching), it signals the QUIC stack to migrate the connection. Conversely, if a direct path degrades or becomes unreachable, the selector triggers an automatic fallback to the relay address.
Automatic Failover and Recovery
The migration process operates transparently to the application layer:
- Initial connection often establishes over a relay server when direct connectivity is blocked by NAT or firewall rules
- Path probing occurs continuously in the background, measuring RTT on direct addresses discovered via hole-punching
- Migration trigger fires when a direct path's RTT drops below the relay path threshold for a sustained period
- Seamless switch moves the QUIC connection to the direct path without dropping streams or requiring re-authentication
- Failover protection ensures that if the direct path later fails, the connection automatically migrates back to the relay path
This mechanism ensures that iroh connections are resilient to network changes, temporarily switching to relays when direct paths fail, and returning to optimal direct paths when they become available.
Implementing Connection Migration
To leverage connection migration in your application, configure the endpoint with migration enabled and the biased RTT selector:
use iroh::endpoint::{Endpoint, Options};
use iroh::socket::{BiasedRttPathSelector, TransportConfig};
use std::sync::Arc;
// Configure transport to allow address migration
let mut transport = TransportConfig::default();
transport.server_handshake_migration(true);
let opts = Options {
transports: vec![transport],
path_selector: Arc::new(BiasedRttPathSelector::default()),
..Default::default()
};
let endpoint = Endpoint::bind(opts).await?;
You can inspect which path type is currently active using the MappedAddrs trait:
let conn = endpoint.connect(peer_id, None).await?;
println!("Active address: {}", conn.active_addr());
// Outputs either a direct IP or relay server address depending on current path
For testing purposes, you can simulate network changes to force migration between paths, though in production this happens automatically based on the selector's RTT measurements.
Summary
- QUIC migration is enabled via
server_handshake_migration(true)iniroh/src/socket.rs, allowing source address changes without connection teardown - Path tracking occurs through
RemoteMapiniroh/src/socket/remote_map.rs, which maintains all direct and relay addresses for each peer - Smart selection is performed by
BiasedRttPathSelector, which continuously monitors RTT and migrates to the lowest-latency available path - Zero-downtime failover ensures connections survive network transitions, automatically moving between relay and direct paths as network conditions change
- Application transparency means the migration happens at the transport layer; streams and application state persist through path changes
Frequently Asked Questions
Does connection migration drop in-flight packets?
No. QUIC's address migration includes path validation and packet number spaces that ensure in-flight packets are not lost during the transition. The BiasedRttPathSelector only migrates after confirming path viability through active probing, ensuring seamless continuity.
How quickly does iroh switch from relay to direct connections?
The switch typically occurs within seconds of the direct path becoming viable. The BiasedRttPathSelector measures RTT continuously and triggers migration once the direct path demonstrates consistently lower latency than the relay path, usually after successful NAT hole-punching completes.
Can I disable automatic migration in iroh?
Yes. While migration is enabled by default in the transport configuration, you can create a custom TransportConfig without server_handshake_migration(true) or implement a custom path selector that pins connections to specific addresses regardless of RTT measurements.
Does this work with symmetric NATs and strict firewalls?
Yes. iroh's architecture handles symmetric NATs by maintaining the relay path as a persistent fallback. Even when direct hole-punching fails or is blocked by firewall rules, the connection remains functional over the relay. Migration only occurs to direct paths when they are actually reachable, ensuring the connection never breaks due to failed migration attempts.
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 →