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