1use std::{
22 net::IpAddr,
23 sync::Arc,
24 time::{Duration, SystemTime, UNIX_EPOCH},
25};
26
27use anyhow::{Context, bail};
28use sciparse::{
29 identifier::isd_asn::IsdAsn,
30 path::{
31 ScionPath,
32 combinator::graph::{InputSegment, MultiGraph, number_of_hops},
33 },
34 reexport::tinyvec::ArrayVec,
35 segment::{SegmentFp, SignedPathSegment},
36};
37
38use crate::pg_wap::{
39 grants::{DstGrant, GrantManager, GrantedSegmentId, SegmentGrantGuard, TargetGrantGuard},
40 segments::{PairGuard, SegmentManager, SegmentStoreId, SegmentsIter},
41 sni::{CustomerDomainRef, WapSNI},
42};
43
44#[derive(Clone)]
48pub struct PathManager(Arc<PathManagerInner>);
49
50struct PathManagerInner {
52 segments: SegmentManager,
53 grant: GrantManager,
54}
55
56impl PathManager {
57 pub fn new(segments: SegmentManager, grant: GrantManager) -> Self {
59 Self(Arc::new(PathManagerInner { segments, grant }))
60 }
61
62 pub async fn best_path(
77 &self,
78 client_ip: IpAddr,
79 dst_sni: &WapSNI,
80 src: IsdAsn,
81 dst: IsdAsn,
82 now: SystemTime,
83 ) -> Result<Option<UsedPath>, BestPathError> {
84 if src.is_wildcard() || dst.is_wildcard() {
85 return Err(BestPathError::WildcardIA);
86 }
87
88 let auth_segments = self
90 .0
91 .grant
92 .dst_grant(client_ip, dst_sni.customer_domain(), now)
93 .ok_or(BestPathError::UnauthorizedDestination)?;
94
95 if src == dst {
97 return Ok(Some(UsedPath {
98 path: ScionPath::local(src).expect("checked above that src is not a wildcard"),
99 src,
100 dst,
101 segments: ArrayVec::new(),
102 }));
103 }
104
105 let store_segments = self
109 .0
110 .segments
111 .segments(src, dst, now)
112 .await
113 .context("Failed to get segments for the given (src, dst) pair")?;
114
115 let inputs = store_segments
116 .iter_core_segments()
117 .map(InputSegment::new_core)
118 .chain(
119 store_segments
120 .iter_non_core_segments()
121 .map(InputSegment::new_non_core),
122 )
123 .chain(
124 auth_segments
125 .iter_core_segments()
126 .map(InputSegment::new_core),
127 )
128 .chain(
129 auth_segments
130 .iter_non_core_segments()
131 .map(InputSegment::new_non_core),
132 );
133
134 let Some((path, fps)) = best_solution(src, dst, inputs, |_| true) else {
137 return Ok(None);
138 };
139
140 let mut segments = ArrayVec::new();
142 for fp in fps {
143 segments.push(
144 segment_source(fp, src, dst, &store_segments, &auth_segments)
145 .ok_or(BestPathError::UnauthorizedSegments)?,
146 );
147 }
148
149 Ok(Some(UsedPath {
150 path,
151 src,
152 dst,
153 segments,
154 }))
155 }
156
157 pub async fn refresh_path(&self, used: &UsedPath, now: SystemTime) -> anyhow::Result<UsedPath> {
163 if used.segments.is_empty() {
164 return Ok(used.clone());
166 }
167
168 let mut segments = Vec::with_capacity(used.segments.len());
169 for id in used.segments.iter() {
170 let Some(segment) = self.resolve(id, now).await else {
171 bail!("Segment {:?} is no longer available", id);
172 };
173 segments.push((id.is_core(), segment));
174 }
175
176 let inputs = segments.iter().map(|(is_core, segment)| {
177 if *is_core {
178 InputSegment::new_core(segment)
179 } else {
180 InputSegment::new_non_core(segment)
181 }
182 });
183
184 let expected: Vec<SegmentFp> = used.segments.iter().map(SegmentSourceId::fp).collect();
188 let (path, _segment_fps) =
189 best_solution(used.src, used.dst, inputs, |fps| fps == &expected[..])
190 .context("Refreshed segments did not combine into a path")?;
191
192 Ok(UsedPath {
193 path,
194 src: used.src,
195 dst: used.dst,
196 segments: used.segments.clone(),
197 })
198 }
199
200 pub async fn hold_segments(
209 &self,
210 used: &UsedPath,
211 now: SystemTime,
212 ) -> anyhow::Result<PathSegmentsGuard> {
213 let mut guards = Vec::new();
214
215 for id in used.segments.iter() {
216 let SegmentSourceId::Store(id) = id else {
217 continue;
218 };
219
220 guards.push(
221 self.0
222 .segments
223 .hold_pair(id.src(), id.dst(), now)
224 .await
225 .with_context(|| {
226 format!(
227 "Failed to hold the segments of ({}, {})",
228 id.src(),
229 id.dst()
230 )
231 })?,
232 );
233 }
234
235 Ok(PathSegmentsGuard { _guards: guards })
236 }
237
238 pub fn watch_grants(
245 &self,
246 client_ip: IpAddr,
247 customer_domain: CustomerDomainRef<'_>,
248 used: &UsedPath,
249 now: SystemTime,
250 ) -> anyhow::Result<GrantWatch> {
251 let target = self
252 .0
253 .grant
254 .watch_grant(client_ip, customer_domain, now)
255 .with_context(|| format!("No live grant of {client_ip} for {customer_domain}"))?;
256
257 let mut segments = Vec::new();
258
259 for id in used.segments.iter() {
260 let SegmentSourceId::Auth(id) = id else {
261 continue;
262 };
263
264 segments.push(
265 self.0
266 .grant
267 .watch_segment_grant(client_ip, customer_domain, id, now)
268 .with_context(|| {
269 format!(
270 "No live grant of {client_ip} for segment {} of the path",
271 id.fp()
272 )
273 })?,
274 );
275 }
276
277 Ok(GrantWatch { target, segments })
278 }
279
280 async fn resolve(&self, id: &SegmentSourceId, now: SystemTime) -> Option<SignedPathSegment> {
282 match id {
283 SegmentSourceId::Store(id) => self.0.segments.segment(*id, now).await,
284 SegmentSourceId::Auth(id) => self.0.grant.segment(id, now),
285 }
286 }
287}
288
289#[derive(thiserror::Error, Debug)]
291pub enum BestPathError {
292 #[error("the client is not authorized to access the given destination")]
294 UnauthorizedDestination,
295 #[error("the client is not authorized to use one or more of the given segments")]
297 UnauthorizedSegments,
298 #[error("src or dst IA contained a wildcard, this is not allowed")]
300 WildcardIA,
301 #[error("failed to compute the best path: {0}")]
303 Other(#[from] anyhow::Error),
304}
305
306fn best_solution<'segments>(
310 src: IsdAsn,
311 dst: IsdAsn,
312 inputs: impl Iterator<Item = InputSegment<'segments, sciparse::segment::SignedAsEntry>>,
313 accept: impl Fn(&[SegmentFp]) -> bool,
314) -> Option<(ScionPath, ArrayVec<[SegmentFp; 3]>)> {
315 let mut graph = MultiGraph::new(number_of_hops);
318 graph.add_segments(inputs);
319
320 graph
321 .get_paths(src, dst)
322 .iter()
323 .filter_map(|solution| solution.path().ok().flatten())
324 .find(|(_, fps)| accept(fps))
325}
326
327fn segment_source(
331 fp: SegmentFp,
332 src: IsdAsn,
333 dst: IsdAsn,
334 store: &SegmentsIter<'_>,
335 granted: &DstGrant,
336) -> Option<SegmentSourceId> {
337 if store.has_core_segment(fp) {
340 return Some(SegmentSourceId::Store(SegmentStoreId::Core {
341 src,
342 dst,
343 fp,
344 }));
345 }
346 if store.has_non_core_segment(fp) {
347 return Some(SegmentSourceId::Store(SegmentStoreId::NonCore {
348 src,
349 dst,
350 fp,
351 }));
352 }
353 if granted.has_core_segment(fp) {
354 return Some(SegmentSourceId::Auth(GrantedSegmentId::Core(fp)));
355 }
356 if granted.has_non_core_segment(fp) {
357 return Some(SegmentSourceId::Auth(GrantedSegmentId::NonCore(fp)));
358 }
359
360 None
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Hash)]
368pub enum SegmentSourceId {
369 Store(SegmentStoreId),
371 Auth(GrantedSegmentId),
373}
374
375impl SegmentSourceId {
376 pub fn fp(&self) -> SegmentFp {
378 match self {
379 Self::Store(id) => id.fp(),
380 Self::Auth(id) => id.fp(),
381 }
382 }
383
384 pub fn is_core(&self) -> bool {
386 match self {
387 Self::Store(id) => id.is_core(),
388 Self::Auth(id) => id.is_core(),
389 }
390 }
391}
392
393impl Default for SegmentSourceId {
395 fn default() -> Self {
396 Self::Store(SegmentStoreId::Core {
397 src: IsdAsn(0),
398 dst: IsdAsn(0),
399 fp: SegmentFp::default(),
400 })
401 }
402}
403
404pub struct GrantWatch {
408 target: TargetGrantGuard,
410 segments: Vec<SegmentGrantGuard>,
412}
413
414impl GrantWatch {
415 pub async fn expired(&self) {
420 let expiries = std::iter::once(self.target.expired())
421 .chain(self.segments.iter().map(SegmentGrantGuard::expired))
422 .map(|token| Box::pin(token.cancelled()));
423
424 futures::future::select_all(expiries).await;
425 }
426}
427
428pub struct PathSegmentsGuard {
433 _guards: Vec<PairGuard>,
434}
435
436#[derive(Debug, Clone)]
440pub struct UsedPath {
441 pub path: ScionPath,
443 pub src: IsdAsn,
445 pub dst: IsdAsn,
447 pub segments: ArrayVec<[SegmentSourceId; 3]>,
449}
450
451impl UsedPath {
452 pub fn expiration(&self) -> Option<SystemTime> {
456 self.path
457 .expiration()
458 .map(|secs| UNIX_EPOCH + Duration::from_secs(u64::from(secs)))
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use futures::FutureExt;
465 use sciparse::segment::Segments;
466
467 use super::*;
468 use crate::pg_wap::{
469 crpc::model::AuthSegments,
470 test_util::{
471 Fixture, IDLE_EVICTION_TIME, MAX_FETCH_INTERVAL, MockFetcher, at, client_ip, core_ia,
472 core_segment, core_store_id, down_segment, granted_core_id, granted_id, leaf_ia,
473 non_core_store_id, other_leaf_ia, sni, store_id, up_segment,
474 },
475 };
476
477 #[tokio::test]
478 async fn best_path_combines_granted_segments_and_reports_them() {
479 let up = up_segment(0);
480 let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
481
482 assert!(
483 fixture
484 .paths
485 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
486 .await
487 .is_err(),
488 "a client without a grant for the target gets no path at all"
489 );
490
491 fixture.grant_non_core(Vec::new(), at(0));
493 assert!(
494 fixture
495 .paths
496 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
497 .await
498 .expect("path combination succeeds")
499 .is_none(),
500 "without any segments there is no path"
501 );
502
503 fixture.grant_non_core(vec![up.clone()], at(0));
504
505 let used = fixture
506 .paths
507 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
508 .await
509 .expect("path combination succeeds")
510 .expect("the granted segment yields a path");
511
512 assert_eq!(
513 used.segments.as_slice(),
514 [SegmentSourceId::Auth(granted_id(&up))],
515 "the path must report the grant it depends on"
516 );
517 }
518
519 #[tokio::test]
520 async fn best_path_reports_public_segments_as_public() {
521 let up = up_segment(0);
522 let fixture = Fixture::new(
523 MockFetcher::with_up_segments(vec![up.clone()]),
524 Duration::from_secs(100),
525 );
526 fixture.grant_non_core(vec![up.clone()], at(0));
529
530 let used = fixture
531 .paths
532 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
533 .await
534 .expect("path combination succeeds")
535 .expect("the public segment yields a path");
536
537 assert_eq!(
538 used.segments.as_slice(),
539 [SegmentSourceId::Store(store_id(&up))]
540 );
541 }
542
543 async fn three_segment_fixture() -> (Fixture, UsedPath) {
548 let fixture = Fixture::new(
549 MockFetcher::new(Segments {
550 core_segments: vec![core_segment(0)],
551 ..Segments::default()
552 }),
553 Duration::from_secs(10 * 24 * 3600),
554 );
555 fixture.grant_for(
556 client_ip(),
557 sni().customer_domain(),
558 AuthSegments {
559 up_segments: vec![up_segment(0)],
560 down_segments: vec![down_segment(0)],
561 ..AuthSegments::default()
562 },
563 at(0),
564 );
565
566 let used = fixture
567 .paths
568 .best_path(client_ip(), &sni(), leaf_ia(), other_leaf_ia(), at(0))
569 .await
570 .expect("path combination succeeds")
571 .expect("up, core and down combine into a path");
572
573 (fixture, used)
574 }
575
576 #[tokio::test]
577 async fn best_path_combines_up_core_and_down_and_tags_every_source() {
578 let (_fixture, used) = three_segment_fixture().await;
579
580 assert_eq!(
581 used.segments.as_slice(),
582 [
583 SegmentSourceId::Auth(granted_id(&up_segment(0))),
584 SegmentSourceId::Store(core_store_id(&core_segment(0), leaf_ia(), other_leaf_ia())),
585 SegmentSourceId::Auth(granted_id(&down_segment(0))),
586 ],
587 "the path reports its three segments in path order, each tagged with its source"
588 );
589 }
590
591 #[tokio::test]
592 async fn best_path_combines_a_granted_core_segment_with_public_up_and_down() {
593 let fixture = Fixture::new(
594 MockFetcher::new(Segments {
595 up_segments: vec![up_segment(0)],
596 down_segments: vec![down_segment(0)],
597 ..Segments::default()
598 }),
599 Duration::from_secs(100),
600 );
601 fixture.grant_for(
603 client_ip(),
604 sni().customer_domain(),
605 AuthSegments {
606 core_segments: vec![core_segment(0)],
607 ..AuthSegments::default()
608 },
609 at(0),
610 );
611
612 let used = fixture
613 .paths
614 .best_path(client_ip(), &sni(), leaf_ia(), other_leaf_ia(), at(0))
615 .await
616 .expect("path combination succeeds")
617 .expect("the granted core segment closes the gap between the public ones");
618
619 assert_eq!(
620 used.segments.as_slice(),
621 [
622 SegmentSourceId::Store(non_core_store_id(
623 &up_segment(0),
624 leaf_ia(),
625 other_leaf_ia()
626 )),
627 SegmentSourceId::Auth(granted_core_id(&core_segment(0))),
628 SegmentSourceId::Store(non_core_store_id(
629 &down_segment(0),
630 leaf_ia(),
631 other_leaf_ia()
632 )),
633 ],
634 "a granted core segment is reported as granted, not as public"
635 );
636
637 let watch = fixture
639 .paths
640 .watch_grants(client_ip(), sni().customer_domain(), &used, at(0))
641 .expect("the client is authorized for the path");
642 fixture.grant.clean(at(101));
643 assert!(watch.expired().now_or_never().is_some());
644 }
645
646 #[tokio::test]
647 async fn refresh_path_keeps_a_three_segment_path_intact() {
648 let (fixture, used) = three_segment_fixture().await;
649
650 let _guard = fixture
653 .paths
654 .hold_segments(&used, at(0))
655 .await
656 .expect("the public pair of the path can be held");
657
658 let refreshed_at = at(MAX_FETCH_INTERVAL.as_secs() + 1);
661 fixture.grant_for(
662 client_ip(),
663 sni().customer_domain(),
664 AuthSegments {
665 up_segments: vec![up_segment(600)],
666 down_segments: vec![down_segment(600)],
667 ..AuthSegments::default()
668 },
669 refreshed_at,
670 );
671 fixture.fetcher.set_segments(Segments {
672 core_segments: vec![core_segment(600)],
673 ..Segments::default()
674 });
675 fixture.segments.maintain(refreshed_at).await;
676
677 let refreshed = fixture
678 .paths
679 .refresh_path(&used, refreshed_at)
680 .await
681 .expect("refreshing succeeds");
682
683 assert_eq!(
684 refreshed.segments, used.segments,
685 "the refreshed path must use the very same segments, in the same order"
686 );
687 assert_eq!(
688 refreshed.path.fingerprint(),
689 used.path.fingerprint(),
690 "the refreshed path is the same path, so uplinks keep their key"
691 );
692 assert_eq!(
693 refreshed.expiration(),
694 used.expiration().map(|old| old + Duration::from_secs(600)),
695 "all three segments moved, so the whole path lives 600s longer"
696 );
697 }
698
699 #[tokio::test]
700 async fn refresh_path_fails_once_a_public_segment_is_evicted() {
701 let up = up_segment(0);
702 let fixture = Fixture::new(
703 MockFetcher::with_up_segments(vec![up.clone()]),
704 Duration::from_secs(10 * 24 * 3600),
705 );
706 fixture.grant_non_core(Vec::new(), at(0));
707
708 let used = fixture
709 .paths
710 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
711 .await
712 .expect("path combination succeeds")
713 .expect("the public segment yields a path");
714 assert_eq!(
715 used.segments.as_slice(),
716 [SegmentSourceId::Store(store_id(&up))]
717 );
718
719 let evicted_at = at(IDLE_EVICTION_TIME.as_secs() + 1);
721 fixture.segments.maintain(evicted_at).await;
722
723 assert!(
724 fixture.paths.refresh_path(&used, evicted_at).await.is_err(),
725 "a path over an evicted public segment cannot be rebuilt"
726 );
727 }
728
729 #[tokio::test]
730 async fn best_path_fails_when_public_segments_cannot_be_fetched() {
731 let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
732 fixture.grant_non_core(vec![up_segment(0)], at(0));
733 fixture.fetcher.set_failing(true);
734
735 assert!(
736 fixture
737 .paths
738 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
739 .await
740 .is_err(),
741 "a path cannot be computed without knowing the public segments"
742 );
743
744 fixture.fetcher.set_failing(false);
745 assert!(
746 fixture
747 .paths
748 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
749 .await
750 .expect("path combination succeeds")
751 .is_some(),
752 "and the failure leaves nothing behind that would keep it from succeeding later"
753 );
754 }
755
756 #[tokio::test]
757 async fn refresh_path_picks_up_a_later_expiration() {
758 let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(10 * 24 * 3600));
759 fixture.grant_non_core(vec![up_segment(0)], at(0));
760
761 let used = fixture
762 .paths
763 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
764 .await
765 .expect("path combination succeeds")
766 .expect("the granted segment yields a path");
767
768 let fresher = up_segment(600);
770 assert_eq!(
771 fresher.fingerprint(),
772 up_segment(0).fingerprint(),
773 "a fresher copy of a segment keeps its fingerprint"
774 );
775 fixture.grant_non_core(vec![fresher], at(0));
776
777 let refreshed = fixture
778 .paths
779 .refresh_path(&used, at(0))
780 .await
781 .expect("refreshing succeeds");
782
783 assert_eq!(
784 refreshed.segments, used.segments,
785 "the refreshed path uses the same segments"
786 );
787 assert_eq!(
788 refreshed.path.fingerprint(),
789 used.path.fingerprint(),
790 "the refreshed path is the same path, so uplinks keep their key"
791 );
792 assert_eq!(
793 refreshed.expiration(),
794 used.expiration().map(|old| old + Duration::from_secs(600)),
795 "the refreshed path lives 600s longer"
796 );
797 }
798
799 #[tokio::test]
800 async fn refresh_path_fails_once_the_grant_is_gone() {
801 let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
802 fixture.grant_non_core(vec![up_segment(0)], at(0));
803
804 let used = fixture
805 .paths
806 .best_path(client_ip(), &sni(), leaf_ia(), core_ia(), at(0))
807 .await
808 .expect("path combination succeeds")
809 .expect("the granted segment yields a path");
810
811 fixture.grant.clean(at(101));
812
813 assert!(
814 fixture.paths.refresh_path(&used, at(101)).await.is_err(),
815 "a path over a segment whose grant is gone cannot be refreshed"
816 );
817 }
818
819 #[tokio::test]
820 async fn best_path_rejects_wildcards_and_handles_local_paths() {
821 let fixture = Fixture::new(MockFetcher::empty(), Duration::from_secs(100));
822
823 assert!(
824 fixture
825 .paths
826 .best_path(client_ip(), &sni(), leaf_ia(), IsdAsn(0), at(0))
827 .await
828 .is_err()
829 );
830
831 assert!(
832 fixture
833 .paths
834 .best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
835 .await
836 .is_err(),
837 "an AS local path still requires a grant for the target"
838 );
839
840 fixture.grant_non_core(Vec::new(), at(0));
841
842 let used = fixture
843 .paths
844 .best_path(client_ip(), &sni(), leaf_ia(), leaf_ia(), at(0))
845 .await
846 .expect("path combination succeeds")
847 .expect("an AS local path always exists");
848 assert!(used.segments.is_empty());
849 }
850}