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