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 scion_proto::{address::IsdAsn, path::PathInterface};
25use scion_sdk_reqwest_connect_rpc::client::CrpcClientError;
26use scion_sdk_utils::backoff::ExponentialBackoff;
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)]
46pub struct ScionRouter {
47    /// The internal interface socket address of the SCION router.
48    internal_interface: net::SocketAddr,
49    /// The list of SCION interfaces available on the SCION router.
50    interfaces: Vec<u16>,
51}
52
53/// Information about a discovered underlay.
54#[derive(Clone, Debug)]
55pub enum UnderlayInfo {
56    /// A snap control plane API address.
57    Snap(Url),
58    /// A SCION router.
59    Udp(Vec<ScionRouter>),
60}
61
62struct PeriodicUnderlayDiscoveryInner {
63    pub underlays: ArcSwap<Vec<(IsdAsn, UnderlayInfo)>>,
64    /// Map of underlay next hops to make the lookup faster.
65    pub udp_underlay_next_hops: ArcSwap<HashMap<PathInterface, net::SocketAddr>>,
66}
67
68/// Implementation of the UnderlayDiscovery trait that periodically discovers underlays.
69/// When created starts a background task that periodically discovers underlays
70/// and updates the underlays.
71pub struct PeriodicUnderlayDiscovery {
72    inner: Arc<PeriodicUnderlayDiscoveryInner>,
73    task: JoinHandle<()>,
74}
75
76impl Drop for PeriodicUnderlayDiscovery {
77    fn drop(&mut self) {
78        self.task.abort();
79    }
80}
81
82impl UnderlayDiscovery for PeriodicUnderlayDiscovery {
83    fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)> {
84        self.inner
85            .underlays
86            .load()
87            .iter()
88            .filter(|(ia, _)| isd_as.matches(*ia))
89            .map(|(ia, info)| (*ia, info.clone()))
90            .collect()
91    }
92
93    fn isd_ases(&self) -> HashSet<IsdAsn> {
94        HashSet::from_iter(self.inner.underlays.load().iter().map(|(ia, _)| *ia))
95    }
96
97    fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr> {
98        self.inner
99            .udp_underlay_next_hops
100            .load()
101            .get(&interface)
102            .cloned()
103    }
104}
105
106impl PeriodicUnderlayDiscovery {
107    /// Creates a new periodic underlay discovery.
108    /// Does an initial underlay discovery and returns an error if it fails.
109    pub async fn new(
110        api_client: Arc<dyn EndhostApiClient>,
111        fetch_interval: Duration,
112        backoff: ExponentialBackoff,
113    ) -> Result<Self, CrpcClientError> {
114        let (initial_underlays, initial_udp_underlay_next_hops) =
115            discover_underlays(&api_client).await?;
116        tracing::debug!(
117            underlays=?initial_underlays,
118            "Successfully discovered initial underlays"
119        );
120        let inner = Arc::new(PeriodicUnderlayDiscoveryInner {
121            underlays: ArcSwap::new(Arc::new(initial_underlays)),
122            udp_underlay_next_hops: ArcSwap::new(Arc::new(initial_udp_underlay_next_hops)),
123        });
124
125        let inner_clone = inner.clone();
126        let task = tokio::spawn(async move {
127            loop {
128                tokio::time::sleep(fetch_interval).await;
129                let mut failed_attempts = 0;
130                loop {
131                    match discover_underlays(&api_client).await {
132                        Ok((underlays, udp_underlay_next_hops)) => {
133                            tracing::debug!(
134                                underlays=?underlays,
135                                "Successfully discovered underlays"
136                            );
137                            inner_clone.underlays.store(Arc::new(underlays));
138                            inner_clone
139                                .udp_underlay_next_hops
140                                .store(Arc::new(udp_underlay_next_hops));
141                            break;
142                        }
143                        Err(e) => {
144                            failed_attempts += 1;
145                            tracing::warn!(err = ?e, attempt = failed_attempts, "Failed to discover underlays");
146                            tokio::time::sleep(backoff.duration(failed_attempts)).await;
147                        }
148                    }
149                }
150            }
151        });
152
153        Ok(Self { inner, task })
154    }
155}
156
157async fn discover_underlays(
158    api_client: &Arc<dyn EndhostApiClient>,
159) -> Result<
160    (
161        Vec<(IsdAsn, UnderlayInfo)>,
162        HashMap<PathInterface, net::SocketAddr>,
163    ),
164    CrpcClientError,
165> {
166    let res = api_client.list_underlays(IsdAsn::WILDCARD.into()).await?;
167    let mut udp_underlays = HashMap::new();
168    for underlay in res.udp_underlay.into_iter() {
169        let entry = udp_underlays.entry(underlay.isd_as).or_insert(vec![]);
170        entry.push(ScionRouter {
171            internal_interface: underlay.internal_interface,
172            interfaces: underlay.interfaces,
173        });
174    }
175
176    // Create a direct lookup map for the UDP underlay next hops.
177    let mut udp_underlay_next_hops = HashMap::new();
178    for (isd_as, routers) in udp_underlays.iter() {
179        for router in routers.iter() {
180            for interface_id in router.interfaces.iter() {
181                udp_underlay_next_hops.insert(
182                    PathInterface {
183                        isd_asn: (*isd_as).into(),
184                        id: *interface_id,
185                    },
186                    router.internal_interface,
187                );
188            }
189        }
190    }
191
192    // Create the underlays list.
193    let mut underlays: Vec<(IsdAsn, UnderlayInfo)> = udp_underlays
194        .into_iter()
195        .map(|(isd_as, routers)| (isd_as.into(), UnderlayInfo::Udp(routers)))
196        .collect();
197    for underlay in res.snap_underlay.iter() {
198        for isd_as in underlay.isd_ases.iter() {
199            underlays.push((
200                (*isd_as).into(),
201                UnderlayInfo::Snap(underlay.address.clone()),
202            ));
203        }
204    }
205    Ok((underlays, udp_underlay_next_hops))
206}