Skip to main content

scion_stack/
underlays.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//! SCION stack underlay implementations.
15
16use std::sync::Arc;
17
18use scion_proto::address::{IsdAsn, ScionAddr, SocketAddr};
19use scion_sdk_reqwest_connect_rpc::token_source::TokenSource;
20use scion_sdk_utils::backoff::ExponentialBackoff;
21use tokio::net::UdpSocket;
22use url::Url;
23
24use crate::{
25    scionstack::{
26        AsyncUdpUnderlaySocket, DynUnderlayStack, ScionSocketBindError, UnderlaySocket,
27        builder::PreferredUnderlay, scmp_handler::ScmpHandler,
28    },
29    underlays::{
30        discovery::{UnderlayDiscovery, UnderlayInfo},
31        snap::{SnapAsyncUdpSocket, SnapUnderlaySocket},
32        udp::{LocalIpResolver, UdpAsyncUdpUnderlaySocket, UdpUnderlaySocket},
33    },
34};
35
36pub mod discovery;
37pub mod snap;
38pub mod udp;
39
40/// Configuration needed to create a SNAP socket(s).
41pub struct SnapSocketConfig {
42    /// Source for SNAP token. If this is None, no SNAP sockets
43    /// can be bound.
44    pub snap_token_source: Option<Arc<dyn TokenSource>>,
45    /// Backoff for reconnecting a SNAP tunnel.
46    pub reconnect_backoff: ExponentialBackoff,
47}
48
49/// Underlay stack.
50pub struct UnderlayStack {
51    preferred_underlay: PreferredUnderlay,
52    underlay_discovery: Arc<dyn UnderlayDiscovery>,
53    /// Resolver for the local IP address for UDP underlay sockets.
54    local_ip_resolver: Arc<dyn LocalIpResolver>,
55    snap_socket_config: SnapSocketConfig,
56}
57
58impl UnderlayStack {
59    /// Creates a new underlay stack.
60    pub fn new(
61        preferred_underlay: PreferredUnderlay,
62        underlay_discovery: Arc<dyn UnderlayDiscovery>,
63        local_ip_resolver: Arc<dyn LocalIpResolver>,
64        snap_socket_config: SnapSocketConfig,
65    ) -> Self {
66        Self {
67            preferred_underlay,
68            underlay_discovery,
69            local_ip_resolver,
70            snap_socket_config,
71        }
72    }
73
74    /// Selects the first underlay that matches the requested isd as. If available, the preferred
75    /// underlay type is returned.
76    ///
77    /// XXX(uniquefine): We only use the ISD-AS to select the underlay, the bind address is ignored.
78    /// In the unlikely case that user requests a specific IP, but a wildcard ISD-AS, it could in
79    /// theory happen that we select the wrong underlay.
80    fn select_underlay(&self, requested_isd_as: IsdAsn) -> Option<(IsdAsn, UnderlayInfo)> {
81        let underlays = self.underlay_discovery.underlays(requested_isd_as);
82        match self.preferred_underlay {
83            PreferredUnderlay::Snap => {
84                if let Some(underlay) = underlays
85                    .iter()
86                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Snap(_)))
87                {
88                    return Some(underlay.clone());
89                }
90            }
91            PreferredUnderlay::Udp => {
92                if let Some(underlay) = underlays
93                    .iter()
94                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Udp(_)))
95                {
96                    return Some(underlay.clone());
97                }
98            }
99        }
100        underlays.into_iter().next()
101    }
102
103    async fn bind_snap_socket(
104        &self,
105        bind_addr: Option<scion_proto::address::SocketAddr>,
106        isd_as: IsdAsn,
107        cp_url: Url,
108        token_source: Option<Arc<dyn TokenSource>>,
109    ) -> Result<SnapUnderlaySocket, ScionSocketBindError> {
110        let token_source = token_source.ok_or(ScionSocketBindError::DataplaneError(
111            "cannot bind SNAP socket without SNAP token (source)".into(),
112        ))?;
113
114        if let Some(SocketAddr::Svc(_)) = bind_addr {
115            return Err(ScionSocketBindError::InvalidBindAddress(
116                bind_addr.unwrap(),
117                "service addresses can't be bound".to_string(),
118            ));
119        }
120
121        let socket = SnapUnderlaySocket::new(
122            isd_as,
123            bind_addr.and_then(|addr| addr.local_address()),
124            cp_url,
125            "localhost".to_string(),
126            self.underlay_discovery.clone(),
127            token_source,
128            self.snap_socket_config.reconnect_backoff,
129        )
130        .await?;
131        Ok(socket)
132    }
133
134    fn resolve_udp_bind_addr(
135        &self,
136        isd_as: IsdAsn,
137        bind_addr: Option<SocketAddr>,
138    ) -> Result<SocketAddr, ScionSocketBindError> {
139        let bind_addr = match bind_addr {
140            Some(addr) => {
141                if addr.is_service() {
142                    return Err(ScionSocketBindError::InvalidBindAddress(
143                        addr,
144                        "service addresses can't be bound".to_string(),
145                    ));
146                }
147                addr
148            }
149            None => {
150                let local_address = *self.local_ip_resolver.local_ips().first().ok_or(
151                    ScionSocketBindError::UnderlayUnavailable("no local IP address found".into()),
152                )?;
153                SocketAddr::new(ScionAddr::new(isd_as, local_address.into()), 0)
154            }
155        };
156        Ok(bind_addr)
157    }
158
159    async fn bind_udp_socket(
160        &self,
161        isd_as: IsdAsn,
162        bind_addr: Option<SocketAddr>,
163    ) -> Result<(SocketAddr, UdpSocket), ScionSocketBindError> {
164        let bind_addr = self.resolve_udp_bind_addr(isd_as, bind_addr)?;
165        let local_addr =
166            bind_addr
167                .local_address()
168                .ok_or(ScionSocketBindError::InvalidBindAddress(
169                    bind_addr,
170                    "Service addresses can't be bound".to_string(),
171                ))?;
172        let socket = UdpSocket::bind(local_addr).await.map_err(|e| {
173            match e.kind() {
174                std::io::ErrorKind::AddrInUse => {
175                    ScionSocketBindError::PortAlreadyInUse(local_addr.port())
176                }
177                std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::InvalidInput => {
178                    ScionSocketBindError::InvalidBindAddress(
179                        bind_addr,
180                        format!("Failed to bind socket: {e:#}"),
181                    )
182                }
183                _ => ScionSocketBindError::Other(Box::new(e)),
184            }
185        })?;
186        let local_addr = socket.local_addr().map_err(|e| {
187            ScionSocketBindError::Other(
188                anyhow::anyhow!("failed to get local address: {e}").into_boxed_dyn_error(),
189            )
190        })?;
191        let bind_addr = SocketAddr::new(
192            ScionAddr::new(bind_addr.isd_asn(), local_addr.ip().into()),
193            local_addr.port(),
194        );
195        Ok((bind_addr, socket))
196    }
197}
198
199impl DynUnderlayStack for UnderlayStack {
200    fn bind_socket(
201        &self,
202        _kind: crate::scionstack::SocketKind,
203        bind_addr: Option<scion_proto::address::SocketAddr>,
204    ) -> futures::future::BoxFuture<
205        '_,
206        Result<Box<dyn crate::scionstack::UnderlaySocket>, crate::scionstack::ScionSocketBindError>,
207    > {
208        Box::pin(async move {
209            let requested_isd_as = bind_addr
210                .map(|addr| addr.isd_asn())
211                .unwrap_or(IsdAsn::WILDCARD);
212            match self.select_underlay(requested_isd_as) {
213                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
214                    Ok(Box::new(
215                        self.bind_snap_socket(
216                            bind_addr,
217                            isd_as,
218                            cp_url,
219                            self.snap_socket_config.snap_token_source.clone(),
220                        )
221                        .await?,
222                    ) as Box<dyn UnderlaySocket>)
223                }
224                Some((isd_as, UnderlayInfo::Udp(_))) => {
225                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
226                    Ok(Box::new(UdpUnderlaySocket::new(
227                        socket,
228                        bind_addr,
229                        self.underlay_discovery.clone(),
230                    )) as Box<dyn UnderlaySocket>)
231                }
232                None => Err(
233                    crate::scionstack::ScionSocketBindError::UnderlayUnavailable(
234                        format!(
235                            "no underlay available to bind the requested ISD-AS {requested_isd_as}"
236                        )
237                        .into(),
238                    ),
239                ),
240            }
241        })
242    }
243
244    fn bind_async_udp_socket(
245        &self,
246        bind_addr: Option<scion_proto::address::SocketAddr>,
247        scmp_handlers: Vec<Box<dyn ScmpHandler>>,
248    ) -> futures::future::BoxFuture<
249        '_,
250        Result<
251            std::sync::Arc<dyn crate::scionstack::AsyncUdpUnderlaySocket>,
252            crate::scionstack::ScionSocketBindError,
253        >,
254    > {
255        Box::pin(async move {
256            match self.select_underlay(
257                bind_addr
258                    .map(|addr| addr.isd_asn())
259                    .unwrap_or(IsdAsn::WILDCARD),
260            ) {
261                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
262                    let socket = self
263                        .bind_snap_socket(
264                            bind_addr,
265                            isd_as,
266                            cp_url,
267                            self.snap_socket_config.snap_token_source.clone(),
268                        )
269                        .await?;
270                    let async_udp_socket = SnapAsyncUdpSocket::new(socket, scmp_handlers);
271                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
272                }
273                Some((isd_as, UnderlayInfo::Udp(_))) => {
274                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
275                    let async_udp_socket = UdpAsyncUdpUnderlaySocket::new(
276                        bind_addr,
277                        self.underlay_discovery.clone(),
278                        socket,
279                        scmp_handlers,
280                    );
281                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
282                }
283                None => {
284                    Err(
285                        crate::scionstack::ScionSocketBindError::UnderlayUnavailable(
286                            "no underlay available".into(),
287                        ),
288                    )
289                }
290            }
291        })
292    }
293
294    fn local_ases(&self) -> Vec<IsdAsn> {
295        let mut isd_ases: Vec<IsdAsn> = self.underlay_discovery.isd_ases().into_iter().collect();
296        isd_ases.sort();
297        isd_ases
298    }
299}