Skip to main content

scion_stack/
stack.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
15//! # The SCION endhost stack.
16//!
17//! [`ScionStack`] is a stateful object that is the conceptual equivalent of the
18//! TCP/IP-stack found in today's common operating systems. It is meant to be
19//! instantiated once per process.
20//!
21//! ## Basic Usage
22//!
23//! ### Creating a path-aware socket (recommended)
24//!
25//! ```
26//! use anapaya_aa_client::AnapayaAaTokenSource;
27//! use scion_stack::stack::{ScionStack, ScionStackBuilder};
28//! use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
29//! use url::Url;
30//!
31//! # async fn socket_example() -> Result<(), Box<dyn std::error::Error>> {
32//! // Create a SCION stack builder
33//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
34//! let aa_url: Url = "https://auth.scion.anapaya.net".parse()?;
35//! let token_source = AnapayaAaTokenSource::new("aakey_...", &aa_url, "my-device-id")?;
36//! let builder = ScionStackBuilder::new().with_auth_token_source(token_source);
37//!
38//! let scion_stack = builder.build().await?;
39//! let socket = scion_stack.bind(None).await?;
40//!
41//! // Parse destination address
42//! let destination: ScionSocketIpAddr = "1-ff00:0:111,[192.168.1.1]:8080".parse()?;
43//!
44//! socket.send_to(b"hello", destination).await?;
45//! let mut buffer = [0u8; 1024];
46//! let (len, src) = socket.recv_from(&mut buffer).await?;
47//! println!("Received: {:?} from {:?}", &buffer[..len], src);
48//!
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ### Creating a connected socket.
54//!
55//! ```
56//! use scion_stack::stack::{ScionStack, ScionStackBuilder};
57//! use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
58//! use url::Url;
59//!
60//! # async fn connected_socket_example() -> Result<(), Box<dyn std::error::Error>> {
61//! # let aa_url: Url = "https://auth.scion.anapaya.net".parse()?;
62//! # let token_source =
63//! #     anapaya_aa_client::AnapayaAaTokenSource::new("aakey_...", &aa_url, "my-device-id")?;
64//! // Create a SCION stack builder
65//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
66//! let builder = ScionStackBuilder::new().with_auth_token_source(token_source);
67//!
68//! // Parse destination address
69//! let destination: ScionSocketIpAddr = "1-ff00:0:111,[192.168.1.1]:8080".parse()?;
70//!
71//! let scion_stack = builder.build().await?;
72//! let connected_socket = scion_stack.connect(destination, None).await?;
73//! connected_socket.send(b"hello").await?;
74//! let mut buffer = [0u8; 1024];
75//! let len = connected_socket.recv(&mut buffer).await?;
76//! println!("Received: {:?}", &buffer[..len]);
77//!
78//! # Ok(())
79//! # }
80//! ```
81//!
82//! ### Creating a path-unaware socket
83//!
84//! ```
85//! use scion_stack::stack::{ScionStack, ScionStackBuilder};
86//! use sciparse::{address::ip_socket_addr::ScionSocketIpAddr, path::ScionPath};
87//! use url::Url;
88//!
89//! # async fn basic_socket_example() -> Result<(), Box<dyn std::error::Error>> {
90//! # let aa_url: Url = "https://auth.scion.anapaya.net".parse()?;
91//! # let token_source =
92//! #     anapaya_aa_client::AnapayaAaTokenSource::new("aakey_...", &aa_url, "my-device-id")?;
93//! // Create a SCION stack builder
94//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
95//! let builder = ScionStackBuilder::new().with_auth_token_source(token_source);
96//!
97//! // Parse addresses
98//! let bind_addr: ScionSocketIpAddr = "1-ff00:0:110,[127.0.0.1]:8080".parse()?;
99//! let destination: ScionSocketIpAddr = "1-ff00:0:111,[127.0.0.1]:9090".parse()?;
100//!
101//! // Create a local path for demonstration
102//! let path = ScionPath::local(bind_addr.isd_asn()).expect("not a wildcard AS");
103//!
104//! let scion_stack = builder.build().await?;
105//! let socket = scion_stack.bind_path_unaware(Some(bind_addr)).await?;
106//! socket.send_to_via(b"hello", destination, &path).await?;
107//! let mut buffer = [0u8; 1024];
108//! let (len, sender) = socket.recv_from(&mut buffer).await?;
109//! println!("Received: {:?} from {:?}", &buffer[..len], sender);
110//!
111//! # Ok(())
112//! # }
113//! ```
114//!
115//! ### Resolving SCION TXT records
116//!
117//! ```
118//! use scion_stack::resolver::{ScionDnsResolver, txt::ScionTxtDnsResolver};
119//!
120//! # async fn resolve_example() -> Result<(), Box<dyn std::error::Error>> {
121//! let resolver = ScionTxtDnsResolver::new()?;
122//! let addresses = resolver.resolve("example.com").await?;
123//!
124//! for address in addresses {
125//!     println!("Resolved: {}", address);
126//! }
127//!
128//! # Ok(())
129//! # }
130//! ```
131//!
132//! ## Advanced Usage
133//!
134//! ### Inspecting and choosing paths manually
135//!
136//! Path-aware sockets manage path selection automatically. To choose a path deliberately, use a
137//! path-unaware socket ([`ScionStack::bind_path_unaware`]) together with a
138//! [`PathFetcher`](crate::path::fetcher::traits::PathFetcher) obtained from
139//! [`create_path_fetcher`](ScionStack::create_path_fetcher): fetch the set of available paths,
140//! pick one, and send over it with
141//! [`send_to_via`](crate::stack::PathUnawareUdpScionSocket::send_to_via).
142//!
143//! ```no_run
144//! use scion_stack::{path::fetcher::traits::PathFetcher, stack::ScionStackBuilder};
145//! use sciparse::address::ip_socket_addr::ScionSocketIpAddr;
146//!
147//! # async fn manual_path_example() -> Result<(), Box<dyn std::error::Error>> {
148//! # let aa_url: url::Url = "https://auth.scion.anapaya.net".parse()?;
149//! # let token_source =
150//! #     anapaya_aa_client::AnapayaAaTokenSource::new("aakey_...", &aa_url, "my-device-id")?;
151//! let stack = ScionStackBuilder::new()
152//!     .with_auth_token_source(token_source)
153//!     .build()
154//!     .await?;
155//!
156//! let bind_addr: ScionSocketIpAddr = "1-ff00:0:110,[127.0.0.1]:8080".parse()?;
157//! let destination: ScionSocketIpAddr = "1-ff00:0:111,[127.0.0.1]:9090".parse()?;
158//!
159//! let socket = stack.bind_path_unaware(Some(bind_addr)).await?;
160//!
161//! // Fetch the available paths to the destination and pick one deliberately.
162//! let paths = stack
163//!     .create_path_fetcher()
164//!     .fetch_paths(bind_addr.isd_asn(), destination.isd_asn())
165//!     .await?;
166//! let path = paths.first().ok_or("no path to destination")?;
167//!
168//! socket.send_to_via(b"hello", destination, path).await?;
169//! # Ok(())
170//! # }
171//! ```
172
173pub mod builder;
174pub mod scmp_handler;
175pub mod socket;
176
177use std::{borrow::Cow, fmt, net, sync::Arc, time::Duration};
178
179use async_trait::async_trait;
180use futures::future::BoxFuture;
181use reqwest_connect_rpc::client::{CrpcClientCreationError, CrpcClientError};
182use sciparse::{
183    address::ip_socket_addr::ScionSocketIpAddr,
184    identifier::{isd::Isd, isd_asn::IsdAsn},
185    packet::view::ScionRawPacketView,
186};
187use snap_tun::client::ConnectSnapTunSocketError;
188pub use socket::{PathUnawareUdpScionSocket, RawScionSocket, ScmpScionSocket, UdpScionSocket};
189use url::Url;
190
191// Re-export the main types from the modules
192pub use self::builder::ScionStackBuilder;
193use crate::{
194    internal::Subscribers,
195    path::{
196        PathStrategy,
197        fetcher::{PathFetcherImpl, traits::SegmentFetcher},
198        manager::{
199            MultiPathManager, MultiPathManagerConfig,
200            traits::{PathWaitError, PathWaitTimeoutError},
201        },
202        policy::PathPolicy,
203    },
204    stack::{
205        scmp_handler::{ScmpErrorHandler, ScmpErrorReceiver},
206        socket::SendErrorReceiver,
207    },
208};
209
210/// The SCION stack can be used to create path-aware SCION sockets or even Quic over SCION
211/// connections.
212///
213/// The SCION stack abstracts over the underlay stack that is used for the underlying
214/// transport.
215pub struct ScionStack {
216    endhost_api: Option<Url>,
217    default_segment_fetcher: Arc<dyn SegmentFetcher>,
218    underlay: Arc<dyn DynUnderlayStack>,
219    scmp_error_receivers: Subscribers<dyn ScmpErrorReceiver>,
220    send_error_receivers: Subscribers<dyn SendErrorReceiver>,
221}
222
223// Intentionally shows only the endhost API URL; the underlay/fetcher/receivers are not `Debug`.
224#[allow(clippy::missing_fields_in_debug)]
225impl fmt::Debug for ScionStack {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.debug_struct("ScionStack")
228            .field("endhost_api", &self.endhost_api)
229            .finish()
230    }
231}
232
233impl ScionStack {
234    pub(crate) fn new(
235        endhost_api: Option<Url>,
236        default_segment_fetcher: Arc<dyn SegmentFetcher>,
237        underlay: Arc<dyn DynUnderlayStack>,
238    ) -> Self {
239        Self {
240            endhost_api,
241            default_segment_fetcher,
242            underlay,
243            scmp_error_receivers: Subscribers::new(),
244            send_error_receivers: Subscribers::new(),
245        }
246    }
247
248    /// Create a path-aware SCION socket with automatic path management.
249    ///
250    /// # Arguments
251    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
252    ///   used.
253    ///
254    /// # Returns
255    /// A path-aware SCION socket.
256    pub async fn bind(
257        &self,
258        bind_addr: Option<ScionSocketIpAddr>,
259    ) -> Result<UdpScionSocket, ScionSocketBindError> {
260        self.bind_with_config(bind_addr, SocketConfig::default())
261            .await
262    }
263
264    /// Create a path-aware SCION socket with custom configuration.
265    ///
266    /// # Arguments
267    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
268    ///   used.
269    /// * `socket_config` - Configuration for the socket.
270    ///
271    /// # Returns
272    /// A path-aware SCION socket.
273    pub async fn bind_with_config(
274        &self,
275        bind_addr: Option<ScionSocketIpAddr>,
276        mut socket_config: SocketConfig,
277    ) -> Result<UdpScionSocket, ScionSocketBindError> {
278        let socket = PathUnawareUdpScionSocket::new(
279            self.underlay
280                .bind_socket(SocketKind::Udp, bind_addr)
281                .await?,
282            vec![Box::new(ScmpErrorHandler::new(
283                self.scmp_error_receivers.clone(),
284            ))],
285        );
286
287        if !socket_config.disable_default_segment_fetcher {
288            socket_config
289                .segment_fetchers
290                .push(("Endhost API".into(), self.default_segment_fetcher.clone()));
291        }
292        let fetcher = PathFetcherImpl::new(
293            socket_config.segment_fetchers,
294            socket_config.segment_fetcher_timeout,
295        );
296
297        // Use default scorers if none are configured.
298        if socket_config.path_strategy.scoring.is_empty() {
299            socket_config.path_strategy.scoring.use_default_scorers();
300        }
301
302        let pather = Arc::new(
303            MultiPathManager::new(
304                MultiPathManagerConfig::default(),
305                fetcher,
306                socket_config.path_strategy,
307            )
308            .expect("should not fail with default configuration"),
309        );
310
311        // Register the path manager as a SCMP error receiver and send error receiver.
312        self.scmp_error_receivers.register(pather.clone());
313        self.send_error_receivers.register(pather.clone());
314
315        Ok(UdpScionSocket::new(
316            socket,
317            pather,
318            socket_config.connect_timeout,
319            self.send_error_receivers.clone(),
320        ))
321    }
322
323    /// Create a connected path-aware SCION socket with automatic path management.
324    ///
325    /// # Arguments
326    /// * `remote_addr` - The remote address to connect to.
327    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
328    ///   used.
329    ///
330    /// # Returns
331    /// A connected path-aware SCION socket.
332    pub async fn connect(
333        &self,
334        remote_addr: ScionSocketIpAddr,
335        bind_addr: Option<ScionSocketIpAddr>,
336    ) -> Result<UdpScionSocket, ScionSocketConnectError> {
337        let socket = self.bind(bind_addr).await?;
338        socket.connect(remote_addr).await
339    }
340
341    /// Create a connected path-aware SCION socket with custom configuration.
342    ///
343    /// # Arguments
344    /// * `remote_addr` - The remote address to connect to.
345    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
346    ///   used.
347    /// * `socket_config` - Configuration for the socket
348    ///
349    /// # Returns
350    /// A connected path-aware SCION socket.
351    pub async fn connect_with_config(
352        &self,
353        remote_addr: ScionSocketIpAddr,
354        bind_addr: Option<ScionSocketIpAddr>,
355        socket_config: SocketConfig,
356    ) -> Result<UdpScionSocket, ScionSocketConnectError> {
357        let socket = self.bind_with_config(bind_addr, socket_config).await?;
358        socket.connect(remote_addr).await
359    }
360
361    /// Create a socket that can send and receive SCMP messages.
362    ///
363    /// # Arguments
364    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
365    ///   used.
366    ///
367    /// # Returns
368    /// A SCMP socket.
369    pub async fn bind_scmp(
370        &self,
371        bind_addr: Option<ScionSocketIpAddr>,
372    ) -> Result<ScmpScionSocket, ScionSocketBindError> {
373        let socket = self
374            .underlay
375            .bind_socket(SocketKind::Scmp, bind_addr)
376            .await?;
377        Ok(ScmpScionSocket::new(socket))
378    }
379
380    /// Create a raw SCION socket.
381    /// A raw SCION socket can be used to send and receive raw SCION packets.
382    /// It is still bound to a specific UDP port because this is needed for packets
383    /// to be routed in a dispatcherless autonomous system. See <https://docs.scion.org/en/latest/dev/design/router-port-dispatch.html> for a more detailed explanation.
384    ///
385    /// # Arguments
386    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
387    ///   used.
388    ///
389    /// # Returns
390    /// A raw SCION socket.
391    pub async fn bind_raw(
392        &self,
393        bind_addr: Option<ScionSocketIpAddr>,
394    ) -> Result<RawScionSocket, ScionSocketBindError> {
395        let socket = self
396            .underlay
397            .bind_socket(SocketKind::Raw, bind_addr)
398            .await?;
399        Ok(RawScionSocket::new(socket))
400    }
401
402    /// Create a path-unaware SCION socket for advanced use cases.
403    ///
404    /// This socket can send and receive datagrams, but requires explicit paths for sending.
405    /// Use this when you need full control over path selection.
406    ///
407    /// # Arguments
408    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
409    ///   used.
410    ///
411    /// # Returns
412    /// A path-unaware SCION socket.
413    pub async fn bind_path_unaware(
414        &self,
415        bind_addr: Option<ScionSocketIpAddr>,
416    ) -> Result<PathUnawareUdpScionSocket, ScionSocketBindError> {
417        let socket = self
418            .underlay
419            .bind_socket(SocketKind::Udp, bind_addr)
420            .await?;
421
422        Ok(PathUnawareUdpScionSocket::new(socket, vec![]))
423    }
424
425    /// Get the list of local ISD-ASes available on the endhost.
426    ///
427    /// # Returns
428    ///
429    /// A list of local ISD-AS identifiers.
430    pub fn local_ases(&self) -> Vec<IsdAsn> {
431        self.underlay.local_ases()
432    }
433
434    /// Get the currently selected endhost API URL, if any.
435    pub fn endhost_api(&self) -> Option<Url> {
436        self.endhost_api.clone()
437    }
438
439    /// Creates a path manager with default configuration.
440    pub fn create_path_manager(&self) -> MultiPathManager<PathFetcherImpl> {
441        let fetcher = PathFetcherImpl::new(
442            vec![("Endhost API".into(), self.default_segment_fetcher.clone())],
443            DEFAULT_SEGMENT_FETCHER_TIMEOUT,
444        );
445        let mut strategy = PathStrategy::default();
446
447        strategy.scoring.use_default_scorers();
448
449        MultiPathManager::new(MultiPathManagerConfig::default(), fetcher, strategy)
450            .expect("should not fail with default configuration")
451    }
452
453    /// Creates a path fetcher with default configuration.
454    ///
455    /// A [`PathFetcher`](crate::path::fetcher::traits::PathFetcher) exposes the *set* of paths to a
456    /// destination, whereas the socket (via the path manager returned by
457    /// [`create_path_manager`](Self::create_path_manager)) automatically selects one. Use this when
458    /// the application wants to inspect the available paths and choose one deliberately, then send
459    /// over it with [`send_to_via`](crate::stack::UdpScionSocket::send_to_via).
460    ///
461    /// ```no_run
462    /// use scion_stack::{path::fetcher::traits::PathFetcher, stack::ScionStackBuilder};
463    /// use sciparse::identifier::isd_asn::IsdAsn;
464    ///
465    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
466    /// let stack = ScionStackBuilder::new().build().await?;
467    /// let src: IsdAsn = "1-ff00:0:110".parse()?;
468    /// let dst: IsdAsn = "2-ff00:0:222".parse()?;
469    ///
470    /// let paths = stack.create_path_fetcher().fetch_paths(src, dst).await?;
471    /// for path in &paths {
472    ///     println!("{path}");
473    /// }
474    /// # Ok(())
475    /// # }
476    /// ```
477    pub fn create_path_fetcher(&self) -> PathFetcherImpl {
478        PathFetcherImpl::new(
479            vec![("Endhost API".into(), self.default_segment_fetcher.clone())],
480            DEFAULT_SEGMENT_FETCHER_TIMEOUT,
481        )
482    }
483}
484
485/// Default timeout for creating a connected socket
486pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
487
488/// Default timeout for segment fetchers to avoid waiting indefinitely for slow or unresponsive
489/// fetchers.
490pub const DEFAULT_SEGMENT_FETCHER_TIMEOUT: Duration = Duration::from_secs(60);
491
492/// Configuration for a path aware socket.
493pub struct SocketConfig {
494    pub(crate) segment_fetchers: Vec<(String, Arc<dyn SegmentFetcher>)>,
495    pub(crate) segment_fetcher_timeout: Duration,
496    pub(crate) disable_default_segment_fetcher: bool,
497    pub(crate) path_strategy: PathStrategy,
498    pub(crate) connect_timeout: Duration,
499}
500
501impl Default for SocketConfig {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507impl SocketConfig {
508    /// Creates a new default socket configuration.
509    #[must_use]
510    pub fn new() -> Self {
511        Self {
512            segment_fetchers: Vec::new(),
513            segment_fetcher_timeout: DEFAULT_SEGMENT_FETCHER_TIMEOUT,
514            disable_default_segment_fetcher: false,
515            path_strategy: PathStrategy::default(),
516            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
517        }
518    }
519
520    /// Adds a path policy.
521    ///
522    /// Path policies can restrict the set of usable paths based on their characteristics.
523    /// E.g. filtering out paths that go through certain ASes.
524    ///
525    /// See [`HopPatternPolicy`](sciparse::path::policy::hop_pattern::HopPatternPolicy) and
526    /// [`AclPolicy`](sciparse::path::policy::acl::AclPolicy)
527    #[must_use]
528    pub fn with_path_policy(mut self, policy: impl PathPolicy) -> Self {
529        self.path_strategy.add_policy(policy);
530        self
531    }
532
533    /// Sets connection timeout for `connect` functions
534    ///
535    /// Defaults to [`DEFAULT_CONNECT_TIMEOUT`]
536    #[must_use]
537    pub fn with_connection_timeout(mut self, timeout: Duration) -> Self {
538        self.connect_timeout = timeout;
539        self
540    }
541
542    /// Add an additional segment fetcher.
543    ///
544    /// By default, only path segments retrieved via the default segment fetcher are used. Adding
545    /// additional segment fetchers enables to build paths from different segment sources.
546    #[must_use]
547    pub fn with_segment_fetcher(mut self, name: String, fetcher: Arc<dyn SegmentFetcher>) -> Self {
548        self.segment_fetchers.push((name, fetcher));
549        self
550    }
551
552    /// Disable fetching path segments from the default segment fetcher.
553    #[must_use]
554    pub fn disable_default_segment_fetcher(mut self) -> Self {
555        self.disable_default_segment_fetcher = true;
556        self
557    }
558
559    /// Sets the segment fetcher timeout. The timeout prevents waiting indefinitely for slow or
560    /// unresponsive segment fetchers. If a fetcher does not respond within the timeout, it will be
561    /// skipped for the current path lookup.
562    ///
563    /// Defaults to [`DEFAULT_SEGMENT_FETCHER_TIMEOUT`].
564    #[must_use]
565    pub fn with_segment_fetcher_timeout(mut self, timeout: Duration) -> Self {
566        self.segment_fetcher_timeout = timeout;
567        self
568    }
569}
570
571/// Error return when binding a socket.
572#[derive(Debug, thiserror::Error)]
573#[non_exhaustive]
574pub enum ScionSocketBindError {
575    /// The provided bind address cannot be bound to.
576    /// E.g. because it is not assigned to the endhost or because the address
577    /// type is not supported.
578    #[error(transparent)]
579    InvalidBindAddress(InvalidBindAddressError),
580    /// The provided port is already in use.
581    #[error("port {0} is already in use")]
582    PortAlreadyInUse(u16),
583    /// Failed to connect to SNAP data plane.
584    #[error(transparent)]
585    SnapConnectionError(SnapConnectionError),
586    /// No underlay available to bind the requested address.
587    #[error("underlay unavailable for the requested ISD: {0}")]
588    NoUnderlayAvailable(Isd),
589    /// An error that is not covered by the variants above.
590    #[error("other error: {0}")]
591    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
592}
593
594impl ScionSocketBindError {
595    /// Returns whether the failure is transient, so that a retry may help.
596    ///
597    /// Prefer this over matching the variants: a new variant would silently fall into a caller's
598    /// wildcard arm.
599    #[must_use]
600    pub fn is_transient(&self) -> bool {
601        match self {
602            Self::InvalidBindAddress(error) => error.is_transient(),
603            Self::SnapConnectionError(error) => error.is_transient(),
604            // The underlay set is kept up to date by discovery but it's not treated as transient
605            // error.
606            Self::NoUnderlayAvailable(_) => false,
607            Self::PortAlreadyInUse(_) => false,
608            Self::Other(_) => false, // Not classifiable
609        }
610    }
611}
612
613/// Error related to the bind address of the socket.
614#[derive(Debug, thiserror::Error, PartialEq, Eq)]
615#[non_exhaustive]
616pub enum InvalidBindAddressError {
617    /// The requested bind address cannot be bound to.
618    #[error("cannot bind to requested address: {0}")]
619    CannotBindToRequestedAddress(ScionSocketIpAddr, Cow<'static, str>),
620    /// The assigned address does not match the requested address.
621    /// This is likely due to NAT.
622    #[error(
623        "assigned address ({assigned_addr}) does not match requested address ({bind_addr}), likely due to NAT"
624    )]
625    AddressMismatch {
626        /// The assigned address.
627        assigned_addr: ScionSocketIpAddr,
628        /// The requested bind address.
629        bind_addr: ScionSocketIpAddr,
630    },
631    /// Could not find any local IP address to bind to.
632    #[error("could not find any local IP address to bind to")]
633    NoLocalIpAddressFound,
634}
635
636impl InvalidBindAddressError {
637    /// Returns whether the failure is transient, so that a retry may help.
638    #[must_use]
639    pub fn is_transient(&self) -> bool {
640        match self {
641            // The host has no address to bind to yet.
642            Self::NoLocalIpAddressFound => true,
643            Self::CannotBindToRequestedAddress(..) | Self::AddressMismatch { .. } => false,
644        }
645    }
646}
647
648/// Error related to the connection to the SNAP data plane.
649#[derive(Debug, thiserror::Error)]
650#[non_exhaustive]
651pub enum SnapConnectionError {
652    /// Snap sockets cannot be bound without a SNAP token source.
653    #[error("SNAP token source is missing")]
654    SnapTokenSourceMissing,
655    /// Error establishing the SNAP tunnel.
656    #[error("error establishing SNAP tunnel")]
657    TunnelEstablishment(#[source] ConnectSnapTunSocketError),
658    /// Failed to create the SNAP control plane client.
659    #[error("failed to create SNAP control plane client")]
660    ControlPlaneClientCreation(#[source] CrpcClientCreationError),
661    /// Failed to discover the SNAP data plane.
662    #[error("failed to discover SNAP data plane")]
663    DataPlaneDiscovery(#[source] CrpcClientError),
664}
665
666impl SnapConnectionError {
667    /// Returns whether the failure is transient, so that a retry may help.
668    ///
669    /// Prefer this over matching the variants: a new variant would silently fall into a caller's
670    /// wildcard arm.
671    #[must_use]
672    pub fn is_transient(&self) -> bool {
673        match self {
674            // The stack was built without a token source, which no retry supplies.
675            Self::SnapTokenSourceMissing => false,
676            Self::ControlPlaneClientCreation(error) => error.is_transient(),
677            Self::TunnelEstablishment(error) => error.is_transient(),
678            Self::DataPlaneDiscovery(error) => error.is_transient(),
679        }
680    }
681}
682
683/// Available kinds of SCION sockets.
684#[derive(Hash, Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
685pub(crate) enum SocketKind {
686    /// UDP socket.
687    Udp,
688    /// SCMP socket.
689    Scmp,
690    /// Raw socket.
691    Raw,
692}
693/// A trait that defines the underlay stack.
694///
695/// The underlay stack is the underlying transport layer that is used to send and receive SCION
696/// packets. Sockets returned by the underlay stack have no path management but allow
697/// sending and receiving SCION packets.
698pub(crate) trait DynUnderlayStack: Send + Sync {
699    fn bind_socket(
700        &self,
701        kind: SocketKind,
702        bind_addr: Option<ScionSocketIpAddr>,
703    ) -> BoxFuture<'_, Result<BoundUnderlaySocket, ScionSocketBindError>>;
704
705    fn local_ases(&self) -> Vec<IsdAsn>;
706}
707
708/// An underlay socket together with the metadata resolved when it was bound.
709///
710/// The [`local_addr`](Self::local_addr) and [`snap_data_plane`](Self::snap_data_plane) are fixed at
711/// bind time and are therefore carried here rather than being queried on every call through the
712/// [`UnderlaySocket`] trait.
713pub(crate) struct BoundUnderlaySocket {
714    /// The underlay socket.
715    pub socket: Box<dyn UnderlaySocket>,
716    /// The local SCION address the socket is bound to.
717    pub local_addr: ScionSocketIpAddr,
718    /// The SNAP data plane the socket is connected to, if a SNAP underlay is used.
719    pub snap_data_plane: Option<net::SocketAddr>,
720}
721
722/// SCION socket connect errors.
723#[derive(Debug, thiserror::Error)]
724#[non_exhaustive]
725pub enum ScionSocketConnectError {
726    /// Could not get a path to the destination
727    #[error("failed to get path to destination: {0}")]
728    PathLookupError(#[from] PathWaitTimeoutError),
729    /// Could not bind the socket
730    #[error(transparent)]
731    BindError(#[from] ScionSocketBindError),
732}
733
734/// SCION socket send errors.
735#[derive(Debug, thiserror::Error)]
736#[non_exhaustive]
737pub enum ScionSocketSendError {
738    /// There was an error looking up the path in the path registry.
739    #[error("path lookup error: {0}")]
740    PathLookupError(#[from] PathWaitError),
741    /// UDP underlay next hop unreachable. This is only
742    /// returned if the selected underlay is UDP.
743    #[error("udp next hop {address:?} unreachable: {isd_as}#{interface_id}: {msg}")]
744    UnderlayNextHopUnreachable {
745        /// ISD-AS of the next hop.
746        isd_as: IsdAsn,
747        /// Interface ID of the next hop.
748        interface_id: u16,
749        /// Address of the next hop, if known.
750        address: Option<net::SocketAddr>,
751        /// Additional message.
752        msg: String,
753    },
754    /// The provided packet is invalid. The underlying socket is
755    /// not able to process the packet.
756    #[error("invalid packet: {0}")]
757    InvalidPacket(Cow<'static, str>),
758    /// The underlying socket is closed.
759    #[error("underlying socket is closed")]
760    Closed,
761    /// IO Error from the underlying connection.
762    #[error("underlying connection returned an I/O error: {0:?}")]
763    IoError(#[from] std::io::Error),
764    /// Error return when send is called on a socket that is not connected.
765    #[error("socket is not connected")]
766    NotConnected,
767}
768
769/// SCION socket receive errors.
770#[derive(Debug, thiserror::Error)]
771#[non_exhaustive]
772pub enum ScionSocketReceiveError {
773    /// I/O error.
774    #[error("i/o error: {0:?}")]
775    IoError(#[from] std::io::Error),
776    /// Error return when recv is called on a socket that is not connected.
777    #[error("socket is not connected")]
778    NotConnected,
779}
780
781/// The maximum size in bytes of a raw SCION packet handled by the underlay.
782///
783/// This is large enough to hold any UDP datagram and can be used to size a receive buffer passed to
784/// [`UnderlaySocket::try_recv`].
785pub(crate) const MAX_UNDERLAY_PACKET_SIZE: usize = 65535;
786
787/// A trait that defines an abstraction over an underlay socket.
788///
789/// The socket sends and receives raw SCION packets. Decoding of the next layer protocol or SCMP
790/// handling is left to the caller.
791///
792/// The core operations [`try_send`](Self::try_send) and [`try_recv`](Self::try_recv) are
793/// synchronous and non-blocking, so the socket can be driven from non-`async` code. The
794/// [`writeable`](Self::writeable) and [`readable`](Self::readable) readiness notifications are
795/// `async`. Blocking-style `send`/`recv` helpers are layered on top by the [`UnderlaySocketExt`]
796/// extension trait, so implementors only provide the core primitives. Receiving is zero-copy into a
797/// caller-owned buffer: `try_recv` writes the packet into `buf` and returns its length, so the
798/// underlay never hides an allocation.
799#[async_trait]
800pub(crate) trait UnderlaySocket: 'static + Send + Sync {
801    /// Attempts to send the raw packet in its entirety.
802    ///
803    /// Returns an error if the underlying socket is not ready to send, or if another error occurs.
804    /// A socket that is not ready reports [`ScionSocketSendError::IoError`] with
805    /// [`std::io::ErrorKind::WouldBlock`].
806    ///
807    /// Takes a [`ScionRawPacketView`] because it needs to read the path to resolve the underlay
808    /// next hop.
809    fn try_send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError>;
810
811    /// Resolves once the underlying socket is ready to send.
812    ///
813    /// A wakeup does not guarantee that the next call to [`try_send`](Self::try_send) will succeed,
814    /// as the socket may have become not ready again in the meantime. The caller should call
815    /// `try_send` again after this future resolves.
816    async fn writeable(&self);
817
818    /// Attempts to receive a raw SCION packet into `buf`, returning the number of bytes written.
819    ///
820    /// On success the packet occupies `buf[..n]` and is guaranteed to decode with
821    /// [`ScionRawPacketView::try_from_slice`]. Returns an error if the underlying socket is not
822    /// ready to receive, or if another error occurs. A socket that is not ready reports
823    /// [`ScionSocketReceiveError::IoError`] with [`std::io::ErrorKind::WouldBlock`].
824    fn try_recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError>;
825
826    /// Resolves once the underlying socket is ready to receive.
827    ///
828    /// A wakeup does not guarantee that the next call to [`try_recv`](Self::try_recv) will succeed,
829    /// as the socket may have become not ready again in the meantime. The caller should call
830    /// `try_recv` again after this future resolves.
831    async fn readable(&self);
832}
833
834/// Blocking-style convenience methods layered on top of [`UnderlaySocket`].
835///
836/// This is blanket-implemented for every [`UnderlaySocket`], so implementors only ever provide the
837/// core primitives while callers get [`send`](Self::send)/[`recv`](Self::recv) built on top of
838/// them.
839#[async_trait]
840pub(crate) trait UnderlaySocketExt: UnderlaySocket {
841    /// Sends the raw packet, waiting for the socket to become writeable if necessary.
842    ///
843    /// Takes a [`ScionRawPacketView`] because it needs to read the path to resolve the underlay
844    /// next hop.
845    async fn send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError>;
846
847    /// Receives a raw SCION packet into `buf`, waiting for the socket to become readable if
848    /// necessary. Returns the number of bytes written; the packet occupies `buf[..n]`.
849    async fn recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError>;
850}
851
852#[async_trait]
853impl<T: UnderlaySocket + ?Sized> UnderlaySocketExt for T {
854    async fn send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
855        loop {
856            match self.try_send(packet) {
857                Err(ScionSocketSendError::IoError(e))
858                    if e.kind() == std::io::ErrorKind::WouldBlock =>
859                {
860                    self.writeable().await;
861                }
862                result => return result,
863            }
864        }
865    }
866
867    async fn recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
868        loop {
869            match self.try_recv(buf) {
870                Err(ScionSocketReceiveError::IoError(e))
871                    if e.kind() == std::io::ErrorKind::WouldBlock =>
872                {
873                    self.readable().await;
874                }
875                result => return result,
876            }
877        }
878    }
879}
880
881impl Drop for ScionStack {
882    fn drop(&mut self) {
883        tracing::warn!("ScionStack was dropped");
884    }
885}