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 aroundurl::Urlthat 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 fromRelayUrltoArc<RelayConfig>that the endpoint consults to locate available relays.RelayMode(iroh/src/endpoint.rs) – An enum consumed by theEndpointBuilder; useRelayMode::Defaultfor public relays orRelayMode::Customwith 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
RelayMapcontainingRelayUrl→Arc<RelayConfig>mappings. - Use
RelayMode::Customwhen constructing theEndpointviaEndpoint::builder().relay_mode()to override the default public relay set. - Key source files include
iroh-base/src/relay_url.rsfor URL parsing,iroh-relay/src/server.rsfor configuration structs, andiroh/src/endpoint.rsfor the relay mode integration. - Thread safety is handled automatically via
Arc<RelayConfig>inside theRelayMapimplementation (iroh-relay/src/relay_map.rs). - Local testing can mirror the harness in
iroh/tests/patchbay/util.rsby 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →