scion_stack/
scionstack.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_proto::address::SocketAddr;
27//! use scion_stack::scionstack::{ScionStack, ScionStackBuilder};
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 =
34//!     ScionStackBuilder::new(control_plane_addr).with_auth_token("SNAP token".to_string());
35//!
36//! let scion_stack = builder.build().await?;
37//! let socket = scion_stack.bind(None).await?;
38//!
39//! // Parse destination address
40//! let destination: SocketAddr = "1-ff00:0:111,[192.168.1.1]:8080".parse()?;
41//!
42//! socket.send_to(b"hello", destination).await?;
43//! let mut buffer = [0u8; 1024];
44//! let (len, src) = socket.recv_from(&mut buffer).await?;
45//! println!("Received: {:?} from {:?}", &buffer[..len], src);
46//!
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! ### Creating a connected socket.
52//!
53//! ```
54//! use scion_proto::address::SocketAddr;
55//! use scion_stack::scionstack::{ScionStack, ScionStackBuilder};
56//! use url::Url;
57//!
58//! # async fn connected_socket_example() -> Result<(), Box<dyn std::error::Error>> {
59//! // Create a SCION stack builder
60//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
61//! let builder =
62//!     ScionStackBuilder::new(control_plane_addr).with_auth_token("SNAP token".to_string());
63//!
64//! // Parse destination address
65//! let destination: SocketAddr = "1-ff00:0:111,[192.168.1.1]:8080".parse()?;
66//!
67//! let scion_stack = builder.build().await?;
68//! let connected_socket = scion_stack.connect(destination, None).await?;
69//! connected_socket.send(b"hello").await?;
70//! let mut buffer = [0u8; 1024];
71//! let len = connected_socket.recv(&mut buffer).await?;
72//! println!("Received: {:?}", &buffer[..len]);
73//!
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! ### Creating a path-unaware socket
79//!
80//! ```
81//! use scion_proto::{address::SocketAddr, path::Path};
82//! use scion_stack::scionstack::{ScionStack, ScionStackBuilder};
83//! use url::Url;
84//!
85//! # async fn basic_socket_example() -> Result<(), Box<dyn std::error::Error>> {
86//! // Create a SCION stack builder
87//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
88//! let builder =
89//!     ScionStackBuilder::new(control_plane_addr).with_auth_token("SNAP token".to_string());
90//!
91//! // Parse addresses
92//! let bind_addr: SocketAddr = "1-ff00:0:110,[127.0.0.1]:8080".parse()?;
93//! let destination: SocketAddr = "1-ff00:0:111,[127.0.0.1]:9090".parse()?;
94//!
95//! // Create a local path for demonstration
96//! let path: scion_proto::path::Path<bytes::Bytes> = Path::local(bind_addr.isd_asn());
97//!
98//! let scion_stack = builder.build().await?;
99//! let socket = scion_stack.bind_path_unaware(Some(bind_addr)).await?;
100//! socket
101//!     .send_to_via(b"hello", destination, &path.to_slice_path())
102//!     .await?;
103//! let mut buffer = [0u8; 1024];
104//! let (len, sender) = socket.recv_from(&mut buffer).await?;
105//! println!("Received: {:?} from {:?}", &buffer[..len], sender);
106//!
107//! # Ok(())
108//! # }
109//! ```
110//!
111//! ## Advanced Usage
112//!
113//! ### Custom path selection
114//!
115//! ```
116//! // Implement your own path selection logic
117//! use std::sync::Arc;
118//!
119//! use bytes::Bytes;
120//! use chrono::{DateTime, Utc};
121//! use scion_proto::{
122//!     address::{IsdAsn, SocketAddr},
123//!     path::Path,
124//! };
125//! use scion_stack::{
126//!     path::manager::PathManager,
127//!     scionstack::{ScionStack, ScionStackBuilder, UdpScionSocket},
128//!     types::ResFut,
129//! };
130//!
131//! struct MyCustomPathManager;
132//!
133//! impl scion_stack::path::manager::SyncPathManager for MyCustomPathManager {
134//!     fn register_path(
135//!         &self,
136//!         _src: IsdAsn,
137//!         _dst: IsdAsn,
138//!         _now: DateTime<Utc>,
139//!         _path: Path<Bytes>,
140//!     ) {
141//!         // Optionally implement registration logic
142//!     }
143//!
144//!     fn try_cached_path(
145//!         &self,
146//!         _src: IsdAsn,
147//!         _dst: IsdAsn,
148//!         _now: DateTime<Utc>,
149//!     ) -> std::io::Result<Option<Path<Bytes>>> {
150//!         todo!()
151//!     }
152//! }
153//!
154//! impl scion_stack::path::manager::PathManager for MyCustomPathManager {
155//!     fn path_wait(
156//!         &self,
157//!         src: IsdAsn,
158//!         _dst: IsdAsn,
159//!         _now: DateTime<Utc>,
160//!     ) -> impl ResFut<'_, Path<Bytes>, scion_stack::path::manager::PathWaitError> {
161//!         async move { Ok(Path::local(src)) }
162//!     }
163//! }
164//!
165//! # async fn custom_pather_example() -> Result<(), Box<dyn std::error::Error>> {
166//! // Create a SCION stack builder
167//! let control_plane_addr: url::Url = "http://127.0.0.1:1234".parse()?;
168//! let builder =
169//!     ScionStackBuilder::new(control_plane_addr).with_auth_token("SNAP token".to_string());
170//!
171//! // Parse addresses
172//! let bind_addr: SocketAddr = "1-ff00:0:110,[127.0.0.1]:8080".parse()?;
173//! let destination: SocketAddr = "1-ff00:0:111,[127.0.0.1]:9090".parse()?;
174//!
175//! let scion_stack = builder.build().await?;
176//! let path_unaware_socket = scion_stack.bind_path_unaware(Some(bind_addr)).await?;
177//! let socket = UdpScionSocket::new(path_unaware_socket, Arc::new(MyCustomPathManager), None);
178//! socket.send_to(b"hello", destination).await?;
179//!
180//! # Ok(())
181//! # }
182//! ```
183
184pub mod builder;
185pub mod quic;
186pub mod scmp_handler;
187mod socket;
188pub(crate) mod udp_polling;
189
190use std::{
191    borrow::Cow,
192    fmt,
193    pin::Pin,
194    sync::Arc,
195    task::{Context, Poll},
196    time::{Duration, Instant},
197};
198
199use anyhow::Context as _;
200use bytes::Bytes;
201use endhost_api_client::client::EndhostApiClient;
202use futures::future::BoxFuture;
203use quic::{AddressTranslator, Endpoint, ScionAsyncUdpSocket};
204use scion_proto::{
205    address::{EndhostAddr, IsdAsn, SocketAddr},
206    packet::ScionPacketRaw,
207    path::Path,
208};
209pub use socket::{PathUnawareUdpScionSocket, RawScionSocket, ScmpScionSocket, UdpScionSocket};
210
211// Re-export the main types from the modules
212pub use self::builder::ScionStackBuilder;
213pub use self::scmp_handler::{DefaultScmpHandler, ScmpHandler};
214use crate::path::{
215    Shortest,
216    manager::{CachingPathManager, ConnectRpcSegmentFetcher, PathFetcherImpl},
217};
218
219/// Default duration to reserve a port when binding a socket.
220pub const DEFAULT_RESERVED_TIME: Duration = Duration::from_secs(3);
221
222/// The SCION stack can be used to create path-aware SCION sockets or even Quic over SCION
223/// connections.
224///
225/// The SCION stack abstracts over the underlay stack that is used for the underlying
226/// transport.
227pub struct ScionStack {
228    client: Arc<dyn EndhostApiClient>,
229    underlay: Arc<dyn DynUnderlayStack>,
230}
231
232impl fmt::Debug for ScionStack {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        f.debug_struct("ScionStack")
235            .field("client", &"Arc<ConnectRpcClient>")
236            .field("underlay", &"Arc<dyn DynUnderlayStack>")
237            .finish()
238    }
239}
240
241impl ScionStack {
242    pub(crate) fn new(
243        client: Arc<dyn EndhostApiClient>,
244        underlay: Arc<dyn DynUnderlayStack>,
245    ) -> Self {
246        Self { client, underlay }
247    }
248
249    /// Create a path-aware SCION socket with automatic path management.
250    ///
251    /// # Arguments
252    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
253    ///   used.
254    ///
255    /// # Returns
256    /// A path-aware SCION socket.
257    pub async fn bind(
258        &self,
259        bind_addr: Option<SocketAddr>,
260    ) -> Result<UdpScionSocket, ScionSocketBindError> {
261        let socket = self.bind_path_unaware(bind_addr).await?;
262        let pather = self.default_path_manager();
263        Ok(UdpScionSocket::new(socket, pather, None))
264    }
265
266    /// Create a connected path-aware SCION socket with automatic path management.
267    ///
268    /// # Arguments
269    /// * `remote_addr` - The remote address to connect to.
270    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
271    ///   used.
272    ///
273    /// # Returns
274    /// A connected path-aware SCION socket.
275    pub async fn connect(
276        &self,
277        remote_addr: SocketAddr,
278        bind_addr: Option<SocketAddr>,
279    ) -> Result<UdpScionSocket, ScionSocketBindError> {
280        let socket = self.bind(bind_addr).await?;
281        Ok(socket.connect(remote_addr))
282    }
283
284    /// Bind a socket with controlled time for port allocation.
285    ///
286    /// This allows tests to control port reservation timing without sleeping.
287    pub async fn bind_with_time(
288        &self,
289        bind_addr: Option<SocketAddr>,
290        now: std::time::Instant,
291    ) -> Result<UdpScionSocket, ScionSocketBindError> {
292        let socket = self
293            .underlay
294            .bind_socket_with_time(SocketKind::Udp, bind_addr, now)
295            .await?;
296        let pather = self.default_path_manager();
297        Ok(UdpScionSocket::new(
298            PathUnawareUdpScionSocket::new(socket),
299            pather,
300            None,
301        ))
302    }
303
304    /// Create a socket that can send and receive SCMP messages.
305    ///
306    /// # Arguments
307    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
308    ///   used.
309    ///
310    /// # Returns
311    /// A SCMP socket.
312    pub async fn bind_scmp(
313        &self,
314        bind_addr: Option<SocketAddr>,
315    ) -> Result<ScmpScionSocket, ScionSocketBindError> {
316        let socket = self
317            .underlay
318            .bind_socket(SocketKind::Scmp, bind_addr)
319            .await?;
320        Ok(ScmpScionSocket::new(socket))
321    }
322
323    /// Create a raw SCION socket.
324    /// A raw SCION socket can be used to send and receive raw SCION packets.
325    /// It is still bound to a specific UDP port because this is needed for packets
326    /// 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.
327    ///
328    /// # Arguments
329    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
330    ///   used.
331    ///
332    /// # Returns
333    /// A raw SCION socket.
334    pub async fn bind_raw(
335        &self,
336        bind_addr: Option<SocketAddr>,
337    ) -> Result<RawScionSocket, ScionSocketBindError> {
338        let socket = self
339            .underlay
340            .bind_socket(SocketKind::Raw, bind_addr)
341            .await?;
342        Ok(RawScionSocket::new(socket))
343    }
344
345    /// Create a path-unaware SCION socket for advanced use cases.
346    ///
347    /// This socket can send and receive datagrams, but requires explicit paths for sending.
348    /// Use this when you need full control over path selection.
349    ///
350    /// # Arguments
351    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
352    ///   used.
353    ///
354    /// # Returns
355    /// A path-unaware SCION socket.
356    pub async fn bind_path_unaware(
357        &self,
358        bind_addr: Option<SocketAddr>,
359    ) -> Result<PathUnawareUdpScionSocket, ScionSocketBindError> {
360        let socket = self
361            .underlay
362            .bind_socket(SocketKind::Udp, bind_addr)
363            .await?;
364        Ok(PathUnawareUdpScionSocket::new(socket))
365    }
366
367    /// Create a QUIC over SCION endpoint.
368    ///
369    /// This is a convenience method that creates a QUIC (quinn) endpoint over a SCION socket.
370    ///
371    /// # Arguments
372    /// * `bind_addr` - The address to bind the socket to. If None, an available address will be
373    ///   used.
374    /// * `config` - The quinn endpoint configuration.
375    /// * `server_config` - The quinn server configuration.
376    /// * `runtime` - The runtime to spawn tasks on.
377    ///
378    /// # Returns
379    /// A QUIC endpoint that can be used to accept or create QUIC connections.
380    pub async fn quic_endpoint(
381        &self,
382        bind_addr: Option<SocketAddr>,
383        config: quinn::EndpointConfig,
384        server_config: Option<quinn::ServerConfig>,
385        runtime: Option<Arc<dyn quinn::Runtime>>,
386    ) -> anyhow::Result<Endpoint> {
387        let socket = self.underlay.bind_async_udp_socket(bind_addr).await?;
388        let address_translator = Arc::new(AddressTranslator::default());
389        let sync_path_manager = self.default_path_manager();
390        let pather = sync_path_manager.clone();
391
392        let socket = Arc::new(ScionAsyncUdpSocket::new(
393            socket,
394            pather,
395            address_translator.clone(),
396        ));
397
398        let runtime = match runtime {
399            Some(runtime) => runtime,
400            None => quinn::default_runtime().context("No runtime found")?,
401        };
402
403        Ok(Endpoint::new_with_abstract_socket(
404            config,
405            server_config,
406            socket,
407            runtime,
408            sync_path_manager,
409            address_translator,
410        )?)
411    }
412
413    /// Get the list of local addresses assigned to the endhost.
414    ///
415    /// # Returns
416    ///
417    /// A list of local addresses assigned to the endhost.
418    pub fn local_addresses(&self) -> Vec<EndhostAddr> {
419        self.underlay.local_addresses()
420    }
421
422    /// Get an instance of the default path manager.
423    ///
424    /// # Returns
425    ///
426    /// An instance of the default path manager.
427    pub fn default_path_manager(&self) -> Arc<CachingPathManager> {
428        let fetcher = PathFetcherImpl::new(ConnectRpcSegmentFetcher::new(self.client.clone()));
429        Arc::new(CachingPathManager::start(Shortest::default(), fetcher))
430    }
431}
432
433/// Error return when binding a socket.
434#[derive(Debug, thiserror::Error)]
435pub enum ScionSocketBindError {
436    /// The provided bind address cannot be bount to.
437    /// E.g. because it is not assigned to the endhost or because the address
438    /// type is not supported.
439    #[error("invalid bind address {0}: {1}")]
440    InvalidBindAddress(SocketAddr, String),
441    /// The provided port is already in use.
442    #[error("port {0} is already in use")]
443    PortAlreadyInUse(u16),
444    /// An error that is not covered by the variants above.
445    #[error("other error: {0}")]
446    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
447    /// Internal error.
448    #[error(
449        "internal error in the SCION stack, this should never happen, please report this to the developers: {0}"
450    )]
451    Internal(String),
452}
453
454/// Available kinds of SCION sockets.
455#[derive(Hash, Eq, PartialEq, Clone, Debug, Ord, PartialOrd)]
456pub enum SocketKind {
457    /// UDP socket.
458    Udp,
459    /// SCMP socket.
460    Scmp,
461    /// Raw socket.
462    Raw,
463}
464/// A trait that defines the underlay stack.
465///
466/// The underlay stack is the underlying transport layer that is used to send and receive SCION
467/// packets. Sockets returned by the underlay stack have no path management but allow
468/// sending and receiving SCION packets.
469pub(crate) trait UnderlayStack: Send + Sync {
470    type Socket: UnderlaySocket + 'static;
471    type AsyncUdpSocket: AsyncUdpUnderlaySocket + 'static;
472
473    fn bind_socket(
474        &self,
475        kind: SocketKind,
476        bind_addr: Option<SocketAddr>,
477    ) -> BoxFuture<'_, Result<Self::Socket, ScionSocketBindError>>;
478
479    fn bind_socket_with_time(
480        &self,
481        kind: SocketKind,
482        bind_addr: Option<SocketAddr>,
483        now: Instant,
484    ) -> BoxFuture<'_, Result<Self::Socket, ScionSocketBindError>>;
485
486    fn bind_async_udp_socket(
487        &self,
488        bind_addr: Option<SocketAddr>,
489    ) -> BoxFuture<'_, Result<Self::AsyncUdpSocket, ScionSocketBindError>>;
490    /// Get the list of local addresses assigned to or configured on the endhost.
491    fn local_addresses(&self) -> Vec<EndhostAddr>;
492}
493
494/// Dyn safe trait for an underlay stack.
495pub(crate) trait DynUnderlayStack: Send + Sync {
496    fn bind_socket(
497        &self,
498        kind: SocketKind,
499        bind_addr: Option<SocketAddr>,
500    ) -> BoxFuture<'_, Result<Box<dyn UnderlaySocket>, ScionSocketBindError>>;
501
502    fn bind_socket_with_time(
503        &self,
504        kind: SocketKind,
505        bind_addr: Option<SocketAddr>,
506        now: Instant,
507    ) -> BoxFuture<'_, Result<Box<dyn UnderlaySocket>, ScionSocketBindError>>;
508
509    fn bind_async_udp_socket(
510        &self,
511        bind_addr: Option<SocketAddr>,
512    ) -> BoxFuture<'_, Result<Arc<dyn AsyncUdpUnderlaySocket>, ScionSocketBindError>>;
513
514    fn local_addresses(&self) -> Vec<EndhostAddr>;
515}
516
517impl<U: UnderlayStack> DynUnderlayStack for U {
518    fn bind_socket(
519        &self,
520        kind: SocketKind,
521        bind_addr: Option<SocketAddr>,
522    ) -> BoxFuture<'_, Result<Box<dyn UnderlaySocket>, ScionSocketBindError>> {
523        Box::pin(async move {
524            let socket = self.bind_socket(kind, bind_addr).await?;
525            Ok(Box::new(socket) as Box<dyn UnderlaySocket>)
526        })
527    }
528
529    fn bind_socket_with_time(
530        &self,
531        kind: SocketKind,
532        bind_addr: Option<SocketAddr>,
533        now: Instant,
534    ) -> BoxFuture<'_, Result<Box<dyn UnderlaySocket>, ScionSocketBindError>> {
535        Box::pin(async move {
536            let socket = self.bind_socket_with_time(kind, bind_addr, now).await?;
537            Ok(Box::new(socket) as Box<dyn UnderlaySocket>)
538        })
539    }
540
541    fn bind_async_udp_socket(
542        &self,
543        bind_addr: Option<SocketAddr>,
544    ) -> BoxFuture<'_, Result<Arc<dyn AsyncUdpUnderlaySocket>, ScionSocketBindError>> {
545        Box::pin(async move {
546            let socket = self.bind_async_udp_socket(bind_addr).await?;
547            Ok(Arc::new(socket) as Arc<dyn AsyncUdpUnderlaySocket>)
548        })
549    }
550
551    fn local_addresses(&self) -> Vec<EndhostAddr> {
552        <Self as UnderlayStack>::local_addresses(self)
553    }
554}
555
556/// SCION socket send errors.
557#[derive(Debug, thiserror::Error)]
558pub enum ScionSocketSendError {
559    /// There was an error looking up the path in the path registry.
560    #[error("path lookup error: {0}")]
561    PathLookupError(Cow<'static, str>),
562    /// The desination is not reachable. E.g. because no path is available.
563    #[error("network unreachable: {0}")]
564    NetworkUnreachable(NetworkError),
565    /// The provided packet is invalid. The underlying socket is
566    /// not able to process the packet.
567    #[error("invalid packet: {0}")]
568    InvalidPacket(Cow<'static, str>),
569    /// The underlying socket is closed.
570    #[error("underlying socket is closed")]
571    Closed,
572    /// IO Error from the underlying connection.
573    #[error("underlying connection returned an I/O error: {0:?}")]
574    IoError(#[from] std::io::Error),
575    /// Error return when send is called on a socket that is not connected.
576    #[error("socket is not connected")]
577    NotConnected,
578}
579
580/// Network errors.
581#[derive(Debug, thiserror::Error)]
582pub enum NetworkError {
583    /// The destination is unreachable.
584    #[error("destination unreachable: {0}")]
585    DestinationUnreachable(String),
586    /// Underlay next hop unreachable.
587    #[error("next hop unreachable: {isd_as}#{interface_id}: {msg}")]
588    UnderlayNextHopUnreachable {
589        /// ISD-AS of the next hop.
590        isd_as: IsdAsn,
591        /// Interface ID of the next hop.
592        interface_id: u16,
593        /// Additional message.
594        msg: String,
595    },
596}
597
598/// SCION socket receive errors.
599#[derive(Debug, thiserror::Error)]
600pub enum ScionSocketReceiveError {
601    /// Path buffer too small.
602    #[error("provided path buffer is too small (at least 1024 bytes required)")]
603    PathBufTooSmall,
604    /// I/O error.
605    #[error("i/o error: {0:?}")]
606    IoError(#[from] std::io::Error),
607    /// Error return when recv is called on a socket that is not connected.
608    #[error("socket is not connected")]
609    NotConnected,
610}
611
612/// A trait that defines an abstraction over an asynchronous underlay socket.
613pub(crate) trait UnderlaySocket: 'static + Send + Sync {
614    /// Send a raw packet. Takes a ScionPacketRaw because it needs to read the path
615    /// to resolve the underlay next hop.
616    fn send<'a>(
617        &'a self,
618        packet: ScionPacketRaw,
619    ) -> BoxFuture<'a, Result<(), ScionSocketSendError>>;
620
621    fn recv<'a>(&'a self) -> BoxFuture<'a, Result<ScionPacketRaw, ScionSocketReceiveError>>;
622
623    fn local_addr(&self) -> SocketAddr;
624}
625
626/// A trait that defines an asynchronous path unaware UDP socket.
627/// This can be used to implement the [quinn::AsyncUdpSocket] trait.
628pub(crate) trait AsyncUdpUnderlaySocket: Send + Sync {
629    fn create_io_poller(self: Arc<Self>) -> Pin<Box<dyn udp_polling::UdpPoller>>;
630    /// Try to send a raw SCION UDP packet. Path resolution and packet encoding is
631    /// left to the caller.
632    /// This function should return std::io::ErrorKind::WouldBlock if the packet cannot be sent
633    /// immediately.
634    fn try_send(&self, raw_packet: ScionPacketRaw) -> Result<(), std::io::Error>;
635    /// Poll for receiving a SCION packet with sender and path.
636    fn poll_recv_from_with_path(
637        &self,
638        cx: &mut Context,
639    ) -> Poll<std::io::Result<(SocketAddr, Bytes, Path)>>;
640    fn local_addr(&self) -> SocketAddr;
641}