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}