How to Implement Custom Path Selection in iroh: A Complete Guide

To implement custom path selection in iroh, create a type that implements the PathSelector trait and register it with EndpointBuilder::path_selector using an Arc<dyn PathSelector>.

The iroh networking library uses a pluggable PathSelector trait to determine which network path (IPv4, IPv6, relay, or custom transport) to use for QUIC connections. While the default BiasedRttPathSelector ranks paths by latency and transport tier, you can implement your own strategy to prioritize specific networks, enforce application-level constraints, or integrate custom metrics.

Understanding the PathSelector Trait

The PathSelector trait defines the contract for path selection logic in iroh. Located in iroh/src/socket/remote_map/remote_state.rs at line 1419, this trait requires implementing a single method that evaluates available paths and returns your selection.

Trait Definition and Required Method

The trait definition requires:

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

This method receives a PathSelectionContext containing the currently selected path and all candidate paths, and must return a PathSelection indicating which path to use.

PathSelectionContext and PathSelectionData

The context provides two essential methods:

  • ctx.current() – Returns the currently active path
  • ctx.paths() – Returns an iterator over PathSelectionData for all candidate paths

Each PathSelectionData exposes:

  • network_path().remote() – The underlying FourTuple (remote address)
  • Optional PathStats including RTT, loss rates, and other metrics

Implementing a Custom PathSelector

To create a custom selector, define a struct and implement the PathSelector trait. The implementation should iterate through available paths using ctx.paths(), evaluate each path based on your criteria, and return a PathSelection containing your chosen path.

Core Implementation Pattern

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

struct MyCustomSelector;

impl PathSelector for MyCustomSelector {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        let mut sel = PathSelection::none();
        
        // Your selection logic here
        for psd in ctx.paths() {
            let remote = psd.network_path().remote();
            // Evaluate path...
            if should_select_this_path(remote) {
                sel.set(&psd);
                return sel;
            }
        }
        
        sel // Return empty to keep current path
    }
}

Registering Your Selector with the Endpoint

After implementing the trait, register your selector using the endpoint builder API in iroh/src/endpoint.rs (lines 818-840). The EndpointBuilder::path_selector method accepts an Arc<dyn PathSelector>:

let endpoint = iroh::Endpoint::builder()
    .path_selector(Arc::new(MyCustomSelector))
    .build()?;

The Arc allows the selector to be shared safely across threads, and the endpoint will use your logic for all subsequent path selections.

Example: Preferring Custom Transports

Here is a complete implementation that prioritizes a custom transport named TestTransport, falling back to the default BiasedRttPathSelector when unavailable:

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

struct PreferTestTransport;

impl PathSelector for PreferTestTransport {
    fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
        // First pass: look for custom transport paths
        // Addr::Other indicates user-defined transports
        for psd in ctx.paths() {
            let remote = psd.network_path().remote();
            if let Addr::Other(_) = remote {
                let mut sel = PathSelection::none();
                sel.set(&psd);
                return sel;
            }
        }

        // Fallback to default biased-RTT selection
        let default = iroh::socket::biased_rtt_path_selector::BiasedRttPathSelector::default();
        default.select(ctx)
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = iroh::Endpoint::builder()
        .path_selector(Arc::new(PreferTestTransport))
        .build()?;
    
    // Use endpoint for connections...
    Ok(())
}

This example checks if Addr::Other(_) is present (indicating a user-defined transport in iroh/src/socket/transports.rs) and delegates to the built-in selector otherwise.

Key Source Files and Implementation Details

Understanding these specific locations in the iroh codebase helps when implementing custom path selection:

Summary

  • Implement the PathSelector trait from iroh/src/socket/remote_map/remote_state.rs to define custom path selection logic
  • Use PathSelectionContext to access the current path and iterate over candidates via ctx.paths()
  • Return PathSelection::none() to retain the current path, or call set() on a candidate to switch
  • Register your selector with EndpointBuilder::path_selector using an Arc<dyn PathSelector>
  • Delegate to BiasedRttPathSelector::default() for fallback behavior when custom criteria are not met

Frequently Asked Questions

What is the default path selection algorithm in iroh?

The default algorithm is BiasedRttPathSelector, implemented in iroh/src/socket/biased_rtt_path_selector.rs. It ranks paths first by transport tier (primary versus backup) and then by a biased RTT calculation that gives preference to lower-latency connections.

How do I access path statistics in my custom selector?

Each PathSelectionData in the iterator returned by ctx.paths() contains optional PathStats accessible through the path's metadata. These statistics include RTT measurements and packet loss rates, allowing you to implement latency-aware or reliability-based selection criteria.

Can I combine multiple selection criteria?

Yes. You can implement multi-stage selection by evaluating paths in order of priority. For example, first check for custom transports, then filter by maximum RTT thresholds, and finally fall back to the default BiasedRttPathSelector if no paths meet your specific criteria.

What happens if my selector returns an empty PathSelection?

Returning PathSelection::none() (the default empty selection) signals the endpoint to keep the currently active path. This is useful when no better alternative exists or when you want to maintain stability rather than switching paths frequently.

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 →