Skip to main content

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, net, sync::Arc, time::Duration};
17
18use endhost_api_client::client::CrpcEndhostApiClient;
19// Re-export for consumer
20pub use scion_sdk_reqwest_connect_rpc::client::CrpcClientError;
21use scion_sdk_reqwest_connect_rpc::token_source::{TokenSource, static_token::StaticTokenSource};
22use scion_sdk_utils::backoff::{BackoffConfig, ExponentialBackoff};
23use url::Url;
24
25use crate::{
26    scionstack::ScionStack,
27    underlays::{
28        SnapSocketConfig, UnderlayStack,
29        discovery::PeriodicUnderlayDiscovery,
30        udp::{LocalIpResolver, TargetAddrLocalIpResolver},
31    },
32};
33
34const DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL: Duration = Duration::from_secs(600);
35const DEFAULT_SNAP_TUNNEL_RECONNECT_BACKOFF: BackoffConfig = BackoffConfig {
36    minimum_delay_secs: 0.5,
37    maximum_delay_secs: 10.0,
38    factor: 1.2,
39    jitter_secs: 0.1,
40};
41
42/// Builder for creating a [ScionStack].
43///
44/// # Example
45///
46/// ```no_run
47/// use scion_stack::scionstack::builder::ScionStackBuilder;
48/// use url::Url;
49///
50/// async fn setup_scion_stack() {
51///     let control_plane_url: Url = "http://127.0.0.1:1234".parse().unwrap();
52///
53///     let scion_stack = ScionStackBuilder::new(control_plane_url)
54///         .with_auth_token("snap_token".to_string())
55///         .build()
56///         .await
57///         .unwrap();
58/// }
59/// ```
60pub struct ScionStackBuilder {
61    endhost_api_url: Url,
62    endhost_api_token_source: Option<Arc<dyn TokenSource>>,
63    auth_token_source: Option<Arc<dyn TokenSource>>,
64    preferred_underlay: PreferredUnderlay,
65    snap: SnapUnderlayConfig,
66    udp: UdpUnderlayConfig,
67}
68
69impl ScionStackBuilder {
70    /// Create a new [ScionStackBuilder].
71    ///
72    /// The stack uses the the endhost API to discover the available data planes.
73    /// By default, udp dataplanes are preferred over snap dataplanes.
74    pub fn new(endhost_api_url: Url) -> Self {
75        Self {
76            endhost_api_url,
77            endhost_api_token_source: None,
78            auth_token_source: None,
79            preferred_underlay: PreferredUnderlay::Udp,
80            snap: SnapUnderlayConfig::default(),
81            udp: UdpUnderlayConfig::default(),
82        }
83    }
84
85    /// When discovering data planes, prefer SNAP data planes if available.
86    pub fn with_prefer_snap(mut self) -> Self {
87        self.preferred_underlay = PreferredUnderlay::Snap;
88        self
89    }
90
91    /// When discovering data planes, prefer UDP data planes if available.
92    pub fn with_prefer_udp(mut self) -> Self {
93        self.preferred_underlay = PreferredUnderlay::Udp;
94        self
95    }
96
97    /// Set a token source to use for authentication with the endhost API.
98    pub fn with_endhost_api_auth_token_source(mut self, source: impl TokenSource) -> Self {
99        self.endhost_api_token_source = Some(Arc::new(source));
100        self
101    }
102
103    /// Set a static token to use for authentication with the endhost API.
104    pub fn with_endhost_api_auth_token(mut self, token: String) -> Self {
105        self.endhost_api_token_source = Some(Arc::new(StaticTokenSource::from(token)));
106        self
107    }
108
109    /// Set a token source to use for authentication both with the endhost API and the SNAP control
110    /// plane.
111    /// If a more specific token source is set, it takes precedence over this token source.
112    pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
113        self.auth_token_source = Some(Arc::new(source));
114        self
115    }
116
117    /// Set a static token to use for authentication both with the endhost API and the SNAP control
118    /// plane.
119    /// If a more specific token is set, it takes precedence over this token.
120    pub fn with_auth_token(mut self, token: String) -> Self {
121        self.auth_token_source = Some(Arc::new(StaticTokenSource::from(token)));
122        self
123    }
124
125    /// Set SNAP underlay specific configuration for the SCION stack.
126    pub fn with_snap_underlay_config(mut self, config: SnapUnderlayConfig) -> Self {
127        self.snap = config;
128        self
129    }
130
131    /// Set UDP underlay specific configuration for the SCION stack.
132    pub fn with_udp_underlay_config(mut self, config: UdpUnderlayConfig) -> Self {
133        self.udp = config;
134        self
135    }
136
137    /// Build the SCION stack.
138    ///
139    /// # Returns
140    ///
141    /// A new SCION stack.
142    pub async fn build(self) -> Result<ScionStack, BuildScionStackError> {
143        let ScionStackBuilder {
144            endhost_api_url,
145            endhost_api_token_source,
146            auth_token_source,
147            preferred_underlay,
148            snap,
149            udp,
150        } = self;
151
152        let endhost_api_client = {
153            let mut client = CrpcEndhostApiClient::new(&endhost_api_url)
154                .map_err(BuildScionStackError::EndhostApiClientSetupError)?;
155            if let Some(token_source) = endhost_api_token_source.or(auth_token_source.clone()) {
156                client.use_token_source(token_source);
157            }
158            Arc::new(client)
159        };
160
161        // XXX(bunert): Add support for statically configured underlays.
162
163        let underlay_discovery = PeriodicUnderlayDiscovery::new(
164            endhost_api_client.clone(),
165            udp.udp_next_hop_resolver_fetch_interval,
166            // TODO(uniquefine): make this configurable.
167            ExponentialBackoff::new(0.5, 10.0, 2.0, 0.5),
168        )
169        .await
170        .map_err(BuildScionStackError::UnderlayDiscoveryError)?;
171
172        // Use the endhost API URL to resolve the local IP addresses for the UDP underlay
173        // sockets.
174        // Here we assume that the interface used to reach the endhost API is
175        // the same as the interface used to reach the data planes.
176        let local_ip_resolver: Arc<dyn LocalIpResolver> = match udp.local_ips {
177            Some(ips) => Arc::new(ips),
178            None => {
179                Arc::new(
180                    TargetAddrLocalIpResolver::new(endhost_api_url.clone())
181                        .map_err(BuildUdpScionStackError::LocalIpResolutionError)?,
182                )
183            }
184        };
185
186        let underlay_stack = UnderlayStack::new(
187            preferred_underlay,
188            Arc::new(underlay_discovery),
189            local_ip_resolver,
190            SnapSocketConfig {
191                snap_token_source: snap.snap_token_source.or(auth_token_source),
192                reconnect_backoff: ExponentialBackoff::new_from_config(
193                    snap.tunnel_reconnect_backoff,
194                ),
195            },
196        );
197
198        Ok(ScionStack::new(
199            endhost_api_client,
200            Arc::new(underlay_stack),
201        ))
202    }
203}
204
205/// Build SCION stack errors.
206#[derive(thiserror::Error, Debug)]
207pub enum BuildScionStackError {
208    /// Discovery returned no underlay or no underlay was provided.
209    #[error("no underlay available: {0}")]
210    UnderlayUnavailable(Cow<'static, str>),
211    /// Error making the underlay discovery request to the endhost API.
212    /// E.g. because the endhost API is not reachable.
213    /// This error is only returned if the underlay is not statically configured.
214    #[error("underlay discovery request error: {0:#}")]
215    UnderlayDiscoveryError(CrpcClientError),
216    /// Error setting up the endhost API client.
217    #[error("endhost API client setup error: {0:#}")]
218    EndhostApiClientSetupError(anyhow::Error),
219    /// Error building the SNAP SCION stack.
220    /// This error is only returned if a SNAP underlay is used.
221    #[error(transparent)]
222    Snap(#[from] BuildSnapScionStackError),
223    /// Error building the UDP SCION stack.
224    /// This error is only returned if a UDP underlay is used.
225    #[error(transparent)]
226    Udp(#[from] BuildUdpScionStackError),
227    /// Internal error, this should never happen.
228    #[error("internal error: {0:#}")]
229    Internal(anyhow::Error),
230}
231
232/// Build SNAP SCION stack errors.
233#[derive(thiserror::Error, Debug)]
234pub enum BuildSnapScionStackError {
235    /// Discovery returned no SNAP data plane.
236    #[error("no SNAP data plane available: {0}")]
237    DataPlaneUnavailable(Cow<'static, str>),
238    /// Error setting up the SNAP control plane client.
239    #[error("control plane client setup error: {0:#}")]
240    ControlPlaneClientSetupError(anyhow::Error),
241    /// Error making the data plane discovery request to the SNAP control plane.
242    #[error("data plane discovery request error: {0:#}")]
243    DataPlaneDiscoveryError(CrpcClientError),
244}
245
246/// Build UDP SCION stack errors.
247#[derive(thiserror::Error, Debug)]
248pub enum BuildUdpScionStackError {
249    /// Error resolving the local IP addresses.
250    #[error("local IP resolution error: {0:#}")]
251    LocalIpResolutionError(anyhow::Error),
252}
253
254/// Preferred underlay type (if available).
255pub enum PreferredUnderlay {
256    /// SNAP underlay.
257    Snap,
258    /// UDP underlay.
259    Udp,
260}
261
262/// SNAP underlay configuration.
263pub struct SnapUnderlayConfig {
264    snap_token_source: Option<Arc<dyn TokenSource>>,
265    snap_dp_index: usize,
266    tunnel_reconnect_backoff: BackoffConfig,
267}
268
269impl Default for SnapUnderlayConfig {
270    fn default() -> Self {
271        Self {
272            snap_token_source: None,
273            snap_dp_index: 0,
274            tunnel_reconnect_backoff: DEFAULT_SNAP_TUNNEL_RECONNECT_BACKOFF,
275        }
276    }
277}
278
279impl SnapUnderlayConfig {
280    /// Create a new [SnapUnderlayConfigBuilder] to configure the SNAP underlay.
281    pub fn builder() -> SnapUnderlayConfigBuilder {
282        SnapUnderlayConfigBuilder(Self::default())
283    }
284}
285
286/// SNAP underlay configuration builder.
287pub struct SnapUnderlayConfigBuilder(SnapUnderlayConfig);
288
289impl SnapUnderlayConfigBuilder {
290    /// Set a static token to use for authentication with the SNAP control plane.
291    pub fn with_auth_token(mut self, token: String) -> Self {
292        self.0.snap_token_source = Some(Arc::new(StaticTokenSource::from(token)));
293        self
294    }
295
296    /// Set a token source to use for authentication with the SNAP control plane.
297    pub fn with_auth_token_source(mut self, source: impl TokenSource) -> Self {
298        self.0.snap_token_source = Some(Arc::new(source));
299        self
300    }
301
302    /// Set the index of the SNAP data plane to use.
303    ///
304    /// # Arguments
305    ///
306    /// * `dp_index` - The index of the SNAP data plane to use.
307    pub fn with_snap_dp_index(mut self, dp_index: usize) -> Self {
308        self.0.snap_dp_index = dp_index;
309        self
310    }
311
312    /// Set the parameters for the exponential backoff configuration for reconnecting a SNAP tunnel.
313    ///
314    /// # Arguments
315    ///
316    /// * `minimum_delay_secs` - The minimum delay in seconds.
317    /// * `maximum_delay_secs` - The maximum delay in seconds.
318    /// * `factor` - The factor to multiply the delay by.
319    /// * `jitter_secs` - The jitter in seconds.
320    pub fn with_tunnel_reconnect_backoff(
321        mut self,
322        minimum_delay_secs: Duration,
323        maximum_delay_secs: Duration,
324        factor: f32,
325        jitter_secs: Duration,
326    ) -> Self {
327        self.0.tunnel_reconnect_backoff = BackoffConfig {
328            minimum_delay_secs: minimum_delay_secs.as_secs_f32(),
329            maximum_delay_secs: maximum_delay_secs.as_secs_f32(),
330            factor,
331            jitter_secs: jitter_secs.as_secs_f32(),
332        };
333        self
334    }
335
336    /// Build the SNAP stack configuration.
337    ///
338    /// # Returns
339    ///
340    /// A new SNAP stack configuration.
341    pub fn build(self) -> SnapUnderlayConfig {
342        self.0
343    }
344}
345
346/// UDP underlay configuration.
347pub struct UdpUnderlayConfig {
348    udp_next_hop_resolver_fetch_interval: Duration,
349    local_ips: Option<Vec<net::IpAddr>>,
350}
351
352impl Default for UdpUnderlayConfig {
353    fn default() -> Self {
354        Self {
355            udp_next_hop_resolver_fetch_interval: DEFAULT_UDP_NEXT_HOP_RESOLVER_FETCH_INTERVAL,
356            local_ips: None,
357        }
358    }
359}
360
361impl UdpUnderlayConfig {
362    /// Create a new [UdpUnderlayConfigBuilder] to configure the UDP underlay.
363    pub fn builder() -> UdpUnderlayConfigBuilder {
364        UdpUnderlayConfigBuilder(Self::default())
365    }
366}
367
368/// UDP underlay configuration builder.
369pub struct UdpUnderlayConfigBuilder(UdpUnderlayConfig);
370
371impl UdpUnderlayConfigBuilder {
372    /// Set the local IP addresses to use for the UDP underlay.
373    /// If not set, the UDP underlay will use the local IP that can reach the endhost API.
374    pub fn with_local_ips(mut self, local_ips: Vec<net::IpAddr>) -> Self {
375        self.0.local_ips = Some(local_ips);
376        self
377    }
378
379    /// Set the interval at which the UDP next hop resolver fetches the next hops
380    /// from the endhost API.
381    pub fn with_udp_next_hop_resolver_fetch_interval(mut self, fetch_interval: Duration) -> Self {
382        self.0.udp_next_hop_resolver_fetch_interval = fetch_interval;
383        self
384    }
385
386    /// Build the UDP underlay configuration.
387    pub fn build(self) -> UdpUnderlayConfig {
388        self.0
389    }
390}