1use std::{borrow::Cow, collections::HashMap, net, sync::Arc, time::Duration};
17
18use endhost_api_client::client::{CrpcEndhostApiClient, EndhostApiClient};
19use endhost_api_models::underlays::{ScionRouter, Snap};
20use rand::SeedableRng;
21use rand_chacha::ChaCha8Rng;
22use scion_proto::address::{EndhostAddr, IsdAsn};
23pub use scion_sdk_reqwest_connect_rpc::client::CrpcClientError;
25use scion_sdk_reqwest_connect_rpc::token_source::TokenSource;
26use snap_control::client::{ControlPlaneApi, CrpcSnapControlClient};
27use tracing::info;
28use url::Url;
29
30use super::DynUnderlayStack;
31use crate::{
32 scionstack::{DefaultScmpHandler, ScionStack, ScmpHandler},
33 snap_tunnel::{SessionRenewal, SnapTunnel, SnapTunnelError},
34 underlays::{
35 snap::{NewSnapUnderlayStackError, SnapUnderlayStack},
36 udp::{
37 LocalIpResolver, TargetAddrLocalIpResolver, UdpUnderlayStack,
38 underlay_resolver::UdpUnderlayResolver,
39 },
40 },
41};
42
43const DEFAULT_RESERVED_TIME: Duration = Duration::from_secs(3);
44const DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL: Duration = Duration::from_secs(600);
45
46pub const DEFAULT_RECEIVE_CHANNEL_SIZE: usize = 1000;
49
50type ScmpHandlerFactory =
52 Box<dyn FnOnce(Arc<SnapTunnel>) -> Arc<dyn ScmpHandler> + Sync + Send + 'static>;
53
54pub struct ScionStackBuilder {
73 endhost_api_url: Url,
74 endhost_api_token_source: Option<Arc<dyn TokenSource>>,
75 auth_token_source: Option<Arc<dyn TokenSource>>,
76 underlay: Underlay,
77 snap: SnapUnderlayConfig,
78 udp: UdpUnderlayConfig,
79 receive_channel_size: usize,
80}
81
82impl ScionStackBuilder {
83 pub fn new(endhost_api_url: Url) -> Self {
88 Self {
89 endhost_api_url,
90 endhost_api_token_source: None,
91 auth_token_source: None,
92 underlay: Underlay::Discover {
93 preferred_underlay: PreferredUnderlay::Udp,
94 isd_as: IsdAsn::WILDCARD,
95 },
96 snap: SnapUnderlayConfig::default(),
97 udp: UdpUnderlayConfig::default(),
98 receive_channel_size: DEFAULT_RECEIVE_CHANNEL_SIZE,
99 }
100 }
101
102 pub fn with_prefer_snap(mut self) -> Self {
104 self.underlay = Underlay::Discover {
105 preferred_underlay: PreferredUnderlay::Snap,
106 isd_as: IsdAsn::WILDCARD,
107 };
108 self
109 }
110
111 pub fn with_prefer_udp(mut self) -> Self {
113 self.underlay = Underlay::Discover {
114 preferred_underlay: PreferredUnderlay::Udp,
115 isd_as: IsdAsn::WILDCARD,
116 };
117 self
118 }
119
120 pub fn with_discover_underlay_isd_as(mut self, isd_as: IsdAsn) -> Self {
122 if let Underlay::Discover {
123 preferred_underlay, ..
124 } = self.underlay
125 {
126 self.underlay = Underlay::Discover {
127 preferred_underlay,
128 isd_as,
129 };
130 }
131 self
132 }
133
134 pub fn with_static_snap_underlay(mut self, control_planes: Vec<Snap>) -> Self {
136 self.underlay = Underlay::Snap(control_planes);
137 self
138 }
139
140 pub fn with_static_udp_underlay(self, data_planes: Vec<ScionRouter>) -> Self {
142 Self {
143 underlay: Underlay::Udp(data_planes),
144 ..self
145 }
146 }
147
148 pub fn with_endhost_api_auth_token_source(mut self, source: impl TokenSource) -> Self {
150 self.endhost_api_token_source = Some(Arc::new(source));
151 self
152 }
153
154 pub fn with_endhost_api_auth_token(mut self, token: String) -> Self {
156 self.endhost_api_token_source = Some(Arc::new(token));
157 self
158 }
159
160 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
164 self.auth_token_source = Some(Arc::new(source));
165 self
166 }
167
168 pub fn with_auth_token(mut self, token: String) -> Self {
172 self.auth_token_source = Some(Arc::new(token));
173 self
174 }
175
176 pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
178 self.snap = config;
179 self
180 }
181
182 pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
184 self.udp = config;
185 self
186 }
187
188 pub async fn build(self) -> Result<ScionStack, BuildScionStackError> {
194 let ScionStackBuilder {
195 endhost_api_url,
196 endhost_api_token_source,
197 auth_token_source,
198 underlay,
199 snap,
200 udp,
201 receive_channel_size,
202 } = self;
203
204 let endhost_api_client = {
205 let mut client = CrpcEndhostApiClient::new(&endhost_api_url)
206 .map_err(BuildScionStackError::EndhostApiClientSetupError)?;
207 if let Some(token_source) = endhost_api_token_source.or(auth_token_source.clone()) {
208 client.use_token_source(token_source);
209 }
210 Arc::new(client)
211 };
212
213 let underlays = match underlay {
215 Underlay::Discover {
216 preferred_underlay,
217 isd_as,
218 } => {
219 discover_underlays(endhost_api_client.as_ref(), preferred_underlay, isd_as).await?
220 }
221 Underlay::Snap(control_planes) => {
222 if control_planes.is_empty() {
223 return Err(BuildScionStackError::UnderlayUnavailable(
224 "no snap control plane provided".into(),
225 ));
226 }
227 DiscoveredUnderlays::Snap(control_planes)
228 }
229 Underlay::Udp(routers) => {
230 if routers.is_empty() {
231 return Err(BuildScionStackError::UnderlayUnavailable(
232 "no udp router provided".into(),
233 ));
234 }
235 DiscoveredUnderlays::Udp(routers)
236 }
237 };
238
239 let underlay: Arc<dyn DynUnderlayStack> = match underlays {
241 DiscoveredUnderlays::Snap(control_planes) => {
242 let cp = control_planes
244 .first()
245 .ok_or(BuildScionStackError::UnderlayUnavailable(
247 "no snap control plane provided".into(),
248 ))?;
249 info!(%cp, "using snap underlay");
250 let default_scmp_handler = snap.default_scmp_handler.unwrap_or_else(|| {
252 Box::new(|tunnel| Arc::new(DefaultScmpHandler::new(tunnel)))
253 });
254 let mut snap_cp_client = CrpcSnapControlClient::new(&cp.address)
255 .map_err(BuildSnapScionStackError::ControlPlaneClientSetupError)?;
256 if let Some(token_source) = snap.snap_token_source.or(auth_token_source) {
257 snap_cp_client.use_token_source(token_source);
258 }
259 let snap_cp_client = Arc::new(snap_cp_client);
260
261 let session_grants = snap_cp_client
263 .create_data_plane_sessions()
264 .await
265 .map_err(BuildSnapScionStackError::DataPlaneDiscoveryError)?;
266 Arc::new(
267 SnapUnderlayStack::new(
268 snap_cp_client.clone(),
269 session_grants,
270 snap.requested_addresses,
271 snap.ports_rng.unwrap_or_else(ChaCha8Rng::from_os_rng),
272 snap.ports_reserved_time,
273 default_scmp_handler,
274 receive_channel_size,
275 snap.session_auto_renewal,
276 )
277 .await
278 .map_err(|e| {
279 match e {
280 NewSnapUnderlayStackError::SnapTunnelError(e) => {
281 BuildSnapScionStackError::DataPlaneConnectionError(e)
282 }
283 NewSnapUnderlayStackError::NoSessionGrants => {
284 BuildSnapScionStackError::DataPlaneUnavailable(
285 "create data plane sessions returned no session grants".into(),
286 )
287 }
288 }
289 })?,
290 )
291 }
292 DiscoveredUnderlays::Udp(data_planes) => {
293 info!(?data_planes, "using udp underlay");
294 let local_ip_resolver: Arc<dyn LocalIpResolver> = match udp.local_ips {
295 Some(ips) => Arc::new(ips),
296 None => {
297 Arc::new(
298 TargetAddrLocalIpResolver::new(endhost_api_url.clone())
299 .map_err(BuildUdpScionStackError::LocalIpResolutionError)?,
300 )
301 }
302 };
303
304 Arc::new(UdpUnderlayStack::new(
305 Arc::new(UdpUnderlayResolver::new(
306 endhost_api_client.clone(),
307 udp.udp_next_hop_resolver_fetch_interval,
308 data_planes
309 .into_iter()
310 .flat_map(|dp| {
311 dp.interfaces
312 .into_iter()
313 .map(move |i| ((dp.isd_as, i), dp.internal_interface))
314 })
315 .collect::<HashMap<(IsdAsn, u16), net::SocketAddr>>(),
316 )),
317 local_ip_resolver,
318 receive_channel_size,
319 ))
320 }
321 };
322
323 Ok(ScionStack::new(endhost_api_client, underlay))
324 }
325}
326
327#[derive(thiserror::Error, Debug)]
329pub enum BuildScionStackError {
330 #[error("no underlay available: {0}")]
332 UnderlayUnavailable(Cow<'static, str>),
333 #[error("underlay discovery request error: {0:#}")]
337 UnderlayDiscoveryError(CrpcClientError),
338 #[error("endhost API client setup error: {0:#}")]
340 EndhostApiClientSetupError(anyhow::Error),
341 #[error(transparent)]
344 Snap(#[from] BuildSnapScionStackError),
345 #[error(transparent)]
348 Udp(#[from] BuildUdpScionStackError),
349 #[error("internal error: {0:#}")]
351 Internal(anyhow::Error),
352}
353
354#[derive(thiserror::Error, Debug)]
356pub enum BuildSnapScionStackError {
357 #[error("no SNAP data plane available: {0}")]
359 DataPlaneUnavailable(Cow<'static, str>),
360 #[error("control plane client setup error: {0:#}")]
362 ControlPlaneClientSetupError(anyhow::Error),
363 #[error("data plane discovery request error: {0:#}")]
365 DataPlaneDiscoveryError(CrpcClientError),
366 #[error("error connecting to data plane: {0:#}")]
368 DataPlaneConnectionError(#[from] SnapTunnelError),
369}
370
371#[derive(thiserror::Error, Debug)]
373pub enum BuildUdpScionStackError {
374 #[error("local IP resolution error: {0:#}")]
376 LocalIpResolutionError(anyhow::Error),
377}
378
379enum PreferredUnderlay {
380 Snap,
381 Udp,
382}
383
384enum Underlay {
385 Discover {
386 preferred_underlay: PreferredUnderlay,
387 isd_as: IsdAsn,
389 },
390 Snap(Vec<Snap>),
391 Udp(Vec<ScionRouter>),
392}
393
394pub struct SnapUnderlayConfig {
396 snap_token_source: Option<Arc<dyn TokenSource>>,
397 requested_addresses: Vec<EndhostAddr>,
398 default_scmp_handler: Option<ScmpHandlerFactory>,
399 snap_dp_index: usize,
400 session_auto_renewal: Option<SessionRenewal>,
401 ports_rng: Option<ChaCha8Rng>,
402 ports_reserved_time: Duration,
403}
404
405impl Default for SnapUnderlayConfig {
406 fn default() -> Self {
407 Self {
408 snap_token_source: None,
409 requested_addresses: vec![],
410 ports_reserved_time: DEFAULT_RESERVED_TIME,
411 snap_dp_index: 0,
412 default_scmp_handler: None,
413 session_auto_renewal: Some(SessionRenewal::default()),
414 ports_rng: None,
415 }
416 }
417}
418
419impl SnapUnderlayConfig {
420 pub fn builder() -> SnapUnderlayConfigBuilder {
422 SnapUnderlayConfigBuilder(Self::default())
423 }
424}
425
426pub struct SnapUnderlayConfigBuilder(SnapUnderlayConfig);
428
429impl SnapUnderlayConfigBuilder {
430 pub fn with_auth_token(mut self, token: String) -> Self {
432 self.0.snap_token_source = Some(Arc::new(token));
433 self
434 }
435
436 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
438 self.0.snap_token_source = Some(Arc::new(source));
439 self
440 }
441
442 pub fn with_requested_addresses(mut self, requested_addresses: Vec<EndhostAddr>) -> Self {
451 self.0.requested_addresses = requested_addresses;
452 self
453 }
454
455 pub fn with_ports_rng(mut self, rng: ChaCha8Rng) -> Self {
461 self.0.ports_rng = Some(rng);
462 self
463 }
464
465 pub fn with_ports_reserved_time(mut self, reserved_time: Duration) -> Self {
471 self.0.ports_reserved_time = reserved_time;
472 self
473 }
474
475 pub fn with_default_scmp_handler(mut self, default_scmp_handler: ScmpHandlerFactory) -> Self {
481 self.0.default_scmp_handler = Some(Box::new(default_scmp_handler));
482 self
483 }
484
485 pub fn with_session_auto_renewal(mut self, interval: Duration) -> Self {
491 self.0.session_auto_renewal = Some(SessionRenewal::new(interval));
492 self
493 }
494
495 pub fn with_snap_dp_index(mut self, dp_index: usize) -> Self {
501 self.0.snap_dp_index = dp_index;
502 self
503 }
504
505 pub fn build(self) -> SnapUnderlayConfig {
511 self.0
512 }
513}
514
515pub struct UdpUnderlayConfig {
517 udp_next_hop_resolver_fetch_interval: Duration,
518 local_ips: Option<Vec<net::IpAddr>>,
519}
520
521impl Default for UdpUnderlayConfig {
522 fn default() -> Self {
523 Self {
524 udp_next_hop_resolver_fetch_interval: DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL,
525 local_ips: None,
526 }
527 }
528}
529
530impl UdpUnderlayConfig {
531 pub fn builder() -> UdpUnderlayConfigBuilder {
533 UdpUnderlayConfigBuilder(Self::default())
534 }
535}
536
537pub struct UdpUnderlayConfigBuilder(UdpUnderlayConfig);
539
540impl UdpUnderlayConfigBuilder {
541 pub fn with_local_ips(mut self, local_ips: Vec<net::IpAddr>) -> Self {
544 self.0.local_ips = Some(local_ips);
545 self
546 }
547
548 pub fn with_udp_next_hop_resolver_fetch_interval(mut self, fetch_interval: Duration) -> Self {
551 self.0.udp_next_hop_resolver_fetch_interval = fetch_interval;
552 self
553 }
554
555 pub fn build(self) -> UdpUnderlayConfig {
557 self.0
558 }
559}
560
561#[derive(Debug)]
562enum DiscoveredUnderlays {
563 Snap(Vec<Snap>),
564 Udp(Vec<ScionRouter>),
565}
566
567async fn discover_underlays(
569 client: &dyn EndhostApiClient,
570 preferred_underlay: PreferredUnderlay,
571 isd_as: IsdAsn,
572) -> Result<DiscoveredUnderlays, BuildScionStackError> {
573 let res = client
575 .list_underlays(isd_as)
576 .await
577 .map_err(BuildScionStackError::UnderlayDiscoveryError)?;
578 let (has_udp, has_snap) = (!res.udp_underlay.is_empty(), !res.snap_underlay.is_empty());
579
580 match (has_udp, has_snap) {
581 (true, true) => {
582 match preferred_underlay {
583 PreferredUnderlay::Snap => Ok(DiscoveredUnderlays::Snap(res.snap_underlay)),
584 PreferredUnderlay::Udp => Ok(DiscoveredUnderlays::Udp(res.udp_underlay)),
585 }
586 }
587 (true, false) => Ok(DiscoveredUnderlays::Udp(res.udp_underlay)),
588 (false, true) => Ok(DiscoveredUnderlays::Snap(res.snap_underlay)),
589 (false, false) => {
590 Err(BuildScionStackError::UnderlayUnavailable(
591 "discovery returned no underlay".into(),
592 ))
593 }
594 }
595}