use std::{sync::Arc, time::Duration};
use rustc_hash::FxHashMap;
use tracing::trace;
use super::{
remote_map::{PathSelection, PathSelectionContext, PathSelectionData, PathSelector},
transports::AddrKind,
};
use crate::socket::transports::FourTuple;
const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3);
const RTT_SWITCHING_MIN: Duration = Duration::from_millis(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum TransportType {
Primary,
Backup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct TransportBias {
transport_type: TransportType,
rtt_bias: i128,
}
impl TransportBias {
pub(crate) fn primary() -> Self {
Self {
transport_type: TransportType::Primary,
rtt_bias: 0,
}
}
fn backup() -> Self {
Self {
transport_type: TransportType::Backup,
rtt_bias: 0,
}
}
pub(crate) fn with_rtt_advantage(mut self, advantage: Duration) -> Self {
self.rtt_bias -= advantage.as_nanos() as i128;
self
}
#[cfg(all(test, feature = "unstable-custom-transports"))]
pub(crate) fn with_rtt_disadvantage(mut self, disadvantage: Duration) -> Self {
self.rtt_bias += disadvantage.as_nanos() as i128;
self
}
}
#[derive(Debug, Clone)]
pub(crate) struct BiasedRttPathSelector {
biases: Arc<FxHashMap<AddrKind, TransportBias>>,
}
impl Default for BiasedRttPathSelector {
fn default() -> Self {
let mut map = FxHashMap::default();
map.insert(AddrKind::IpV4, TransportBias::primary());
map.insert(
AddrKind::IpV6,
TransportBias::primary().with_rtt_advantage(IPV6_RTT_ADVANTAGE),
);
map.insert(AddrKind::Relay, TransportBias::backup());
Self {
biases: Arc::new(map),
}
}
}
impl BiasedRttPathSelector {
#[cfg(all(test, feature = "unstable-custom-transports"))]
pub(crate) fn with_bias(self, kind: AddrKind, bias: TransportBias) -> Self {
let mut map = (*self.biases).clone();
map.insert(kind, bias);
Self {
biases: Arc::new(map),
}
}
fn bias_for(&self, addr: &FourTuple) -> TransportBias {
self.biases
.get(&addr.addr_kind())
.copied()
.unwrap_or_else(TransportBias::primary)
}
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)
}
}
impl PathSelector for BiasedRttPathSelector {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
let current = ctx.current();
let mut best: Option<(PathSelectionData<'_>, (TransportType, i128))> = None;
let mut current_key: Option<(TransportType, i128)> = None;
trace!("dumping path RTTs");
for psd in ctx.paths() {
let network_path = psd.network_path();
let Some(stats) = psd.stats() else {
continue;
};
let rtt = stats.rtt;
trace!(%network_path, ?rtt);
let key = self.sort_key(network_path, rtt);
if Some(network_path) == current && current_key.is_none_or(|c| key < c) {
current_key = Some(key);
}
if best.as_ref().is_none_or(|(_, b)| key < *b) {
best = Some((psd, key));
}
}
let mut selection = PathSelection::none();
let Some((best_psd, (best_tier, best_biased))) = best else {
return selection;
};
let Some((current_tier, current_biased)) = current_key else {
selection.set(&best_psd);
return selection;
};
if current_tier != best_tier {
selection.set(&best_psd);
} else if best_biased + RTT_SWITCHING_MIN.as_nanos() as i128 <= current_biased {
selection.set(&best_psd);
}
selection
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use iroh_base::{EndpointId, RelayUrl};
use noq::PathStats;
use super::*;
use crate::socket::{
remote_map::{PathSelectionContext, PathSelectionData},
transports::{self, Addr},
};
fn v4(port: u16) -> transports::FourTuple {
transports::FourTuple::from_remote(Addr::Ip(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
port,
))))
}
fn v6(port: u16) -> transports::FourTuple {
transports::FourTuple::from_remote(Addr::Ip(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
port,
0,
0,
))))
}
fn relay(port: u16) -> transports::FourTuple {
let url = format!("https://relay{port}.iroh.computer")
.parse::<RelayUrl>()
.unwrap();
transports::FourTuple::from_remote(Addr::Relay(
url,
EndpointId::from_bytes(&[0u8; 32]).unwrap(),
))
}
fn psd(addr: &transports::FourTuple, rtt_ms: u64) -> PathSelectionData<'_> {
let mut stats = PathStats::default();
stats.rtt = Duration::from_millis(rtt_ms);
PathSelectionData::for_test(addr, Some(stats))
}
fn select_with_default(
current: Option<&transports::FourTuple>,
paths: Vec<PathSelectionData<'_>>,
) -> Option<transports::FourTuple> {
let ctx = PathSelectionContext::for_test(current, paths);
BiasedRttPathSelector::default()
.select(&ctx)
.selected()
.cloned()
}
#[test]
fn ipv6_wins_over_ipv4_within_bias() {
let v4 = v4(1);
let v6 = v6(1);
let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 10)]);
assert_eq!(chosen.as_ref(), Some(&v6));
let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 12)]);
assert_eq!(chosen.as_ref(), Some(&v6));
let chosen = select_with_default(None, vec![psd(&v4, 10), psd(&v6, 20)]);
assert_eq!(chosen.as_ref(), Some(&v4));
}
#[test]
fn primary_wins_over_backup_regardless_of_rtt() {
let v4 = v4(1);
let relay = relay(1);
let chosen = select_with_default(None, vec![psd(&v4, 100), psd(&relay, 10)]);
assert!(matches!(
chosen.as_ref().map(|t| t.remote()),
Some(Addr::Ip(_))
));
let chosen = select_with_default(None, vec![psd(&v4, 1000), psd(&relay, 1)]);
assert!(matches!(
chosen.as_ref().map(|t| t.remote()),
Some(Addr::Ip(_))
));
}
#[test]
fn same_tier_only_switches_with_significant_rtt_diff() {
let v4_1 = v4(1);
let v4_2 = v4(2);
let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 18)]);
assert_eq!(chosen, None);
let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 16)]);
assert_eq!(chosen, None);
let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 15)]);
assert_eq!(chosen.as_ref(), Some(&v4_2));
let chosen = select_with_default(Some(&v4_1), vec![psd(&v4_1, 20), psd(&v4_2, 14)]);
assert_eq!(chosen.as_ref(), Some(&v4_2));
}
#[test]
fn no_current_path_selects_best() {
let v4_1 = v4(1);
let v4_2 = v4(2);
let chosen = select_with_default(None, vec![psd(&v4_1, 20), psd(&v4_2, 10)]);
assert_eq!(chosen.as_ref(), Some(&v4_2));
}
#[test]
fn empty_paths_returns_none() {
assert_eq!(select_with_default(None, vec![]), None);
let v4 = v4(1);
assert_eq!(select_with_default(Some(&v4), vec![]), None);
}
}