# Iroh Hole Punching for Direct Connections: How NAT Traversal Works in n0-computer/iroh

> Learn how Iroh hole punching upgrades relay connections to direct end-to-end encrypted paths. Discover NAT traversal and UDP path establishment for n0-computer/iroh.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-06-21

---

**Iroh hole punching automatically upgrades relay-mediated QUIC connections into direct, end-to-end encrypted paths between peers behind NATs by coordinating NAT traversal address discovery and UDP path establishment.**

Iroh is a peer-to-peer networking stack designed to prioritize direct connectivity even when both peers reside behind restrictive firewalls or NAT devices. When two endpoints first connect, they typically communicate through a **home relay** to guarantee reachability, but iroh hole punching for direct connections works in the background to eliminate that relay hop as soon as a direct path becomes available. This process reduces latency, minimizes bandwidth costs, and ensures optimal performance for real-time applications.

## Why Iroh Implements Hole Punching for Direct Connections

When an iroh endpoint starts, it first establishes a connection to a **home relay** to ensure it remains reachable from anywhere on the internet. While this relay guarantees connectivity, it introduces extra latency and bandwidth overhead compared to a direct path.

**Hole punching** solves this by allowing two peers to discover a direct UDP path through their respective NATs. According to the iroh documentation, the relay is essential for initial connectivity, but the system continuously attempts to upgrade to a direct connection to improve performance. If NAT conditions prevent a direct path, traffic continues over the relay transparently without dropping the connection.

## The Architecture Behind Iroh Hole Punching

All connection management and hole punching logic resides in the **RemoteStateActor**, found 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 actor monitors QUIC connections, gathers candidate addresses, and decides when to initiate hole punching attempts.

### RemoteStateActor and Connection Management

The **RemoteStateActor** manages the lifecycle of every peer connection. It periodically gathers **candidate NAT traversal addresses** from two sources:

- The QUIC connection itself via `conn.get_remote_nat_traversal_addresses()`
- The local watcher of direct addresses

When new candidates appear on either side, the actor evaluates whether to schedule a hole punch. This design ensures that endpoint mobility—such as switching from Wi-Fi to cellular—triggers new attempts to find optimal paths.

### The Hole Punching Trigger Mechanism

The core decision logic lives in `RemoteStateActor::trigger_holepunching` (lines 504–518). This method filters active connections to find the most suitable client-side connection, collects candidate addresses, and determines whether a new hole punch is necessary.

```rust
// Simplified view of the trigger logic
fn trigger_holepunching(&mut self) {
    // Pick a client-side connection with the lowest ConnId
    let conn = self.connections.iter()
        .filter_map(|(id, s)| s.handle.upgrade().map(|c| (*id, c)))
        .filter(|(_, c)| c.side().is_client())
        .min_by_key(|(id, _)| *id)
        .map(|(_, c)| c)?;
    
    // Collect remote and local NAT candidates
    let remote_candidates = conn.get_remote_nat_traversal_addresses()?.into_iter().collect();
    let local_candidates = self.state.local_candidates();

    // Decide whether a new hole-punch is needed
    let new_candidates = self.state.last_holepunch
        .as_ref()
        .map(|last| !remote_candidates.is_subset(&last.remote_candidates)
                 || !local_candidates.is_subset(&last.local_candidates))
        .unwrap_or(true);

    // Schedule or execute the punch
    if new_candidates { self.state.do_holepunching(conn); }
    else { /* schedule next attempt */ }
}

```

The logic compares current candidates against the previous attempt (`last_holepunch`). If either local or remote candidates have changed, it triggers `do_holepunching`, which instructs the underlying QUIC library (**noq**) to exchange NAT traversal information and open a direct UDP path.

### Path Selection and Failover Strategy

Once hole punching succeeds, `RemoteStateActor::apply_selected_path` (lines 682–702) updates the **selected_path** to the direct route and closes the less efficient relay-based paths. This atomic switch ensures minimal interruption to active data streams.

If hole punching fails, the actor retains the relay path as a fallback and retries after a configurable interval. The system prunes paths that repeatedly fail from the candidate set, as implemented in [`remote_state/path_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state/path_state.rs) (lines 243–254), preventing futile reattempts against incompatible NAT configurations.

## Code Examples: Establishing Direct Connections in Iroh

Iroh handles hole punching automatically once you bind endpoints using the standard presets. The following example demonstrates creating two endpoints and establishing a connection that will automatically upgrade to a direct path.

### Creating Endpoints with Automatic Hole Punching

```rust
use iroh::{Endpoint, endpoint::presets};
use iroh::protocol::Router;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Both peers use the same preset (includes DNS & relay config)
    let a = Endpoint::bind(presets::N0).await?;
    let b = Endpoint::bind(presets::N0).await?;

    // Router on `a` just echoes data
    let router = Router::builder(a.clone())
        .accept(b"example/echo/0", Echo)
        .spawn();

    // Make `b` dial `a` by its public key (the endpoint Id)
    let conn = b.connect(a.addr(), b"example/echo/0").await?;
    let (mut send, mut recv) = conn.open_bi().await?;
    send.write_all(b"hello").await?;
    send.finish()?;
    let reply = recv.read_to_end(64).await?;
    assert_eq!(&reply, b"hello");

    // `a` and `b` will have performed hole punching behind the scenes.
    Ok(())
}

```

### Verifying the Selected Path

To confirm whether a direct connection has been established, inspect the `RemoteInfo` after connection setup:

```rust
use iroh::endpoint::Endpoint;
use iroh::endpoint::presets::N0;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let ep = Endpoint::bind(N0).await?;
    // After a connection is established...
    let info = ep.remote_info().await?; // Returns RemoteInfo (see remote_state.rs)
    println!("selected path: {:?}", info.selected_path);
    Ok(())
}

```

The `selected_path` field indicates whether the connection is using a direct UDP path or still routing through the relay. When hole punching succeeds, this value updates to reflect the direct endpoint addresses.

## Summary

- **Iroh hole punching** transforms relay-mediated connections into direct paths between NATed peers, reducing latency and bandwidth costs.
- The **RemoteStateActor** 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) orchestrates the process by gathering NAT traversal candidates and triggering punches via `trigger_holepunching`.
- Successful punches update the **selected_path** and close relay connections, while failures retain the relay as a fallback with automatic retry every 5 seconds (`HOLEPUNCH_ATTEMPTS_INTERVAL`).
- Failed paths are automatically pruned from consideration to prevent repeated unsuccessful attempts.
- Applications using the standard `Endpoint` API with presets like `N0` receive automatic hole punching without manual configuration.

## Frequently Asked Questions

### What is iroh hole punching?

Iroh hole punching is a NAT traversal mechanism that allows two peers behind firewalls or NAT devices to establish a direct UDP connection. The system uses a relay server for initial connectivity, then coordinates address discovery and UDP NAT traversal to open a direct path, upgrading the connection transparently without application intervention.

### How long does hole punching take in iroh?

Hole punching occurs asynchronously after the initial connection is established. The `RemoteStateActor` evaluates candidates continuously and attempts hole punching whenever new addresses are discovered. If a direct path is possible, it typically establishes within seconds, though the exact timing depends on NAT types and network conditions. The system retries every 5 seconds (`HOLEPUNCH_ATTEMPTS_INTERVAL`) if the first attempt fails.

### What happens if hole punching fails in iroh?

If hole punching fails, iroh maintains the connection over the relay path without dropping packets or interrupting the application. The `RemoteStateActor` continues to monitor for new candidate addresses and will retry periodically. Paths that consistently fail are pruned from the candidate set to optimize future attempts, ensuring the system only tries viable routes.

### Does iroh require a relay server for hole punching to work?

Yes, iroh requires at least one **home relay** to facilitate hole punching. The relay serves as the initial meeting point where peers exchange their NAT traversal addresses (via `get_remote_nat_traversal_addresses`). Without the relay, peers cannot discover each other's candidates to coordinate the hole punch. Once the direct path is established, the relay is no longer used for data transmission, but it remains available for fallback connectivity.