How to Configure Custom Relay Servers in Iroh: A Complete Guide

To configure custom relay servers in Iroh, construct a RelayMap containing your RelayUrl and RelayConfig pairs, then pass it to Endpoint::builder().relay_mode(RelayMode::Custom(map)) before calling bind().

Iroh uses relay servers as fallback infrastructure to forward QUIC traffic when direct hole-punching between peers fails. By default, the library connects to the public relay fleet operated by the project, but you can configure custom relay servers in Iroh to route traffic through private infrastructure, reduce latency with geographically close nodes, or isolate test environments. This implementation requires coordinating four core types across the iroh-base, iroh-relay, and iroh crates.

Core Components for Custom Relay Configuration

Four primary types work together to enable custom relay configuration, each defined in specific source files within the n0-computer/iroh repository:

  • RelayUrl (iroh-base/src/relay_url.rs) – A thin wrapper around url::Url that validates relay addresses.
  • RelayConfig (iroh-relay/src/server.rs) – Holds the HTTP/QUIC bind address, TLS configuration, and access control settings for a single relay instance.
  • RelayMap (iroh-relay/src/relay_map.rs) – A thread-safe map from RelayUrl to Arc<RelayConfig> that the endpoint consults to locate available relays.
  • RelayMode (iroh/src/endpoint.rs) – An enum consumed by the EndpointBuilder; use RelayMode::Default for public relays or RelayMode::Custom with your map for private infrastructure.

Step-by-Step Configuration Process

Parse the Relay URL

First, create a RelayUrl from a string using the standard FromStr implementation located in iroh-base/src/relay_url.rs:

use iroh_base::RelayUrl;

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

This validates the URL format and scheme requirements specific to Iroh relay protocols.

Create a RelayConfig

Next, instantiate a RelayConfig by pairing the URL with a QuicConfig (defined in iroh-relay/src/server.rs). The QUIC configuration controls transport settings for the relay client connection:

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

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

RelayConfig::new binds an internal HTTP listener and prepares the QUIC server parameters according to the implementation in iroh-relay/src/server.rs.

Build the RelayMap

Insert the configuration into a RelayMap using the functional insert method from iroh-relay/src/relay_map.rs. This returns a new map instance rather than mutating in place:

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

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

The map uses Arc<RelayConfig> internally to enable thread-safe sharing across async tasks.

Configure the Endpoint

Finally, pass the map to the endpoint builder via RelayMode::Custom. The conversion from RelayMode to internal transport configuration occurs in iroh/src/endpoint.rs (around line 1920 in the match logic):

use iroh::{Endpoint, RelayMode};

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

Once bound, the endpoint will consult your custom map when fallback relay connections are required.

Practical Implementation Examples

Minimal Custom Relay Setup

This complete example demonstrates wiring a single custom relay into an Iroh 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<()> {
    // Parse the relay URL
    let url = "https://my.relay.example.com".parse::<RelayUrl>()?;
    
    // Build configuration with default QUIC settings
    let quic = QuicConfig::default();
    let cfg = RelayConfig::new(url.clone(), quic);
    
    // Insert into the map
    let relay_map = RelayMap::empty()
        .insert(url, Arc::new(cfg))
        .expect("first insertion cannot fail");
    
    // Create endpoint with custom relay mode
    let endpoint = Endpoint::builder()
        .relay_mode(RelayMode::Custom(relay_map))
        .bind()
        .await?;
    
    Ok(())
}

Loading Relays from TOML Configuration

For production deployments, load relay URLs from a configuration file and build the map programmatically:

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

Mirror the test harness pattern from iroh/tests/patchbay/util.rs to spawn an in-process relay for integration testing:

use iroh::{Endpoint, RelayMode};
use iroh_base::RelayUrl;
use iroh_relay::{RelayConfig, RelayMap, quic::QuicConfig, server::{Server, RelayServerConfig}};
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 = RelayServerConfig::new(bind_ip);
    // Use auto-generated self-signed cert for testing
    config.tls = None;

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

    // Build RelayConfig pointing at the local server
    let relay_cfg = RelayConfig::new(url.clone(), QuicConfig::default());
    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?;
    
    Ok(())
}

This pattern allows tests to run without external network dependencies.

Why Configure Custom Relay Servers?

Private networks – Corporate or campus deployments often require traffic to remain within internal infrastructure rather than traversing public relays.

Testing and CI – The lab_with_relay pattern in iroh/tests/patchbay/util.rs demonstrates how to spin up temporary relays bound to [::]:80, enabling hermetic integration tests.

Performance optimization – Deploying relays geographically close to your user base reduces latency compared to the globally distributed default relay fleet.

Summary

  • Custom relay configuration requires building a RelayMap containing RelayUrl → Arc<RelayConfig> mappings.
  • Use RelayMode::Custom when constructing the Endpoint via Endpoint::builder().relay_mode() to override the default public relay set.
  • Key source files include iroh-base/src/relay_url.rs for URL parsing, iroh-relay/src/server.rs for configuration structs, and iroh/src/endpoint.rs for the relay mode integration.
  • Thread safety is handled automatically via Arc<RelayConfig> inside the RelayMap implementation (iroh-relay/src/relay_map.rs).
  • Local testing can mirror the harness in iroh/tests/patchbay/util.rs by spawning in-process relay servers with auto-generated TLS certificates.

Frequently Asked Questions

What is the default relay mode in Iroh?

The default mode is RelayMode::Default, which automatically connects to the public relay infrastructure operated by the Iroh project. To use custom infrastructure, switch to RelayMode::Custom and provide your own RelayMap as shown in the configuration examples above.

Can I run a relay server without TLS for local development?

Yes. Set config.tls = None in your RelayServerConfig (defined in iroh-relay/src/server.rs) when creating the server. The library will typically auto-generate self-signed certificates for local QUIC development, or you can disable TLS entirely for testing purposes as demonstrated in the local test relay example.

How does RelayMap handle concurrent access?

RelayMap is designed to be thread-safe and uses Arc<RelayConfig> internally to share configuration data across async tasks without cloning the underlying configuration struct. This implementation resides in iroh-relay/src/relay_map.rs and allows safe concurrent reads from multiple connection attempts.

What is the difference between RelayConfig and RelayServerConfig?

RelayConfig (used in iroh-relay/src/server.rs) configures a client-side view of a relay—telling the Iroh endpoint how to connect to a specific relay. RelayServerConfig configures the actual relay server process itself, including the HTTP bind address, TLS certificates, and access controls when running your own relay infrastructure.

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 →