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 relayed and direct paths without interrupting the logical connection or requiring application-level reconnections.

Iroh's networking layer, built on the Quinn QUIC stack, enables robust peer-to-peer connectivity by allowing connections to dynamically migrate between network paths. This architecture ensures that applications can initiate communication through a relay server to traverse NATs and firewalls, then transparently upgrade to direct UDP connections when hole punching succeeds. The migration mechanism relies on specific transport configurations and intelligent path selection algorithms implemented in the socket layer.

Enabling QUIC Address Migration

The foundation of connection migration in iroh rests on enabling QUIC's native address migration feature in the transport configuration. When constructing an endpoint, the code explicitly sets server_handshake_migration(true) on the TransportConfig, instructing the QUIC stack to allow the connection's source address to change after the initial handshake.

In iroh/src/socket.rs, the transport configuration is initialized as follows:

// Enable QUIC address migration for the endpoint
let mut transport_config = TransportConfig::default();
transport_config.server_handshake_migration(true);

This configuration, defined in iroh/src/socket/transports.rs, permits the QUIC connection to survive network path changes. Without this flag, changing from a relayed address to a direct IP would terminate the connection rather than migrating it.

Path Selection with BiasedRttPathSelector

Iroh uses the BiasedRttPathSelector to determine which available path provides the best connectivity. This selector, implemented in iroh/src/socket/biased_rtt_path_selector.rs, continuously measures round-trip time (RTT) across all viable paths and prefers the route with the lowest latency while maintaining alternative paths as fallbacks.

The selector is instantiated and assigned to the endpoint in iroh/src/socket.rs:

path_selector: Arc::new(BiasedRttPathSelector::default()),

When the selector detects that a direct path offers significantly lower RTT than the current relayed connection, it signals the QUIC stack to initiate a migration. Conversely, if the active direct path degrades or packets stop arriving, the selector automatically fails over to the next best option, which may be the relay address.

Tracking Remote Addresses via RemoteMap

To support migration, iroh maintains a comprehensive view of all reachable addresses for each peer through the RemoteMap structure in iroh/src/socket/remote_map.rs. This component stores every discovered address for a peer, including direct IPs obtained through NAT hole punching and any relay server addresses.

The MappedAddrs trait exposes these addresses to the connection logic, allowing the system to evaluate multiple candidate paths simultaneously. When a new direct address becomes available via hole punching, it is added to the RemoteMap, making it immediately eligible for the path selector's consideration.

The Migration Lifecycle

The complete migration process from relay to direct connection operates automatically through the following sequence:

  1. Initial Connection: The peer connection begins via a relay server to ensure immediate connectivity despite NATs or firewalls.
  2. Discovery: Simultaneously, iroh attempts NAT hole punching to discover direct UDP paths, adding successful candidates to the RemoteMap.
  3. Evaluation: The BiasedRttPathSelector continuously compares RTT measurements between the relay path and any direct paths.
  4. Migration Trigger: When a direct path demonstrates lower latency, the QUIC stack initiates address migration to the direct socket, re-binding the connection without tearing down the logical session.
  5. Continuous Monitoring: If the direct path later becomes unstable or unreachable, the selector triggers a migration back to the relay path, ensuring connection resilience.

Configuring Connection Migration

To enable connection migration in your iroh application, configure the endpoint with migration-enabled transport settings 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 the currently active path through the connection's MappedAddrs implementation:

let conn = endpoint.connect(peer_id, None).await?;
println!("Active address: {:?}", conn.active_addr());

Summary

  • QUIC migration is enabled via server_handshake_migration(true) in the transport configuration, allowing connections to survive address changes.
  • BiasedRttPathSelector continuously monitors RTT across all available paths to select the optimal route between relay and direct connections.
  • RemoteMap maintains all viable addresses for a peer, including those discovered through NAT hole punching, enabling seamless path switching.
  • Automatic failover occurs when path quality degrades, with the system migrating back to relay connections if direct paths fail, all without application intervention.

Frequently Asked Questions

Does migration cause packet loss or connection interruption?

No, QUIC's address migration preserves the logical connection state throughout the transition. The protocol uses connection IDs rather than 4-tuples (IP + port) to identify connections, allowing packets to arrive from new addresses without resetting the stream. Applications continue sending and receiving data uninterrupted during the migration from relay to direct paths.

How does iroh decide when to switch from relay to direct?

The BiasedRttPathSelector compares round-trip times between the current active path and alternative paths stored in the RemoteMap. When a direct path consistently demonstrates lower latency than the relay connection, the selector triggers a migration. This process is continuous, meaning iroh constantly evaluates whether a newly available direct path offers better performance than the current route.

Can a connection migrate back to relay after switching to direct?

Yes, the migration is fully bidirectional. If the direct path's RTT degrades sharply or packets stop arriving entirely, the BiasedRttPathSelector removes that path from the active set. The QUIC stack then automatically migrates back to the relay address maintained in the RemoteMap, ensuring the connection remains viable even when direct connectivity deteriorates.

What transport configuration is required to support migration?

You must explicitly enable server_handshake_migration(true) on the TransportConfig when building the endpoint, as seen in iroh/src/socket.rs. Additionally, you should use the BiasedRttPathSelector as the path selector to ensure intelligent routing decisions. Without these configurations, iroh will not migrate connections between relay and direct paths, treating each address change as a new connection attempt.

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 →