How to Implement Path Selection Strategies in Iroh: A Complete Guide to Custom Network Path Selection

Iroh enables custom path selection strategies through the PathSelector trait, which allows you to implement network path selection logic by processing candidate paths via PathSelectionContext and returning a PathSelection to determine which QUIC path carries application traffic.

The n0-computer/iroh library provides a pluggable architecture for network path selection that determines which QUIC path carries your application traffic. By implementing path selection strategies in iroh, you can customize whether connections prioritize low latency, specific transport types, or custom policies. The selector integrates directly into the endpoint lifecycle, evaluating candidates on every path change.

The Path Selection Architecture

The PathSelector Trait Contract

The foundation of path selection in iroh rests on the PathSelector trait defined in iroh/src/socket/remote_map/remote_state.rs. This trait defines a single required method:

fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection;

Implementers receive a selection context containing the current active path and an iterator over candidate paths. The method must return a PathSelection indicating which path to use. Create a new selection with PathSelection::none(), then call selection.set(&candidate) to activate a specific path.

Selection Context and Candidate Data

The PathSelectionContext struct bundles the currently selected transport as an Option<&FourTuple> together with live connection state. Calling ctx.paths() yields PathSelectionData instances for every open QUIC path.

Each PathSelectionData provides access to the network path and statistics via the stats() method, which returns PathStats containing live RTT measurements, loss rates, and other transport metrics. Check for None if the path closed concurrently.

The Default Implementation: Biased RTT Selection

Iroh ships with BiasedRttPathSelector in iroh/src/socket/biased_rtt_path_selector.rs, which implements a sophisticated default strategy combining transport preferences with latency measurements.

Transport Type Bias

The selector classifies each AddrKind (IPv4, IPv6, Relay, or custom) as either Primary or Backup. Primary transports are preferred over backups. By default, only the Relay transport serves as backup, while IPv4 and IPv6 are primary.

RTT Bias and Switching Logic

The implementation adds a configurable nanosecond bias (rtt_bias) to measured RTT values. IPv6 receives a small default advantage of approximately 3 milliseconds (IPV6_RTT_ADVANTAGE).

The selection algorithm sorts paths by (transport_type, biased_rtt). When the best candidate belongs to a different transport tier, the selector switches immediately. Within the same tier, switching occurs only if the candidate's biased RTT exceeds RTT_SWITCHING_MIN (approximately 5 milliseconds) better than the current path, preventing flapping under network jitter.

Implementing a Custom Path Selector

To create a custom path selection strategy in iroh, implement the PathSelector trait and wire it into your endpoint.

Example: Lowest-RTT Selector

This implementation ignores transport tiers and always selects the path with the smallest raw RTT:

use iroh::socket::remote_map::{
    PathSelection, PathSelectionContext, PathSelectionData, PathSelector,
};
use std::sync::Arc;

/// A selector that always picks the path with the smallest raw RTT,
/// regardless of whether it is primary or backup.
#[derive(Debug, Default)]
struct LowestRttSelector;

impl PathSelector for LowestRttSelector {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        let mut best: Option<(&PathSelectionData<'_>, i128)> = None;

        for psd in ctx.paths() {
            // If stats are unavailable (e.g. the path closed concurrently) skip it.
            let Some(stats) = psd.stats() else { continue };
            let rtt = stats.rtt.as_nanos() as i128;

            if best.as_ref().map_or(true, |(_, cur_rtt)| rtt < *cur_rtt) {
                best = Some((psd, rtt));
            }
        }

        let mut selection = PathSelection::none();
        if let Some((best_psd, _)) = best {
            selection.set(best_psd);
        }
        selection
    }
}

Example: Custom Transport Bias

When using the unstable-custom-transports feature, you can assign custom biases to new transport types:

use iroh::socket::{
    biased_rtt_path_selector::{BiasedRttPathSelector, TransportBias},
    transports::AddrKind,
};
use std::time::Duration;
use std::sync::Arc;

// Give the new transport a 10 ms RTT advantage.
let selector = BiasedRttPathSelector::default()
    .with_bias(AddrKind::Custom, TransportBias::primary().with_rtt_advantage(Duration::from_millis(10)));

let endpoint = iroh::Endpoint::builder()
    .path_selector(Arc::new(selector))
    .bind()
    .await?;

Wiring Your Selector to the Endpoint

The selector lives as an Arc<dyn PathSelector> inside the Socket struct (iroh/src/socket.rs). By default, the endpoint builder creates Arc::new(BiasedRttPathSelector::default()) as seen in iroh/src/endpoint.rs.

To use a custom selector, call the path_selector method on the endpoint builder before binding:

let endpoint = iroh::Endpoint::builder()
    .path_selector(Arc::new(LowestRttSelector::default()))
    .bind()
    .await?;

Summary

  • Path selection in iroh is governed by the PathSelector trait defined in iroh/src/socket/remote_map/remote_state.rs.
  • Implement the select method to receive PathSelectionContext and return PathSelection to control which QUIC path carries traffic.
  • Default behavior uses BiasedRttPathSelector in iroh/src/socket/biased_rtt_path_selector.rs, which prioritizes primary transports and uses RTT bias with a 5ms switching threshold.
  • Access candidate statistics through PathSelectionData.stats(), returning PathStats with live RTT and loss data.
  • Plug your implementation into the endpoint via Endpoint::builder().path_selector(Arc::new(your_selector)) before calling bind().

Frequently Asked Questions

What is the PathSelector trait in iroh?

The PathSelector trait defines the contract for path selection strategies in iroh. It requires a single select method that receives a PathSelectionContext reference and returns a PathSelection indicating which network path should carry application traffic. The trait is defined in iroh/src/socket/remote_map/remote_state.rs and enables pluggable path selection logic.

How does the default BiasedRttPathSelector work?

BiasedRttPathSelector implements a two-tier strategy that classifies transports as Primary or Backup, preferring primary paths. It adds configurable RTT biases (giving IPv6 a ~3ms advantage by default) and sorts paths by transport tier then biased latency. It switches immediately between tiers, but requires a ~5ms improvement (RTT_SWITCHING_MIN) within the same tier to prevent flapping.

Can I implement custom transport biases?

Yes, when using the unstable-custom-transports feature, you can configure BiasedRttPathSelector with custom biases for new AddrKind variants using with_bias(). Alternatively, implement a completely custom PathSelector that applies your own policy logic to the PathSelectionData and PathStats available in the selection context.

Where is path selection evaluated in the iroh codebase?

Path selection is evaluated in the socket layer, specifically within iroh/src/socket.rs where the selector is stored as Arc<dyn PathSelector>. The selection logic triggers when path conditions change, using the PathSelectionContext constructed in iroh/src/socket/remote_map/remote_state.rs to provide candidates and current state to your selector's select method.

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 →