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

> Debug iroh networks with Path Selection and RelayOnly mode. Learn how a custom proxy selector forces traffic through the relay for easier protocol analysis.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-06-18

---

**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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) (lines 90-92):

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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:

```rust
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`](https://github.com/n0-computer/iroh/blob/main/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:

- **[`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)** – Contains the `Socket` struct, the `Options` builder, and the canonical comment defining RelayOnly mode (lines 4-15).
- **[`iroh/src/socket/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/biased_rtt_path_selector.rs)** – Reference implementation showing how to rank paths by latency and bias toward direct connections.
- **[`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs)** – Defines the `Addr` enum variants (`Ip`, `Relay`, `Custom`) that selectors must return.
- **[`iroh/src/socket/remote_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map.rs)** – Manages the mapping between synthetic mapped addresses and underlying transport endpoints, used by selectors during address translation.
- **[`iroh-base/src/relay_url.rs`](https://github.com/n0-computer/iroh/blob/main/iroh-base/src/relay_url.rs)** – Implements the `RelayUrl` type returned by selectors for relay paths.

## 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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/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`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) for the selector integration and [`iroh/src/socket/biased_rtt_path_selector.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/biased_rtt_path_selector.rs) for the default algorithm. Examine [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs) for the address types selectors return, and [`iroh/src/socket/remote_map.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/remote_map.rs) for how the socket maps between logical and physical addresses.