use std::{
net::IpAddr,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use anyhow::{Context, bail};
use sciparse::{
identifier::isd_asn::IsdAsn,
path::{
ScionPath,
combinator::graph::{InputSegment, MultiGraph, number_of_hops},
},
reexport::tinyvec::ArrayVec,
segment::{SegmentFp, SignedPathSegment},
};
use tokio_util::sync::CancellationToken;
use crate::pg_wap2::{
auth::{AuthService, DstGrant, GrantedSegmentId},
segments::{PairGuard, SegmentManager, SegmentStoreId, SegmentsIter},
sni::{CustomerDomainRef, WapSNI},
};
#[derive(Clone)]
pub struct PathManager(Arc<PathManagerInner>);
struct PathManagerInner {
segments: SegmentManager,
auth: AuthService,
}
impl PathManager {
pub fn new(segments: SegmentManager, auth: AuthService) -> Self {
Self(Arc::new(PathManagerInner { segments, auth }))
}
pub async fn best_path(
&self,
client_ip: IpAddr,
dst_sni: &WapSNI,
src: IsdAsn,
dst: IsdAsn,
now: SystemTime,
) -> anyhow::Result<Option<UsedPath>> {
if src.is_wildcard() || dst.is_wildcard() {
bail!("Source and destination ISD-ASNs must be specified, wildcards are not allowed");
}
let auth_segments = self.0.auth.dst_grant(client_ip, dst_sni.customer_domain(), now).context(
"IP address is not authorized for the given destination SNI or the grant has expired",
)?;
if src == dst {
return Ok(Some(UsedPath {
path: ScionPath::local(src).expect("checked above that src is not a wildcard"),
src,
dst,
segments: ArrayVec::new(),
}));
}
let store_segments = self
.0
.segments
.segments(src, dst, now)
.await
.context("Failed to get segments for the given (src, dst) pair")?;
let inputs = store_segments
.iter_core_segments()
.map(InputSegment::new_core)
.chain(
store_segments
.iter_non_core_segments()
.map(InputSegment::new_non_core),
)
.chain(
auth_segments
.iter_core_segments()
.map(InputSegment::new_core),
)
.chain(
auth_segments
.iter_non_core_segments()
.map(InputSegment::new_non_core),
);
let Some((path, fps)) = best_solution(src, dst, inputs, |_| true) else {
return Ok(None);
};
let mut segments = ArrayVec::new();
for fp in fps {
segments.push(
segment_source(fp, src, dst, &store_segments, &auth_segments)
.context("The path used a segment that is no longer available in either the store or the grant")?,
);
}
Ok(Some(UsedPath {
path,
src,
dst,
segments,
}))
}
pub async fn refresh_path(&self, used: &UsedPath, now: SystemTime) -> anyhow::Result<UsedPath> {
if used.segments.is_empty() {
return Ok(used.clone());
}
let mut segments = Vec::with_capacity(used.segments.len());
for id in used.segments.iter() {
let Some(segment) = self.resolve(id, now).await else {
bail!("Segment {:?} is no longer available", id);
};
segments.push((id.is_core(), segment));
}
let inputs = segments.iter().map(|(is_core, segment)| {
if *is_core {
InputSegment::new_core(segment)
} else {
InputSegment::new_non_core(segment)
}
});
let expected: Vec<SegmentFp> = used.segments.iter().map(SegmentSourceId::fp).collect();
let (path, _segment_fps) =
best_solution(used.src, used.dst, inputs, |fps| fps == &expected[..])
.context("Refreshed segments did not combine into a path")?;
Ok(UsedPath {
path,
src: used.src,
dst: used.dst,
segments: used.segments.clone(),
})
}
pub async fn hold_segments(
&self,
used: &UsedPath,
now: SystemTime,
) -> anyhow::Result<PathSegmentsGuard> {
let mut guards = Vec::new();
for id in used.segments.iter() {
let SegmentSourceId::Store(id) = id else {
continue;
};
guards.push(
self.0
.segments
.hold_pair(id.src(), id.dst(), now)
.await
.with_context(|| {
format!(
"Failed to hold the segments of ({}, {})",
id.src(),
id.dst()
)
})?,
);
}
Ok(PathSegmentsGuard { _guards: guards })
}
pub fn watch_grants(
&self,
client_ip: IpAddr,
customer_domain: CustomerDomainRef<'_>,
used: &UsedPath,
now: SystemTime,
) -> anyhow::Result<GrantWatch> {
let dst_grant = self
.0
.auth
.watch_grant(client_ip, customer_domain, now)
.with_context(|| format!("No live grant of {client_ip} for {customer_domain}"))?;
let mut grants = vec![dst_grant];
for id in used.segments.iter() {
let SegmentSourceId::Auth(id) = id else {
continue;
};
grants.push(
self.0
.auth
.watch_segment_grant(client_ip, customer_domain, id, now)
.with_context(|| {
format!(
"No live grant of {client_ip} for segment {} of the path",
id.fp()
)
})?,
);
}
Ok(GrantWatch { grants })
}
async fn resolve(&self, id: &SegmentSourceId, now: SystemTime) -> Option<SignedPathSegment> {
match id {
SegmentSourceId::Store(id) => self.0.segments.segment(*id, now).await,
SegmentSourceId::Auth(id) => self.0.auth.segment(id, now),
}
}
}
fn best_solution<'segments>(
src: IsdAsn,
dst: IsdAsn,
inputs: impl Iterator<Item = InputSegment<'segments, sciparse::segment::SignedAsEntry>>,
accept: impl Fn(&[SegmentFp]) -> bool,
) -> Option<(ScionPath, ArrayVec<[SegmentFp; 3]>)> {
let mut graph = MultiGraph::new(number_of_hops);
graph.add_segments(inputs);
graph
.get_paths(src, dst)
.iter()
.filter_map(|solution| solution.path().ok().flatten())
.find(|(_, fps)| accept(fps))
}
fn segment_source(
fp: SegmentFp,
src: IsdAsn,
dst: IsdAsn,
store: &SegmentsIter<'_>,
granted: &DstGrant,
) -> Option<SegmentSourceId> {
if store.has_core_segment(fp) {
return Some(SegmentSourceId::Store(SegmentStoreId::Core {
src,
dst,
fp,
}));
}
if store.has_non_core_segment(fp) {
return Some(SegmentSourceId::Store(SegmentStoreId::NonCore {
src,
dst,
fp,
}));
}
if granted.has_core_segment(fp) {
return Some(SegmentSourceId::Auth(GrantedSegmentId::Core(fp)));
}
if granted.has_non_core_segment(fp) {
return Some(SegmentSourceId::Auth(GrantedSegmentId::NonCore(fp)));
}
None
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SegmentSourceId {
Store(SegmentStoreId),
Auth(GrantedSegmentId),
}
impl SegmentSourceId {
pub fn fp(&self) -> SegmentFp {
match self {
Self::Store(id) => id.fp(),
Self::Auth(id) => id.fp(),
}
}
pub fn is_core(&self) -> bool {
match self {
Self::Store(id) => id.is_core(),
Self::Auth(id) => id.is_core(),
}
}
}
impl Default for SegmentSourceId {
fn default() -> Self {
Self::Store(SegmentStoreId::Core {
src: IsdAsn(0),
dst: IsdAsn(0),
fp: SegmentFp::default(),
})
}
}
pub struct GrantWatch {
grants: Vec<CancellationToken>,
}
impl GrantWatch {
pub async fn expired(&self) {
if self.grants.is_empty() {
debug_assert!(false, "a grant watch without grants never resolves");
std::future::pending::<()>().await;
}
let expiries = self.grants.iter().map(|grant| Box::pin(grant.cancelled()));
futures::future::select_all(expiries).await;
}
}
pub struct PathSegmentsGuard {
_guards: Vec<PairGuard>,
}
#[derive(Debug, Clone)]
pub struct UsedPath {
pub path: ScionPath,
pub src: IsdAsn,
pub dst: IsdAsn,
pub segments: ArrayVec<[SegmentSourceId; 3]>,
}
impl UsedPath {
pub fn expiration(&self) -> Option<SystemTime> {
self.path
.expiration()
.map(|secs| UNIX_EPOCH + Duration::from_secs(u64::from(secs)))
}
}
#[cfg(test)]
mod tests {
use futures::FutureExt;
use sciparse::segment::Segments;
use super::*;
use crate::pg_wap2::test_util::{
Fixture, IDLE_EVICTION_TIME, MAX_FETCH_INTERVAL, MockFetcher, at, client_ip, core_ia,
core_segment, core_store_id, down_segment, granted_core_id, granted_id, leaf_ia,
non_core_store_id, other_leaf_ia, sni, store_id, up_segment,
};
#[tokio::test]
async fn best_path_combines_granted_segments_and_reports_them() {
let up = up_segment(0);
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.is_err(),
"a client without a grant for the target gets no path at all"
);
fixture.grant_non_core(Vec::new(), at(0));
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.is_none(),
"without any segments there is no path"
);
fixture.grant_non_core(vec![up.clone()], at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted segment yields a path");
assert_eq!(
used.segments.as_slice(),
[SegmentSourceId::Auth(granted_id(&up))],
"the path must report the grant it depends on"
);
}
#[tokio::test]
async fn best_path_reports_public_segments_as_public() {
let up = up_segment(0);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up.clone()]),
Duration::from_secs(100),
);
fixture.grant_non_core(vec![up.clone()], at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the public segment yields a path");
assert_eq!(
used.segments.as_slice(),
[SegmentSourceId::Store(store_id(&up))]
);
}
async fn three_segment_fixture() -> (Fixture, UsedPath) {
let fixture = Fixture::new(
MockFetcher::new(Segments {
core_segments: vec![core_segment(0)],
..Segments::default()
}),
Duration::from_secs(10 * 24 * 3600),
);
fixture.grant_for(
client_ip(),
sni().customer_domain(),
Vec::new(),
vec![up_segment(0), down_segment(0)],
at(0),
);
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), other_leaf_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("up, core and down combine into a path");
(fixture, used)
}
#[tokio::test]
async fn best_path_combines_up_core_and_down_and_tags_every_source() {
let (_fixture, used) = three_segment_fixture().await;
assert_eq!(
used.segments.as_slice(),
[
SegmentSourceId::Auth(granted_id(&up_segment(0))),
SegmentSourceId::Store(core_store_id(&core_segment(0), leaf_ia(), other_leaf_ia())),
SegmentSourceId::Auth(granted_id(&down_segment(0))),
],
"the path reports its three segments in path order, each tagged with its source"
);
}
#[tokio::test]
async fn best_path_combines_a_granted_core_segment_with_public_up_and_down() {
let fixture = Fixture::new(
MockFetcher::new(Segments {
up_segments: vec![up_segment(0)],
down_segments: vec![down_segment(0)],
..Segments::default()
}),
Duration::from_secs(100),
);
fixture.grant_for(
client_ip(),
sni().customer_domain(),
vec![core_segment(0)],
Vec::new(),
at(0),
);
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), other_leaf_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted core segment closes the gap between the public ones");
assert_eq!(
used.segments.as_slice(),
[
SegmentSourceId::Store(non_core_store_id(
&up_segment(0),
leaf_ia(),
other_leaf_ia()
)),
SegmentSourceId::Auth(granted_core_id(&core_segment(0))),
SegmentSourceId::Store(non_core_store_id(
&down_segment(0),
leaf_ia(),
other_leaf_ia()
)),
],
"a granted core segment is reported as granted, not as public"
);
let watch = fixture
.paths
.watch_grants(client_ip(), sni().customer_domain(), &used, at(0))
.expect("the client is authorized for the path");
fixture.auth.clean(at(101));
assert!(watch.expired().now_or_never().is_some());
}
#[tokio::test]
async fn refresh_path_keeps_a_three_segment_path_intact() {
let (fixture, used) = three_segment_fixture().await;
let _guard = fixture
.paths
.hold_segments(&used, at(0))
.await
.expect("the public pair of the path can be held");
let refreshed_at = at(MAX_FETCH_INTERVAL.as_secs() + 1);
fixture.grant_for(
client_ip(),
sni().customer_domain(),
Vec::new(),
vec![up_segment(600), down_segment(600)],
refreshed_at,
);
fixture.fetcher.set_segments(Segments {
core_segments: vec![core_segment(600)],
..Segments::default()
});
fixture.segments.maintain(refreshed_at).await;
let refreshed = fixture
.paths
.refresh_path(&used, refreshed_at)
.await
.expect("refreshing succeeds");
assert_eq!(
refreshed.segments, used.segments,
"the refreshed path must use the very same segments, in the same order"
);
assert_eq!(
refreshed.path.fingerprint(),
used.path.fingerprint(),
"the refreshed path is the same path, so uplinks keep their key"
);
assert_eq!(
refreshed.expiration(),
used.expiration().map(|old| old + Duration::from_secs(600)),
"all three segments moved, so the whole path lives 600s longer"
);
}
#[tokio::test]
async fn refresh_path_fails_once_a_public_segment_is_evicted() {
let up = up_segment(0);
let fixture = Fixture::new(
MockFetcher::with_up_segments(vec![up.clone()]),
Duration::from_secs(10 * 24 * 3600),
);
fixture.grant_non_core(Vec::new(), at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the public segment yields a path");
assert_eq!(
used.segments.as_slice(),
[SegmentSourceId::Store(store_id(&up))]
);
let evicted_at = at(IDLE_EVICTION_TIME.as_secs() + 1);
fixture.segments.maintain(evicted_at).await;
assert!(
fixture.paths.refresh_path(&used, evicted_at).await.is_err(),
"a path over an evicted public segment cannot be rebuilt"
);
}
#[tokio::test]
async fn best_path_fails_when_public_segments_cannot_be_fetched() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture.grant_non_core(vec![up_segment(0)], at(0));
fixture.fetcher.set_failing(true);
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.is_err(),
"a path cannot be computed without knowing the public segments"
);
fixture.fetcher.set_failing(false);
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.is_some(),
"and the failure leaves nothing behind that would keep it from succeeding later"
);
}
#[tokio::test]
async fn refresh_path_picks_up_a_later_expiration() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(10 * 24 * 3600));
fixture.grant_non_core(vec![up_segment(0)], at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted segment yields a path");
let fresher = up_segment(600);
assert_eq!(
fresher.fingerprint(),
up_segment(0).fingerprint(),
"a fresher copy of a segment keeps its fingerprint"
);
fixture.grant_non_core(vec![fresher], at(0));
let refreshed = fixture
.paths
.refresh_path(&used, at(0))
.await
.expect("refreshing succeeds");
assert_eq!(
refreshed.segments, used.segments,
"the refreshed path uses the same segments"
);
assert_eq!(
refreshed.path.fingerprint(),
used.path.fingerprint(),
"the refreshed path is the same path, so uplinks keep their key"
);
assert_eq!(
refreshed.expiration(),
used.expiration().map(|old| old + Duration::from_secs(600)),
"the refreshed path lives 600s longer"
);
}
#[tokio::test]
async fn refresh_path_fails_once_the_grant_is_gone() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
fixture.grant_non_core(vec![up_segment(0)], at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("the granted segment yields a path");
fixture.auth.clean(at(101));
assert!(
fixture.paths.refresh_path(&used, at(101)).await.is_err(),
"a path over a segment whose grant is gone cannot be refreshed"
);
}
#[tokio::test]
async fn best_path_rejects_wildcards_and_handles_local_paths() {
let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), IsdAsn(0), at(0))
.await
.is_err()
);
assert!(
fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
.await
.is_err(),
"an AS local path still requires a grant for the target"
);
fixture.grant_non_core(Vec::new(), at(0));
let used = fixture
.paths
.best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
.await
.expect("path combination succeeds")
.expect("an AS local path always exists");
assert!(used.segments.is_empty());
}
}