# How iroh Connection Establishment Works: Direct QUIC and Relay Server Fallback

> Explore how iroh establishes connections using direct QUIC and automatically falls back to relay servers when NAT traversal fails. Simplify your networking.

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

---

**iroh attempts direct QUIC connections first, then automatically falls back to relay servers when NAT traversal fails, using a unified `Connection` abstraction that hides transport details from application code.**

iroh's connection establishment strategy prioritizes low-latency direct peer-to-peer links while maintaining reliable connectivity through relay servers when network conditions require it. In the n0-computer/iroh repository, this hybrid approach is implemented across the endpoint and transport layers, allowing developers to use a single `Endpoint::connect` API regardless of whether the underlying path is direct or relayed.

## Direct QUIC Connection Establishment

### Endpoint Configuration and Binding

The connection process begins in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs), where `Endpoint::new` constructs the QUIC transport layer. This initializes the UDP socket and configures the TLS 1.3 credentials required for QUIC. The transport implementation resides in [`src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/quic.rs), which wraps the `quinn` library to handle the actual wire protocol.

When the endpoint is ready, `Endpoint::connect` (lines 1190–1210 in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs)) spawns a `Connecting` future that initiates the dial process through `quic::dial`.

### The QUIC Handshake Implementation

The direct path uses standard QUIC with TLS 1.3. The handshake completes when the local endpoint receives a **pong** response from the remote peer, at which point the `Connection` object is returned to the caller. This logic is implemented between lines 150–190 in [`src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/quic.rs).

If the handshake succeeds, all subsequent datagrams travel over the encrypted QUIC stream without additional overhead. If it fails with `ConnectionError::Timeout` or `ConnectionError::NoRoute`, iroh automatically triggers the relay fallback mechanism.

### Zero-RTT Session Resumption

iroh supports **Zero-RTT** (Zero Round-Trip Time) data transmission when reconnecting to a previously known peer. The `ZeroRttStatus` enum tracks whether early data was accepted during the handshake, allowing the endpoint to begin sending application data before the TLS 1.3 handshake formally completes. This optimization is handled in the connection setup logic around lines 1150–1190 in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs).

## Relay Server Fallback Path

When direct QUIC dialing fails or the remote peer is behind a restrictive NAT, iroh uses the relay infrastructure. The relay system is managed by the `RelayActor` subsystem in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs).

### Relay Actor State Management

The `RelayActor` maintains a map of active relay connections, spawning one `ActiveRelayActor` per relay URL. When a datagram needs to be sent to a remote whose home relay is known, `RelayActor::active_relay_handle_for_endpoint` (lines 59–86) either reuses an existing actor or spawns a new one to handle the connection lifecycle.

### Relay Dialing and Handshake Protocol

Each `ActiveRelayActor` runs a state machine that begins with the `run_dialing` state. It repeatedly calls `ClientBuilder::connect()` with a **`CONNECT_TIMEOUT` of 10 seconds**. Once the TCP or QUIC connection to the relay is established, the client sends a `ClientToRelayMsg::Handshake` message.

The handshake completes when the client receives a **Pong** from the relay server, handled in `handle_relay_msg` (lines 92–100). Until this pong arrives, the connection is considered tentative; receipt transitions the actor to `ConnectedRelayState::established`.

### Keep-Alive Mechanisms

To maintain NAT mappings and detect dead connections, the relay actor sends periodic `Ping` frames every **15 seconds** (`PING_INTERVAL`). If a corresponding `Pong` never arrives, the connection is marked as a handshake failure and the actor enters an exponential backoff before retrying. This logic is implemented around lines 65–71 in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs).

### Datagram Forwarding Logic

Once the relay handshake is complete, `ActiveRelayActor::run_connected` forwards incoming `RelayToClientMsg::Datagrams` to the `AsyncUdpSocket` via an `mpsc` channel (`relay_datagrams_recv`). Outgoing datagrams are queued on `relay_datagrams_send` and transmitted in batches of **`SEND_DATAGRAM_BATCH_SIZE` (20)** to optimize throughput. This forwarding loop is found at lines 124–132.

### Home Relay Lifecycle Management

Each endpoint designates one relay as its **home relay**, which remains persistently connected even when idle. The `HomeRelayWatch` watchable (lines 440–452) monitors this status, while non-home relays are shut down after **`RELAY_INACTIVE_CLEANUP_TIME` (60 seconds)** of inactivity to conserve resources.

## Unified Connection API

Both direct and relay paths expose the same `Connection` type, ensuring that higher-level APIs (such as file transfer or search) remain agnostic to the transport method. The `Endpoint::connect` method tries the direct path first; only upon failure does it query the `RelayMap` (configured in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs)) to discover the remote's home relay and initialize the fallback path.

You can inspect the transport type at runtime using `Connection::is_direct()`, which checks whether the underlying transport is a `quic::Connection` or a `relay::Connection` as implemented in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).

## Code Examples

### Establishing a Direct Connection

```rust
use iroh::Endpoint;
use iroh::endpoint::EndpointId;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create a local endpoint listening on a random UDP port
    let endpoint = Endpoint::builder().bind().await?;
    
    // Remote peer's endpoint id (obtained out-of-band)
    let remote_id: EndpointId = "...".parse()?;
    
    // Attempt direct QUIC connection; returns a Connecting future
    let conn = endpoint.connect(remote_id).await?;
    println!("Direct QUIC connection established: {}", conn.remote_id());
    Ok(())
}

```

*Source*: The `connect` implementation is in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs) (lines 1190–1210).

### Automatic Relay Fallback

```rust
use iroh::Endpoint;
use iroh::endpoint::EndpointId;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Configure endpoint with public relay map
    let endpoint = Endpoint::builder()
        .relay_map(iroh_relay::client::RelayMap::default())
        .bind()
        .await?;
    
    let remote_id: EndpointId = "...".parse()?;
    
    // connect() tries direct first, then falls back to RelayActor automatically
    let conn = endpoint.connect(remote_id).await?;
    println!("Connection ready: {}", conn.remote_id());
    Ok(())
}

```

*Source*: Fallback logic is triggered in `Endpoint::connect` and `RelayActor::try_send_datagram` (around line 990 in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs)).

### Inspecting Connection Type

```rust
use iroh::Endpoint;
use iroh::endpoint::EndpointId;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let endpoint = Endpoint::builder().bind().await?;
    let remote = "...".parse::<EndpointId>()?;
    
    let conn = endpoint.connect(remote).await?;
    if conn.is_direct() {
        println!("Using direct QUIC connection");
    } else {
        println!("Using relay server connection");
    }
    Ok(())
}

```

*Source*: `Connection::is_direct()` is defined in [`src/endpoint/connection.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/connection.rs).

## Summary

- **iroh connection establishment** prioritizes direct QUIC connections using UDP and TLS 1.3 handshakes implemented in [`src/endpoint/quic.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint/quic.rs).
- When direct connections fail, the system automatically falls back to relay servers managed by `RelayActor` and `ActiveRelayActor` in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs).
- Both paths use the same `Connection` abstraction, with `Connection::is_direct()` available to inspect the transport type when needed.
- Relay connections use a **10-second connect timeout**, **15-second ping intervals**, and batch datagram transmission in groups of **20** to optimize performance.
- The home relay mechanism ensures persistent connectivity while cleaning up inactive relay connections after **60 seconds** of idleness.

## Frequently Asked Questions

### How does iroh decide when to use a relay server instead of a direct connection?

iroh always attempts a direct QUIC connection first through `Endpoint::connect` in [`src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/src/endpoint.rs). If the handshake times out or returns `ConnectionError::NoRoute`, the endpoint queries the `RelayMap` for the remote peer's home relay and initiates a relay connection via `RelayActor::active_relay_handle_for_endpoint`. This fallback happens automatically without requiring changes to application code.

### What is the timeout for establishing a relay connection?

The `ActiveRelayActor` uses a `CONNECT_TIMEOUT` of **10 seconds** when dialing a relay server via `ClientBuilder::connect()`. If the TCP or QUIC connection to the relay cannot be established within this window, the actor enters a backoff state before retrying, as implemented in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs).

### Can iroh send data before the QUIC handshake completes?

Yes. iroh supports **Zero-RTT** (Zero Round-Trip Time) data transmission when resuming a previously established session. The `ZeroRttStatus` enum tracks whether early data was accepted, allowing the endpoint to begin transmitting before the TLS 1.3 handshake finishes, reducing latency for subsequent connections to known peers.

### How does iroh keep relay connections alive?

Each `ActiveRelayActor` sends periodic `Ping` frames every **15 seconds** (`PING_INTERVAL`). The connection is considered established only after receiving a corresponding `Pong` from the relay server. If no `Pong` arrives, the handshake is marked as failed and the actor backs off before attempting to reconnect, as defined in [`src/socket/transports/relay/actor.rs`](https://github.com/n0-computer/iroh/blob/main/src/socket/transports/relay/actor.rs).