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 pathctx.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:
iroh/src/socket/remote_map/remote_state.rs(line 1419) – Contains thePathSelectortrait definitioniroh/src/socket/biased_rtt_path_selector.rs– Default implementation ranking paths by transport tier and biased RTTiroh/src/endpoint.rs(lines 818-840) – ProvidesEndpointBuilder::path_selectorfor registrationiroh/src/socket.rs(lines 2141, 2182, 2598) – Shows where the default selector is installed in the socket implementationiroh/examples/custom-transport.rs– Working example of a custom transport with path selection
Summary
- Implement the
PathSelectortrait fromiroh/src/socket/remote_map/remote_state.rsto define custom path selection logic - Use
PathSelectionContextto access the current path and iterate over candidates viactx.paths() - Return
PathSelection::none()to retain the current path, or callset()on a candidate to switch - Register your selector with
EndpointBuilder::path_selectorusing anArc<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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →