How Iroh Handles NAT Traversal: Hole-Punching and Relay Architecture

Iroh automatically establishes peer-to-peer connections across NAT boundaries using a combination of address discovery, UDP hole-punching, and relay fallback, all orchestrated by the RemoteState actor without requiring manual configuration.

Iroh is an open-source distributed systems toolkit built by n0-computer that prioritizes direct connectivity. Understanding how Iroh handles NAT traversal is essential for developers building decentralized applications that need to work across home routers, corporate firewalls, and mobile networks.

The Three-Stage NAT Traversal Process

Iroh’s networking stack follows a systematic approach to bypass Network Address Translation (NAT) barriers:

  1. Address Discovery – Each endpoint discovers and advertises local and public (STUN-discovered) addresses.
  2. Direct Path Attempt – The system attempts UDP hole-punching to establish a direct QUIC connection between peers.
  3. Relay Fallback – If direct connectivity fails, traffic transparently routes through iroh-relay servers.

Managing Candidate Addresses in RemoteState

The core orchestration logic resides in [iroh/src/socket/remote_map/remote_state.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs). This file maintains the RemoteState struct, which tracks potential connection paths for each remote peer:

/// Information about the last holepunching attempt.
last_holepunch: Option<HolepunchAttempt>,
/// When the next holepunch should be tried.
scheduled_holepunch: Option<Instant>,

When the endpoint learns new addresses—whether through local interface detection, STUN discovery, or relay advertisements—the state machine evaluates whether to initiate a new hole-punch attempt. The candidates_changed flag signals when the available paths have updated, triggering re-evaluation of the connection strategy.

Triggering Hole-Punching Attempts

The trigger_holepunching method in [remote_state.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs) serves as the decision engine for direct connectivity attempts. It implements several guard clauses to prevent unnecessary network traffic:

fn trigger_holepunching(&mut self) {
    // … several early‑exit checks …
    if let Some(ref last_hp) = self.state.last_holepunch {
        // Avoid hammering the remote if we already succeeded recently.
        if !self.candidates_changed && last_hp.succeeded_recently() {
            return;
        }
    }
    // Choose the most promising candidate pair and start a hole‑punch.
    self.initiate_holepunch(candidate_pair);
}

This method activates under three conditions:

  • Local network addresses change (new local interfaces or ports)
  • New remote candidates are discovered via STUN or relay
  • A scheduled retry timer fires, ensuring persistent attempts to establish direct paths

Path State Management and Pruning

Not all candidates succeed. Iroh tracks the success and failure history of each potential path in [iroh/src/socket/remote_map/remote_state/path_state.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state/path_state.rs). This module implements exponential back-off and pruning logic to avoid repeatedly attempting failed paths:

  • Failed hole-punch attempts are recorded with timestamps
  • Back-off intervals increase exponentially to reduce network load
  • Dead candidates are pruned from the active set after repeated failures

This state management ensures that the system efficiently allocates resources to viable paths while quickly abandoning routes blocked by symmetric NATs or firewall rules.

Relay Fallback Architecture

When hole-punching fails—particularly when both peers sit behind symmetric NATs—Iroh transparently falls back to relay transport. The implementation in [iroh/src/socket/transports/relay.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs) provides TURN-style packet forwarding:

This hybrid approach ensures connectivity in 100% of scenarios while preferring direct paths for optimal latency and throughput.

Validating NAT Traversal: The Test Harness

The robustness of Iroh's NAT traversal is verified in [iroh/tests/patchbay/nat.rs](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs). This comprehensive test matrix simulates various network topologies:

  • No NAT – Direct public IP connectivity
  • Home NAT – Standard consumer router with port mapping
  • Corporate NAT – Symmetric NAT with strict filtering

The test harness validates that RemoteState correctly establishes direct connections when possible and falls back to relays when necessary.

Code Example: Automatic NAT Traversal in Practice

Iroh's NAT traversal requires zero configuration. The following Rust example demonstrates how the client automatically handles complex NAT scenarios:

use iroh::client::Client;
use iroh::endpoint::{self, Endpoint};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create a client with the default configuration.
    // The client automatically starts the networking stack,
    // which includes NAT discovery, hole‑punching, and relay fallback.
    let client = Client::new_default().await?;

    // Connect to a remote peer (identified by its public key).
    // No extra configuration is required – the underlying
    // endpoint will perform NAT traversal as needed.
    let remote_key = "<remote‑public‑key>";
    let mut conn = client.connect(remote_key).await?;

    // Once the connection is established, we can send data.
    conn.send(b"hello from behind NAT").await?;
    Ok(())
}

CLI Usage

The same automatic traversal works via command line:


# Start an iroh node (it automatically runs NAT discovery).

iroh --listen 0.0.0.0:0

# In another shell (perhaps on a different network),

# sync a directory with the first node – the library will

# hole‑punch or use the relay transparently.

iroh sync ./my_folder --remote <first‑node‑peer‑id>

Summary

Frequently Asked Questions

What is hole-punching in Iroh's NAT traversal?

Hole-punching is a technique where both peers simultaneously send UDP packets to each other's public addresses to create a stateful mapping in their respective NAT routers. In [remote_state.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs), the trigger_holepunching method coordinates these simultaneous attempts, allowing direct QUIC connections even when both peers are behind NATs.

How does Iroh decide when to use a relay server?

Iroh attempts direct hole-punching first whenever new address candidates are discovered or scheduled retries occur. Only after all candidate pairs fail—tracked in [path_state.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state/path_state.rs) with exponential back-off—does the connection transparently upgrade to relay transport. This decision happens automatically within the RemoteState actor without application intervention.

Can Iroh connect through corporate firewalls and symmetric NATs?

Yes. While symmetric NATs prevent traditional hole-punching by randomizing source ports, Iroh's relay fallback in [transports/relay.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs) ensures connectivity. The test suite in [nat.rs](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs) specifically validates behavior against corporate-grade NAT configurations, ensuring reliable operation in restrictive network environments.

Do I need to configure STUN servers or relay URLs manually?

No. Iroh ships with default relay URLs defined in [iroh-base/src/relay_url.rs](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs) and automatically discovers public addresses using built-in STUN infrastructure. Advanced users can override these settings, but the default configuration provides out-of-the-box NAT traversal for most deployments.

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 →