# How Iroh's Hole-Punching Mechanism Works for NAT Traversal

> Discover how Iroh bypasses NAT using a smart three-phase hole-punching process. Learn about port mapping, candidate exchange, and orchestrated UDP attempts for seamless peer connections.

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

---

**Iroh performs NAT traversal through a coordinated three-phase process involving port mapping for external address discovery, candidate exchange between peers, and a remote state actor that orchestrates UDP hole-punching attempts whenever new network paths become available.**

Iroh is a peer-to-peer networking stack built by n0-computer that enables direct connections between nodes behind restrictive NATs. Its hole-punching mechanism automatically discovers public endpoints, exchanges network candidates with remote peers, and attempts to establish direct UDP paths before falling back to relay connections.

## Port Mapper Integration: Discovering Public Endpoints

When an Iroh node initializes with `PortmapperConfig::Enabled` (the default configuration), it spawns a background client in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) that continuously monitors the network for external address changes. This client invokes `procure_mapping()` periodically to communicate with local network gateways via UPnP, PCP, or NAT-PMP protocols.

The port mapper returns the external `SocketAddrV4` representing the node's public-facing endpoint. This address is exposed through a `watch::Receiver` channel, allowing the rest of the stack to react immediately when the router assigns a new port mapping. According to the source code in [`portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/portmapper.rs) (lines 58-65), this discovery happens asynchronously and updates the node's view of its own network topology without blocking the main event loop.

## Candidate Exchange Protocol

Once the external address is known, 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) manages the lifecycle of connection candidates. When the external address changes, the actor calls `update_local_direct_address` (lines 92-100) to recompute the set of local `DirectAddr` candidates and push these updates to all active connections.

Each connection can request the remote peer's NAT traversal addresses via `conn.get_remote_nat_traversal_addresses()`. The response— a vector of `SocketAddrV4` addresses—is converted into a `BTreeSet` for efficient comparison and deduplication. This exchange happens over the relay protocol, ensuring that even before a direct connection exists, both nodes can share their potential contact endpoints.

## The Remote State Actor: Orchestrating Punch Attempts

The core decision logic for when to initiate hole-punching resides in `RemoteStateActor::trigger_holepunching` (lines 44-65). The actor maintains the previous attempt's candidate sets in `last_holepunch` and compares them against current local and remote candidates. If either set has grown—meaning new public addresses have appeared since the last attempt—the actor immediately schedules a new hole-punch.

If the candidate sets remain unchanged, the actor respects the `HOLEPUNCH_ATTEMPTS_INTERVAL` constant and schedules a retry for later. This prevents aggressive polling while ensuring that transient network conditions or delayed STUN responses don't permanently block direct connectivity.

## Performing the Hole-Punch

When the actor decides to attempt a connection, it invokes `do_holepunching` in [`remote_state.rs`](https://github.com/n0-computer/iroh/blob/main/remote_state.rs) (lines 660-695). This function creates an asynchronous task that:

1. Sends a UDP packet from each local candidate address to each remote candidate address
2. Waits for a matching inbound packet to cross the NAT boundary
3. Upon successful packet exchange, establishes a direct QUIC (NOQ) path between the nodes

The mechanism relies on the simultaneous open principle: by sending outbound packets to the remote candidate, the local NAT creates a temporary mapping that allows the remote peer's return traffic to pass through.

## Path State Management and Relay Fallback

Iroh tracks the outcome of each attempt in `PathState` structures. Successful hole-punches are marked as `HolepunchSucceeded`, while failures are recorded as `HolepunchFailed` and pruned after a timeout (lines 240-275) to keep the candidate set tidy and prevent repeated attempts on dead paths.

If hole-punching fails—such as when both peers sit behind symmetric NATs that perform strict endpoint filtering—Iroh automatically falls back to a relay connection. This fallback logic lives in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) (line 1080) and guarantees connectivity even in the worst NAT scenarios by routing traffic through a trusted intermediary.

## Implementation Example

To enable hole-punching in your Iroh application, ensure the port mapper is enabled (default) and spawn an endpoint:

```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

- **Port mapping** in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) continuously discovers the node's external `SocketAddrV4` via UPnP/PCP/NAT-PMP and broadcasts changes through a watch channel.
- **Candidate management** 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) tracks local and remote addresses, triggering hole-punch attempts only when new candidates appear.
- **UDP hole-punching** sends simultaneous probe packets to create NAT mappings, establishing direct QUIC paths when packets successfully cross both firewalls.
- **Automatic fallback** to relay connections occurs when `PathState` records persistent `HolepunchFailed` states, ensuring connectivity regardless of NAT topology.

## Frequently Asked Questions

### What happens if both peers are behind symmetric NATs?

When both peers use symmetric NATs—which map each internal destination to a unique external port—standard hole-punching typically fails because the predicted ports never align. In this scenario, Iroh's transport selector in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) detects the failure and transparently falls back to a relay connection, maintaining connectivity without application-level intervention.

### How does Iroh detect its public IP address?

Iroh uses the `procure_mapping()` function in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) to query local network gateways via UPnP, PCP, or NAT-PMP protocols. These protocols ask the router to report the external port mapping created for the internal socket, revealing the public `SocketAddrV4` that remote peers can use to initiate connections.

### What is the difference between a direct candidate and a relay candidate?

A **direct candidate** represents a public IP address and port obtained through the port mapper or STUN that could allow a peer-to-peer UDP connection. A **relay candidate** represents the address of a relay server that can forward traffic when direct paths fail. The `RemoteStateActor` prioritizes direct candidates but maintains relay candidates as guaranteed fallback options.

### How long does Iroh wait between hole-punching attempts?

Iroh respects the `HOLEPUNCH_ATTEMPTS_INTERVAL` constant when scheduling retries. The exact duration is implementation-defined in the source, but the mechanism ensures that attempts are spaced sufficiently to allow NAT mappings to propagate and to avoid overwhelming the network with probe packets, while still attempting new punches immediately when fresh candidates are discovered.