scion_stack/scionstack/
builder.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! SCION stack builder.
15
16use 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};
23// Re-export for consumer
24pub 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
45/// Default size for the socket's receive channel (in packets).
46/// 64KiB max payload size * 1000 ~= 64MiB if full.
47pub const DEFAULT_RECEIVE_CHANNEL_SIZE: usize = 1000;
48
49/// Type alias for the complex SCMP handler factory type to reduce type complexity
50type ScmpHandlerFactory =
51    Box<dyn FnOnce(Arc<SnapTunnel>) -> Arc<dyn ScmpHandler> + Sync + Send + 'static>;
52
53/// Builder for creating a [ScionStack].
54///
55/// # Example
56///
57/// ```no_run
58/// use scion_stack::scionstack::builder::ScionStackBuilder;
59/// use url::Url;
60///
61/// async fn setup_scion_stack() {
62///     let control_plane_url: Url = "http://127.0.0.1:1234".parse().unwrap();
63///
64///     let scion_stack = ScionStackBuilder::new(control_plane_url)
65///         .with_auth_token("snap_token".to_string())
66///         .build()
67///         .await
68///         .unwrap();
69/// }
70/// ```
71pub 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    /// Create a new [ScionStackBuilder].
83    ///
84    /// The stack uses the the endhost API to discover the available data planes.
85    /// By default, udp dataplanes are preferred over snap dataplanes.
86    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    /// When discovering data planes, prefer SNAP data planes if available.
102    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    /// When discovering data planes, prefer UDP data planes if available.
111    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    /// When discovering underlays, query only for the given ISD-AS.
120    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    /// Use a SNAP underlay with the provided list of SNAP control planes.
134    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    /// Use a UDP underlay with the provided list of SCION routers (UDP data planes).
140    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    /// Set a token source to use for authentication with the endhost API.
148    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    /// Set a static token to use for authentication with the endhost API.
154    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    /// Set a token source to use for authentication both with the endhost API and the SNAP control
160    /// plane.
161    /// If a more specific token source is set, it takes precedence over this token source.
162    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    /// Set a static token to use for authentication both with the endhost API and the SNAP control
168    /// plane.
169    /// If a more specific token is set, it takes precedence over this token.
170    pub fn with_auth_token(mut self, token: String) -> Self {
171        self.auth_token_source = Some(Arc::new(token));
172        self
173    }
174
175    /// Set SNAP underlay specific configuration for the SCION stack.
176    pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
177        self.snap = config;
178        self
179    }
180
181    /// Set UDP underlay specific configuration for the SCION stack.
182    pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
183        self.udp = config;
184        self
185    }
186
187    /// Build the SCION stack.
188    ///
189    /// # Returns
190    ///
191    /// A new SCION stack.
192    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        // Discover available underlays
213        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        // Construct the appropriate underlay stack based on available data planes
239        let underlay: Arc<dyn DynUnderlayStack> = match underlays {
240            DiscoveredUnderlays::Snap(control_planes) => {
241                // XXX(uniquefine): For now we just pick the first SNAP control plane.
242                let cp = control_planes
243                    .first()
244                    // This will never happen because we checked that there is at least one.
245                    .ok_or(BuildScionStackError::UnderlayUnavailable(
246                        "no snap control plane provided".into(),
247                    ))?;
248                tracing::info!(%cp, "Using SNAP underlay");
249                // We have SNAP data planes available, construct a SNAP underlay
250                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                // Get data planes from the snap CP API
261                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/// Build SCION stack errors.
327#[derive(thiserror::Error, Debug)]
328pub enum BuildScionStackError {
329    /// Discovery returned no underlay or no underlay was provided.
330    #[error("no underlay available: {0}")]
331    UnderlayUnavailable(Cow<'static, str>),
332    /// Error making the underlay discovery request to the endhost API.
333    /// E.g. because the endhost API is not reachable.
334    /// This error is only returned if the underlay is not statically configured.
335    #[error("underlay discovery request error: {0:#}")]
336    UnderlayDiscoveryError(CrpcClientError),
337    /// Error setting up the endhost API client.
338    #[error("endhost API client setup error: {0:#}")]
339    EndhostApiClientSetupError(anyhow::Error),
340    /// Error building the SNAP SCION stack.
341    /// This error is only returned if a SNAP underlay is used.
342    #[error(transparent)]
343    Snap(#[from] BuildSnapScionStackError),
344    /// Error building the UDP SCION stack.
345    /// This error is only returned if a UDP underlay is used.
346    #[error(transparent)]
347    Udp(#[from] BuildUdpScionStackError),
348    /// Internal error, this should never happen.
349    #[error("internal error: {0:#}")]
350    Internal(anyhow::Error),
351}
352
353/// Build SNAP SCION stack errors.
354#[derive(thiserror::Error, Debug)]
355pub enum BuildSnapScionStackError {
356    /// Discovery returned no SNAP data plane.
357    #[error("no SNAP data plane available: {0}")]
358    DataPlaneUnavailable(Cow<'static, str>),
359    /// Error setting up the SNAP control plane client.
360    #[error("control plane client setup error: {0:#}")]
361    ControlPlaneClientSetupError(anyhow::Error),
362    /// Error making the data plane discovery request to the SNAP control plane.
363    #[error("data plane discovery request error: {0:#}")]
364    DataPlaneDiscoveryError(CrpcClientError),
365    /// Error connecting to the SNAP data plane.
366    #[error("error connecting to data plane: {0:#}")]
367    DataPlaneConnectionError(#[from] SnapTunnelError),
368}
369
370/// Build UDP SCION stack errors.
371#[derive(thiserror::Error, Debug)]
372pub enum BuildUdpScionStackError {
373    /// Error resolving the local IP addresses.
374    #[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        /// The ISD-AS to discover the underlay for.
387        isd_as: IsdAsn,
388    },
389    Snap(Vec<Snap>),
390    Udp(Vec<ScionRouter>),
391}
392
393/// SNAP underlay configuration.
394pub 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    /// Create a new [SnapUnderlayConfigBuilder] to configure the SNAP underlay.
420    pub fn builder() -> SnapUnderlayConfigBuilder {
421        SnapUnderlayConfigBuilder(Self::default())
422    }
423}
424
425/// SNAP underlay configuration builder.
426pub struct SnapUnderlayConfigBuilder(SnapUnderlayConfig);
427
428impl SnapUnderlayConfigBuilder {
429    /// Set a static token to use for authentication with the SNAP control plane.
430    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    /// Set a token source to use for authentication with the SNAP control plane.
436    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    /// Set the addresses to request from the SNAP server.
442    /// Note, that the server may choose not to assign all requested addresses
443    /// and may assign additional addresses.
444    /// Use assigned_addresses() to get the final list of addresses.
445    ///
446    /// # Arguments
447    ///
448    /// * `requested_addresses` - The addresses to request from the SNAP server.
449    pub fn with_requested_addresses(mut self, requested_addresses: Vec<EndhostAddr>) -> Self {
450        self.0.requested_addresses = requested_addresses;
451        self
452    }
453
454    /// Set the random number generator used for port allocation.
455    ///
456    /// # Arguments
457    ///
458    /// * `rng` - The random number generator.
459    pub fn with_ports_rng(mut self, rng: ChaCha8Rng) -> Self {
460        self.0.ports_rng = Some(rng);
461        self
462    }
463
464    /// Set how long ports are reserved after they are released.
465    ///
466    /// # Arguments
467    ///
468    /// * `reserved_time` - The reserved time for ports.
469    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    /// Set the default SCMP handler.
475    ///
476    /// # Arguments
477    ///
478    /// * `default_scmp_handler` - The default SCMP handler.
479    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    /// Set the automatic session renewal.
485    ///
486    /// # Arguments
487    ///
488    /// * `session_auto_renewal` - The automatic session renewal.
489    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    /// Set the index of the SNAP data plane to use.
495    ///
496    /// # Arguments
497    ///
498    /// * `dp_index` - The index of the SNAP data plane to use.
499    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    /// Build the SNAP stack configuration.
505    ///
506    /// # Returns
507    ///
508    /// A new SNAP stack configuration.
509    pub fn build(self) -> SnapUnderlayConfig {
510        self.0
511    }
512}
513
514/// UDP underlay configuration.
515pub 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    /// Create a new [UdpUnderlayConfigBuilder] to configure the UDP underlay.
531    pub fn builder() -> UdpUnderlayConfigBuilder {
532        UdpUnderlayConfigBuilder(Self::default())
533    }
534}
535
536/// UDP underlay configuration builder.
537pub struct UdpUnderlayConfigBuilder(UdpUnderlayConfig);
538
539impl UdpUnderlayConfigBuilder {
540    /// Set the local IP addresses to use for the UDP underlay.
541    /// If not set, the UDP underlay will use the local IP that can reach the endhost API.
542    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    /// Set the interval at which the UDP next hop resolver fetches the next hops
548    /// from the endhost API.
549    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    /// Build the UDP underlay configuration.
555    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
566/// Helper function to discover data plane addresses from the control plane.
567async fn discover_underlays(
568    client: &dyn EndhostApiClient,
569    preferred_underlay: PreferredUnderlay,
570    isd_as: IsdAsn,
571) -> Result<DiscoveredUnderlays, BuildScionStackError> {
572    // Retrieve the data plane addresses using the control plane API
573    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}