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