1mod priority_connect;
17
18use std::{borrow::Cow, fmt, net, sync::Arc, time::Duration};
19
20use endhost_api_client::client::CrpcEndhostApiClient;
21use rand::seq::IndexedRandom;
22use reqwest_connect_rpc::{
23 client::CrpcClientError,
24 token_source::{TokenSource, static_token::StaticTokenSource},
25};
26use scion_sdk_utils::backoff::ExponentialBackoff;
27use url::Url;
28use x25519_dalek::StaticSecret;
29
30pub use crate::underlays::udp::{OutboundIpResolver, TargetAddrOutboundIpResolver};
31use crate::{
32 ea_source::{
33 EndhostApiSource, EndhostApiSourceError, StaticEndhostApiDiscovery, StaticEndhostApis,
34 },
35 path::fetcher::{EndhostApiSegmentFetcher, traits::SegmentFetcher},
36 stack::ScionStack,
37 underlays::{
38 SnapSocketConfig, UnderlayStack,
39 discovery::{PeriodicUnderlayDiscovery, UnderlayDiscovery},
40 },
41};
42
43const DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL: Duration = Duration::from_secs(600);
44const DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS: usize = 5;
45const DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP: usize = 2;
46const DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY: Duration = Duration::from_millis(500);
47
48type OutboundIpResolverFactory = Box<dyn FnOnce(Url) -> Arc<dyn OutboundIpResolver> + Send>;
50
51pub struct ScionStackBuilder {
71 crpc_client: Option<reqwest::Client>,
72 endhost_api_token_source: Option<Arc<dyn TokenSource>>,
73 auth_token_source: Option<Arc<dyn TokenSource>>,
74 endhost_api_source: Arc<dyn EndhostApiSource>,
75 preferred_underlay: PreferredUnderlay,
76 endhost_api_discovery: EndhostApiDiscoveryConfig,
77 snap: SnapUnderlayConfig,
78 udp: UdpUnderlayConfig,
79}
80
81impl ScionStackBuilder {
82 #[must_use]
87 pub fn new() -> Self {
88 Self {
89 crpc_client: None,
90 endhost_api_token_source: None,
91 auth_token_source: None,
92 endhost_api_source: Arc::new(StaticEndhostApiDiscovery::global()),
93 preferred_underlay: PreferredUnderlay::Udp,
94 endhost_api_discovery: EndhostApiDiscoveryConfig::default(),
95 snap: SnapUnderlayConfig::default(),
96 udp: UdpUnderlayConfig::default(),
97 }
98 }
99
100 #[must_use]
104 pub fn with_preferred_underlay(mut self, preferred: PreferredUnderlay) -> Self {
105 self.preferred_underlay = preferred;
106 self
107 }
108
109 #[must_use]
114 pub fn with_crpc_client(mut self, crpc_client: reqwest::Client) -> Self {
115 self.crpc_client = Some(crpc_client);
116 self
117 }
118
119 #[must_use]
125 pub fn with_endhost_api(mut self, endhost_api_url: Url) -> Self {
126 let source = StaticEndhostApis::new().add_group(vec![endhost_api_url]);
127 self.endhost_api_source = Arc::new(source);
128
129 self
130 }
131
132 #[must_use]
136 pub fn with_endhost_api_discovery_source(mut self, source: impl EndhostApiSource) -> Self {
137 self.endhost_api_source = Arc::new(source);
138 self
139 }
140
141 #[must_use]
143 pub fn with_endhost_api_auth_token_source(mut self, source: impl TokenSource) -> Self {
144 self.endhost_api_token_source = Some(Arc::new(source));
145 self
146 }
147
148 #[must_use]
150 pub fn with_endhost_api_auth_token(mut self, token: String) -> Self {
151 self.endhost_api_token_source = Some(Arc::new(StaticTokenSource::from(token)));
152 self
153 }
154
155 #[must_use]
159 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
160 self.auth_token_source = Some(Arc::new(source));
161 self
162 }
163
164 #[must_use]
168 pub fn with_auth_token(mut self, token: String) -> Self {
169 self.auth_token_source = Some(Arc::new(StaticTokenSource::from(token)));
170 self
171 }
172
173 #[must_use]
179 pub fn with_endhost_api_discovery_max_groups(mut self, max_groups: usize) -> Self {
180 self.endhost_api_discovery.max_groups = max_groups;
181 self
182 }
183
184 #[must_use]
191 pub fn with_anapaya_ead_apis_per_group(mut self, apis_per_group: usize) -> Self {
192 self.endhost_api_discovery.apis_per_group = apis_per_group;
193 self
194 }
195
196 #[must_use]
205 pub fn with_endhost_api_discovery_per_group_delay(mut self, per_group_delay: Duration) -> Self {
206 self.endhost_api_discovery.per_group_delay = per_group_delay;
207 self
208 }
209
210 #[must_use]
212 pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
213 self.snap = config;
214 self
215 }
216
217 #[must_use]
219 pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
220 self.udp = config;
221 self
222 }
223
224 pub async fn build(self) -> Result<ScionStack, BuildScionStackError> {
230 let ScionStackBuilder {
231 crpc_client,
232 endhost_api_token_source,
233 auth_token_source,
234 endhost_api_source,
235 preferred_underlay,
236 endhost_api_discovery,
237 snap,
238 udp,
239 } = self;
240
241 let api_groups = endhost_api_source.endhost_apis().await?;
246 let api_groups: Vec<Vec<Url>> = {
247 let mut rng = rand::rng();
248 api_groups
249 .into_iter()
250 .map(|g| g.apis.into_iter().map(|a| a.address).collect::<Vec<_>>())
251 .filter(|group| !group.is_empty())
252 .take(endhost_api_discovery.max_groups)
253 .map(|group: Vec<Url>| {
254 group
255 .sample(&mut rng, endhost_api_discovery.apis_per_group)
256 .cloned()
257 .collect()
258 })
259 .collect()
260 };
261
262 if api_groups.is_empty() {
263 return Err(BuildScionStackError::EndhostApiSourceError(
265 EndhostApiSourceError::new("endhost API discovery returned no APIs", false),
266 ));
267 }
268
269 let token_source: Option<Arc<dyn TokenSource>> =
270 endhost_api_token_source.or(auth_token_source.clone());
271 let crpc_c = crpc_client.clone();
272 let discover_underlays = move |url: Url| {
273 let token_source = token_source.clone();
274 let crpc_c = crpc_c.clone();
275 let url = url.clone();
276 async move {
277 let mut client = match crpc_c {
278 Some(client) => {
279 CrpcEndhostApiClient::new_with_client(&url, client)
280 .map_err(ApiAttemptError::client_setup)?
281 }
282 None => {
283 CrpcEndhostApiClient::new(&url).map_err(ApiAttemptError::client_setup)?
284 }
285 };
286 if let Some(token_source) = &token_source {
287 client.use_token_source(token_source.clone());
288 }
289 let client = Arc::new(client);
290 let discovery = PeriodicUnderlayDiscovery::new(
291 client.clone(),
292 udp.udp_next_hop_resolver_fetch_interval,
293 ExponentialBackoff::new(0.5, 10.0, 2.0, 0.5),
294 )
295 .await
296 .map_err(ApiAttemptError::underlay_discovery)?;
297 Ok((client, discovery))
298 }
299 };
300
301 let (api_url, (endhost_api_client, underlay_discovery)) =
302 priority_connect::try_priority_groups(
303 api_groups,
304 discover_underlays,
305 endhost_api_discovery.per_group_delay,
306 )
307 .await
308 .map_err(|errors| {
309 BuildScionStackError::AllEndhostApisFailed(AllEndhostApisFailed::new(errors))
310 })?;
311 tracing::info!(url=%api_url, "Selected endhost API");
312
313 let outbound_ip_resolver: Arc<dyn OutboundIpResolver> =
317 (udp.outbound_ip_resolver_factory)(api_url.clone());
318
319 let underlay_stack = UnderlayStack::new(
320 preferred_underlay,
321 Arc::new(underlay_discovery),
322 outbound_ip_resolver,
323 snap.static_identity.unwrap_or_else(StaticSecret::random),
324 SnapSocketConfig {
325 crpc_client: snap.crpc_client.or(crpc_client),
326 snap_token_source: snap.snap_token_source.or(auth_token_source),
327 },
328 );
329
330 Ok(ScionStack::new(
331 Some(api_url),
332 Arc::new(EndhostApiSegmentFetcher::new(endhost_api_client)),
333 Arc::new(underlay_stack),
334 ))
335 }
336
337 fn build_static_udp_underlay(
352 underlay_discovery: Arc<dyn UnderlayDiscovery>,
353 outbound_ip_resolver: Arc<dyn OutboundIpResolver>,
354 default_segment_fetcher: Arc<dyn SegmentFetcher>,
355 ) -> ScionStack {
356 let underlay_stack = UnderlayStack::new(
357 PreferredUnderlay::Udp,
358 underlay_discovery,
359 outbound_ip_resolver,
360 StaticSecret::random(),
361 SnapSocketConfig {
362 crpc_client: None,
363 snap_token_source: None,
364 },
365 );
366
367 ScionStack::new(None, default_segment_fetcher, Arc::new(underlay_stack))
368 }
369}
370
371impl Default for ScionStackBuilder {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377impl ScionStack {
378 #[must_use]
393 pub fn static_udp_underlay(
394 underlay_discovery: Arc<dyn UnderlayDiscovery>,
395 outbound_ip_resolver: Arc<dyn OutboundIpResolver>,
396 default_segment_fetcher: Arc<dyn SegmentFetcher>,
397 ) -> ScionStack {
398 ScionStackBuilder::build_static_udp_underlay(
399 underlay_discovery,
400 outbound_ip_resolver,
401 default_segment_fetcher,
402 )
403 }
404}
405
406#[derive(thiserror::Error, Debug)]
408#[non_exhaustive]
409pub enum BuildScionStackError {
410 #[error("no underlay available: {0}")]
412 UnderlayUnavailable(Cow<'static, str>),
413 #[error(transparent)]
415 AllEndhostApisFailed(#[from] AllEndhostApisFailed),
416 #[error(transparent)]
418 EndhostApiSourceError(#[from] EndhostApiSourceError),
419 #[error(transparent)]
422 Snap(#[from] BuildSnapScionStackError),
423 #[error("internal error")]
425 Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
426}
427
428#[derive(thiserror::Error, Debug)]
433#[non_exhaustive]
434pub enum BuildSnapScionStackError {
435 #[error("no SNAP data plane available: {0}")]
437 DataPlaneUnavailable(Cow<'static, str>),
438 #[error("control plane client setup error")]
440 ControlPlaneClientSetup(#[source] Box<dyn std::error::Error + Send + Sync>),
441 #[error("data plane discovery request error")]
443 DataPlaneDiscovery(#[source] Box<dyn std::error::Error + Send + Sync>),
444}
445
446#[derive(Debug)]
450pub struct AllEndhostApisFailed {
451 failures: Vec<(Url, ApiAttemptError)>,
452}
453
454impl AllEndhostApisFailed {
455 pub(crate) fn new(failures: Vec<(Url, ApiAttemptError)>) -> Self {
456 Self { failures }
457 }
458
459 #[must_use]
461 pub fn failures(&self) -> &[(Url, ApiAttemptError)] {
462 &self.failures
463 }
464
465 #[must_use]
468 pub fn is_transient(&self) -> bool {
469 !self.failures.is_empty() && self.failures.iter().all(|(_, err)| err.is_transient())
470 }
471}
472
473impl fmt::Display for AllEndhostApisFailed {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 write!(f, "all {} endhost API(s) failed", self.failures.len())?;
476 let mut sep = ": ";
477 for (url, err) in &self.failures {
478 write!(f, "{sep}{url} ({err})")?;
479 sep = "; ";
480 }
481 Ok(())
482 }
483}
484
485impl std::error::Error for AllEndhostApisFailed {}
486
487#[derive(thiserror::Error, Debug)]
493#[non_exhaustive]
494pub enum ApiAttemptError {
495 #[error("client setup")]
497 ClientSetup {
498 transient: bool,
500 #[source]
502 source: Box<dyn std::error::Error + Send + Sync>,
503 },
504 #[error("underlay discovery")]
506 UnderlayDiscovery {
507 transient: bool,
509 #[source]
511 source: Box<dyn std::error::Error + Send + Sync>,
512 },
513}
514
515impl ApiAttemptError {
516 pub(crate) fn client_setup(error: anyhow::Error) -> Self {
517 Self::ClientSetup {
520 transient: false,
521 source: error.into_boxed_dyn_error(),
522 }
523 }
524
525 pub(crate) fn underlay_discovery(error: CrpcClientError) -> Self {
526 Self::UnderlayDiscovery {
527 transient: is_transient_crpc_error(&error),
528 source: Box::new(error),
529 }
530 }
531
532 #[must_use]
534 pub fn is_transient(&self) -> bool {
535 match self {
536 Self::ClientSetup { transient, .. } | Self::UnderlayDiscovery { transient, .. } => {
537 *transient
538 }
539 }
540 }
541}
542
543fn is_transient_crpc_error(error: &CrpcClientError) -> bool {
546 matches!(error, CrpcClientError::ConnectionError { .. })
547}
548
549pub struct EndhostApiDiscoveryConfig {
554 max_groups: usize,
556 apis_per_group: usize,
558 per_group_delay: Duration,
561}
562
563impl Default for EndhostApiDiscoveryConfig {
564 fn default() -> Self {
565 Self {
566 max_groups: DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS,
567 apis_per_group: DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP,
568 per_group_delay: DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY,
569 }
570 }
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575#[non_exhaustive]
576pub enum PreferredUnderlay {
577 Snap,
579 Udp,
581}
582
583#[derive(Default)]
588pub struct SnapUnderlayConfig {
589 crpc_client: Option<reqwest::Client>,
590 snap_token_source: Option<Arc<dyn TokenSource>>,
591 snap_dp_index: usize,
592 static_identity: Option<StaticSecret>,
594}
595
596impl SnapUnderlayConfig {
597 #[must_use]
599 pub fn with_auth_token(mut self, token: String) -> Self {
600 self.snap_token_source = Some(Arc::new(StaticTokenSource::from(token)));
601 self
602 }
603
604 #[must_use]
606 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
607 self.snap_token_source = Some(Arc::new(source));
608 self
609 }
610
611 #[must_use]
613 pub fn with_crpc_client(mut self, client: reqwest::Client) -> Self {
614 self.crpc_client = Some(client);
615 self
616 }
617
618 #[must_use]
620 pub fn with_snap_dp_index(mut self, dp_index: usize) -> Self {
621 self.snap_dp_index = dp_index;
622 self
623 }
624
625 #[must_use]
629 pub fn with_static_identity(mut self, identity: StaticSecret) -> Self {
630 self.static_identity = Some(identity);
631 self
632 }
633}
634
635pub struct UdpUnderlayConfig {
640 udp_next_hop_resolver_fetch_interval: Duration,
641 outbound_ip_resolver_factory: OutboundIpResolverFactory,
642}
643
644impl Default for UdpUnderlayConfig {
645 fn default() -> Self {
646 Self {
647 udp_next_hop_resolver_fetch_interval: DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL,
648 outbound_ip_resolver_factory: Box::new(move |url| {
649 Arc::new(TargetAddrOutboundIpResolver::new(url, vec![]))
650 }),
651 }
652 }
653}
654
655impl UdpUnderlayConfig {
656 #[must_use]
662 pub fn with_outbound_ips(mut self, outbound_ips: Vec<net::IpAddr>) -> Self {
663 self.outbound_ip_resolver_factory =
664 Box::new(move |_url| Arc::new(outbound_ips) as Arc<dyn OutboundIpResolver>);
665 self
666 }
667
668 #[must_use]
677 pub fn with_outbound_ip_resolver(
678 mut self,
679 resolver: impl OutboundIpResolver + 'static,
680 ) -> Self {
681 let resolver = Arc::new(resolver) as Arc<dyn OutboundIpResolver>;
682 self.outbound_ip_resolver_factory = Box::new(move |_url| resolver.clone());
683 self
684 }
685
686 #[must_use]
698 pub fn with_outbound_ip_resolver_factory<F, R>(mut self, factory: F) -> Self
699 where
700 F: FnOnce(Url) -> R + Send + 'static,
701 R: OutboundIpResolver + 'static,
702 {
703 self.outbound_ip_resolver_factory =
704 Box::new(move |url| Arc::new(factory(url)) as Arc<dyn OutboundIpResolver>);
705 self
706 }
707
708 #[must_use]
711 pub fn with_udp_next_hop_resolver_fetch_interval(mut self, fetch_interval: Duration) -> Self {
712 self.udp_next_hop_resolver_fetch_interval = fetch_interval;
713 self
714 }
715}
716
717#[cfg(test)]
718mod tests {
719 use std::borrow::Cow;
720
721 use reqwest_connect_rpc::client::CrpcClientError;
722 use url::Url;
723
724 use super::*;
725
726 fn connection_error() -> CrpcClientError {
727 CrpcClientError::ConnectionError {
728 context: Cow::Borrowed("test"),
729 source: Box::new(std::io::Error::other("boom")),
730 }
731 }
732
733 fn non_connection_error() -> CrpcClientError {
734 CrpcClientError::DecodeError {
735 context: Cow::Borrowed("test"),
736 source: Some(Box::new(std::io::Error::other("boom"))),
737 body: None,
738 }
739 }
740
741 #[test]
742 fn api_attempt_error_transient_classification() {
743 assert!(ApiAttemptError::underlay_discovery(connection_error()).is_transient());
745 assert!(!ApiAttemptError::underlay_discovery(non_connection_error()).is_transient());
747 assert!(!ApiAttemptError::client_setup(anyhow::anyhow!("invalid url")).is_transient());
749 }
750
751 #[test]
752 fn all_endhost_apis_failed_transient_classification() {
753 let url: Url = "http://example.com".parse().expect("valid url");
754
755 assert!(!AllEndhostApisFailed::new(vec![]).is_transient());
757
758 assert!(
760 AllEndhostApisFailed::new(vec![(
761 url.clone(),
762 ApiAttemptError::underlay_discovery(connection_error()),
763 )])
764 .is_transient()
765 );
766
767 assert!(
769 !AllEndhostApisFailed::new(vec![
770 (
771 url.clone(),
772 ApiAttemptError::underlay_discovery(connection_error()),
773 ),
774 (
775 url,
776 ApiAttemptError::underlay_discovery(non_connection_error())
777 ),
778 ])
779 .is_transient()
780 );
781 }
782}