How to Configure Custom Relay Servers in Iroh

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, you parse URLs using the standard FromStr trait:

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, you instantiate it using RelayConfig::new:

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. Create an empty map with RelayMap::empty() and populate it using insert:

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 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:

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:

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; RelayConfig::new in iroh-relay/src/server.rs; RelayMap::insert in iroh-relay/src/relay_map.rs; RelayMode::Custom handling in 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 demonstrating the expected structure:

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

Load and convert this configuration using serde:

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 demonstrates spawning an in-process relay server for integration testing. This pattern creates a temporary relay bound to localhost:

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 (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 pattern enables hermetic integration tests without external network dependencies.

Summary

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 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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →