# How the Portmapper in Iroh Assists with NAT Traversal: UPnP, PCP, and NAT-PMP Explained

> Learn how iroh's portmapper automates NAT traversal using UPnP, PCP, and NAT-PMP. It exposes your internal socket's public address to remote peers for seamless connectivity.

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

---

**The portmapper in iroh automates NAT traversal by negotiating temporary port mappings via UPnP, PCP, or NAT-PMP protocols, exposing the internal socket's public address to remote peers while ensuring mappings are cleaned up on shutdown.**

When running peer-to-peer QUIC connections in the `n0-computer/iroh` ecosystem, both endpoints often sit behind routers performing Network Address Translation (NAT). Inbound packets are blocked unless the router maintains a specific forwarding rule. The **portmapper** component solves this by dynamically requesting port mappings from the local gateway and broadcasting the resulting public address to the network.

## Configuring the Portmapper in the Endpoint Builder

The portmapper is controlled through `PortmapperConfig`, which is passed to the endpoint builder during initialization. This configuration determines whether the system attempts to negotiate mappings with the local router.

In [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs), the builder exposes the `portmapper_config` method. The default value is `PortmapperConfig::Enabled`, which activates the mapping logic:

```rust
use iroh::endpoint::{Endpoint, PortmapperConfig};

let endpoint = Endpoint::builder()
    .portmapper_config(PortmapperConfig::Enabled)   // Activates UPnP/PCP/NAT-PMP
    .build()
    .await?;

```

Set the configuration to `PortmapperConfig::Disabled` to skip mapping entirely. This is useful when running in environments where gateway protocols are unavailable or when operating behind symmetric NATs that resist port mapping.

## Client Initialization and Feature Gating

When the endpoint builds, the `create_client` function in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) converts the configuration into a concrete `portmapper::Client`. The implementation uses conditional compilation to determine behavior:

- **Native targets** (non-WebAssembly): Returns a live client that communicates with the OS's UPnP/PCP service via `::portmapper::Client::default()`.
- **WebAssembly or disabled feature**: Returns a **no-op** client that retains only a watch channel, providing the same API surface without network operations.

This abstraction allows the rest of the codebase to remain agnostic to whether port mapping is actually available.

## The Portmapping Lifecycle

### Procuring Mappings During Address Updates

The `DirectAddrUpdateState` struct in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) owns the `port_mapper: portmapper::Client` instance. Before each network report probe, it invokes `procure_mapping()` to request or refresh the NAT binding:

```rust
// Inside DirectAddrUpdateState::run (iroh/src/socket.rs)
self.port_mapper.procure_mapping()

```

This call initiates the negotiation with the local gateway to open a public port and forward traffic to the internal socket.

### Cleanup and Deactivation

When the socket shuts down or the endpoint reconfigures, the system calls `deactivate()` on the portmapper client. This removes the port mapping from the gateway, preventing stale NAT entries that could block future connection attempts or create security vulnerabilities.

The lifecycle hooks are located at lines 1005-1008 in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), ensuring deterministic cleanup regardless of how the connection terminates.

## Broadcasting Public Addresses

Once a mapping is established, the portmapper client exposes the external address through a `watch_external_address()` receiver. The endpoint's address-lookup service subscribes to this receiver in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) (lines 83-89).

When the mapping resolves to a `SocketAddrV4`, the service publishes this address to the distributed address-lookup system. Remote peers can then discover and connect directly to this public endpoint, bypassing the need for relay servers and achieving direct, low-latency communication.

## Integration with Network Reporting

The net-report component, responsible for probing relays and detecting captive portals, coordinates with the portmapper to ensure mappings stay fresh. In [`iroh/src/net_report/reportgen.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/net_report/reportgen.rs), the system creates a **portmapper future** that runs in parallel with network probes.

This concurrent execution ensures that while the endpoint determines its public visibility and relay capabilities, the port mapping is simultaneously refreshed or renegotiated, minimizing the time between network changes and address publication.

## Practical Implementation Example

The following example demonstrates enabling the portmapper, monitoring the external address, and disabling the feature for specific network conditions:

```rust
use iroh::endpoint::{Endpoint, PortmapperConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build endpoint with portmapper enabled (default)
    let ep = Endpoint::builder()
        .portmapper_config(PortmapperConfig::Enabled)
        .build()
        .await?;

    // Monitor external address assignment
    let mut watcher = ep.portmapper().watch_external_address();
    if let Some(addr) = *watcher.borrow() {
        println!("Public address: {}", addr);
    } else {
        watcher.changed().await?;
        println!("Public address: {}", watcher.borrow().as_ref().unwrap());
    }

    // Disable for symmetric NAT scenarios or testing
    let ep_no_pm = Endpoint::builder()
        .portmapper_config(PortmapperConfig::Disabled)
        .build()
        .await?;

    Ok(())
}

```

The `watch_external_address()` method forwards to `portmapper::Client::watch_external_address`, providing real-time updates as the NAT mapping changes.

## Summary

- **Protocol Support**: The portmapper negotiates with gateways using UPnP, PCP, or NAT-PMP to open temporary public ports.
- **Automatic Lifecycle**: Mappings are procured before network probes and deactivated on shutdown via `DirectAddrUpdateState` in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs).
- **Address Propagation**: The `watch_external_address()` receiver publishes discovered `SocketAddrV4` addresses to the distributed lookup service.
- **Feature Gating**: Native targets use live clients; WebAssembly falls back to no-op implementations without code changes.
- **Parallel Execution**: The net-report component spawns portmapper futures alongside connectivity probes to ensure timely mapping updates.

## Frequently Asked Questions

### What protocols does the iroh portmapper support?

The portmapper supports **UPnP**, **PCP** (Port Control Protocol), and **NAT-PMP** (NAT Port Mapping Protocol). These are implemented in the underlying `portmapper` crate and wrapped by the client in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs).

### How do I disable the portmapper if I'm behind a symmetric NAT?

Pass `PortmapperConfig::Disabled` to the endpoint builder. Symmetric NATs typically randomize source ports for each outbound connection, making static port mappings ineffective. Disabling the feature prevents unnecessary network traffic and failed negotiation attempts.

### Why does the portmapper use a watch channel for external addresses?

The `watch_external_address()` method returns a `tokio::sync::watch` receiver that updates when the gateway assigns or changes the public port. This allows the address-lookup service to react asynchronously to network changes without polling, ensuring remote peers always receive the current valid address.

### Does the portmapper work on WebAssembly targets?

No. When compiling for WebAssembly, `create_client` in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) returns a no-op client that maintains the API surface but performs no network operations. This ensures the `iroh` crate compiles for WASM environments where raw socket access and gateway protocols are unavailable.