What Is the Role of Relay Servers in Iroh?

Relay servers in Iroh serve as encrypted fallback transports that forward QUIC datagrams between peers when direct hole-punching through NATs fails, ensuring universal connectivity without compromising end-to-end encryption.

Iroh implements a "dial-by-public-key" architecture where endpoints first attempt to establish direct peer-to-peer connections. When symmetric NATs, firewall rules, or UDP restrictions block direct paths, the role of relay servers in Iroh becomes critical as they provide a resilient WebSocket-based tunnel that preserves the protocol's zero-trust security model.

Why Relay Servers Matter in Iroh's Network Stack

Iroh endpoints prioritize direct QUIC connections using NAT hole-punching. However, certain network conditions—such as symmetric NATs, strict corporate firewalls, or mobile carrier restrictions—prevent direct UDP connectivity. In these scenarios, Iroh falls back to a relay server, which acts as a publicly reachable HTTP/WebSocket proxy that forwards encrypted traffic between peers.

According to the n0-computer/iroh source code, the relay is not a traditional VPN or proxy; it operates at the datagram level, forwarding ClientToRelayMsg and RelayToClientMsg packets without access to plaintext payloads.

Core Responsibilities of Relay Servers

The implementation in iroh-relay/src/server.rs defines several critical duties that ensure reliable connectivity across diverse network conditions.

Fallback Transport via WebSocket

When direct QUIC establishment fails, clients open a WebSocket connection to the relay server. The relay then forwards encrypted datagrams between the two peers using the ClientToRelayMsg and RelayToClientMsg protocols. This fallback is transparent to the application layer, which continues to use QUIC semantics.

The HTTP service infrastructure is established in iroh-relay/src/server.rs (beginning at line 12), with request handlers such as root_handler, probe_handler, and serve_no_content_handler wired into the server routing table.

QUIC Address Discovery (QAD)

Beyond simple relaying, the server performs QUIC Address Discovery (QAD) to enable fast-path optimization. After the initial WebSocket exchange, the relay advertises a dedicated QUIC endpoint that clients can use for subsequent connections, reducing latency for future sessions.

The QAD service spawns from the Server::spawn function (lines 441-456 in iroh-relay/src/server.rs), creating a QuicServer instance that runs alongside the WebSocket relay.

Authentication and Access Control

Each client presents an EndpointId (derived from its public key) and optionally an authentication token. The relay implements the AccessControl trait to enforce connection policies:

  • Default behavior: The AllowAll implementation (lines 336-342 in iroh-relay/src/server.rs) permits all connections for open relays.
  • Custom policies: Implement the AccessControl trait to create blocklists, rate limiting, or token-based authentication.

The RelayConfig::access field (line 145) holds an Arc<dyn DynAccessControl>, allowing runtime configuration of access policies.

Health Monitoring and Metrics

Production relay deployments expose standard endpoints for operational monitoring:

  • /healthz for health checks
  • /robots.txt and /generate_204 for captive portal detection
  • Prometheus-compatible metrics endpoints

These handlers are defined in iroh-relay/src/server.rs (lines 86-115) and help mobile operators and network administrators detect and manage relay traffic.

Client Key Caching

High-load relays utilize a memory cache for client public keys to reduce allocation overhead. The default capacity is defined in iroh-relay/src/defaults.rs (line 18) as DEFAULT_KEY_CACHE_CAPACITY, set to 56 MiB to support approximately 1 million concurrent clients.

How the Relay Map Works

Clients do not hardcode single relay addresses. Instead, they use a relay map (iroh-relay/src/relay_map.rs), a client-side data structure containing one or more RelayConfig entries. This map allows endpoints to:

  • Try multiple relays in sequence
  • Attach common authentication tokens across all entries
  • Update relay URLs at runtime without recompilation

The map is constructed from RelayUrl instances (e.g., https://relay.iroh.link) and integrated into the client builder before establishing connections.

Practical Implementation: Code Examples

Starting a Local Relay Server for Testing

The Server::spawn function (lines 891-950 in iroh-relay/src/server.rs) provides a programmatic way to launch relays for integration testing:

use iroh_relay::{Server, ServerConfig, RelayConfig};
use std::net::Ipv4Addr;

// Create a simple relay that listens on a random local port
let relay_cfg = RelayConfig::new((Ipv4Addr::LOCALHOST, 0));
let server = Server::spawn(ServerConfig {
    relay: Some(relay_cfg),
    quic: None,
    metrics_addr: None,
}).await.unwrap();

// Obtain the HTTP address (e.g., `http://127.0.0.1:12345`)
let http_url = format!("http://{}", server.http_addr().unwrap());
println!("Relay listening at {http_url}");

Configuring a Client to Use Public Relays

Clients use RelayMap to discover and configure relay endpoints. The RelayMap::with_auth_token method (lines 148-159 in iroh-relay/src/relay_map.rs) attaches authentication credentials:

use iroh_base::RelayUrl;
use iroh_relay::RelayMap;
use iroh::client::ClientBuilder;

// Parse a public relay URL
let relay_url: RelayUrl = "https://relay.iroh.link".parse().unwrap();

// Build a RelayMap with a single entry
let relay_map = RelayMap::from_iter(vec![relay_url.clone()]);

// Attach authentication if required
let relay_map = relay_map.with_auth_token("my-secret-token");

// Construct the client with relay fallback enabled
let client = ClientBuilder::new(relay_map, secret_key, dns_resolver())
    .connect()
    .await?;

Implementing Custom Access Control

Create custom access policies by implementing the AccessControl trait (lines 78-96 in iroh-relay/src/server.rs):

use std::sync::Arc;
use iroh_relay::{Access, AccessControl, ClientRequest};

struct BlocklistAccessControl {
    blocked: EndpointId,
}

#[async_trait::async_trait]
impl AccessControl for BlocklistAccessControl {
    async fn on_connect(&self, request: &ClientRequest) -> Access {
        if request.endpoint_id() == self.blocked {
            Access::Deny {
                reason: Some("blocked endpoint".into()),
            }
        } else {
            Access::Allow
        }
    }
}

// Apply the custom policy when building the relay
let access = Arc::new(BlocklistAccessControl { blocked: some_endpoint_id });
let relay_cfg = RelayConfig::new((std::net::Ipv4Addr::LOCALHOST, 0))
    .with_access(access);

Summary

  • Relay servers in Iroh provide encrypted fallback connectivity when direct hole-punching fails, using WebSocket tunnels that preserve end-to-end encryption.
  • The relay server implementation in iroh-relay/src/server.rs handles WebSocket transport, QUIC Address Discovery (QAD), authentication, and health monitoring.
  • Access control is configurable via the AccessControl trait, with AllowAll as the default permissive implementation.
  • Clients use a relay map (iroh-relay/src/relay_map.rs) to manage multiple relay endpoints and authentication tokens.
  • Production relays utilize key caching (56 MiB default) to efficiently handle millions of concurrent clients.

Frequently Asked Questions

Does the relay server decrypt my traffic?

No. The relay server forwards encrypted QUIC datagrams between peers using the ClientToRelayMsg and RelayToClientMsg protocols. It never possesses the private keys necessary to decrypt payloads, maintaining Iroh's end-to-end encryption guarantees even when traffic routes through public relays.

How does a client choose which relay server to use?

Clients consult the relay map (iroh-relay/src/relay_map.rs), which contains one or more RelayConfig entries. The client attempts connections to relays in sequence until it establishes a successful WebSocket tunnel. This map can be configured at runtime with multiple URLs for high availability.

Can I run my own private relay server?

Yes. You can spawn a custom relay using Server::spawn from iroh-relay/src/server.rs with a custom RelayConfig and AccessControl implementation. This allows you to deploy private relays behind corporate firewalls or in specific geographic regions while maintaining compatibility with the Iroh protocol.

What is the difference between relay mode and direct QUIC mode?

Direct QUIC mode establishes a peer-to-peer UDP connection between endpoints, providing optimal latency and throughput. Relay mode activates only when direct hole-punching fails, tunneling encrypted datagrams through a WebSocket connection to a publicly reachable relay. The application layer remains unaware of which transport mode is active, as Iroh handles the fallback automatically.

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 →