# How Iroh Handles Relay vs Direct Connections: NAT Traversal Implementation Guide

> Discover how Iroh handles relay vs direct connections. Learn its NAT traversal techniques for seamless peer-to-peer communication, falling back to relays when necessary.

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

---

**Iroh's networking layer establishes connections directly when possible but automatically falls back to relay servers when NAT or firewall restrictions prevent peer-to-peer paths.**

The n0-computer/iroh framework provides robust NAT traversal by attempting direct connections first, then seamlessly switching to relayed communication when necessary. This hybrid approach ensures connectivity across diverse network conditions while optimizing for low-latency direct paths whenever available.

## Understanding Relay and Direct Connection Modes

Iroh distinguishes between two fundamental transport modes: direct peer-to-peer connections and relayed connections through intermediary servers.

### Direct Peer-to-Peer Connections

Direct connections represent the optimal path where iroh establishes a UDP-based QUIC connection between two endpoints without intermediary hops. These connections offer lower latency and higher throughput since data flows directly between peers.

### Relayed Connections via Relay Servers

When NAT devices, firewalls, or asymmetric routing prevent direct connectivity, iroh utilizes relay servers as intermediaries. The relay server forwards encrypted traffic between peers without decrypting the contents, maintaining end-to-end security while enabling connectivity in restrictive network environments.

## Configuring Relay Support in Iroh

Relay functionality centers around the `RelayMap`, a configuration structure that tells endpoints which relay servers are available for fallback connections.

### Setting Up the Relay Map

When constructing an `Endpoint`, you supply a relay map through the builder pattern. According to the implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) at line 452, the builder stores this configuration via:

```rust
.relay_mode(RelayMode::Custom(relay_map))

```

The default test relay uses the URL `https://relay.test`, which the framework recognizes as a valid relay endpoint during connection establishment.

### Spawning a Test Relay Server

For integration testing, iroh can spin up an in-process relay server. As implemented in [`iroh-relay/src/lib.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/lib.rs) at lines 515-521, the server binds to `[::]:80` and registers the test URL:

```rust
let url: RelayUrl = "https://relay.test".parse().expect("valid relay url");
let relay_map: RelayMap = RelayConfig::new(url, quic).into();

```

The `run_relay_server()` function returns both the running server instance and the `RelayMap` required by connecting endpoints.

## Connection Establishment and Path Selection

Iroh implements sophisticated path discovery that evaluates multiple potential routes simultaneously, preferring direct connectivity while maintaining relay options as fallback.

### Address Discovery and EndpointAddr

Each discovered address resolves to an `EndpointAddr` structure that carries metadata about the path type. The `is_relay()` method, defined in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) at lines 1086-1087, distinguishes between direct and relay addresses:

- **Direct addresses**: Return `false` from `is_relay()`, representing actual IP:port combinations reachable through the public internet
- **Relay addresses**: Return `true` from `is_relay()`, representing paths through the configured relay infrastructure

### Path Preference Logic

During the connection handshake, the endpoint gathers candidate addresses from multiple sources including DNS resolution and the relay map. The implementation in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) (lines 2468-2688) filters and sorts these paths, attempting direct addresses first before falling back to relay options.

If all direct connection attempts fail due to NAT traversal timeouts or ICMP unreachable errors, the framework automatically retries using the first available relay address, ensuring connection reliability even in challenging network topologies.

## Detecting the Active Connection Type

Once established, connections expose their transport characteristics through the path inspection API, allowing applications to adapt behavior based on latency and bandwidth constraints.

### Runtime Path Inspection

You can determine whether an active connection traverses a relay by checking the path state. As shown in the test utilities at [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) line 382, the detection function appears as:

```rust
pub(crate) fn is_relayed(conn: &iroh::endpoint::Connection) -> bool {
    conn.path().is_relay()
}

```

This returns `true` when the current path flows through a relay server, and `false` for direct peer-to-peer connections.

### Forcing Relay-Only Mode

Test utilities sometimes require simulating worst-case network conditions by restricting endpoints to relay-only communication. The helper function at [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) line 474 demonstrates stripping non-relay addresses:

```rust
fn addr_relay_only(addr: EndpointAddr) -> EndpointAddr {
    EndpointAddr::from_parts(
        addr.id,
        addr.addrs.into_iter().filter(|a| a.is_relay()).collect()
    )
}

```

This filtering ensures the endpoint attempts only relay-based connections, useful for validating relay infrastructure behavior or simulating symmetric NAT scenarios.

## Connection Lifecycle and Fallback Behavior

Iroh connections are dynamic, capable of migrating between transport modes as network conditions change without requiring application-level reconnection.

### From Relay to Direct Path Migration

The framework continuously monitors path quality through the connection lifecycle. While the initial handshake might establish a relayed connection due to aggressive connection timeouts, ongoing NAT hole-punching attempts in the background can discover direct paths post-establishment.

When a direct path becomes available, iroh can migrate the connection from relayed to direct mode transparently. Conversely, if a direct path degrades or becomes unavailable, the connection may fall back to the relay. This behavior is extensively tested in the patchbay integration tests, demonstrating that `conn.path().is_relay()` returns different values at different points in the connection lifetime based on real-time path availability.

## Summary

- **Relay configuration** relies on the `RelayMap` structure passed through `Endpoint::builder().relay_mode(RelayMode::Custom(relay_map))` at initialization time
- **Path discovery** generates both direct and relay `EndpointAddr` instances, with the `is_relay()` method distinguishing transport types
- **Connection preference** prioritizes direct addresses during handshake, automatically falling back to relay addresses when NAT traversal fails
- **Runtime introspection** uses `conn.path().is_relay()` to determine the current transport mode of established connections
- **Dynamic migration** allows connections to transition between relay and direct modes as network paths become available or unavailable

## Frequently Asked Questions

### How does iroh decide between relay and direct connections?

Iroh attempts direct connections first during the initial handshake, trying public IP addresses discovered through DNS or previous connections. Only if these attempts timeout or receive unreachable errors does the framework fall back to addresses marked with `is_relay() == true`. This logic resides in the path selection implementation within [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) around lines 2468-2688.

### Can I force an iroh endpoint to use only relay connections?

Yes, by filtering the endpoint's address list to include only relay addresses before establishing connections. The test utility `addr_relay_only` in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) demonstrates this by filtering with `.filter(|a| a.is_relay())`, effectively preventing direct connection attempts while maintaining relay functionality.

### How do I check if an active connection is using a relay?

Call `conn.path().is_relay()` on the connection object, which returns a boolean indicating whether the active path traverses a relay server. This mechanism, used in the patchbay test suite at line 382, allows applications to monitor transport quality and adjust timeouts or buffer sizes accordingly.

### What is the default relay URL for testing in iroh?

The standard test relay uses `https://relay.test`, which resolves to the in-process relay server spawned by `iroh_relay::run_relay_server()`. This URL is hardcoded in test configurations within [`iroh-relay/src/lib.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/lib.rs) and binds to `[::]:80` when running the local relay infrastructure.