Skip to main content

scion_stack/path/
fetcher.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A [`PathFetcher`] is responsible for providing paths between two ISD-ASes.
16//!
17//! The default implementation [`PathFetcherImpl`] uses a [`SegmentFetcher`] to fetch path segments
18//! and combine them into end-to-end paths.
19
20use std::sync::Arc;
21
22use endhost_api_client::client::EndhostApiClient;
23use sciparse::{identifier::isd_asn::IsdAsn, path::ScionPath};
24
25use crate::path::fetcher::traits::{
26    PathFetchError, PathFetcher, SegmentFetchError, SegmentFetcher, Segments,
27};
28
29/// Path fetcher traits and types.
30pub mod traits {
31    use std::{borrow::Cow, future::Future};
32
33    use sciparse::{identifier::isd_asn::IsdAsn, path::ScionPath, segment::SignedPathSegment};
34
35    /// Path fetcher trait.
36    pub trait PathFetcher: Send + Sync + 'static {
37        /// Fetch paths between source and destination ISD-AS.
38        fn fetch_paths(
39            &self,
40            src: IsdAsn,
41            dst: IsdAsn,
42        ) -> impl Future<Output = Result<Vec<ScionPath>, PathFetchError>> + Send + '_;
43    }
44
45    /// Path fetch errors.
46    #[derive(Debug, thiserror::Error)]
47    #[non_exhaustive]
48    pub enum PathFetchError {
49        /// Segment fetch failed.
50        #[error("failed to fetch segments: {0}")]
51        FetchSegments(#[from] SegmentFetchError),
52
53        /// No paths found.
54        #[error("no paths found")]
55        NoPathsFound,
56
57        /// Non network related internal error.
58        #[error("internal error: {0}")]
59        InternalError(Cow<'static, str>),
60    }
61
62    /// Segment fetcher trait.
63    #[async_trait::async_trait]
64    pub trait SegmentFetcher: Send + Sync + 'static {
65        /// Fetch path segments between src and dst.
66        async fn fetch_segments(
67            &self,
68            src: IsdAsn,
69            dst: IsdAsn,
70        ) -> Result<Segments, SegmentFetchError>;
71    }
72
73    /// Error type returned by a [`SegmentFetcher`].
74    ///
75    /// A boxed error so each implementor can return its own error type; it is threaded through
76    /// [`PathFetchError::FetchSegments`], preserving the source chain.
77    // Deliberately a boxed trait object rather than a concrete enum: `SegmentFetcher` is
78    // user-implemented, so the crate does not dictate the error type. See API_CONVENTIONS.md.
79    pub type SegmentFetchError = Box<dyn std::error::Error + Send + Sync>;
80
81    /// Path segments.
82    #[derive(Debug)]
83    #[non_exhaustive]
84    pub struct Segments {
85        /// Core segments.
86        pub core_segments: Vec<SignedPathSegment>,
87        /// Non-core segments.
88        pub non_core_segments: Vec<SignedPathSegment>,
89    }
90
91    impl Segments {
92        /// Creates a new set of path segments.
93        #[must_use]
94        pub fn new(
95            core_segments: Vec<SignedPathSegment>,
96            non_core_segments: Vec<SignedPathSegment>,
97        ) -> Self {
98            Self {
99                core_segments,
100                non_core_segments,
101            }
102        }
103    }
104}
105
106/// Path fetcher.
107pub struct PathFetcherImpl {
108    segment_fetchers: Vec<(String, Arc<dyn SegmentFetcher>)>,
109    // Timeout for each segment fetcher to avoid waiting indefinitely for slow or unresponsive
110    // fetchers.
111    timeout: std::time::Duration,
112}
113
114impl PathFetcherImpl {
115    /// Creates a new path fetcher.
116    pub fn new(
117        segment_fetchers: Vec<(String, Arc<dyn SegmentFetcher>)>,
118        timeout: std::time::Duration,
119    ) -> Self {
120        Self {
121            segment_fetchers,
122            timeout,
123        }
124    }
125}
126
127impl PathFetcher for PathFetcherImpl {
128    async fn fetch_paths(
129        &self,
130        src: IsdAsn,
131        dst: IsdAsn,
132    ) -> Result<Vec<ScionPath>, PathFetchError> {
133        let mut all_core_segments = Vec::new();
134        let mut all_non_core_segments = Vec::new();
135
136        // Fetch segments from all fetchers concurrently.
137        let fetch_tasks: Vec<_> = self
138            .segment_fetchers
139            .iter()
140            .map(|(_, fetcher)| {
141                tokio::time::timeout(self.timeout, fetcher.fetch_segments(src, dst))
142            })
143            .collect();
144
145        let results = futures::future::join_all(fetch_tasks).await;
146
147        // Track errors and successes
148        let mut errors = Vec::new();
149
150        for (i, result) in results.into_iter().enumerate() {
151            let fetcher_name = &self.segment_fetchers[i].0;
152            match result {
153                Ok(res) => {
154                    match res {
155                        Ok(segments) => {
156                            tracing::info!(
157                                name = %fetcher_name,
158                                n_core_segments = segments.core_segments.len(),
159                                n_non_core_segments = segments.non_core_segments.len(),
160                                %src,
161                                %dst,
162                                "Segment fetcher succeeded"
163                            );
164                            all_core_segments.extend(segments.core_segments);
165                            all_non_core_segments.extend(segments.non_core_segments);
166                        }
167                        Err(e) => {
168                            errors.push((fetcher_name.clone(), e));
169                        }
170                    }
171                }
172                Err(e) => {
173                    errors.push((
174                        fetcher_name.clone(),
175                        Box::new(e) as Box<dyn std::error::Error + Send + Sync>,
176                    ));
177                }
178            }
179        }
180
181        let paths =
182            sciparse::path::combinator::combine(src, dst, all_core_segments, all_non_core_segments);
183
184        for (fetcher_name, error) in &errors {
185            tracing::warn!(
186                name = %fetcher_name,
187                %error,
188                %src,
189                %dst,
190                "Segment fetcher failed"
191            );
192        }
193
194        // If there were errors but we still have paths, we still return the paths and only log the
195        // fetcher errors.
196        if let Some((_name, err)) = errors.into_iter().next()
197            && paths.is_empty()
198        {
199            return Err(PathFetchError::FetchSegments(err));
200        }
201
202        Ok(paths)
203    }
204}
205
206/// Segment fetcher that uses the endhost API via Connect-RPC to fetch segments.
207pub struct EndhostApiSegmentFetcher {
208    client: Arc<dyn EndhostApiClient>,
209}
210
211impl EndhostApiSegmentFetcher {
212    /// Creates a new endhost API segment fetcher.
213    pub fn new(client: Arc<dyn EndhostApiClient>) -> Self {
214        Self { client }
215    }
216}
217
218#[async_trait::async_trait]
219impl SegmentFetcher for EndhostApiSegmentFetcher {
220    async fn fetch_segments(
221        &self,
222        src: IsdAsn,
223        dst: IsdAsn,
224    ) -> Result<Segments, SegmentFetchError> {
225        let resp = self
226            .client
227            .list_segments(src, dst, 128, String::new())
228            .await?;
229
230        tracing::trace!(
231            n_core=resp.segments.core_segments.len(),
232            n_up=resp.segments.up_segments.len(),
233            n_down=resp.segments.down_segments.len(),
234            src = %src,
235            dst = %dst,
236            "Received segments from endhost API"
237        );
238
239        let (core_segments, non_core_segments) = resp.segments.split_parts();
240        Ok(Segments {
241            core_segments: core_segments.into_iter().collect(),
242            non_core_segments: non_core_segments.into_iter().collect(),
243        })
244    }
245}