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