# How Iroh Handles NAT Traversal and Hole Punching: A Deep Dive into the Implementation

> Discover how Iroh achieves NAT traversal and hole punching with its sophisticated three-phase process, ensuring reliable peer-to-peer connections even through complex networks. Learn about port mapping, candidate exchange, and ...

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

---

**Iroh performs NAT traversal through a coordinated three-phase process involving port mapping discovery, candidate address exchange via relay protocol, and a remote-state actor that orchestrates hole-punching attempts, automatically falling back to relay connections when direct paths fail.**

Iroh, the open-source networking library from n0-computer, enables direct peer-to-peer connections even when nodes reside behind restrictive NAT devices. Understanding how Iroh handles NAT traversal and hole punching requires examining its implementation across multiple core modules. The system combines external address discovery, candidate exchange, and stateful connection management to establish direct QUIC paths without manual configuration.

## The Three Pillars of Iroh's NAT Traversal

### Port Mapper Integration

When `PortmapperConfig` is set to `Enabled` (the default), [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) initializes a real `portmapper::Client` rather than a no-op stub. This client periodically invokes `procure_mapping()` to discover external addresses via UPnP, PCP, or NAT-PMP protocols. The discovered `SocketAddrV4` is exposed through a `watch::Receiver`, allowing the stack to react dynamically to address changes.

### Candidate Exchange Protocol

Each peer maintains a set of local candidates (`DirectAddr`s) that represent potentially reachable addresses. When a connection initiates, peers exchange these candidates through the relay protocol. The `RemoteStateActor` manages this exchange, requesting remote NAT traversal addresses via `conn.get_remote_nat_traversal_addresses()` and storing them as a `BTreeSet<SocketAddrV4>` for comparison against local candidates.

### Remote-State Actor

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) serves as the decision engine for hole punching. It tracks previous attempt states (`last_holepunch`) and compares current candidate sets against historical data. When new public addresses appear on either side, the actor triggers fresh hole-punching attempts while respecting `HOLEPUNCH_ATTEMPTS_INTERVAL` to prevent excessive retries.

## Step-by-Step Hole Punching Flow

**Step 1: External Address Discovery**

The port mapper client continuously monitors for external address changes. When `procure_mapping()` returns a new address, the system updates its local candidate set.

**Step 2: Local Candidate Updates**

The `update_local_direct_address` method (lines 92-100 in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs)) recomputes available direct addresses and pushes them to all active connections. This ensures peers always have current reachability information.

**Step 3: Candidate Comparison**

Before initiating a hole punch, the actor evaluates whether the cumulative candidate set has grown since the last attempt. This logic, found in lines 44-65 of [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs), prevents redundant attempts when network conditions remain stable.

**Step 4: Performing the Hole Punch**

When new candidates are detected, `do_holepunching` (lines 660-695) creates a dedicated task that sends UDP packets from local candidates to each remote candidate address. This simultaneous transmission attempts to open pinholes in both NAT devices. If successful, a direct QUIC path is established.

**Step 5: Path State Management**

Successful attempts are recorded as `HolepunchSucceeded` in `PathState`, while failures are marked `HolepunchFailed` and pruned after timeout (lines 240-275). This keeps candidate sets clean and relevant.

**Step 6: Relay Fallback**

When hole punching fails due to symmetric NAT or other restrictive configurations, Iroh automatically falls back to relay connections. The transport selector in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) (around line 1080) handles this black-hole fallback seamlessly.

## Implementing NAT Traversal in Your Application

Here's how to enable and utilize Iroh's hole punching in your own code:

```rust
// 1. Enable the port-mapper (default) when building a node
let builder = iroh::endpoint::Builder::default()
    .portmapper_config(iroh::endpoint::PortmapperConfig::Enabled {});

// 2. Start the endpoint – a background task will keep the external address up-to-date
let endpoint = builder.spawn().await?;

// 3. Connect to a remote peer (remote's public key known)
//    The call returns immediately; the endpoint will try a direct hole-punch behind the scenes.
let conn = endpoint.connect(remote_peer_id).await?;

// 4. Optionally watch the external address for debugging
let mut ext_addr_rx = endpoint.watch_external_address();
while let Some(Some(addr)) = ext_addr_rx.recv().await {
    println!("Our public address is {addr}");
}

```

## Summary

- Iroh's NAT traversal relies on three coordinated components: port mapping discovery, candidate exchange, and the remote-state actor.
- The `PortmapperConfig::Enabled` setting in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) drives external address discovery via UPnP/PCP/NAT-PMP.
- The `RemoteStateActor` in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) manages candidate sets and decides when to trigger `do_holepunching` based on address changes.
- Hole punching involves sending UDP packets to candidate addresses to open NAT pinholes, establishing direct QUIC paths when successful.
- Failed attempts are automatically cleaned up from `PathState`, with seamless fallback to relay connections handled by the transport selector.

## Frequently Asked Questions

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

When hole punching fails, typically due to symmetric NAT or firewall restrictions, Iroh automatically falls back to relay connections. The transport selector in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) detects these black-hole scenarios and routes traffic through the relay protocol, ensuring connectivity is maintained even when direct paths cannot be established.

### How does Iroh discover its public IP address behind a NAT?

Iroh uses the portmapper client configured via `PortmapperConfig::Enabled` to periodically call `procure_mapping()`. This function communicates with UPnP, PCP, or NAT-PMP devices on the local network to obtain the external `SocketAddrV4`, which is then exposed through a `watch::Receiver` for the rest of the stack to consume.

### What triggers a new hole-punching attempt in Iroh?

The `RemoteStateActor` compares current candidate address sets against the previous attempt stored in `last_holepunch`. If either the local or remote candidate set grows (indicating new public addresses), the actor initiates a fresh hole-punching attempt via `do_holepunching`, while respecting the `HOLEPUNCH_ATTEMPTS_INTERVAL` to prevent aggressive retry loops.

### Where is Iroh's hole-punching logic tested?

End-to-end testing for NAT traversal scenarios resides in [`iroh/tests/patchbay/nat.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs). This test suite exercises hole punching across various NAT configurations, validating that the port mapper, candidate exchange, and remote-state actor work correctly together to establish direct connections.