# How Iroh Hole Punching Works for NAT Traversal: A Deep Dive into the Implementation

> Discover how iroh's NAT traversal works. Explore its three stage process port mapping, candidate exchange, and deferred hole-punch attempts for seamless connectivity.

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

---

**Iroh performs NAT traversal through a coordinated three-stage process involving port mapping, candidate exchange, and deferred hole-punch attempts managed by a remote-state actor.**

Iroh is an open-source distributed systems toolkit by n0-computer that implements robust NAT traversal to establish direct QUIC connections between peers behind firewalls. Understanding how iroh hole punching works requires examining the interplay between the port-mapper client, the relay protocol for candidate exchange, and the state machine that orchestrates punch attempts. This guide walks through the actual source code implementation, showing exactly how the library discovers public addresses, exchanges them with peers, and punches holes through NAT devices.

## The Three Pillars of Iroh NAT Traversal

Iroh's NAT traversal strategy rests on three coordinated components that operate continuously in the background.

### Port-Mapper Integration

The **port-mapper client** discovers the public address and port of a node sitting behind a NAT. When `PortmapperConfig::Enabled` is specified (the default), [`iroh/src/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/util.rs) creates a real `portmapper::Client`; otherwise, a no-op stub is used. This client periodically invokes `procure_mapping()`, which communicates with UPnP, PCP, or NAT-PMP devices to obtain the external `SocketAddrV4`.

The result is exposed as a `watch::Receiver`, allowing the rest of the stack to react immediately when the public address changes. You can see this implementation in [`portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/portmapper.rs) at lines 58–65.

### Candidate Exchange

Once local addresses are discovered, **candidate exchange** occurs through the relay protocol. Each side transmits its list of reachable public addresses to the other peer. These addresses are collected as `DirectAddr` candidates and stored in the connection's remote state. The exchange happens transparently during the connection handshake, ensuring both peers have a complete view of potential direct paths before attempting to punch holes.

### Remote-State Actor

The **remote-state actor** serves as the decision engine. It tracks candidate sets, determines when new hole-punch attempts are warranted, launches the attempts, and records successes or failures. This actor lives 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) and manages the lifecycle of every direct connection attempt.

## Step-by-Step Hole-Punching Flow

The following steps trace the exact flow from configuration to successful direct connection, referencing specific functions and line numbers from the iroh source code.

### Address Discovery and Mapping

When you spawn an iroh endpoint with the default configuration, the system immediately begins discovering its external address. The `procure_mapping()` function in [`portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/portmapper.rs) (lines 58–65) queries local network gateways using UPnP or NAT-PMP protocols. Upon success, it returns a `SocketAddrV4` representing the node's public-facing endpoint.

### Broadcasting Local Candidates

Whenever the external address changes, `RemoteStateActor::update_local_direct_address` recomputes the set of local candidates and pushes them to all active connections. This function appears in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) at lines 92–100, ensuring that peers always have the freshest possible addresses for direct connection attempts.

### Exchanging Remote Candidates

Each connection can request the remote peer's NAT traversal addresses via `conn.get_remote_nat_traversal_addresses()`. Inside `RemoteStateActor::trigger_holepunching` (lines 32–38), this response is converted into a `BTreeSet<SocketAddrV4>`. These addresses represent the remote node's view of its own publicly reachable endpoints.

### Deciding When to Hole-Punch

The actor maintains the previous attempt's candidate sets in `last_holepunch`. The decision logic at lines 44–56 and 57–65 of [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) implements the following rules:

- If the remote candidate set has grown (new public addresses appeared), trigger a new hole-punch.
- If the local candidate set has grown, trigger a new hole-punch.
- If the sets are unchanged, schedule a retry respecting `HOLEPUNCH_ATTEMPTS_INTERVAL` to avoid excessive traffic.

This deduplication prevents redundant attempts while ensuring that new network conditions (like switching from Wi-Fi to cellular) trigger immediate reconciliation.

### Performing the Hole-Punch

When a new attempt is required, `self.state.do_holepunching(conn)` is called. The implementation in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) (lines 660–695) spawns a dedicated hole-punch task that:

1. Sends a UDP packet from each local candidate address to each remote candidate address.
2. Waits for a matching inbound packet.
3. If packets cross both NATs successfully, establishes a direct QUIC path (NOQ).

This simultaneous coordinate send—known as the "birthday paradox" approach—maximizes the probability that both NAT devices create mapping entries for the direct flow.

### Path State Management

Successful hole-punches are recorded in `PathState` as `HolepunchSucceeded`, while failed attempts are marked `HolepunchFailed` and pruned after a timeout. This logic appears in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) at lines 240–275, keeping the candidate set tidy and preventing stale addresses from cluttering future attempts.

### Relay Fallback

If hole-punching fails—such as when encountering a symmetric NAT that blocks the coordinate open—iroh automatically falls back to a relay connection. The transport selector in [`src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports.rs) (around line 1080) implements this black-hole fallback, guaranteeing connectivity even in the worst NAT scenarios.

## Configuring Hole Punching in Your Application

Enabling and monitoring hole punching requires minimal configuration. The following Rust example demonstrates enabling the port-mapper, spawning an endpoint, and watching the external address:

```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 keeps the external address up-to-date
let endpoint = builder.spawn().await?;

// 3. Connect to a remote peer; the endpoint tries direct hole-punching automatically
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}");
}

```

The `connect()` call returns immediately, but behind the scenes, the endpoint initiates the candidate exchange and hole-punching sequence described above.

## Key Source Files

Understanding iroh's NAT traversal requires familiarity with these specific modules:

- **[`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs)** – Wraps the `portmapper` crate, provides external address discovery, and supplies a watch channel for address updates.
- **[`iroh/src/socket/remote_map/remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map/remote_state.rs)** – Core actor managing candidate sets, punch decisions, and the actual punching implementation.
- **[`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs)** – Selects between direct, hole-punched, or relay transports based on path status.
- **[`iroh/tests/patchbay/nat.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs)** – End-to-end tests exercising hole-punching across diverse NAT configurations.

## Summary

- **Port-mapper integration** continuously discovers public addresses via UPnP/NAT-PMP and exposes them through a watch channel.
- **Candidate exchange** happens through the relay protocol, with each peer sharing its `DirectAddr` set via `RemoteStateActor`.
- **Decision logic** in `trigger_holepunching` only initiates new attempts when candidate sets change, respecting `HOLEPUNCH_ATTEMPTS_INTERVAL` between retries.
- **Execution** occurs in `do_holepunching` (lines 660–695), which sends coordinated UDP packets to cross both NAT devices.
- **Fallback** to relay connections occurs automatically when direct paths fail, handled by the transport selector in [`transports.rs`](https://github.com/n0-computer/iroh/blob/main/transports.rs).

## Frequently Asked Questions

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

If the coordinated UDP packets fail to cross both NATs—typically due to symmetric NAT or aggressive firewall rules—iroh automatically falls back to a relay connection. The transport selector in [`src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports.rs) detects the black-hole condition and routes traffic through the relay protocol, ensuring connectivity is maintained even when direct paths are impossible.

### How does iroh discover its public IP address?

Iroh uses the `portmapper` crate through [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs). When enabled, the `procure_mapping()` function periodically queries local network gateways using UPnP, PCP, or NAT-PMP protocols. The discovered external `SocketAddrV4` is then exposed via a `watch::Receiver`, allowing the `RemoteStateActor` to update candidate sets immediately when network conditions change.

### What triggers a new hole-punch attempt?

The `RemoteStateActor` compares the current local and remote candidate sets against the `last_holepunch` snapshot. According to the logic in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) (lines 44–65), a new attempt triggers when either set grows (indicating new addresses are available). If the sets remain unchanged, the actor respects `HOLEPUNCH_ATTEMPTS_INTERVAL` and defers the retry to prevent network congestion.

### Does iroh support symmetric NAT traversal?

Iroh attempts symmetric NAT traversal through the birthday paradox approach implemented in `do_holepunching`, where both sides simultaneously send packets to multiple candidate addresses. However, symmetric NATs that randomize source ports for every destination can still block this technique. In such cases, iroh transparently falls back to relay connections, maintaining connectivity without requiring application-level changes.