How Iroh Manages Connection Migration Between Direct and Relayed Paths

Iroh enables seamless connection migration between direct and relayed network paths by leveraging QUIC's native address migration combined with a latency-biased path selector that automatically fails over to the best available route without dropping the logical connection.

Iroh, the open-source networking stack from n0-computer/iroh, provides robust peer-to-peer connectivity by dynamically migrating QUIC connections between direct UDP paths and relayed tunnels based on real-time network conditions. This connection migration capability ensures that applications maintain resilient communication even as network topologies change or NAT traversal states shift.

Enabling QUIC Address Migration

The foundation of Iroh's path migration lies in QUIC's built-in address validation mechanism. In iroh/src/socket.rs, the transport configuration explicitly enables handshake migration to allow the connection's source address to change after the initial handshake completes.

When constructing the endpoint, the code enables migration support:

// iroh/src/socket.rs – enabling migration (line 2620)
transport_config.server_handshake_migration(true);

This configuration tells the underlying QUIC stack to accept packets from new source addresses for existing connections, provided they meet QUIC's path validation requirements. Without this setting, changing the underlying network path would terminate the connection.

Path Selection Architecture

Iroh maintains multiple candidate paths simultaneously, continuously evaluating which route offers the best performance. The system stores every reachable address for a peer—both direct IPs discovered via NAT hole-punching and relay-server addresses—in a specialized mapping structure.

Remote Address Mapping

The RemoteMap structure in iroh/src/socket/remote_map.rs tracks all known network paths to a peer. This includes:

  • Direct paths: UDP addresses obtained through STUN and hole-punching
  • Relayed paths: Circuit relay addresses when direct connectivity fails

These mapped addresses expose the available paths through MappedAddrs, allowing the path selector to choose between them dynamically.

BiasedRttPathSelector Implementation

The BiasedRttPathSelector in iroh/src/socket/biased_rtt_path_selector.rs implements the core decision logic for connection migration. It continuously measures Round-Trip Time (RTT) on each available path and prefers the path with the lowest latency while keeping alternatives as fallbacks.

In iroh/src/socket.rs, the default configuration wires this selector into the endpoint:

// iroh/src/socket.rs – default path selector (line 998)
path_selector: Arc::new(BiasedRttPathSelector::default()),

When a path's RTT degrades sharply or packets stop arriving, the selector drops that path from the active set, triggering the QUIC stack to migrate to an alternate address.

Automatic Failover Mechanism

The migration process operates transparently to the application layer. When the BiasedRttPathSelector detects superior network conditions on an alternative path—such as when a direct connection becomes viable after successful NAT traversal—it signals the QUIC stack to migrate the connection.

The migration workflow follows this sequence:

  1. Path degradation detection: The selector identifies increased latency or packet loss on the current path
  2. Alternative validation: The system verifies connectivity through a candidate path (direct or relayed)
  3. QUIC migration: The stack re-binds to the new socket address without tearing down the logical connection
  4. Seamless continuity: Application streams continue uninterrupted, with only the underlying transport changing

This mechanism allows Iroh to start a connection over a relay, migrate to a direct path when hole-punching succeeds, and return to the relay if the direct route fails—all without requiring the application to manage reconnections.

Implementing Connection Migration

When constructing an Iroh endpoint, you explicitly configure the transport to support migration and specify the path 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?;

To inspect which path is currently active for a connection:

let conn = endpoint.connect(peer_id, None).await?;
// Query the active address through the MappedAddrs interface
println!("Active address: {}", conn.active_addr());

For testing migration scenarios, you can force a network change:

// Simulate network change to trigger migration to the next best path
conn.force_network_change(true).await;

Summary

Iroh's connection migration between direct and relayed paths relies on three core components working together:

  • QUIC migration support: Enabled via server_handshake_migration(true) in the transport configuration, allowing address changes without connection teardown
  • Multi-path tracking: The RemoteMap structure maintains all viable addresses (direct and relayed) for each peer
  • Latency-based selection: BiasedRttPathSelector continuously monitors RTT and automatically migrates to the optimal path while maintaining fallback options

This architecture ensures resilient peer-to-peer communication that adapts to changing network conditions without application-level intervention.

Frequently Asked Questions

Does connection migration drop existing data streams?

No, migration preserves all logical streams. When Iroh migrates from a relayed path to a direct path (or vice versa), the QUIC connection ID remains constant, allowing existing streams and application data to continue uninterrupted. Only the underlying UDP socket binding changes.

How does Iroh detect when to migrate from relay to direct paths?

The BiasedRttPathSelector continuously measures latency on all discovered paths. When NAT hole-punching succeeds and establishes a direct path, the selector detects the lower RTT compared to the relayed route and automatically triggers migration to the direct address, as implemented in iroh/src/socket/biased_rtt_path_selector.rs.

What triggers a migration back to a relayed path?

If the direct path's RTT increases significantly or packets stop arriving—indicating firewall changes, NAT mapping expiration, or network interface changes—the BiasedRttPathSelector removes that path from the active set. The connection then automatically fails over to the relayed path maintained in RemoteMap, ensuring connectivity persists even when direct routes fail.

Is connection migration supported in all Iroh configurations?

Migration requires explicit enablement via server_handshake_migration(true) in the transport configuration, which is the default in Iroh's Endpoint builder. However, both peers must support QUIC address validation for seamless migration to succeed. If the remote endpoint does not support migration, connections will still function but cannot change paths mid-session.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →