How iroh's Path Selector Chooses Between Direct and Relay Connections
iroh uses a pluggable PathSelector interface with a default BiasedRttPathSelector implementation that sorts available network paths by transport tier and biased round-trip time to determine whether to use a direct IP connection or a relay.
The path selection mechanism in the n0-computer/iroh repository governs how nodes negotiate the optimal network path for QUIC connections. By categorizing transports as either primary (direct IPv4/IPv6) or backup (relay) and applying deterministic RTT biases, the system ensures low-latency direct connections while maintaining reliable relay fallback.
Transport Classification and Tier System
The BiasedRttPathSelector defined in iroh/src/socket/biased_rtt_path_selector.rs categorizes every candidate path into a transport tier that determines its priority regardless of measured latency. Direct IP addresses are classified as Primary transports, while relay connections are classified as Backup transports.
The default bias map constructed in BiasedRttPathSelector::default (lines 96-103) assigns the following characteristics:
- IPv4 (
AddrKind::IpV4): Primary transport with no RTT bias - IPv6 (
AddrKind::IpV6): Primary transport with a 3 ms advantage (IPV6_RTT_ADVANTAGE) to prefer modern dual-stack endpoints - Relay (
AddrKind::Relay): Backup transport with no RTT bias, used only when no primary path exists
This tier system ensures that any available direct path automatically outranks a relay connection, even if the relay exhibits lower measured latency.
Biased RTT Calculation and Sorting
For each candidate path, the selector computes a sort key using the sort_key method that combines the transport tier with a biased latency value:
fn sort_key(&self, addr: &FourTuple, rtt: Duration) -> (TransportType, i128) {
let bias = self.bias_for(addr);
let biased_rtt = (rtt.as_nanos() as i128).saturating_add(bias.rtt_bias);
(bias.transport_type, biased_rtt)
}
The bias.rtt_bias value is negative when granting an advantage (e.g., IPv6 receives -3ms converted to nanoseconds) and positive when imposing a penalty. The resulting tuple (TransportType, biased_rtt) is ordered lexicographically, ensuring all primary transports sort before backup transports, with the lowest biased RTT breaking ties within each tier.
Selection Logic and Path Switching
When evaluating available paths via PathSelectionContext, the select method (lines 70-83 in biased_rtt_path_selector.rs) implements hysteresis to prevent connection flapping:
-
Cross-tier switching: If the best candidate belongs to a different tier than the current path (e.g., moving from backup relay to primary direct), the selector immediately switches regardless of RTT values. This allows instant migration to direct paths as soon as they become available.
-
Same-tier switching: When both current and candidate paths share the same tier, the selector requires the new path to be at least 5 ms better (
RTT_SWITCHING_MIN) than the current one before switching. This threshold absorbs network jitter and prevents oscillation between paths of similar quality.
Integration with Remote State Management
The RemoteMap struct in iroh/src/socket/remote_map.rs instantiates the path selector once during node initialization and stores it in Tasks.path_selector. Each RemoteStateActor receives a clone of this Arc<dyn PathSelector> and invokes the select method whenever path conditions change, such as new RTT measurements from quinn or discovered direct addresses via hole punching.
The chosen FourTuple determines whether subsequent QUIC packets route through a direct IP address or the relay's endpoint, with the selector re-evaluating this decision continuously as network conditions evolve.
Customizing Path Selection Behavior
Developers can inject custom selectors or modify the default biases to alter path preferences.
Using the Default Selector
The following pattern demonstrates how the default selector prioritizes direct connections over relay despite higher latency:
use iroh::socket::biased_rtt_path_selector::BiasedRttPathSelector;
use iroh::socket::remote_map::{PathSelectionContext, PathSelectionData};
use iroh::socket::transports::{self, Addr, FourTuple};
use std::time::Duration;
// Create a selector with the built‑in biases.
let selector = BiasedRttPathSelector::default();
// Mock two candidate paths: an IPv4 direct path and a relay backup.
let v4 = FourTuple::from_remote(Addr::Ip("127.0.0.1:1234".parse().unwrap()));
let relay = FourTuple::from_remote(Addr::Relay(
"https://relay.iroh.computer".parse().unwrap(),
iroh_base::EndpointId::from_bytes(&[0u8; 32]).unwrap(),
));
// Attach fake RTT measurements.
let paths = vec![
PathSelectionData::for_test(&v4, Some(noq::PathStats { rtt: Duration::from_millis(30), ..Default::default() })),
PathSelectionData::for_test(&relay, Some(noq::PathStats { rtt: Duration::from_millis(5), ..Default::default() })),
];
// No current path (first selection).
let ctx = PathSelectionContext::for_test(None, paths);
let chosen = selector.select(&ctx).selected().cloned();
assert_eq!(chosen.map(|p| p.remote()), Some(Addr::Ip(_))); // direct wins despite higher RTT
Modifying Transport Biases
To adjust the preference hierarchy, override specific transport biases:
use iroh::socket::biased_rtt_path_selector::{
BiasedRttPathSelector, TransportBias, TransportType,
};
use iroh::socket::transports::AddrKind;
// Start from the default selector.
let selector = BiasedRttPathSelector::default()
// Give the relay a 2 ms advantage (negative bias → more preferred).
.with_bias(
AddrKind::Relay,
TransportBias {
transport_type: TransportType::Backup,
rtt_bias: -(Duration::from_millis(2).as_nanos() as i128),
},
);
Custom selectors can be passed to RemoteMap::new to control path selection across the entire node runtime.
Summary
- iroh implements path selection via a pluggable
PathSelectortrait, defaulting toBiasedRttPathSelector. - Primary transports (direct IPv4/IPv6) always outrank backup transports (relay), with IPv6 receiving a 3 ms preference over IPv4.
- The selector uses biased RTT calculations where negative bias values indicate transport advantages.
- Cross-tier switches happen immediately when direct paths appear, while same-tier switches require a 5 ms RTT improvement to prevent flapping.
- The
RemoteMapowns the selector and distributes it toRemoteStateActorinstances, which re-evaluate paths on every RTT update.
Frequently Asked Questions
What happens when a direct connection becomes available while using a relay?
When a direct path is discovered, the selector recognizes a cross-tier switch from backup (relay) to primary (direct) and immediately migrates the connection, even if the direct path has a higher RTT than the relay. This behavior is hardcoded in BiasedRttPathSelector::select to prioritize direct connectivity.
Why does IPv6 have a 3 ms advantage over IPv4?
The IPV6_RTT_ADVANTAGE constant in biased_rtt_path_selector.rs provides a slight preference for IPv6 endpoints to encourage the use of modern dual-stack infrastructure when latency is otherwise equivalent. This bias is applied as a negative offset during the RTT calculation phase.
How does the selector prevent constant switching between paths?
The selector implements a 5 ms hysteresis threshold (RTT_SWITCHING_MIN) for same-tier comparisons. A new path must demonstrate at least 5 ms lower biased RTT than the current path before the selector switches, absorbing micro-variations in network latency that would otherwise cause connection instability.
Can I force iroh to use relay connections exclusively?
Yes, by implementing a custom PathSelector that assigns TransportType::Backup to direct addresses and TransportType::Primary to relay addresses (or by setting a large positive RTT bias on direct transports). You would then inject this custom selector into RemoteMap::new during node initialization to override the default BiasedRttPathSelector behavior.
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 →