# Understanding iroh Relay Servers and NAT Traversal: P2P Connectivity in n0-computer/iroh

> Learn how iroh relay servers and NAT traversal enable P2P connectivity in n0-computer/iroh. Discover QUIC connections, TURN-like intermediaries, and hole-punching for seamless communication.

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

---

**Iroh endpoints achieve peer-to-peer connectivity through a three-tier fallback system: direct QUIC connections when possible, relay servers acting as TURN-like intermediaries when behind NAT, and aggressive hole-punching attempts to upgrade relayed connections to direct paths whenever firewall topology permits.**

Iroh is a peer-to-peer networking library built on QUIC that powers the n0-computer/iroh ecosystem. When endpoints cannot communicate directly due to NAT or firewall restrictions, the library employs sophisticated relay servers and traversal mechanisms to maintain connectivity. This article examines the implementation details of iroh relay servers and NAT traversal strategies based on the actual source code architecture.

## How iroh Relay Servers Function

When constructing an endpoint, developers configure relay behavior through the **`RelayMode`** enum defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs). By default, endpoints use `RelayMode::Default`, which resolves to the first entry in the **`RelayMap`**—a configuration structure mapping `RelayUrl` values to `RelayConfig` instances.

The builder API in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) (lines 55-71) handles transport configuration:

```rust
pub fn relay_mode(mut self, relay_mode: RelayMode) -> Self {
    let transport: Option<_> = relay_mode.into();
    match transport {
        Some(transport) => {
            if let Some(og) = self
                .transports
                .iter_mut()
                .find(|t| matches!(t, TransportConfig::Relay { .. }))
            {
                *og = transport;
            } else {
                self.transports.push(transport);
            }
        }
        None => {
            self.transports.retain(|t| !matches!(t, TransportConfig::Relay { .. }));
        }
    }
    self
}

```

On the client side, the **`RelayActor`** maintains active relay connections in a `BTreeMap<RelayUrl, ActiveRelayHandle>`. For each configured relay, iroh spawns an **`ActiveRelayActor`** that manages the connection lifecycle:

- **Dialing**: Attempts connection with exponential backoff and a **`CONNECT_TIMEOUT` of 10 seconds** (`ActiveRelayActor::run_dialing`)
- **Keepalives**: Transmits ping frames every **`PING_INTERVAL` of 15 seconds** to maintain NAT mappings
- **Datagram forwarding**: Handles outbound queuing (`run_sending`) and inbound distribution (`run_connected`)

The relay server implementation resides in [`iroh-relay/src/server/http_server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server/http_server.rs) and [`streams.rs`](https://github.com/n0-computer/iroh/blob/main/streams.rs), implementing a QUIC-based protocol defined in [`iroh-relay/src/protos/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/protos/relay.rs). The server authenticates clients via signed challenges ([`handshake.rs`](https://github.com/n0-computer/iroh/blob/main/handshake.rs)) and routes datagrams based on the remote `EndpointId`.

## NAT Classification and Hole-Punching

Iroh classifies NAT behavior using the **`NatKind`** enum located in [`iroh/tests/patchbay/nat.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs), which models real-world firewall behavior:

```rust
enum NatKind {
    None,      // Publicly routable
    Easiest,   // Full-cone (endpoint-independent mapping & filtering)
    Easy,      // Port-restricted cone (EIM / APDF)
    Hard,      // Symmetric NAT (EDM / APDF)
}

```

The **hole-punching algorithm** implemented in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs) operates through three phases:

1. **Address discovery**: Endpoints publish their external addresses to the relay via the `RelayMap` infrastructure
2. **Simultaneous UDP send**: Both peers transmit datagrams to the other's externally discovered address, attempting to create matching firewall state
3. **Direct path detection**: The `Connection::wait_ip` method (line 408) monitors incoming packets; when a packet arrives from a direct source address, the connection transitions from relayed to direct mode

When both endpoints sit behind **Hard (symmetric) NAT**, hole-punching fails because the port mapping depends on the destination address. In these cases, the connection remains relayed indefinitely, which is why corresponding test cases in the NAT matrix are marked `#[ignore]`.

## Port Mapping for Direct Connectivity

Iroh optionally supports UPnP, PCP, and NAT-PMP through the portmapper feature gated in [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs). When enabled via `PortmapperConfig::Enabled`, the system calls `portmapper::Client::procure_mapping` to request a public port mapping from the local router.

The client initialization handles feature availability gracefully:

```rust
pub(crate) fn create_client(config: &PortmapperConfig) -> Client {
    match config {
        #[cfg(all(not(wasm_browser), feature = "portmapper"))]
        PortmapperConfig::Enabled {} => Client::Enabled(::portmapper::Client::default()),
        _ => {
            let (tx, rx) = watch::channel(None);
            Client::Disabled { _tx: tx, rx }
        }
    }
}

```

Successful port mapping publishes the resulting public address to the relay infrastructure, allowing peers to establish direct connections without relay assistance. This is particularly effective for home networks where the router supports these protocols, though many corporate firewalls block such requests.

## Practical Implementation Example

The following example demonstrates configuring a custom relay, enabling port mapping, and establishing a connection while checking the path type:

```rust
use iroh::{Endpoint, endpoint::presets};
use iroh_base::RelayUrl;
use url::Url;

#[tokio::main]
async fn main() -> Result<(), iroh::Error> {
    // Configure a custom relay server
    let custom_relay: RelayUrl = "https://my-relay.example.com".parse().unwrap();
    let relay_cfg = iroh_relay::RelayConfig::new(custom_relay, false);
    let relay_map = iroh_relay::RelayMap::from_iter(
        std::iter::once((relay_cfg.url.clone(), relay_cfg))
    );

    let ep = Endpoint::builder(presets::N0)
        .relay_mode(iroh::RelayMode::Custom(relay_map))
        .bind()
        .await?;

    // Publish external address if port mapping is available
    ep.add_external_addr("203.0.113.42:0".parse().unwrap()).await;

    // Connect to remote peer
    let remote_id = "a1b2c3d4e5f6...".parse().unwrap();
    let remote_addr = iroh_base::EndpointAddr {
        id: remote_id,
        relay_urls: vec![custom_relay.clone()],
        ip_addrs: Vec::new(),
    };
    let conn = ep.connect(remote_addr, b"my-alpn").await?;
    println!("Connected! Direct? {}", conn.is_direct());

    Ok(())
}

```

This implementation leverages [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs) for URL configuration and demonstrates the fallback behavior where `conn.is_direct()` returns false when traffic flows through the relay and true when hole-punching succeeds.

## Summary

- **Relay servers** in iroh act as TURN-like intermediaries, maintaining QUIC connections with `PING_INTERVAL` keepalives to sustain connectivity through symmetric NATs
- **NAT traversal** uses a four-tier classification system (None, Easiest, Easy, Hard) with simultaneous UDP hole-punching attempts monitored via `wait_ip` in the relay actor
- **Port mapping** provides an optional optimization via [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs), requesting router port openings through UPnP/PCP/NAT-PMP when available
- The **ActiveRelayActor** handles connection state machines with 10-second timeouts and automatic reconnection backoff
- Connections automatically upgrade from relayed to direct paths when `Connection::wait_ip` detects incoming packets from non-relay sources

## Frequently Asked Questions

### What happens when both peers are behind symmetric NAT?

When both endpoints are classified as **Hard** (symmetric) NAT, hole-punching fails because each destination address receives a different port mapping from the router. According to the test matrix in [`iroh/tests/patchbay/nat.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/nat.rs), these scenarios fall back to relayed connections indefinitely, maintaining connectivity through the `ActiveRelayActor` datagram forwarding rather than direct UDP.

### How does iroh determine whether to use a relay or direct connection?

The endpoint attempts direct connection first if port mapping or prior address knowledge provides a reachable IP. If direct connection fails or the endpoint is behind NAT, it establishes a relay connection via `RelayMode` configuration. The system continuously monitors for direct paths through `wait_ip` in [`iroh/src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay/actor.rs), automatically upgrading to direct QUIC when hole-punching succeeds.

### Can I deploy my own relay server instead of using the default infrastructure?

Yes. The `RelayMap` structure in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs) allows custom relay configuration. Instantiate a `RelayConfig` with your server URL, create a `RelayMap`, and pass it to `Endpoint::builder().relay_mode(iroh::RelayMode::Custom(relay_map))`. The relay server implementation is available in `iroh-relay/src/server/` and uses the wire protocol defined in [`iroh-relay/src/protos/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/protos/relay.rs).

### Is port mapping required for iroh applications to function?

No. Port mapping via [`iroh/src/portmapper.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/portmapper.rs) is an optional optimization enabled by the `portmapper` feature flag. When disabled or unsupported by the router, iroh falls back to relay servers and hole-punching. The `create_client` function returns a `Client::Disabled` variant that maintains API compatibility through a watch channel, allowing the rest of the system to operate unchanged.