use std::{
collections::{HashMap, HashSet},
net::{self},
sync::Arc,
time::Duration,
};
use arc_swap::ArcSwap;
use endhost_api_client::client::EndhostApiClient;
use reqwest_connect_rpc::client::CrpcClientError;
use scion_sdk_utils::backoff::ExponentialBackoff;
use sciparse::{identifier::isd_asn::IsdAsn, path::metadata::path_interface::PathInterface};
use tokio::task::JoinHandle;
use url::Url;
pub trait UnderlayDiscovery: Send + Sync {
fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)>;
fn isd_ases(&self) -> HashSet<IsdAsn>;
fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr>;
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ScionRouter {
internal_interface: net::SocketAddr,
interfaces: Vec<u16>,
}
impl ScionRouter {
#[must_use]
pub fn new(internal_interface: net::SocketAddr, interfaces: Vec<u16>) -> Self {
Self {
internal_interface,
interfaces,
}
}
#[must_use]
pub fn internal_interface(&self) -> net::SocketAddr {
self.internal_interface
}
#[must_use]
pub fn interfaces(&self) -> &[u16] {
&self.interfaces
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum UnderlayInfo {
Snap(Url),
Udp(Vec<ScionRouter>),
}
#[derive(Clone, Debug)]
pub struct StaticUdpRouter {
pub isd_as: IsdAsn,
pub internal_interface: net::SocketAddr,
pub interfaces: Vec<u16>,
}
pub struct StaticUnderlayDiscovery {
underlays: Vec<(IsdAsn, UnderlayInfo)>,
udp_underlay_next_hops: HashMap<PathInterface, net::SocketAddr>,
}
impl StaticUnderlayDiscovery {
pub fn new(routers: impl IntoIterator<Item = StaticUdpRouter>) -> Self {
let mut grouped: HashMap<IsdAsn, Vec<ScionRouter>> = HashMap::new();
let mut udp_underlay_next_hops = HashMap::new();
for router in routers {
for interface_id in &router.interfaces {
udp_underlay_next_hops.insert(
PathInterface::new(router.isd_as, *interface_id),
router.internal_interface,
);
}
grouped.entry(router.isd_as).or_default().push(ScionRouter {
internal_interface: router.internal_interface,
interfaces: router.interfaces,
});
}
let underlays = grouped
.into_iter()
.map(|(isd_as, routers)| (isd_as, UnderlayInfo::Udp(routers)))
.collect();
Self {
underlays,
udp_underlay_next_hops,
}
}
}
impl UnderlayDiscovery for StaticUnderlayDiscovery {
fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)> {
self.underlays
.iter()
.filter(|(ia, _)| isd_as.matches(*ia))
.map(|(ia, info)| (*ia, info.clone()))
.collect()
}
fn isd_ases(&self) -> HashSet<IsdAsn> {
self.underlays.iter().map(|(ia, _)| *ia).collect()
}
fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr> {
self.udp_underlay_next_hops.get(&interface).copied()
}
}
struct PeriodicUnderlayDiscoveryInner {
pub underlays: ArcSwap<Vec<(IsdAsn, UnderlayInfo)>>,
pub udp_underlay_next_hops: ArcSwap<HashMap<PathInterface, net::SocketAddr>>,
}
pub(crate) struct PeriodicUnderlayDiscovery {
inner: Arc<PeriodicUnderlayDiscoveryInner>,
task: JoinHandle<()>,
}
impl Drop for PeriodicUnderlayDiscovery {
fn drop(&mut self) {
self.task.abort();
}
}
impl UnderlayDiscovery for PeriodicUnderlayDiscovery {
fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)> {
self.inner
.underlays
.load()
.iter()
.filter(|(ia, _)| isd_as.matches(*ia))
.map(|(ia, info)| (*ia, info.clone()))
.collect()
}
fn isd_ases(&self) -> HashSet<IsdAsn> {
self.inner
.underlays
.load()
.iter()
.map(|(ia, _)| *ia)
.collect()
}
fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr> {
self.inner
.udp_underlay_next_hops
.load()
.get(&interface)
.copied()
}
}
impl PeriodicUnderlayDiscovery {
pub(crate) async fn new(
api_client: Arc<dyn EndhostApiClient>,
fetch_interval: Duration,
backoff: ExponentialBackoff,
) -> Result<Self, CrpcClientError> {
let (initial_underlays, initial_udp_underlay_next_hops) =
discover_underlays(&api_client).await?;
tracing::debug!(
underlays=?initial_underlays,
"Successfully discovered initial underlays"
);
let inner = Arc::new(PeriodicUnderlayDiscoveryInner {
underlays: ArcSwap::new(Arc::new(initial_underlays)),
udp_underlay_next_hops: ArcSwap::new(Arc::new(initial_udp_underlay_next_hops)),
});
let inner_clone = inner.clone();
let task = tokio::spawn(async move {
loop {
tokio::time::sleep(fetch_interval).await;
let mut failed_attempts = 0;
loop {
match discover_underlays(&api_client).await {
Ok((underlays, udp_underlay_next_hops)) => {
tracing::debug!(
underlays=?underlays,
"Successfully discovered underlays"
);
inner_clone.underlays.store(Arc::new(underlays));
inner_clone
.udp_underlay_next_hops
.store(Arc::new(udp_underlay_next_hops));
break;
}
Err(e) => {
failed_attempts += 1;
tracing::warn!(err = ?e, attempt = failed_attempts, "Failed to discover underlays");
tokio::time::sleep(backoff.duration(failed_attempts)).await;
}
}
}
}
});
Ok(Self { inner, task })
}
}
async fn discover_underlays(
api_client: &Arc<dyn EndhostApiClient>,
) -> Result<
(
Vec<(IsdAsn, UnderlayInfo)>,
HashMap<PathInterface, net::SocketAddr>,
),
CrpcClientError,
> {
let res = api_client.list_underlays(IsdAsn::WILDCARD).await?;
let mut udp_underlays = HashMap::new();
for underlay in res.udp_underlay {
let entry = udp_underlays.entry(underlay.isd_as).or_insert(vec![]);
entry.push(ScionRouter {
internal_interface: underlay.internal_interface,
interfaces: underlay.interfaces,
});
}
let mut udp_underlay_next_hops = HashMap::new();
for (isd_as, routers) in &udp_underlays {
for router in routers {
for interface_id in &router.interfaces {
udp_underlay_next_hops.insert(
PathInterface {
isd_asn: (*isd_as),
id: *interface_id,
},
router.internal_interface,
);
}
}
}
let mut underlays: Vec<(IsdAsn, UnderlayInfo)> = udp_underlays
.into_iter()
.map(|(isd_as, routers)| (isd_as, UnderlayInfo::Udp(routers)))
.collect();
for underlay in &res.snap_underlay {
for isd_as in &underlay.isd_ases {
underlays.push(((*isd_as), UnderlayInfo::Snap(underlay.address.clone())));
}
}
Ok((underlays, udp_underlay_next_hops))
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
fn isd_as(s: &str) -> IsdAsn {
IsdAsn::from_str(s).unwrap()
}
fn sock(s: &str) -> net::SocketAddr {
s.parse().unwrap()
}
#[test]
fn static_discovery_resolves_configured_next_hops() {
let ia = isd_as("1-ff00:0:110");
let router = sock("127.0.0.1:30041");
let discovery = StaticUnderlayDiscovery::new([StaticUdpRouter {
isd_as: ia,
internal_interface: router,
interfaces: vec![1, 2],
}]);
assert_eq!(
discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 1)),
Some(router)
);
assert_eq!(
discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 2)),
Some(router)
);
}
#[test]
fn static_discovery_unknown_interface_has_no_next_hop() {
let ia = isd_as("1-ff00:0:110");
let discovery = StaticUnderlayDiscovery::new([StaticUdpRouter {
isd_as: ia,
internal_interface: sock("127.0.0.1:30041"),
interfaces: vec![1],
}]);
assert_eq!(
discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 99)),
None
);
assert_eq!(
discovery.resolve_udp_underlay_next_hop(PathInterface::new(isd_as("1-ff00:0:111"), 1)),
None
);
}
#[test]
fn static_discovery_reports_known_isd_ases_and_udp_underlays() {
let ia1 = isd_as("1-ff00:0:110");
let ia2 = isd_as("1-ff00:0:111");
let discovery = StaticUnderlayDiscovery::new([
StaticUdpRouter {
isd_as: ia1,
internal_interface: sock("127.0.0.1:30041"),
interfaces: vec![1],
},
StaticUdpRouter {
isd_as: ia2,
internal_interface: sock("127.0.0.2:30041"),
interfaces: vec![5],
},
]);
let isd_ases = discovery.isd_ases();
assert_eq!(isd_ases.len(), 2);
assert!(isd_ases.contains(&ia1));
assert!(isd_ases.contains(&ia2));
let underlays = discovery.underlays(ia1);
assert_eq!(underlays.len(), 1);
assert_eq!(underlays[0].0, ia1);
assert!(matches!(underlays[0].1, UnderlayInfo::Udp(_)));
}
}