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::{net, sync::Arc};
17
18use scion_proto::address::{Isd, IsdAsn, ScionAddr, SocketAddr};
19use scion_sdk_reqwest_connect_rpc::token_source::TokenSource;
20use scion_sdk_utils::backoff::ExponentialBackoff;
21use socket2::{Domain, Protocol, Socket, Type};
22use tokio::net::UdpSocket;
23use url::Url;
24use x25519_dalek::StaticSecret;
25
26use crate::{
27    scionstack::{
28        AsyncUdpUnderlaySocket, DynUnderlayStack, InvalidBindAddressError, ScionSocketBindError,
29        SnapConnectionError, UnderlaySocket, builder::PreferredUnderlay, scmp_handler::ScmpHandler,
30    },
31    underlays::{
32        discovery::{UnderlayDiscovery, UnderlayInfo},
33        udp::{LocalIpResolver, UdpAsyncUdpUnderlaySocket, UdpUnderlaySocket},
34    },
35};
36
37pub mod discovery;
38pub mod snap;
39pub mod udp;
40
41/// Configuration needed to create a SNAP socket(s).
42pub struct SnapSocketConfig {
43    /// Source for SNAP token. If this is None, no SNAP sockets
44    /// can be bound.
45    pub snap_token_source: Option<Arc<dyn TokenSource>>,
46    /// Backoff for reconnecting a SNAP tunnel.
47    pub reconnect_backoff: ExponentialBackoff,
48}
49
50/// Underlay stack.
51pub struct UnderlayStack {
52    preferred_underlay: PreferredUnderlay,
53    underlay_discovery: Arc<dyn UnderlayDiscovery>,
54    /// Resolver for the local IP address for UDP underlay sockets.
55    local_ip_resolver: Arc<dyn LocalIpResolver>,
56    snap_socket_config: SnapSocketConfig,
57    // TODO(uniquefine): This should be handled by a
58    // global identity registration component.
59    // https://github.com/Anapaya/scion/issues/27486
60    // Generate an register an identity for this socket.
61    snap_static_identity: StaticSecret,
62}
63
64impl UnderlayStack {
65    /// Creates a new underlay stack.
66    pub fn new(
67        preferred_underlay: PreferredUnderlay,
68        underlay_discovery: Arc<dyn UnderlayDiscovery>,
69        local_ip_resolver: Arc<dyn LocalIpResolver>,
70        snap_socket_config: SnapSocketConfig,
71    ) -> Self {
72        Self {
73            preferred_underlay,
74            underlay_discovery,
75            local_ip_resolver,
76            snap_socket_config,
77            snap_static_identity: StaticSecret::random(),
78        }
79    }
80
81    /// Selects the first underlay that matches the requested isd as. If available, the preferred
82    /// underlay type is returned.
83    ///
84    /// XXX(uniquefine): We only use the ISD-AS to select the underlay, the bind address is ignored.
85    /// In the unlikely case that user requests a specific IP, but a wildcard ISD-AS, it could in
86    /// theory happen that we select the wrong underlay.
87    fn select_underlay(&self, requested_isd_as: IsdAsn) -> Option<(IsdAsn, UnderlayInfo)> {
88        let underlays = self.underlay_discovery.underlays(requested_isd_as);
89        match self.preferred_underlay {
90            PreferredUnderlay::Snap => {
91                if let Some(underlay) = underlays
92                    .iter()
93                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Snap(_)))
94                {
95                    return Some(underlay.clone());
96                }
97            }
98            PreferredUnderlay::Udp => {
99                if let Some(underlay) = underlays
100                    .iter()
101                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Udp(_)))
102                {
103                    return Some(underlay.clone());
104                }
105            }
106        }
107        underlays.into_iter().next()
108    }
109
110    async fn bind_snap_socket(
111        &self,
112        bind_addr: Option<scion_proto::address::SocketAddr>,
113        isd_as: IsdAsn,
114        cp_url: Url,
115        token_source: Option<Arc<dyn TokenSource>>,
116    ) -> Result<snap::SnapUnderlaySocket, ScionSocketBindError> {
117        let token_source = token_source.ok_or(ScionSocketBindError::SnapConnectionError(
118            SnapConnectionError::SnapTokenSourceMissing,
119        ))?;
120
121        let local_addr = match bind_addr {
122            Some(addr) => {
123                addr.local_address()
124                    .ok_or(ScionSocketBindError::InvalidBindAddress(
125                        InvalidBindAddressError::ServiceAddress(addr),
126                    ))?
127            }
128            None => "0.0.0.0:0".parse().unwrap(),
129        };
130
131        let bind_addr = SocketAddr::from_std(isd_as, local_addr);
132
133        let udp_socket = bind_udp_underlay_socket(local_addr)?;
134
135        let socket = snap::SnapUnderlaySocket::new(
136            bind_addr,
137            cp_url,
138            udp_socket,
139            token_source,
140            self.snap_static_identity.clone(),
141            1024,
142        )
143        .await?;
144        Ok(socket)
145    }
146
147    fn resolve_udp_bind_addr(
148        &self,
149        isd_as: IsdAsn,
150        bind_addr: Option<SocketAddr>,
151    ) -> Result<SocketAddr, ScionSocketBindError> {
152        let bind_addr = match bind_addr {
153            Some(addr) => {
154                if addr.is_service() {
155                    return Err(ScionSocketBindError::InvalidBindAddress(
156                        InvalidBindAddressError::ServiceAddress(addr),
157                    ));
158                }
159                addr
160            }
161            None => {
162                let local_address = *self.local_ip_resolver.local_ips().first().ok_or(
163                    ScionSocketBindError::InvalidBindAddress(
164                        InvalidBindAddressError::NoLocalIpAddressFound,
165                    ),
166                )?;
167                SocketAddr::new(ScionAddr::new(isd_as, local_address.into()), 0)
168            }
169        };
170        Ok(bind_addr)
171    }
172
173    async fn bind_udp_socket(
174        &self,
175        isd_as: IsdAsn,
176        bind_addr: Option<SocketAddr>,
177    ) -> Result<(SocketAddr, UdpSocket), ScionSocketBindError> {
178        let bind_addr = self.resolve_udp_bind_addr(isd_as, bind_addr)?;
179        let local_addr: net::SocketAddr =
180            bind_addr
181                .local_address()
182                .ok_or(ScionSocketBindError::InvalidBindAddress(
183                    InvalidBindAddressError::ServiceAddress(bind_addr),
184                ))?;
185        let socket = bind_udp_underlay_socket(local_addr)?;
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 => {
233                    Err(
234                        crate::scionstack::ScionSocketBindError::NoUnderlayAvailable(
235                            requested_isd_as.isd(),
236                        ),
237                    )
238                }
239            }
240        })
241    }
242
243    fn bind_async_udp_socket(
244        &self,
245        bind_addr: Option<scion_proto::address::SocketAddr>,
246        scmp_handlers: Vec<Box<dyn ScmpHandler>>,
247    ) -> futures::future::BoxFuture<
248        '_,
249        Result<
250            std::sync::Arc<dyn crate::scionstack::AsyncUdpUnderlaySocket>,
251            crate::scionstack::ScionSocketBindError,
252        >,
253    > {
254        Box::pin(async move {
255            match self.select_underlay(
256                bind_addr
257                    .map(|addr| addr.isd_asn())
258                    .unwrap_or(IsdAsn::WILDCARD),
259            ) {
260                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
261                    let socket = self
262                        .bind_snap_socket(
263                            bind_addr,
264                            isd_as,
265                            cp_url,
266                            self.snap_socket_config.snap_token_source.clone(),
267                        )
268                        .await?;
269                    let async_udp_socket = snap::SnapAsyncUdpSocket::new(socket, scmp_handlers);
270                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
271                }
272                Some((isd_as, UnderlayInfo::Udp(_))) => {
273                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
274                    let async_udp_socket = UdpAsyncUdpUnderlaySocket::new(
275                        bind_addr,
276                        self.underlay_discovery.clone(),
277                        socket,
278                        scmp_handlers,
279                    );
280                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
281                }
282                None => {
283                    Err(
284                        crate::scionstack::ScionSocketBindError::NoUnderlayAvailable(
285                            bind_addr
286                                .map(|addr| addr.isd_asn().isd())
287                                .unwrap_or(Isd::WILDCARD),
288                        ),
289                    )
290                }
291            }
292        })
293    }
294
295    fn local_ases(&self) -> Vec<IsdAsn> {
296        let mut isd_ases: Vec<IsdAsn> = self.underlay_discovery.isd_ases().into_iter().collect();
297        isd_ases.sort();
298        isd_ases
299    }
300}
301
302#[cfg(windows)]
303fn set_exclusive_addr_use(sock: &Socket, enable: bool) -> std::io::Result<()> {
304    use std::{mem, os::windows::io::AsRawSocket};
305
306    use windows_sys::Win32::Networking::WinSock;
307
308    // Winsock expects an int/bool-ish value passed by pointer.
309    let val: u32 = if enable { 1 } else { 0 };
310
311    let rc = unsafe {
312        WinSock::setsockopt(
313            sock.as_raw_socket() as usize,
314            WinSock::SOL_SOCKET,
315            WinSock::SO_EXCLUSIVEADDRUSE,
316            &val as *const _ as *const _,
317            mem::size_of_val(&val) as _,
318        )
319    };
320
321    if rc == 0 {
322        Ok(())
323    } else {
324        Err(std::io::Error::last_os_error())
325    }
326}
327
328/// This is equivalent to tokio::net::UdpSocket::bind(addr) but with the exclusive address use set
329/// to true on windows.
330/// This is because on windows, by default, multiple sockets can bind to the same address:port
331/// if one binds to wildcard address.
332fn bind_udp_underlay_socket(
333    addr: net::SocketAddr,
334) -> Result<tokio::net::UdpSocket, ScionSocketBindError> {
335    let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))
336        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
337    socket
338        .set_nonblocking(true)
339        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
340    if addr.is_ipv6()
341        && let Err(e) = socket.set_only_v6(false)
342    {
343        tracing::debug!(%e, "unable to make socket dual-stack");
344    }
345
346    // XXX(uniquefine): on windows, we need to set the exclusive address use to true to
347    // prevent multiple sockets from binding to the same address.
348    #[cfg(windows)]
349    set_exclusive_addr_use(&socket, true).map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
350
351    socket.bind(&addr.into()).map_err(|e| {
352        match e.kind() {
353            std::io::ErrorKind::AddrInUse => ScionSocketBindError::PortAlreadyInUse(addr.port()),
354            std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::InvalidInput => {
355                ScionSocketBindError::InvalidBindAddress(
356                    InvalidBindAddressError::CannotBindToRequestedAddress(
357                        SocketAddr::from_std(IsdAsn::WILDCARD, addr),
358                        format!("Failed to bind socket: {e:#}").into(),
359                    ),
360                )
361            }
362            #[cfg(windows)]
363            // On windows, if a port is already in use the error returned is sometimes
364            // code 10013 WSAEACCES.
365            // see https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
366            std::io::ErrorKind::PermissionDenied => {
367                ScionSocketBindError::PortAlreadyInUse(addr.port())
368            }
369            _ => ScionSocketBindError::Other(Box::new(e)),
370        }
371    })?;
372
373    tokio::net::UdpSocket::from_std(std::net::UdpSocket::from(socket))
374        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))
375}