How to Configure Custom Relay Servers for Iroh
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 aroundurl::Urlrepresenting the relay address, defined iniroh-base/src/relay_url.rs.RelayConfig– Holds the HTTP/QUIC bind address and TLS settings for a single relay, defined iniroh-relay/src/server.rs.RelayMap– A thread-safe map fromRelayUrltoArc<RelayConfig>, defined iniroh-relay/src/relay_map.rs.RelayMode– An enum used byEndpointBuilderto decide which relays to use, defined iniroh/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 provides a FromStr implementation that validates the URL format.
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 builds a configuration that sets up the internal HTTP listener and QUIC server.
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 adds your configuration.
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 around line 1920 within the RelayMode match logic.
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:
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 demonstrating the expected TOML structure:
[[relays]]
url = "https://my.relay.example.com"
Parse this configuration and build the RelayMap 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(())
}
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:
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.rsdemonstrates 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. - RelayConfig defines the server-side settings for a single relay, implemented in
iroh-relay/src/server.rs. - RelayMap stores multiple relay configurations in a thread-safe map, implemented in
iroh-relay/src/relay_map.rs. - RelayMode::Custom instructs the
Endpointto use your specific relay map instead of the default public set, processed iniroh/src/endpoint.rs. - Create the map by chaining
RelayMap::empty().insert(url, Arc::new(config)), then pass it toEndpoint::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 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. 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →