1use 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
30pub trait UnderlayDiscovery: Send + Sync {
34 fn underlays(&self, isd_as: IsdAsn) -> Vec<(IsdAsn, UnderlayInfo)>;
38 fn isd_ases(&self) -> HashSet<IsdAsn>;
40 fn resolve_udp_underlay_next_hop(&self, interface: PathInterface) -> Option<net::SocketAddr>;
42}
43
44#[derive(Clone, Debug)]
46#[non_exhaustive]
47pub struct ScionRouter {
48 internal_interface: net::SocketAddr,
50 interfaces: Vec<u16>,
52}
53
54impl ScionRouter {
55 #[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 #[must_use]
66 pub fn internal_interface(&self) -> net::SocketAddr {
67 self.internal_interface
68 }
69
70 #[must_use]
72 pub fn interfaces(&self) -> &[u16] {
73 &self.interfaces
74 }
75}
76
77#[derive(Clone, Debug)]
79#[non_exhaustive]
80pub enum UnderlayInfo {
81 Snap(Url),
83 Udp(Vec<ScionRouter>),
85}
86
87#[derive(Clone, Debug)]
89pub struct StaticUdpRouter {
90 pub isd_as: IsdAsn,
92 pub internal_interface: net::SocketAddr,
94 pub interfaces: Vec<u16>,
96}
97
98pub struct StaticUnderlayDiscovery {
107 underlays: Vec<(IsdAsn, UnderlayInfo)>,
108 udp_underlay_next_hops: HashMap<PathInterface, net::SocketAddr>,
109}
110
111impl StaticUnderlayDiscovery {
112 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 pub udp_underlay_next_hops: ArcSwap<HashMap<PathInterface, net::SocketAddr>>,
167}
168
169pub(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 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 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 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}