# How to Handle Connection Migration from Relay to Direct Peer-to-Peer in iroh

> Learn how to handle connection migration from relay to direct peer-to-peer in iroh. Enable QUIC handshake migration and configure a relay map for seamless upgrades.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Enable QUIC handshake migration in your transport configuration, start the endpoint with a relay map for initial connectivity, and iroh automatically upgrades the connection to a direct peer-to-peer path once addresses become reachable.**

The iroh networking library uses QUIC (via the `noq` crate) to seamlessly migrate connections from relayed pathways to direct peer-to-peer links. When endpoints start behind restrictive NATs or firewalls, they require a relay server to establish initial contact. This guide explains how to configure connection migration in the `n0-computer/iroh` repository so your application automatically upgrades to direct paths when network conditions allow.

## How Connection Migration Works in iroh

iroh leverages QUIC's built-in path migration feature to switch from relayed connections to direct paths without dropping the connection. When peers discover valid direct addresses—typically after successful hole-punching or when NAT mappings stabilize—the QUIC stack performs a `PATH_CHALLENGE` and `PATH_RESPONSE` exchange. According to the implementation in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), this process happens automatically once you enable handshake migration in the transport configuration.

## Configuring the Transport for Migration

To allow connections to migrate from relay to direct paths, you must explicitly enable server-side handshake migration in your transport configuration.

```rust
use std::time::Duration;
use noq::TransportConfig;

let mut transport_cfg = TransportConfig::default();
transport_cfg.server_handshake_migration(true);
transport_cfg.keep_alive_interval(Some(Duration::from_secs(1)));

```

The `server_handshake_migration(true)` flag is essential for both client and server endpoints. As implemented in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), the `socket_ep` function applies this configuration to enable the QUIC stack to accept path migration attempts. Additionally, setting the `keep_alive_interval` ensures NAT mappings remain valid during the discovery phase.

## Building the Endpoint with Relay Support

Since peers may not be directly reachable initially, configure the endpoint to use a relay map for the initial handshake. This ensures connectivity even when direct paths are blocked by NAT.

```rust
use iroh::endpoint::{Builder, RelayMode};
use iroh::relay::RelayMap;
use std::sync::Arc;

let relay_map = RelayMap::default();
let mut builder = Builder::new()
    .relay_mode(RelayMode::Custom(relay_map));

// Attach the transport config that enables migration
builder = builder.transport_config(Arc::new(transport_cfg));
let endpoint = builder.build().await?;

```

As demonstrated in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs), the `endpoint_builder` helper uses `RelayMode::Custom` to establish relay-only connectivity first. This pattern allows the connection to initially route through the relay while the library probes for direct paths in the background.

## Monitoring the Migration Status

### Checking if a Connection is Relayed

After establishing a connection, determine whether traffic is still flowing through the relay or has migrated to a direct path by checking the connection state.

```rust
if connection.is_relayed() {
    println!("Currently relayed – waiting for migration");
} else {
    println!("Direct peer-to-peer connection active");
}

```

The `is_relayed` method, available on the `Connection` object and demonstrated in [`iroh/tests/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/util.rs), queries the underlying QUIC path to determine if the relay is still the active path.

### Reacting to Path Changes

For applications that need to trigger logic when migration completes, monitor the path change events provided by the `noq` crate.

```rust
// Listen for path migration events
while let Ok(event) = connection.on_path_changed().await {
    match event {
        PathEvent::Migrated => {
            if !connection.is_relayed() {
                println!("Migrated to direct connection");
            }
        }
        _ => {}
    }
}

```

The `on_path_changed` stream emits `PathEvent::Migrated` when QUIC successfully confirms a new direct path through the `PATH_CHALLENGE` and `PATH_RESPONSE` validation.

## Complete Implementation Example

The following example demonstrates the full workflow: configuring migration, connecting via relay, and waiting for the direct path upgrade.

```rust
use std::sync::Arc;
use std::time::Duration;
use iroh::{
    endpoint::{Builder, RelayMode},
    transport::noq::TransportConfig,
    key::SecretKey,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 1. Prepare secret key
    let secret = SecretKey::generate();
    
    // 2. Configure transport to allow migration
    let mut transport_cfg = TransportConfig::default();
    transport_cfg.server_handshake_migration(true);
    transport_cfg.keep_alive_interval(Some(Duration::from_secs(1)));

    // 3. Build endpoint with relay support
    let relay_map = iroh::relay::RelayMap::default();
    let endpoint = Builder::new()
        .secret_key(secret)
        .relay_mode(RelayMode::Custom(relay_map))
        .transport_config(Arc::new(transport_cfg))
        .build()
        .await?;

    // 4. Connect to remote peer (initially via relay)
    let conn = endpoint.connect("https://relay.example/node-id".parse()?).await?;
    
    // 5. Wait for migration to direct path
    println!("Connected – relayed? {}", conn.is_relayed());
    while conn.is_relayed() {
        conn.wait_path_changed().await?;
        println!("Path changed – checking if relayed: {}", conn.is_relayed());
    }
    
    println!("Direct peer-to-peer connection established");
    Ok(())
}

```

## Summary

- **Enable handshake migration** by setting `transport_cfg.server_handshake_migration(true)` in your `TransportConfig`, as required by the QUIC implementation in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs).
- **Start with relay connectivity** using `RelayMode::Custom` to ensure initial reachability behind NATs and firewalls.
- **Let iroh handle the upgrade** automatically when direct paths become available via QUIC path validation and socket rebinding.
- **Monitor migration status** using `is_relayed()` or the `on_path_changed` event stream to trigger application-level logic when the connection upgrades.

## Frequently Asked Questions

### How do I know if connection migration is enabled correctly?

Verify that `transport_cfg.server_handshake_migration(true)` is called before passing the configuration to the endpoint builder. If enabled, iroh will automatically attempt migration once valid direct addresses are discovered, which you can confirm by polling `connection.is_relayed()` until it returns `false`.

### Can a connection migrate back to the relay if the direct path fails?

Yes, QUIC supports reverting to previous working paths. If the direct path becomes unreachable after migration, the connection can fall back to the relay path provided the relay connection remains valid and the transport configuration supports path migration. The `keep_alive_interval` setting helps maintain the relay path during transitions.

### How long does migration from relay to direct typically take?

Migration latency depends on the time required to establish direct connectivity, typically ranging from a few hundred milliseconds for successful hole-punching to several seconds if STUN probing is required. The library monitors path quality continuously and migrates immediately upon validating a direct path through the `PATH_CHALLENGE` exchange.

### Is there a way to prevent migration and keep traffic on the relay?

While you cannot easily disable migration on an existing connection after enabling it in the transport config, you can control initial connectivity by omitting the `server_handshake_migration` flag. However, disabling migration is not recommended for production use as it prevents optimization to direct paths and increases latency and server load.