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}