Skip to main content

scion_stack/underlays/
discovery.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//! Underlay discovery.
15use std::{
16    collections::{HashMap, HashSet},
17    net::{self},
18    sync::Arc,
19    time::Duration,
20};
21
22use arc_swap::ArcSwap;
23use endhost_api_client::client::EndhostApiClient;
24use reqwest_connect_rpc::client::CrpcClientError;
25use scion_sdk_utils::backoff::ExponentialBackoff;
26use sciparse::{identifier::isd_asn::IsdAsn, path::metadata::path_interface::PathInterface};
27use tokio::task::JoinHandle;
28use url::Url;
29
30/// Trait that exposes available SCION underlay data planes.
31/// The returned underlays are expected to be up too date and
32/// sorted by priority.
33pub trait UnderlayDiscovery: Send + Sync {
34    /// Returns discovered underlays that match the given ISD-AS.
35    /// Wildcard ISD-ASes are supported.
36    /// The underlays are returned in the order of priority.
37    fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)>;
38    /// Returns the set of ISD-ASes for which underlays are available.
39    fn isd_ases(&self) -> HashSet<IsdAsn>;
40    /// Resolves the next hop for the given ISD-AS and interface ID.
41    fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr>;
42}
43
44/// Underlay discovery information for a SCION router.
45#[derive(Clone, Debug)]
46#[non_exhaustive]
47pub struct ScionRouter {
48    /// The internal interface socket address of the SCION router.
49    internal_interface: net::SocketAddr,
50    /// The list of SCION interfaces available on the SCION router.
51    interfaces: Vec<u16>,
52}
53
54impl ScionRouter {
55    /// Creates a new SCION router entry.
56    #[must_use]
57    pub fn new(internal_interface: net::SocketAddr, interfaces: Vec<u16>) -> Self {
58        Self {
59            internal_interface,
60            interfaces,
61        }
62    }
63
64    /// The internal interface socket address of the SCION router (the UDP next hop).
65    #[must_use]
66    pub fn internal_interface(&self) -> net::SocketAddr {
67        self.internal_interface
68    }
69
70    /// The SCION interface IDs reachable via this router.
71    #[must_use]
72    pub fn interfaces(&self) -> &[u16] {
73        &self.interfaces
74    }
75}
76
77/// Information about a discovered underlay.
78#[derive(Clone, Debug)]
79#[non_exhaustive]
80pub enum UnderlayInfo {
81    /// A snap control plane API address.
82    Snap(Url),
83    /// A SCION router.
84    Udp(Vec<ScionRouter>),
85}
86
87/// A single SCION router entry used to build a [`StaticUnderlayDiscovery`].
88#[derive(Clone, Debug)]
89pub struct StaticUdpRouter {
90    /// The ISD-AS the router belongs to.
91    pub isd_as: IsdAsn,
92    /// The internal interface socket address of the SCION router (the UDP next hop).
93    pub internal_interface: net::SocketAddr,
94    /// The SCION interface IDs reachable via this router.
95    pub interfaces: Vec<u16>,
96}
97
98/// Implementation of the [`UnderlayDiscovery`] trait backed by a static, in-memory topology.
99///
100/// Unlike the endhost-API-backed discovery used by [`ScionStackBuilder::build`], this does not
101/// contact an endhost API and runs no background task. It is intended for fully-local UDP stacks
102/// where the topology (SCION routers and the interfaces they serve) is known up front, e.g. from
103/// configuration.
104///
105/// [`ScionStackBuilder::build`]: crate::ScionStackBuilder::build
106pub struct StaticUnderlayDiscovery {
107    underlays: Vec<(IsdAsn, UnderlayInfo)>,
108    udp_underlay_next_hops: HashMap<PathInterface, net::SocketAddr>,
109}
110
111impl StaticUnderlayDiscovery {
112    /// Creates a new static underlay discovery from the given SCION routers.
113    ///
114    /// Routers are grouped by ISD-AS into a single [`UnderlayInfo::Udp`] entry per ISD-AS, and a
115    /// direct next-hop lookup is built mapping each served [`PathInterface`] to the router's
116    /// internal interface.
117    pub fn new(routers: impl IntoIterator<Item = StaticUdpRouter>) -> Self {
118        let mut grouped: HashMap<IsdAsn, Vec<ScionRouter>> = HashMap::new();
119        let mut udp_underlay_next_hops = HashMap::new();
120        for router in routers {
121            for interface_id in &router.interfaces {
122                udp_underlay_next_hops.insert(
123                    PathInterface::new(router.isd_as, *interface_id),
124                    router.internal_interface,
125                );
126            }
127            grouped.entry(router.isd_as).or_default().push(ScionRouter {
128                internal_interface: router.internal_interface,
129                interfaces: router.interfaces,
130            });
131        }
132
133        let underlays = grouped
134            .into_iter()
135            .map(|(isd_as, routers)| (isd_as, UnderlayInfo::Udp(routers)))
136            .collect();
137
138        Self {
139            underlays,
140            udp_underlay_next_hops,
141        }
142    }
143}
144
145impl UnderlayDiscovery for StaticUnderlayDiscovery {
146    fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)> {
147        self.underlays
148            .iter()
149            .filter(|(ia, _)| isd_as.matches(*ia))
150            .map(|(ia, info)| (*ia, info.clone()))
151            .collect()
152    }
153
154    fn isd_ases(&self) -> HashSet<IsdAsn> {
155        self.underlays.iter().map(|(ia, _)| *ia).collect()
156    }
157
158    fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr> {
159        self.udp_underlay_next_hops.get(&interface).copied()
160    }
161}
162
163struct PeriodicUnderlayDiscoveryInner {
164    pub underlays: ArcSwap<Vec<(IsdAsn, UnderlayInfo)>>,
165    /// Map of underlay next hops to make the lookup faster.
166    pub udp_underlay_next_hops: ArcSwap<HashMap<PathInterface, net::SocketAddr>>,
167}
168
169/// Implementation of the `UnderlayDiscovery` trait that periodically discovers underlays.
170/// When created starts a background task that periodically discovers underlays
171/// and updates the underlays.
172pub(crate) struct PeriodicUnderlayDiscovery {
173    inner: Arc<PeriodicUnderlayDiscoveryInner>,
174    task: JoinHandle<()>,
175}
176
177impl Drop for PeriodicUnderlayDiscovery {
178    fn drop(&mut self) {
179        self.task.abort();
180    }
181}
182
183impl UnderlayDiscovery for PeriodicUnderlayDiscovery {
184    fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)> {
185        self.inner
186            .underlays
187            .load()
188            .iter()
189            .filter(|(ia, _)| isd_as.matches(*ia))
190            .map(|(ia, info)| (*ia, info.clone()))
191            .collect()
192    }
193
194    fn isd_ases(&self) -> HashSet<IsdAsn> {
195        self.inner
196            .underlays
197            .load()
198            .iter()
199            .map(|(ia, _)| *ia)
200            .collect()
201    }
202
203    fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr> {
204        self.inner
205            .udp_underlay_next_hops
206            .load()
207            .get(&interface)
208            .copied()
209    }
210}
211
212impl PeriodicUnderlayDiscovery {
213    /// Creates a new periodic underlay discovery.
214    /// Does an initial underlay discovery and returns an error if it fails.
215    pub(crate) async fn new(
216        api_client: Arc<dyn EndhostApiClient>,
217        fetch_interval: Duration,
218        backoff: ExponentialBackoff,
219    ) -> Result<Self, CrpcClientError> {
220        let (initial_underlays, initial_udp_underlay_next_hops) =
221            discover_underlays(&api_client).await?;
222        tracing::debug!(
223            underlays=?initial_underlays,
224            "Successfully discovered initial underlays"
225        );
226        let inner = Arc::new(PeriodicUnderlayDiscoveryInner {
227            underlays: ArcSwap::new(Arc::new(initial_underlays)),
228            udp_underlay_next_hops: ArcSwap::new(Arc::new(initial_udp_underlay_next_hops)),
229        });
230
231        let inner_clone = inner.clone();
232        let task = tokio::spawn(async move {
233            loop {
234                tokio::time::sleep(fetch_interval).await;
235                let mut failed_attempts = 0;
236                loop {
237                    match discover_underlays(&api_client).await {
238                        Ok((underlays, udp_underlay_next_hops)) => {
239                            tracing::debug!(
240                                underlays=?underlays,
241                                "Successfully discovered underlays"
242                            );
243                            inner_clone.underlays.store(Arc::new(underlays));
244                            inner_clone
245                                .udp_underlay_next_hops
246                                .store(Arc::new(udp_underlay_next_hops));
247                            break;
248                        }
249                        Err(e) => {
250                            failed_attempts += 1;
251                            tracing::warn!(err = ?e, attempt = failed_attempts, "Failed to discover underlays");
252                            tokio::time::sleep(backoff.duration(failed_attempts)).await;
253                        }
254                    }
255                }
256            }
257        });
258
259        Ok(Self { inner, task })
260    }
261}
262
263async fn discover_underlays(
264    api_client: &Arc<dyn EndhostApiClient>,
265) -> Result<
266    (
267        Vec<(IsdAsn, UnderlayInfo)>,
268        HashMap<PathInterface, net::SocketAddr>,
269    ),
270    CrpcClientError,
271> {
272    let res = api_client.list_underlays(IsdAsn::WILDCARD).await?;
273    let mut udp_underlays = HashMap::new();
274    for underlay in res.udp_underlay {
275        let entry = udp_underlays.entry(underlay.isd_as).or_insert(vec![]);
276        entry.push(ScionRouter {
277            internal_interface: underlay.internal_interface,
278            interfaces: underlay.interfaces,
279        });
280    }
281
282    // Create a direct lookup map for the UDP underlay next hops.
283    let mut udp_underlay_next_hops = HashMap::new();
284    for (isd_as, routers) in &udp_underlays {
285        for router in routers {
286            for interface_id in &router.interfaces {
287                udp_underlay_next_hops.insert(
288                    PathInterface {
289                        isd_asn: (*isd_as),
290                        id: *interface_id,
291                    },
292                    router.internal_interface,
293                );
294            }
295        }
296    }
297
298    // Create the underlays list.
299    let mut underlays: Vec<(IsdAsn, UnderlayInfo)> = udp_underlays
300        .into_iter()
301        .map(|(isd_as, routers)| (isd_as, UnderlayInfo::Udp(routers)))
302        .collect();
303    for underlay in &res.snap_underlay {
304        for isd_as in &underlay.isd_ases {
305            underlays.push(((*isd_as), UnderlayInfo::Snap(underlay.address.clone())));
306        }
307    }
308    Ok((underlays, udp_underlay_next_hops))
309}
310
311#[cfg(test)]
312mod tests {
313    use std::str::FromStr;
314
315    use super::*;
316
317    fn isd_as(s: &str) -> IsdAsn {
318        IsdAsn::from_str(s).unwrap()
319    }
320
321    fn sock(s: &str) -> net::SocketAddr {
322        s.parse().unwrap()
323    }
324
325    #[test]
326    fn static_discovery_resolves_configured_next_hops() {
327        let ia = isd_as("1-ff00:0:110");
328        let router = sock("127.0.0.1:30041");
329        let discovery = StaticUnderlayDiscovery::new([StaticUdpRouter {
330            isd_as: ia,
331            internal_interface: router,
332            interfaces: vec![1, 2],
333        }]);
334
335        assert_eq!(
336            discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 1)),
337            Some(router)
338        );
339        assert_eq!(
340            discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 2)),
341            Some(router)
342        );
343    }
344
345    #[test]
346    fn static_discovery_unknown_interface_has_no_next_hop() {
347        let ia = isd_as("1-ff00:0:110");
348        let discovery = StaticUnderlayDiscovery::new([StaticUdpRouter {
349            isd_as: ia,
350            internal_interface: sock("127.0.0.1:30041"),
351            interfaces: vec![1],
352        }]);
353
354        assert_eq!(
355            discovery.resolve_udp_underlay_next_hop(PathInterface::new(ia, 99)),
356            None
357        );
358        assert_eq!(
359            discovery.resolve_udp_underlay_next_hop(PathInterface::new(isd_as("1-ff00:0:111"), 1)),
360            None
361        );
362    }
363
364    #[test]
365    fn static_discovery_reports_known_isd_ases_and_udp_underlays() {
366        let ia1 = isd_as("1-ff00:0:110");
367        let ia2 = isd_as("1-ff00:0:111");
368        let discovery = StaticUnderlayDiscovery::new([
369            StaticUdpRouter {
370                isd_as: ia1,
371                internal_interface: sock("127.0.0.1:30041"),
372                interfaces: vec![1],
373            },
374            StaticUdpRouter {
375                isd_as: ia2,
376                internal_interface: sock("127.0.0.2:30041"),
377                interfaces: vec![5],
378            },
379        ]);
380
381        let isd_ases = discovery.isd_ases();
382        assert_eq!(isd_ases.len(), 2);
383        assert!(isd_ases.contains(&ia1));
384        assert!(isd_ases.contains(&ia2));
385
386        let underlays = discovery.underlays(ia1);
387        assert_eq!(underlays.len(), 1);
388        assert_eq!(underlays[0].0, ia1);
389        assert!(matches!(underlays[0].1, UnderlayInfo::Udp(_)));
390    }
391}