# How to Configure Custom Relay Servers in Iroh

> Configure custom relay servers in Iroh by building a RelayMap and passing it to the Endpoint builder with RelayMode::Custom(). Gain control over your Iroh network connections.

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

---

**To configure custom relay servers in Iroh, construct a `RelayMap` containing your `RelayUrl` and `RelayConfig` instances, then pass it to the `Endpoint` builder using `RelayMode::Custom()`.**

Iroh uses relay servers as fallback nodes to forward QUIC traffic when direct hole-punching fails. By default, the library connects to public relays operated by the project via `RelayMode::Default`, but production deployments often require private infrastructure for security, compliance, or latency reasons. This guide demonstrates how to configure custom relay servers in Iroh using the core types defined in `iroh-base` and `iroh-relay`.

## Core Components for Custom Relay Configuration

Understanding four key types is essential before implementing custom relays. These components work together to define, store, and activate custom relay endpoints.

### RelayUrl

The `RelayUrl` type is a thin wrapper around `url::Url` that represents a relay server address. According to the implementation in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs), you parse URLs using the standard `FromStr` trait:

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

```

This validation ensures the URL uses HTTPS scheme and proper formatting before insertion into the relay map.

### RelayConfig

`RelayConfig` holds the HTTP/QUIC bind address, TLS settings, and access control for a single relay. Defined in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs), you instantiate it using `RelayConfig::new`:

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

```

The default `QuicConfig` provides sensible production defaults for the QUIC transport layer.

### RelayMap

`RelayMap` is a thread-safe map from `RelayUrl` to `Arc<RelayConfig>` implemented in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs). Create an empty map with `RelayMap::empty()` and populate it using `insert`:

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

```

The `insert` method returns a new `RelayMap` instance, enabling functional chaining patterns.

### RelayMode

The `RelayMode` enum in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) controls which relays the `Endpoint` uses. Around line 1920, the builder converts `RelayMode::Custom(map)` into the internal transport configuration. Pass your custom map to activate private relays:

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

```

## Implementing Custom Relay Configuration

The following patterns cover production deployment, configuration file loading, and local testing scenarios.

### Minimal Custom Relay Setup

This complete example shows the four-step process: parse URL, create config, build map, and configure the endpoint:

```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<()> {
    // 1. Parse the relay URL
    let url = "https://my.relay.example.com".parse::<RelayUrl>()?;

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

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

    // 4. Create endpoint with custom relay mode
    let endpoint = Endpoint::builder()
        .relay_mode(RelayMode::Custom(relay_map))
        .bind()
        .await?;

    // Connections will now use your custom relay when direct paths fail
    Ok(())
}

```

*Key source references*: `RelayUrl::parse` in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs); `RelayConfig::new` in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs); `RelayMap::insert` in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs); `RelayMode::Custom` handling in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).

### Loading Relays from TOML Configuration

For production deployments, parse relay URLs from configuration files. The repository includes an [`example.config.toml`](https://github.com/n0-computer/iroh/blob/main/example.config.toml) demonstrating the expected structure:

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

```

 Load and convert this configuration using `serde`:

```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(())
}

```

### Running a Local Test Relay

The Iroh test harness in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) demonstrates spawning an in-process relay server for integration testing. This pattern creates a temporary relay bound to localhost:

```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 arbitrary port on local interface
    let bind_ip = ([0, 0, 0, 0], 0);
    let mut config = iroh_relay::RelayServerConfig::new(bind_ip);
    // Use auto-generated self-signed cert for testing
    config.tls = None;

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

    // Build 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?;

    // Endpoint now uses the local relay for all relayed traffic
    Ok(())
}

```

*Source reference*: This implementation mirrors the `lab_with_relay` function in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) (lines 44-66), which spawns temporary relays for CI testing.

## Use Cases for Custom Relay Servers

Configuring custom relay servers addresses several production requirements:

- **Private Networks**: Corporate or campus deployments can isolate traffic from public relays, ensuring data never leaves the organizational perimeter.
- **Geographic Performance**: Running relays close to your user base reduces latency compared to the globally-distributed public defaults.
- **Compliance Requirements**: Regulated industries can maintain complete control over relay infrastructure and logging policies.
- **Testing Isolation**: The [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs) pattern enables hermetic integration tests without external network dependencies.

## Summary

- **`RelayUrl`** validates and wraps relay addresses in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs).
- **`RelayConfig`** encapsulates server settings and is instantiated via `RelayConfig::new` in [`iroh-relay/src/server.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/server.rs).
- **`RelayMap`** stores the URL-to-config mapping using thread-safe `Arc` wrappers, implemented in [`iroh-relay/src/relay_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-relay/src/relay_map.rs).
- **`RelayMode::Custom`** activates your relay map when passed to `Endpoint::builder().relay_mode()`, as processed in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- **Local testing** follows the pattern in [`iroh/tests/patchbay/util.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/tests/patchbay/util.rs), spawning temporary `Server` instances with self-signed certificates.

## Frequently Asked Questions

### How do I parse a relay URL in Iroh?

Parse relay URLs using the standard `FromStr` trait: `"https://relay.example.com".parse::<RelayUrl>()?`. The `RelayUrl` type in [`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs) validates the URL format and ensures HTTPS scheme compliance before returning the parsed struct.

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

`RelayMode::Default` connects to the public relay infrastructure operated by the Iroh project, while `RelayMode::Custom` accepts a `RelayMap` containing your private relay definitions. The endpoint builder processes this mode in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs) around line 1920, injecting your custom relays into the transport configuration.

### Can I run a relay server locally for testing?

Yes. Spawn an in-process relay 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). This approach generates self-signed certificates automatically and returns a `RelayMap` configured to point at the local instance, enabling isolated integration tests.

### How do I configure multiple custom relays?

Create multiple `RelayConfig` instances and insert them into the same `RelayMap` using chained `insert` calls. Each insertion returns a new map instance, allowing you to build a complete relay set before passing the final map to `RelayMode::Custom`. The endpoint will attempt connectivity to all configured relays according to its internal selection logic.