Understanding iroh's Path Selection and RelayOnly Mode for Network Debugging

The iroh networking stack delegates transport selection to a pluggable PathSelector trait, and RelayOnly mode is implemented by providing a custom selector that always returns the relay address, forcing all traffic through the relay for protocol debugging.

The iroh crate provides a robust networking layer that automatically selects between direct UDP paths and relay connections based on real-time conditions. Understanding how the Socket component chooses transport paths is essential for debugging connectivity issues and verifying relay protocol behavior. By implementing a custom PathSelector, developers can force RelayOnly mode to isolate and test relay functionality without modifying the underlying protocol stack.

How Path Selection Works in iroh

The iroh::Socket struct delegates all routing decisions to a PathSelector implementation stored in an Arc<dyn PathSelector>. This design allows the socket to remain agnostic about transport details while the selector evaluates available paths and returns the optimal one for each packet.

The Default Selector

By default, iroh uses the biased_rtt_path_selector located in iroh/src/socket/biased_rtt_path_selector.rs. This implementation ranks available paths by Round-Trip Time (RTT), strongly preferring direct UDP addresses while maintaining relay connections as a fallback. When direct paths exhibit high latency or become unavailable, the selector automatically switches to relay transport without application intervention.

Selector Integration

The Socket stores the selector in a field defined in iroh/src/socket.rs (lines 90-92):

pub struct Socket {
    path_selector: Arc<dyn PathSelector>,
    // ...
}

When constructing a socket via Endpoint::builder(), you can inject a custom selector through the Options struct (lines 60-64 in iroh/src/socket.rs). The actor inside the socket consults this selector on every transmission, receiving a transports::Addr that determines whether to send via UDP or through the relay's WebSocket connection.

What is RelayOnly Mode

RelayOnly mode is not a boolean flag but a configuration state achieved by replacing the default selector with one that exclusively returns relay addresses. According to the source code comments in iroh/src/socket.rs (lines 4-15), this mode exists to "force all packets to be sent over the relay connection, regardless of whether we have a direct UDP address for the given endpoint."

Debugging Applications

This mode serves a specific diagnostic purpose: it allows developers to test the relay protocol implementation within the Socket to ensure reliability when direct UDP connections fail. When active, the entire iroh stack—including hole-punching, address lookup, and keep-alive mechanisms—continues operating normally, but the selector suppresses all direct paths, guaranteeing every packet traverses the relay infrastructure.

Implementing a Custom RelayOnly Selector

To enable RelayOnly mode, implement the PathSelector trait to always return a relay address. Below is a complete example that creates an endpoint using this pattern:

use std::{sync::Arc, net::SocketAddr};
use iroh::{
    socket::{self, PathSelector},
    base::RelayUrl,
    Endpoint,
};

// ---------------------------------------------------------------------
// 1️⃣  A selector that **only** returns the relay address.
// ---------------------------------------------------------------------
struct RelayOnlySelector {
    relay: RelayUrl,
}

impl PathSelector for RelayOnlySelector {
    /// Return the single relay we were given, ignoring any direct address.
    fn select<'a>(
        &'a self,
        _direct_addrs: &'a [socket::transports::Addr],
        _custom_addrs: &'a [socket::transports::Addr],
    ) -> Option<socket::transports::Addr> {
        Some(socket::transports::Addr::Relay(self.relay.clone(), None))
    }
}

// ---------------------------------------------------------------------
// 2️⃣  Build the endpoint, injecting the selector.
// ---------------------------------------------------------------------
async fn make_endpoint(relay_url: RelayUrl) -> anyhow::Result<Endpoint> {
    // Default options (secret key, transport config, …) are created via
    // `Endpoint::builder()`.  We replace the default selector with our own.
    let selector = Arc::new(RelayOnlySelector { relay: relay_url.clone() });

    let endpoint = Endpoint::builder()
        .path_selector(selector)            // <-- custom selector
        .relay(relay_url)                   // configure the relay transport
        .build()
        .await?;

    Ok(endpoint)
}

Key implementation details:

  • The RelayOnlySelector implements the PathSelector trait's select method, discarding any direct or custom addresses passed as parameters.
  • It returns transports::Addr::Relay, defined in iroh/src/socket/transports.rs, which encapsulates the relay URL.
  • The builder passes this selector into Socket::Options, where it replaces the default biased_rtt_path_selector.

Debugging Workflow with RelayOnly Mode

Use this structured approach to verify relay protocol behavior:

  1. Configure both peers with the custom selector. When running the above code on two endpoints, both will report "home relay …" status but never advertise direct addresses to each other.

  2. Capture WebSocket traffic. Use tools like tcpdump -i any port 443 or Wireshark to observe the TLS-encapsulated QUIC frames. Only relay-mediated traffic will appear, confirming no direct UDP leakage.

  3. Inspect relay logs. Enable INFO level logging on the iroh-relay binary to observe the full round-trip of PING/PONG frames, connection handshakes, and any retransmissions.

  4. Compare behaviors. Switch back to the default biased_rtt_path_selector and observe how the socket attempts direct UDP paths before falling back to the relay, validating your relay-only test results.

Key Source Files for Path Selection

Understanding these files is essential for customizing transport behavior:

Summary

  • iroh uses a pluggable PathSelector trait to decouple transport selection from packet transmission logic.
  • The default biased_rtt_path_selector prefers direct UDP paths with relay fallback.
  • RelayOnly mode is achieved by implementing a custom selector that always returns transports::Addr::Relay, as documented in iroh/src/socket.rs.
  • This mode preserves the entire iroh networking stack while forcing all traffic through relay infrastructure, making it ideal for protocol debugging and CI verification.

Frequently Asked Questions

How does iroh's default path selector choose between direct and relay connections?

The default biased_rtt_path_selector ranks candidate paths by measured Round-Trip Time (RTT), strongly preferring direct UDP addresses over relay connections. It maintains both path types simultaneously but selects the direct address unless latency becomes excessive or the direct path becomes unreachable, at which point it falls back to the relay.

What is the purpose of RelayOnly mode in iroh?

RelayOnly mode exists to force all packet transmission through the relay infrastructure regardless of direct connectivity availability. According to the source code comments in iroh/src/socket.rs, this is intended for testing the relay protocol implementation to ensure reliability when two endpoints cannot establish direct UDP connections.

How do I implement a custom PathSelector in iroh?

Implement the PathSelector trait by defining the select method, which receives slices of direct and custom addresses and returns an Option<transports::Addr>. Pass your implementation wrapped in Arc::new() to Endpoint::builder().path_selector() before calling build(). The selector runs inside the socket actor and is consulted for every outgoing datagram.

Which source files should I examine to understand iroh's transport selection?

Start with iroh/src/socket.rs for the selector integration and iroh/src/socket/biased_rtt_path_selector.rs for the default algorithm. Examine iroh/src/socket/transports.rs for the address types selectors return, and iroh/src/socket/remote_map.rs for how the socket maps between logical and physical addresses.

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 →