How iroh Handles Connection Migration Between Direct and Relayed Paths

iroh leverages QUIC-native address migration alongside a latency-biased path selector to transparently migrate active connections between direct UDP sockets and relayed streams without application-level interruption.

The n0-computer/iroh networking stack provides resilient peer-to-peer connectivity by implementing automatic connection migration between direct and relayed paths. Built on QUIC via the quinn crate, iroh enables seamless path switching that allows connections to survive network changes, NAT traversal upgrades, and intermittent connectivity shifts. This architecture ensures that applications maintain logical connections even when underlying transport paths change from relayed streams to direct UDP holes or vice versa.

Enabling QUIC-Level Address Migration

At the transport layer, iroh explicitly enables QUIC address migration to allow connections to survive source address changes. In iroh/src/socket.rs, the transport configuration is initialized with server_handshake_migration(true), which instructs the underlying QUIC stack to permit endpoint address changes after the initial handshake completes.

// iroh/src/socket.rs - configuring the transport for migration
let mut transport_config = TransportConfig::default();
transport_config.server_handshake_migration(true);

This configuration is essential for path migration, as it prevents the QUIC connection from closing when packets arrive from a new source address. Without this setting, any change from a relayed path to a direct path (or vice versa) would require establishing an entirely new connection.

Maintaining Multiple Candidate Paths

iroh maintains awareness of all potential network paths to a peer through the RemoteMap structure defined in iroh/src/socket/remote_map.rs. This component tracks every reachable address discovered for a peer, including direct IP addresses obtained through NAT hole-punching and relay-server addresses used as fallback paths.

The connection exposes these mapping details through the MappedAddrs trait, which provides methods to inspect currently active paths. By keeping multiple candidates available simultaneously, iroh ensures that alternative paths exist when the current active path degrades.

Latency-Biased Path Selection

To determine which path to use actively, iroh implements BiasedRttPathSelector in iroh/src/socket/biased_rtt_path_selector.rs. This selector continuously measures round-trip time (RTT) across all available paths and prioritizes the path with the lowest latency while maintaining others as hot standbys.

// iroh/src/socket.rs - default path selector initialization
path_selector: Arc::new(BiasedRttPathSelector::default()),

When the selector detects that the current direct path has lower latency than the relayed path, it signals the QUIC stack to migrate the connection. Conversely, if the direct path becomes unreachable or RTT increases significantly, the selector automatically fails over to the relayed address.

Automatic Migration Between Path Types

The migration process operates transparently to the application layer. When BiasedRttPathSelector determines that a different path offers superior connectivity, or when the active path stops receiving packets, the QUIC stack initiates a migration to the alternate address. Because server_handshake_migration is enabled, the connection simply re-binds to the new socket without tearing down the logical connection or disrupting existing streams.

This mechanism enables iroh to start connections over a relay server for immediate connectivity, then migrate to a direct UDP path once NAT hole-punching succeeds, and migrate back to the relay if the direct path later becomes unreliable. The application sees a single persistent connection throughout these transitions.

Configuring Endpoint Behavior

To utilize connection migration in your application, configure the endpoint with migration-enabled transports and the biased RTT selector. The following example demonstrates building an endpoint that supports transparent path switching:

use iroh::endpoint::{Endpoint, Options};
use iroh::socket::BiasedRttPathSelector;
use std::sync::Arc;

// Configure transport to allow address migration
let mut transport = iroh::socket::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 the currently active path through the connection's address mapping interface:

let conn = endpoint.connect(peer_id, None).await?;
// Active path reflects current direct or relayed address
println!("Active address: {}", conn.active_addr());

Summary

  • QUIC migration support is explicitly enabled via server_handshake_migration(true) in iroh/src/socket.rs, allowing connections to survive address changes.
  • RemoteMap in iroh/src/socket/remote_map.rs maintains all discovered direct and relayed addresses for each peer.
  • BiasedRttPathSelector in iroh/src/socket/biased_rtt_path_selector.rs continuously monitors RTT to select the optimal active path while keeping alternatives ready.
  • Automatic failover triggers migration when path quality degrades, seamlessly switching between direct UDP and relayed streams without application intervention.
  • Zero-interruption switching ensures that data streams continue flowing during path transitions, with the QUIC stack handling re-binding internally.

Frequently Asked Questions

Does switching between direct and relayed paths drop the connection?

No. Because iroh enables QUIC address migration via server_handshake_migration, the underlying QUIC connection treats path changes as address re-binding rather than connection termination. The logical connection persists, and existing data streams continue uninterrupted while the transport layer switches between direct UDP sockets and relayed paths.

How does iroh decide when to migrate to a direct path?

The BiasedRttPathSelector continuously measures round-trip latency on all available paths. When a direct path exhibits lower RTT than the current relayed path, or when a direct path becomes available after successful NAT hole-punching, the selector triggers a migration to the lower-latency direct connection.

Can applications force connection migration for testing purposes?

While the BiasedRttPathSelector typically handles migration automatically based on network conditions, the underlying QUIC implementation in iroh/src/socket.rs supports address validation mechanisms that allow triggering path validation manually. Applications can simulate network changes or force path re-validation to test migration behavior, though direct manipulation of path selection requires implementing a custom path selector trait.

What happens if both direct and relayed paths fail simultaneously?

If all known paths for a peer become unreachable, the QUIC connection will eventually time out based on the configured idle timeout settings. However, iroh's RemoteMap in iroh/src/socket/remote_map.rs caches multiple addresses per peer, and the system will attempt to re-establish connectivity using alternative addresses before declaring the connection dead.

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 →