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;
22pub use scion_sdk_reqwest_connect_rpc::client::CrpcClientError;
23use scion_sdk_reqwest_connect_rpc::token_source::{TokenSource, static_token::StaticTokenSource};
24use scion_sdk_utils::backoff::ExponentialBackoff;
25use url::Url;
26use x25519_dalek::StaticSecret;
27
28use crate::{
29 ea_source::{
30 EndhostApiSource, EndhostApiSourceError, StaticEndhostApiDiscovery, StaticEndhostApis,
31 },
32 scionstack::ScionStack,
33 underlays::{
34 SnapSocketConfig, UnderlayStack,
35 discovery::PeriodicUnderlayDiscovery,
36 udp::{LocalIpResolver, TargetAddrLocalIpResolver},
37 },
38};
39
40const DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL: Duration = Duration::from_secs(600);
41const DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS: usize = 5;
42const DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP: usize = 2;
43const DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY: Duration = Duration::from_millis(500);
44
45pub struct ScionStackBuilder {
65 endhost_api_token_source: Option<Arc<dyn TokenSource>>,
66 auth_token_source: Option<Arc<dyn TokenSource>>,
67 endhost_api_source: Arc<dyn EndhostApiSource>,
68 preferred_underlay: PreferredUnderlay,
69 endhost_api_discovery: EndhostApiDiscoveryConfig,
70 snap: SnapUnderlayConfig,
71 udp: UdpUnderlayConfig,
72}
73
74impl ScionStackBuilder {
75 pub fn new() -> Self {
80 Self {
81 endhost_api_token_source: None,
82 auth_token_source: None,
83 endhost_api_source: Arc::new(StaticEndhostApiDiscovery::global()),
84 preferred_underlay: PreferredUnderlay::Udp,
85 endhost_api_discovery: EndhostApiDiscoveryConfig::default(),
86 snap: SnapUnderlayConfig::default(),
87 udp: UdpUnderlayConfig::default(),
88 }
89 }
90
91 pub fn with_prefer_snap(mut self) -> Self {
93 self.preferred_underlay = PreferredUnderlay::Snap;
94 self
95 }
96
97 pub fn with_prefer_udp(mut self) -> Self {
99 self.preferred_underlay = PreferredUnderlay::Udp;
100 self
101 }
102
103 pub fn with_endhost_api(mut self, endhost_api_url: Url) -> Self {
109 let source = StaticEndhostApis::new().add_group(vec![endhost_api_url]);
110 self.endhost_api_source = Arc::new(source);
111
112 self
113 }
114
115 pub fn with_endhost_api_discovery_source(mut self, source: impl EndhostApiSource) -> Self {
119 self.endhost_api_source = Arc::new(source);
120 self
121 }
122
123 pub fn with_endhost_api_auth_token_source(mut self, source: impl TokenSource) -> Self {
125 self.endhost_api_token_source = Some(Arc::new(source));
126 self
127 }
128
129 pub fn with_endhost_api_auth_token(mut self, token: String) -> Self {
131 self.endhost_api_token_source = Some(Arc::new(StaticTokenSource::from(token)));
132 self
133 }
134
135 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
139 self.auth_token_source = Some(Arc::new(source));
140 self
141 }
142
143 pub fn with_auth_token(mut self, token: String) -> Self {
147 self.auth_token_source = Some(Arc::new(StaticTokenSource::from(token)));
148 self
149 }
150
151 pub fn with_endhost_api_discovery_max_groups(mut self, max_groups: usize) -> Self {
157 self.endhost_api_discovery.max_groups = max_groups;
158 self
159 }
160
161 pub fn with_endhost_api_discovery_apis_per_group(mut self, apis_per_group: usize) -> Self {
168 self.endhost_api_discovery.apis_per_group = apis_per_group;
169 self
170 }
171
172 pub fn with_endhost_api_discovery_per_group_delay(mut self, per_group_delay: Duration) -> Self {
181 self.endhost_api_discovery.per_group_delay = per_group_delay;
182 self
183 }
184
185 pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
187 self.snap = config;
188 self
189 }
190
191 pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
193 self.udp = config;
194 self
195 }
196
197 pub async fn build(self) -> Result<ScionStack, BuildScionStackError> {
203 let ScionStackBuilder {
204 endhost_api_token_source,
205 auth_token_source,
206 endhost_api_source,
207 preferred_underlay,
208 endhost_api_discovery,
209 snap,
210 udp,
211 } = self;
212
213 let api_groups = endhost_api_source.endhost_apis().await?;
218 let api_groups: Vec<Vec<Url>> = {
219 let mut rng = rand::rng();
220 api_groups
221 .into_iter()
222 .map(|g| g.apis.into_iter().map(|a| a.address).collect::<Vec<_>>())
223 .filter(|group| !group.is_empty())
224 .take(endhost_api_discovery.max_groups)
225 .map(|group: Vec<Url>| {
226 group
227 .sample(&mut rng, endhost_api_discovery.apis_per_group)
228 .cloned()
229 .collect()
230 })
231 .collect()
232 };
233
234 if api_groups.is_empty() {
235 return Err(BuildScionStackError::EndhostApiSourceError(
236 EndhostApiSourceError {
237 error: anyhow::anyhow!("Endhost API discovery returned no APIs"),
238 transient: false,
241 },
242 ));
243 }
244
245 let token_source: Option<Arc<dyn TokenSource>> =
246 endhost_api_token_source.or(auth_token_source.clone());
247 let discover_underlays = move |url: Url| {
248 let token_source = token_source.clone();
249 let url = url.clone();
250 async move {
251 let mut client =
252 CrpcEndhostApiClient::new(&url).map_err(ApiAttemptError::ClientSetup)?;
253 if let Some(token_source) = &token_source {
254 client.use_token_source(token_source.clone());
255 }
256 let client = Arc::new(client);
257 let discovery = PeriodicUnderlayDiscovery::new(
258 client.clone(),
259 udp.udp_next_hop_resolver_fetch_interval,
260 ExponentialBackoff::new(0.5, 10.0, 2.0, 0.5),
261 )
262 .await
263 .map_err(ApiAttemptError::UnderlayDiscovery)?;
264 Ok((client, discovery))
265 }
266 };
267
268 let (api_url, (endhost_api_client, underlay_discovery)) =
269 priority_connect::try_priority_groups(
270 api_groups,
271 discover_underlays,
272 endhost_api_discovery.per_group_delay,
273 )
274 .await
275 .map_err(|errors| {
276 BuildScionStackError::AllEndhostApisFailed(AllEndhostApisFailed(errors))
277 })?;
278 tracing::info!(%api_url, "Successfully selected endhost API");
279
280 let local_ip_resolver: Arc<dyn LocalIpResolver> = match udp.local_ips {
285 Some(ips) => Arc::new(ips),
286 None => {
287 Arc::new(
288 TargetAddrLocalIpResolver::new(api_url.clone())
289 .map_err(BuildUdpScionStackError::LocalIpResolutionError)?,
290 )
291 }
292 };
293
294 let underlay_stack = UnderlayStack::new(
295 preferred_underlay,
296 Arc::new(underlay_discovery),
297 local_ip_resolver,
298 snap.static_identity.unwrap_or_else(StaticSecret::random),
299 SnapSocketConfig {
300 snap_token_source: snap.snap_token_source.or(auth_token_source),
301 },
302 );
303
304 Ok(ScionStack::new(
305 Some(api_url),
306 endhost_api_client,
307 Arc::new(underlay_stack),
308 ))
309 }
310}
311
312impl Default for ScionStackBuilder {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318#[derive(thiserror::Error, Debug)]
320pub enum BuildScionStackError {
321 #[error("no underlay available: {0}")]
323 UnderlayUnavailable(Cow<'static, str>),
324 #[error(transparent)]
326 AllEndhostApisFailed(#[from] AllEndhostApisFailed),
327 #[error("endhost api source error: {0:#}")]
329 EndhostApiSourceError(#[from] EndhostApiSourceError),
330 #[error(transparent)]
333 Snap(#[from] BuildSnapScionStackError),
334 #[error(transparent)]
337 Udp(#[from] BuildUdpScionStackError),
338 #[error("internal error: {0:#}")]
340 Internal(anyhow::Error),
341}
342
343#[derive(thiserror::Error, Debug)]
345pub enum BuildSnapScionStackError {
346 #[error("no SNAP data plane available: {0}")]
348 DataPlaneUnavailable(Cow<'static, str>),
349 #[error("control plane client setup error: {0:#}")]
351 ControlPlaneClientSetupError(anyhow::Error),
352 #[error("data plane discovery request error: {0:#}")]
354 DataPlaneDiscoveryError(CrpcClientError),
355}
356
357#[derive(thiserror::Error, Debug)]
359pub enum BuildUdpScionStackError {
360 #[error("local IP resolution error: {0:#}")]
362 LocalIpResolutionError(anyhow::Error),
363}
364
365#[derive(Debug)]
369pub struct AllEndhostApisFailed(pub Vec<(Url, ApiAttemptError)>);
370
371impl fmt::Display for AllEndhostApisFailed {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 write!(f, "all {} endhost API(s) failed", self.0.len())?;
374 let mut sep = ": ";
375 for (url, err) in &self.0 {
376 write!(f, "{sep}{url} ({err})")?;
377 sep = "; ";
378 }
379 Ok(())
380 }
381}
382
383impl std::error::Error for AllEndhostApisFailed {}
384
385#[derive(thiserror::Error, Debug)]
387pub enum ApiAttemptError {
388 #[error("client setup: {0:#}")]
390 ClientSetup(anyhow::Error),
391 #[error("underlay discovery: {0:#}")]
393 UnderlayDiscovery(CrpcClientError),
394}
395
396pub struct EndhostApiDiscoveryConfig {
401 max_groups: usize,
403 apis_per_group: usize,
405 per_group_delay: Duration,
408}
409
410impl Default for EndhostApiDiscoveryConfig {
411 fn default() -> Self {
412 Self {
413 max_groups: DEFAULT_ENDHOST_API_DISCOVERY_MAX_GROUPS,
414 apis_per_group: DEFAULT_ENDHOST_API_DISCOVERY_APIS_PER_GROUP,
415 per_group_delay: DEFAULT_ENDHOST_API_DISCOVERY_PER_GROUP_DELAY,
416 }
417 }
418}
419
420pub enum PreferredUnderlay {
422 Snap,
424 Udp,
426}
427
428#[derive(Default)]
430pub struct SnapUnderlayConfig {
431 snap_token_source: Option<Arc<dyn TokenSource>>,
432 snap_dp_index: usize,
433 static_identity: Option<StaticSecret>,
435}
436
437impl SnapUnderlayConfig {
438 pub fn builder() -> SnapUnderlayConfigBuilder {
440 SnapUnderlayConfigBuilder(Self::default())
441 }
442}
443
444pub struct SnapUnderlayConfigBuilder(SnapUnderlayConfig);
446
447impl SnapUnderlayConfigBuilder {
448 pub fn with_auth_token(mut self, token: String) -> Self {
450 self.0.snap_token_source = Some(Arc::new(StaticTokenSource::from(token)));
451 self
452 }
453
454 pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
456 self.0.snap_token_source = Some(Arc::new(source));
457 self
458 }
459
460 pub fn with_snap_dp_index(mut self, dp_index: usize) -> Self {
466 self.0.snap_dp_index = dp_index;
467 self
468 }
469
470 pub fn with_static_identity(mut self, identity: StaticSecret) -> Self {
473 self.0.static_identity = Some(identity);
474 self
475 }
476
477 pub fn build(self) -> SnapUnderlayConfig {
483 self.0
484 }
485}
486
487pub struct UdpUnderlayConfig {
489 udp_next_hop_resolver_fetch_interval: Duration,
490 local_ips: Option<Vec<net::IpAddr>>,
491}
492
493impl Default for UdpUnderlayConfig {
494 fn default() -> Self {
495 Self {
496 udp_next_hop_resolver_fetch_interval: DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL,
497 local_ips: None,
498 }
499 }
500}
501
502impl UdpUnderlayConfig {
503 pub fn builder() -> UdpUnderlayConfigBuilder {
505 UdpUnderlayConfigBuilder(Self::default())
506 }
507}
508
509pub struct UdpUnderlayConfigBuilder(UdpUnderlayConfig);
511
512impl UdpUnderlayConfigBuilder {
513 pub fn with_local_ips(mut self, local_ips: Vec<net::IpAddr>) -> Self {
516 self.0.local_ips = Some(local_ips);
517 self
518 }
519
520 pub fn with_udp_next_hop_resolver_fetch_interval(mut self, fetch_interval: Duration) -> Self {
523 self.0.udp_next_hop_resolver_fetch_interval = fetch_interval;
524 self
525 }
526
527 pub fn build(self) -> UdpUnderlayConfig {
529 self.0
530 }
531}