use std::{
collections::BTreeMap,
net::{IpAddr, SocketAddr},
time::{Duration, Instant},
};
use crate::structure::{guid::GuidPrefix, locator::Locator};
pub(crate) const STICKY_SWITCH_MARGIN: Duration = Duration::from_secs(30);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InterfaceSelector {
Ip(IpAddr),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SendRoute {
pub unicast: Option<Locator>,
pub multicast: Option<(Locator, InterfaceSelector)>,
pub fallback: bool,
}
impl SendRoute {
pub fn fallback() -> Self {
Self {
unicast: None,
multicast: None,
fallback: true,
}
}
}
impl Default for SendRoute {
fn default() -> Self {
Self::fallback()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RouteKey {
Unicast(Locator),
Multicast(Locator, InterfaceSelector),
}
#[derive(Clone, Debug)]
pub struct InterfaceObservation {
pub last_seen: Instant,
#[allow(dead_code)]
pub count: u64,
pub source: Option<SocketAddr>,
}
#[derive(Clone, Debug, Default)]
pub struct ObservedRoutes {
by_iface: BTreeMap<InterfaceSelector, InterfaceObservation>,
last_source: Option<SocketAddr>,
current_interface: Option<InterfaceSelector>,
}
impl ObservedRoutes {
fn record(&mut self, iface: Option<InterfaceSelector>, source: SocketAddr) {
self.record_at(iface, source, Instant::now(), STICKY_SWITCH_MARGIN);
}
fn record_at(
&mut self,
iface: Option<InterfaceSelector>,
source: SocketAddr,
now: Instant,
margin: Duration,
) {
self.last_source = Some(source);
if let Some(iface) = iface {
self
.by_iface
.entry(iface)
.and_modify(|o| {
o.last_seen = now;
o.count = o.count.saturating_add(1);
o.source = Some(source);
})
.or_insert(InterfaceObservation {
last_seen: now,
count: 1,
source: Some(source),
});
match self.current_interface {
None => self.current_interface = Some(iface),
Some(cur) if cur == iface => { }
Some(cur) => {
let displace = self
.by_iface
.get(&cur)
.is_none_or(|o| now.saturating_duration_since(o.last_seen) >= margin);
if displace {
self.current_interface = Some(iface);
}
}
}
}
}
pub fn last_source(&self) -> Option<SocketAddr> {
self.last_source
}
pub fn best_interface(&self) -> Option<InterfaceSelector> {
self.current_interface
}
#[cfg(test)]
pub fn interface_count(&self) -> usize {
self.by_iface.len()
}
}
#[derive(Debug, Default)]
pub struct InterfaceObservations {
by_participant: BTreeMap<GuidPrefix, ObservedRoutes>,
}
impl InterfaceObservations {
pub fn new() -> Self {
Self::default()
}
pub fn record(
&mut self,
prefix: GuidPrefix,
iface: Option<InterfaceSelector>,
source: SocketAddr,
) {
self
.by_participant
.entry(prefix)
.or_default()
.record(iface, source);
}
pub fn get(&self, prefix: GuidPrefix) -> Option<&ObservedRoutes> {
self.by_participant.get(&prefix)
}
pub fn remove(&mut self, prefix: GuidPrefix) {
self.by_participant.remove(&prefix);
}
}
pub trait RouteSelector {
fn select(
&self,
advertised_unicast: &[Locator],
advertised_multicast: &[Locator],
observed: Option<&ObservedRoutes>,
local_multicast_ifaces: &[InterfaceSelector],
) -> SendRoute;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct DefaultRouteSelector;
fn first_reachable_udp(locators: &[Locator]) -> Option<Locator> {
locators
.iter()
.copied()
.find(|l| l.is_udp() && !l.is_loopback())
}
impl RouteSelector for DefaultRouteSelector {
fn select(
&self,
advertised_unicast: &[Locator],
advertised_multicast: &[Locator],
observed: Option<&ObservedRoutes>,
local_multicast_ifaces: &[InterfaceSelector],
) -> SendRoute {
let Some(obs) = observed else {
return SendRoute::fallback();
};
let mc_advertised = first_reachable_udp(advertised_multicast);
let chosen_iface = obs
.best_interface()
.filter(|iface| local_multicast_ifaces.contains(iface));
let multicast = match (mc_advertised, chosen_iface) {
(Some(mc), Some(iface)) => Some((mc, iface)),
_ => None,
};
if mc_advertised.is_some() && multicast.is_none() {
return SendRoute::fallback();
}
let unicast = select_unicast(advertised_unicast, obs);
if unicast.is_none() && multicast.is_none() {
return SendRoute::fallback();
}
SendRoute {
unicast,
multicast,
fallback: false,
}
}
}
fn select_unicast(advertised_unicast: &[Locator], obs: &ObservedRoutes) -> Option<Locator> {
let candidates: Vec<Locator> = advertised_unicast
.iter()
.copied()
.filter(|l| l.is_udp() && !l.is_loopback())
.collect();
match candidates.len() {
0 => None,
1 => Some(candidates[0]),
_ => {
let source_ip = obs.last_source().map(|sa| sa.ip());
source_ip.and_then(|ip| {
candidates
.iter()
.copied()
.find(|l| SocketAddr::from(*l).ip() == ip)
})
}
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use super::*;
use crate::structure::guid::GuidPrefix;
fn udp(ip: [u8; 4], port: u16) -> Locator {
Locator::UdpV4(SocketAddrV4::new(
Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]),
port,
))
}
fn sockaddr(ip: [u8; 4], port: u16) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])), port)
}
fn iface(ip: [u8; 4]) -> InterfaceSelector {
InterfaceSelector::Ip(IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])))
}
#[test]
fn no_observation_is_fallback() {
let sel = DefaultRouteSelector;
let route = sel.select(&[udp([10, 0, 0, 5], 7410)], &[], None, &[]);
assert!(route.fallback);
}
#[test]
fn single_unicast_with_observation_narrows() {
let sel = DefaultRouteSelector;
let mut obs = ObservedRoutes::default();
obs.record(Some(iface([10, 0, 0, 1])), sockaddr([10, 0, 0, 5], 7410));
let route = sel.select(
&[udp([10, 0, 0, 5], 7410)],
&[],
Some(&obs),
&[iface([10, 0, 0, 1])],
);
assert!(!route.fallback);
assert_eq!(route.unicast, Some(udp([10, 0, 0, 5], 7410)));
assert_eq!(route.multicast, None);
}
#[test]
fn multicast_tagged_with_observed_interface() {
let sel = DefaultRouteSelector;
let mut obs = ObservedRoutes::default();
obs.record(Some(iface([10, 0, 0, 1])), sockaddr([10, 0, 0, 5], 7410));
let mc = udp([239, 255, 0, 1], 7401);
let route = sel.select(
&[udp([10, 0, 0, 5], 7410)],
&[mc],
Some(&obs),
&[iface([10, 0, 0, 1])],
);
assert_eq!(route.multicast, Some((mc, iface([10, 0, 0, 1]))));
assert!(!route.fallback);
}
#[test]
fn advertised_multicast_but_unknown_interface_falls_back() {
let sel = DefaultRouteSelector;
let mut obs = ObservedRoutes::default();
obs.record(None, sockaddr([10, 0, 0, 5], 7410));
let mc = udp([239, 255, 0, 1], 7401);
let route = sel.select(
&[udp([10, 0, 0, 5], 7410)],
&[mc],
Some(&obs),
&[iface([10, 0, 0, 1])],
);
assert!(route.fallback);
}
#[test]
fn ambiguous_multi_unicast_without_source_match_falls_back() {
let sel = DefaultRouteSelector;
let mut obs = ObservedRoutes::default();
obs.record(None, sockaddr([172, 16, 0, 9], 7410));
let route = sel.select(
&[udp([10, 0, 0, 5], 7410), udp([192, 168, 1, 5], 7410)],
&[],
Some(&obs),
&[],
);
assert!(route.fallback);
}
#[test]
fn multi_unicast_disambiguated_by_source() {
let sel = DefaultRouteSelector;
let mut obs = ObservedRoutes::default();
obs.record(Some(iface([10, 0, 0, 1])), sockaddr([192, 168, 1, 5], 7410));
let route = sel.select(
&[udp([10, 0, 0, 5], 7410), udp([192, 168, 1, 5], 7410)],
&[],
Some(&obs),
&[iface([10, 0, 0, 1])],
);
assert_eq!(route.unicast, Some(udp([192, 168, 1, 5], 7410)));
assert!(!route.fallback);
}
#[test]
fn route_key_dedup() {
use std::collections::BTreeSet;
let mut set = BTreeSet::new();
let k1 = RouteKey::Multicast(udp([239, 255, 0, 1], 7401), iface([10, 0, 0, 1]));
let k2 = RouteKey::Multicast(udp([239, 255, 0, 1], 7401), iface([10, 0, 0, 1]));
let k3 = RouteKey::Unicast(udp([10, 0, 0, 5], 7410));
assert!(set.insert(k1));
assert!(!set.insert(k2)); assert!(set.insert(k3));
assert_eq!(set.len(), 2);
}
#[test]
fn observations_sticky_within_margin() {
let mut obs = InterfaceObservations::new();
let p = GuidPrefix::UNKNOWN;
obs.record(p, Some(iface([10, 0, 0, 1])), sockaddr([10, 0, 0, 5], 7410));
obs.record(p, Some(iface([10, 0, 0, 1])), sockaddr([10, 0, 0, 5], 7410));
obs.record(
p,
Some(iface([192, 168, 1, 1])),
sockaddr([192, 168, 1, 5], 7410),
);
let recorded = obs.get(p).unwrap();
assert_eq!(recorded.interface_count(), 2);
assert_eq!(recorded.best_interface(), Some(iface([10, 0, 0, 1])));
}
#[test]
fn sticky_first_observation_is_chosen() {
let mut obs = ObservedRoutes::default();
let t0 = Instant::now();
obs.record_at(
Some(iface([10, 0, 0, 1])),
sockaddr([10, 0, 0, 5], 7410),
t0,
Duration::from_secs(30),
);
assert_eq!(obs.best_interface(), Some(iface([10, 0, 0, 1])));
}
#[test]
fn sticky_does_not_switch_within_margin() {
let mut obs = ObservedRoutes::default();
let margin = Duration::from_secs(30);
let t0 = Instant::now();
obs.record_at(
Some(iface([10, 0, 0, 1])),
sockaddr([10, 0, 0, 5], 7410),
t0,
margin,
);
obs.record_at(
Some(iface([192, 168, 1, 1])),
sockaddr([192, 168, 1, 5], 7410),
t0 + Duration::from_secs(29),
margin,
);
assert_eq!(obs.best_interface(), Some(iface([10, 0, 0, 1])));
}
#[test]
fn sticky_switches_past_margin() {
let mut obs = ObservedRoutes::default();
let margin = Duration::from_secs(30);
let t0 = Instant::now();
obs.record_at(
Some(iface([10, 0, 0, 1])),
sockaddr([10, 0, 0, 5], 7410),
t0,
margin,
);
obs.record_at(
Some(iface([192, 168, 1, 1])),
sockaddr([192, 168, 1, 5], 7410),
t0 + Duration::from_secs(30),
margin,
);
assert_eq!(obs.best_interface(), Some(iface([192, 168, 1, 1])));
}
#[test]
fn sticky_busy_current_never_displaced() {
let mut obs = ObservedRoutes::default();
let margin = Duration::from_secs(30);
let t0 = Instant::now();
let current = iface([10, 0, 0, 1]);
let challenger = iface([192, 168, 1, 1]);
obs.record_at(Some(current), sockaddr([10, 0, 0, 5], 7410), t0, margin);
for k in 1..=6 {
let t = t0 + Duration::from_secs(10 * k);
obs.record_at(
Some(challenger),
sockaddr([192, 168, 1, 5], 7410),
t,
margin,
);
obs.record_at(
Some(current),
sockaddr([10, 0, 0, 5], 7410),
t + Duration::from_secs(1),
margin,
);
}
assert_eq!(obs.best_interface(), Some(current));
}
}