# How to Configure Custom Relay Servers for Iroh

> Configure custom relay servers for Iroh easily. Learn how to create a RelayMap and pass it to your endpoint builder for enhanced control and flexibility.

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

---

**To configure custom relay servers for Iroh, create a `RelayMap` containing your `RelayUrl` and `RelayConfig` pairs, then pass it to `Endpoint::builder().relay_mode(RelayMode::Custom(map))` before binding the endpoint.**

When building peer-to-peer applications with the n0-computer/iroh library, you may need to configure custom relay servers to handle QUIC traffic fallback instead of using the default public infrastructure. Custom relays enable private network deployments, geographic optimization, and isolated testing environments. This guide walks through the exact source code implementation in `iroh-base`, `iroh-relay`, and `iroh` crates to configure your own relay infrastructure.

## Understanding Iroh's Relay Architecture

Iroh uses **relays** as fallback nodes that forward QUIC traffic when direct hole-punching fails. By default, the library contacts public relays supplied by the project via `RelayMode::Default`. To override this behavior, you must construct a custom **RelayMap** and pass it to the endpoint builder.

The architecture relies on four core types defined across the workspace:

- **`RelayUrl`** – A thin wrapper around `url::Url` representing the relay address, defined in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs).
- **`RelayConfig`** – Holds the HTTP/QUIC bind address and TLS settings for a single relay, defined in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs).
- **`RelayMap`** – A thread-safe map from `RelayUrl` to `Arc<RelayConfig>`, defined in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs).
- **`RelayMode`** – An enum used by `EndpointBuilder` to decide which relays to use, defined in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).

## Step-by-Step Configuration

### Parse the Relay URL

First, create a `RelayUrl` from your relay's HTTPS address. The implementation in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs) provides a `FromStr` implementation that validates the URL format.

```rust
use iroh_base::RelayUrl;

let url = "https://my.relay.example.com".parse::<RelayUrl>()?;

```

### Create the Relay Configuration

Next, instantiate a `RelayConfig` using the parsed URL and default QUIC settings. The `RelayConfig::new` function in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs) builds a configuration that sets up the internal HTTP listener and QUIC server.

```rust
use iroh_relay::{RelayConfig, quic::QuicConfig};

let quic = QuicConfig::default();
let cfg = RelayConfig::new(url.clone(), quic);

```

### Build the Relay Map

Insert the configuration into a `RelayMap`. The `RelayMap::empty()` constructor creates an empty map, and the `insert` method defined in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs) adds your configuration.

```rust
use iroh_relay::RelayMap;
use std::sync::Arc;

let relay_map = RelayMap::empty()
    .insert(url, Arc::new(cfg))
    .expect("first insertion cannot fail");

```

### Configure the Endpoint

Finally, pass the map to the endpoint builder using `RelayMode::Custom`. The conversion takes place in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) around line 1920 within the `RelayMode` match logic.

```rust
use iroh::{Endpoint, RelayMode};

let endpoint = Endpoint::builder()
    .relay_mode(RelayMode::Custom(relay_map))
    .bind()
    .await?;

```

## Complete Implementation Examples

### Minimal Custom Relay Setup

This complete example demonstrates the minimal code required to point an Iroh endpoint at a custom relay:

```rust
use iroh::{Endpoint, RelayMode};
use iroh_base::RelayUrl;
use iroh_relay::{RelayConfig, RelayMap, quic::QuicConfig};
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Parse the relay URL
    let url = "https://my.relay.example.com".parse::<RelayUrl>()?;

    // Build a RelayConfig using default QUIC settings
    let quic = QuicConfig::default();
    let cfg = RelayConfig::new(url.clone(), quic);

    // Insert into a RelayMap
    let relay_map = RelayMap::empty()
        .insert(url, Arc::new(cfg))
        .expect("first insertion cannot fail");

    // Create an endpoint using the custom map
    let endpoint = Endpoint::builder()
        .relay_mode(RelayMode::Custom(relay_map))
        .bind()
        .await?;

    // Connections will now fall back to your custom relay when direct paths fail
    Ok(())
}

```

### Loading from TOML Configuration

For production deployments, you may want to load relay settings from a configuration file. The repository ships with an [`example.config.toml`](https://github.com/n0-computer/iroh/blob/main/example.config.toml) demonstrating the expected TOML structure:

```toml
[[relays]]
url = "https://my.relay.example.com"

```

Parse this configuration and build the `RelayMap` programmatically:

```rust
use iroh::{Endpoint, RelayMode};
use iroh_base::RelayUrl;
use iroh_relay::{RelayConfig, RelayMap, quic::QuicConfig};
use std::{fs, sync::Arc};

#[derive(serde::Deserialize)]
struct Config {
    relays: Vec<RelayEntry>,
}

#[derive(serde::Deserialize)]
struct RelayEntry {
    url: String,
}

fn build_map_from_toml(toml_str: &str) -> anyhow::Result<RelayMap> {
    let cfg: Config = toml::from_str(toml_str)?;
    let mut map = RelayMap::empty();

    for entry in cfg.relays {
        let url = entry.url.parse::<RelayUrl>()?;
        let quic = QuicConfig::default();
        let relay_cfg = RelayConfig::new(url.clone(), quic);
        map = map.insert(url, Arc::new(relay_cfg))
            .expect("first insertion cannot fail");
    }
    Ok(map)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let toml = fs::read_to_string("example.config.toml")?;
    let relay_map = build_map_from_toml(&toml)?;

    let endpoint = Endpoint::builder()
        .relay_mode(RelayMode::Custom(relay_map))
        .bind()
        .await?;
    Ok(())
}

```

### Local Test Relay Setup

For integration testing, you can spawn an in-process relay server bound to a local interface. This pattern mirrors the test harness found in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs):

```rust
use iroh::{Endpoint, RelayMode};
use iroh_base::RelayUrl;
use iroh_relay::{RelayConfig, RelayMap, quic::QuicConfig, server::Server};
use std::sync::Arc;

async fn run_local_relay() -> anyhow::Result<(RelayMap, Server)> {
    // Bind to an arbitrary port on the local interface
    let bind_ip = ([0, 0, 0, 0], 0);
    let mut config = iroh_relay::RelayServerConfig::new(bind_ip);
    // Use a self-signed cert for testing (generated automatically)
    config.tls = None;

    // Start the server (spawns a QUIC listener internally)
    let server = iroh_relay::Server::new(config).await?;
    let url: RelayUrl = format!("https://{}", server.listen_addr()).parse()?;

    // Build a RelayConfig pointing at the just-started server
    let quic = QuicConfig::default();
    let relay_cfg = RelayConfig::new(url.clone(), quic);
    let map = RelayMap::empty()
        .insert(url, Arc::new(relay_cfg))
        .expect("first insertion cannot fail");
    
    Ok((map, server))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let (relay_map, _server) = run_local_relay().await?;

    let endpoint = Endpoint::builder()
        .relay_mode(RelayMode::Custom(relay_map))
        .bind()
        .await?;

    // The endpoint will now use the local relay for any relayed traffic
    Ok(())
}

```

## Why Use Custom Relay Servers?

Configuring custom relays addresses specific deployment scenarios that the default public infrastructure cannot satisfy:

- **Private Networks** – Corporate or campus environments may require keeping traffic within internal infrastructure rather than exposing connections to public relays.
- **Testing and CI** – The test harness pattern in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) demonstrates how to spawn ephemeral relays for isolated integration tests.
- **Performance and Geography** – Running relays close to your user base reduces latency compared to globally distributed public relays, particularly for regions with limited connectivity.

## Summary

- **RelayUrl** validates and stores your relay address, implemented in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs).
- **RelayConfig** defines the server-side settings for a single relay, implemented in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs).
- **RelayMap** stores multiple relay configurations in a thread-safe map, implemented in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs).
- **RelayMode::Custom** instructs the `Endpoint` to use your specific relay map instead of the default public set, processed in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- Create the map by chaining `RelayMap::empty().insert(url, Arc::new(config))`, then pass it to `Endpoint::builder().relay_mode(RelayMode::Custom(map))`.

## Frequently Asked Questions

### What is the difference between RelayMode::Default and RelayMode::Custom?

**`RelayMode::Default`** uses the public relay infrastructure maintained by the Iroh project, which requires no configuration and works out of the box. **`RelayMode::Custom`** accepts a `RelayMap` parameter that you populate with your own `RelayUrl` and `RelayConfig` pairs, directing the endpoint to use your private infrastructure instead of or in addition to the public relays.

### How do I parse a RelayUrl from a string?

Use the standard library's `parse` method with the `RelayUrl` type from `iroh_base`. The implementation in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs) wraps `url::Url` and provides validation. For example: `"https://relay.example.com".parse::<RelayUrl>()?`.

### Can I configure multiple custom relay servers in a single RelayMap?

Yes. The `RelayMap` type is designed to hold multiple entries. After calling `RelayMap::empty()`, repeatedly call `.insert(url, Arc::new(config))` for each relay server you want to configure. The endpoint will use this entire set when attempting to establish relayed connections.

### How do I run a local relay server for integration testing?

Spawn an in-process server using `iroh_relay::Server::new()` with a `RelayServerConfig` bound to localhost, as demonstrated in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs). Generate a `RelayUrl` from the server's listen address, create a `RelayConfig` pointing to it, and insert both into a `RelayMap` passed to your test endpoint. This pattern ensures completely isolated relay behavior for CI pipelines.