How Iroh Handles Network Transitions: WiFi to Cellular Handoff Explained

Iroh automatically adapts to network interface changes by monitoring system state with a platform-native watcher, triggering asynchronous network reports to assess the new path, and atomically updating direct address sets while maintaining relay connectivity as a fallback.

Iroh is a Rust-based networking stack designed for resilient peer-to-peer connectivity across volatile network conditions. When your device switches from WiFi to cellular—or any interface change occurs—the library seamlessly migrates active QUIC connections without application intervention. This article examines the internal architecture that enables these automatic network transitions based on the n0-computer/iroh source code.

The Network Transition Pipeline

When the operating system reports a new local IP address or a removed interface, iroh executes a coordinated multi-stage pipeline to reassess connectivity paths.

Monitoring Interface State with netwatch

The foundation of network transition handling starts with the netwatch subsystem. A netwatch::Monitor instance creates a Watcher that emits a fresh netwatch::State whenever an interface appears, disappears, or its IP addresses change. According to the source in iroh/src/socket.rs at lines 1469-1470, this watcher is stored in Socket::local_interfaces_watcher, ensuring the socket actor maintains a live view of the host's network topology.

Propagating Changes via ActorMessage

Interface state changes drive an internal message passing system. The watcher emits events that translate into ActorMessage::NetworkChange messages, defined in the enum ActorMessage at lines 1319-1322 of iroh/src/socket.rs. The main Actor::run loop receives these messages and immediately calls self.direct_addr_update_state.schedule_run(UpdateReason::LinkChangeMajor, if_state). This method schedules a fresh network report with high priority, indicating a major link change has occurred.

Running the Network Report

The DirectAddrUpdateState::run method creates a net_report::Client and issues a get_report request. This probe discovers the current NAT type, identifies reachable relay servers, and determines viable direct IP routes. As implemented in iroh/src/socket.rs at lines 892-905, the report runs asynchronously and is automatically cancelled if the endpoint begins shutdown. This non-blocking design prevents network transitions from stalling the socket actor's event loop.

Updating Direct Addresses and Remote Maps

When the net-report completes, the socket updates its direct_addrs via store_direct_addresses. If the set of usable IP addresses has changed, the socket publishes the new addresses to the address-lookup service using publish_my_addr and informs the RemoteMap. As shown in lines 511-525 of iroh/src/socket.rs, this process may open new QUIC paths to peers or close stale ones derived from the previous network interface.

Maintaining Connectivity During Transitions

Iroh preserves application-level connectivity even when direct paths temporarily vanish during network handoffs.

Relay Path Persistence

Even if the direct path disappears—such as when switching to cellular without an IPv4 address—the relay transport remains active. The RelayActor, located in iroh/src/socket/transports/relay/actor.rs (lines 21-27), watches the same local_interfaces_watcher and automatically reconnects the WebSocket after the interface recovers. This ensures NAT traversal and peer discovery continue uninterrupted during the transition window.

Intelligent Path Selection

The PathSelector (default implementation: biased_rtt_path_selector) receives updated RTT and availability metrics from the net-report. Based on this data, it may switch the active data path from a WiFi-derived direct address to a cellular-derived one, or fall back to the relay if the new interface cannot reach the remote directly. This logic is orchestrated in iroh/src/socket.rs at lines 1338-1341.

Explicit Control and Testing

While iroh handles transitions automatically, the public API exposes Endpoint::network_change() for manual triggering. This method forces an immediate re-evaluation of network conditions, useful for testing or responding to explicit user signals. The implementation resides in iroh/src/socket.rs at lines 888-894.

Practical Implementation

The following example demonstrates creating an endpoint that automatically handles network transitions:

use iroh::Endpoint;

// Create an endpoint (bind will start the network-watcher automatically)
let endpoint = Endpoint::builder()
    .secret_key(my_secret_key)
    .transports(vec![/* … */])
    .bind()
    .await?;

// The endpoint will automatically adapt when the OS switches from Wi-Fi to cellular.
// If you want to trigger an immediate check (e.g. in a test) you can call:
endpoint.network_change().await;

To observe relay changes during network transitions:

// Subscribe to the socket's home-relay watcher to know when a new relay is selected
let mut relay_watcher = endpoint.socket().home_relay();
while let Some(relays) = relay_watcher.next().await {
    println!("Current relays: {:?}", relays);
}

Summary

  • Automatic Detection: The netwatch::Monitor Watcher in iroh/src/socket.rs tracks interface state changes in real-time.
  • Asynchronous Probing: Network reports run via net_report::Client to assess NAT type and path viability without blocking the actor.
  • Atomic Updates: Direct addresses are refreshed through store_direct_addresses, with changes propagated to the RemoteMap and address-lookup service.
  • Continuous Connectivity: The RelayActor maintains fallback paths during interface outages, while the PathSelector optimizes for the lowest-latency route.
  • Manual Override: The Endpoint::network_change() method allows explicit re-evaluation for testing scenarios.

Frequently Asked Questions

Does iroh drop connections when switching from WiFi to cellular?

No, iroh maintains the logical connection. While individual QUIC paths may close when their underlying IP addresses become invalid, the PathSelector immediately attempts to establish new paths using the updated direct addresses. The relay connection remains active as a fallback, ensuring the session survives the transition.

How long does a network transition take?

The transition latency depends on the net-report duration, which typically completes within milliseconds to a few seconds depending on network conditions. The DirectAddrUpdateState runs the report asynchronously, so application traffic experiences minimal interruption while the new path is being established.

Can I disable automatic network adaptation?

Iroh does not provide a configuration flag to disable interface monitoring because maintaining accurate address sets is critical for QUIC path migration. However, you can control the behavior by selecting specific transports or manually triggering evaluations via Endpoint::network_change() if you need to synchronize transitions with application logic.

What happens if the new network has no public IP address?

When the cellular network only provides CGNAT or IPv6 without public IPv4, the net-report will determine that direct connectivity is limited. The PathSelector will continue using the relay path for NAT traversal, and the RelayActor ensures the WebSocket connection to the relay server is re-established over the new interface.

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 →