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