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