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 ana_gotatun::packet::{Packet, PacketBufPool};
19use scion_proto::{
20    address::{Isd, IsdAsn, ScionAddr, SocketAddr},
21    wire_encoding::WireEncodeVec,
22};
23use scion_sdk_reqwest_connect_rpc::token_source::TokenSource;
24use snap_tun::client::{PACKET_BUF_POOL_SIZE, SnapTunEndpoint};
25use socket2::{Domain, Protocol, Socket, Type};
26use tokio::net::UdpSocket;
27use url::Url;
28use x25519_dalek::StaticSecret;
29
30use crate::{
31    scionstack::{
32        AsyncUdpUnderlaySocket, DynUnderlayStack, InvalidBindAddressError, ScionSocketBindError,
33        SnapConnectionError, UnderlaySocket, builder::PreferredUnderlay, scmp_handler::ScmpHandler,
34    },
35    underlays::{
36        discovery::{UnderlayDiscovery, UnderlayInfo},
37        udp::{LocalIpResolver, UdpAsyncUdpUnderlaySocket, UdpUnderlaySocket},
38    },
39};
40
41pub mod discovery;
42pub mod snap;
43pub mod udp;
44
45/// Configuration needed to create a SNAP socket(s).
46pub struct SnapSocketConfig {
47    /// Source for SNAP token. If this is None, no SNAP sockets
48    /// can be bound.
49    pub snap_token_source: Option<Arc<dyn TokenSource>>,
50}
51
52/// Underlay stack.
53pub struct UnderlayStack {
54    preferred_underlay: PreferredUnderlay,
55    underlay_discovery: Arc<dyn UnderlayDiscovery>,
56    /// Resolver for the local IP address for UDP underlay sockets.
57    local_ip_resolver: Arc<dyn LocalIpResolver>,
58    snap_socket_config: SnapSocketConfig,
59    snap_tunnel_manager: Option<SnapTunEndpoint>,
60    pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
61}
62
63impl UnderlayStack {
64    /// Creates a new underlay stack.
65    pub fn new(
66        preferred_underlay: PreferredUnderlay,
67        underlay_discovery: Arc<dyn UnderlayDiscovery>,
68        local_ip_resolver: Arc<dyn LocalIpResolver>,
69        static_identity: StaticSecret,
70        default_snap_socket_config: SnapSocketConfig,
71    ) -> Self {
72        let snap_tunnel_manager = default_snap_socket_config
73            .snap_token_source
74            .as_ref()
75            .map(|token_source| SnapTunEndpoint::new(token_source.clone(), static_identity));
76        Self {
77            preferred_underlay,
78            underlay_discovery,
79            local_ip_resolver,
80            snap_socket_config: default_snap_socket_config,
81            snap_tunnel_manager,
82            pool: PacketBufPool::new(64),
83        }
84    }
85
86    /// Selects the first underlay that matches the requested isd as. If available, the preferred
87    /// underlay type is returned.
88    ///
89    /// XXX(uniquefine): We only use the ISD-AS to select the underlay, the bind address is ignored.
90    /// In the unlikely case that user requests a specific IP, but a wildcard ISD-AS, it could in
91    /// theory happen that we select the wrong underlay.
92    fn select_underlay(&self, requested_isd_as: IsdAsn) -> Option<(IsdAsn, UnderlayInfo)> {
93        let underlays = self.underlay_discovery.underlays(requested_isd_as);
94        match self.preferred_underlay {
95            PreferredUnderlay::Snap => {
96                if let Some(underlay) = underlays
97                    .iter()
98                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Snap(_)))
99                {
100                    return Some(underlay.clone());
101                }
102            }
103            PreferredUnderlay::Udp => {
104                if let Some(underlay) = underlays
105                    .iter()
106                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Udp(_)))
107                {
108                    return Some(underlay.clone());
109                }
110            }
111        }
112        underlays.into_iter().next()
113    }
114
115    async fn bind_snap_socket(
116        &self,
117        requested_addr: Option<scion_proto::address::SocketAddr>,
118        isd_as: IsdAsn,
119        cp_url: Url,
120    ) -> Result<snap::SnapUnderlaySocket, ScionSocketBindError> {
121        let (Some(token_source), Some(snap_tunnel_manager)) = (
122            self.snap_socket_config.snap_token_source.as_ref(),
123            self.snap_tunnel_manager.as_ref(),
124        ) else {
125            return Err(ScionSocketBindError::SnapConnectionError(
126                SnapConnectionError::SnapTokenSourceMissing,
127            ))?;
128        };
129
130        let local_addr = match requested_addr {
131            Some(addr) => {
132                addr.local_address()
133                    .ok_or(ScionSocketBindError::InvalidBindAddress(
134                        InvalidBindAddressError::ServiceAddress(addr),
135                    ))?
136            }
137            None => {
138                if let Some(cp_addr) = cp_url
139                    .socket_addrs(|| None)
140                    .ok()
141                    .and_then(|addrs| addrs.first().cloned())
142                    && let Some(ip) = source_ip_towards(cp_addr).await
143                {
144                    Ok(net::SocketAddr::new(ip, 0))
145                } else {
146                    Err(ScionSocketBindError::InvalidBindAddress(
147                        InvalidBindAddressError::NoLocalIpAddressFound,
148                    ))
149                }?
150            }
151        };
152
153        let bind_addr = SocketAddr::from_std(isd_as, local_addr);
154
155        let udp_socket = bind_udp_underlay_socket(local_addr)?;
156
157        let socket = snap::SnapUnderlaySocket::new(
158            bind_addr,
159            cp_url,
160            udp_socket,
161            snap_tunnel_manager,
162            token_source.clone(),
163            1024,
164            self.pool.clone(),
165        )
166        .await?;
167
168        let assigned_addr = socket.local_addr();
169        // If the requested address is specified but does not match the assigned address, return an
170        // error.
171        if let Some(requested_addr) = requested_addr
172            // IsdAsn mismatch
173        && requested_addr.isd_asn().matches(assigned_addr.isd_asn())
174            // IP mismatch. Note, that both addresses will have ip addresses.
175        && let Some(requested_socket_addr) = requested_addr.local_address()
176        && let Some(assigned_socket_addr) = assigned_addr.local_address()
177        && ((!requested_socket_addr.ip().is_unspecified() && assigned_socket_addr.ip() != requested_socket_addr.ip())
178            // Port mismatch
179                || (requested_socket_addr.port() != 0 && assigned_socket_addr.port() != requested_socket_addr.port()))
180        {
181            return Err(crate::scionstack::ScionSocketBindError::InvalidBindAddress(
182                crate::scionstack::InvalidBindAddressError::AddressMismatch {
183                    assigned_addr: SocketAddr::from_std(bind_addr.isd_asn(), requested_socket_addr),
184                    bind_addr,
185                },
186            ));
187        }
188
189        Ok(socket)
190    }
191
192    async fn resolve_udp_bind_addr(
193        &self,
194        isd_as: IsdAsn,
195        bind_addr: Option<SocketAddr>,
196    ) -> Result<SocketAddr, ScionSocketBindError> {
197        let bind_addr = match bind_addr {
198            Some(addr) => {
199                if addr.is_service() {
200                    return Err(ScionSocketBindError::InvalidBindAddress(
201                        InvalidBindAddressError::ServiceAddress(addr),
202                    ));
203                }
204                addr
205            }
206            None => {
207                let local_address = *self.local_ip_resolver.local_ips().await.first().ok_or(
208                    ScionSocketBindError::InvalidBindAddress(
209                        InvalidBindAddressError::NoLocalIpAddressFound,
210                    ),
211                )?;
212                SocketAddr::new(ScionAddr::new(isd_as, local_address.into()), 0)
213            }
214        };
215        Ok(bind_addr)
216    }
217
218    async fn bind_udp_socket(
219        &self,
220        isd_as: IsdAsn,
221        bind_addr: Option<SocketAddr>,
222    ) -> Result<(SocketAddr, UdpSocket), ScionSocketBindError> {
223        let bind_addr = self.resolve_udp_bind_addr(isd_as, bind_addr).await?;
224        let local_addr: net::SocketAddr =
225            bind_addr
226                .local_address()
227                .ok_or(ScionSocketBindError::InvalidBindAddress(
228                    InvalidBindAddressError::ServiceAddress(bind_addr),
229                ))?;
230        let socket = bind_udp_underlay_socket(local_addr)?;
231        let local_addr = socket.local_addr().map_err(|e| {
232            ScionSocketBindError::Other(
233                anyhow::anyhow!("failed to get local address: {e}").into_boxed_dyn_error(),
234            )
235        })?;
236        let bind_addr = SocketAddr::new(
237            ScionAddr::new(bind_addr.isd_asn(), local_addr.ip().into()),
238            local_addr.port(),
239        );
240        Ok((bind_addr, socket))
241    }
242}
243
244impl DynUnderlayStack for UnderlayStack {
245    fn bind_socket(
246        &self,
247        _kind: crate::scionstack::SocketKind,
248        bind_addr: Option<scion_proto::address::SocketAddr>,
249    ) -> futures::future::BoxFuture<
250        '_,
251        Result<Box<dyn crate::scionstack::UnderlaySocket>, crate::scionstack::ScionSocketBindError>,
252    > {
253        Box::pin(async move {
254            let requested_isd_as = bind_addr
255                .map(|addr| addr.isd_asn())
256                .unwrap_or(IsdAsn::WILDCARD);
257            match self.select_underlay(requested_isd_as) {
258                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
259                    Ok(
260                        Box::new(self.bind_snap_socket(bind_addr, isd_as, cp_url).await?)
261                            as Box<dyn UnderlaySocket>,
262                    )
263                }
264                Some((isd_as, UnderlayInfo::Udp(_))) => {
265                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
266                    Ok(Box::new(UdpUnderlaySocket::new(
267                        socket,
268                        bind_addr,
269                        self.underlay_discovery.clone(),
270                    )) as Box<dyn UnderlaySocket>)
271                }
272                None => {
273                    Err(
274                        crate::scionstack::ScionSocketBindError::NoUnderlayAvailable(
275                            requested_isd_as.isd(),
276                        ),
277                    )
278                }
279            }
280        })
281    }
282
283    fn bind_async_udp_socket(
284        &self,
285        bind_addr: Option<scion_proto::address::SocketAddr>,
286        scmp_handlers: Vec<Box<dyn ScmpHandler>>,
287    ) -> futures::future::BoxFuture<
288        '_,
289        Result<
290            std::sync::Arc<dyn crate::scionstack::AsyncUdpUnderlaySocket>,
291            crate::scionstack::ScionSocketBindError,
292        >,
293    > {
294        Box::pin(async move {
295            match self.select_underlay(
296                bind_addr
297                    .map(|addr| addr.isd_asn())
298                    .unwrap_or(IsdAsn::WILDCARD),
299            ) {
300                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
301                    let socket = self.bind_snap_socket(bind_addr, isd_as, cp_url).await?;
302                    let async_udp_socket = snap::SnapAsyncUdpSocket::new(socket, scmp_handlers);
303                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
304                }
305                Some((isd_as, UnderlayInfo::Udp(_))) => {
306                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
307                    let async_udp_socket = UdpAsyncUdpUnderlaySocket::new(
308                        bind_addr,
309                        self.underlay_discovery.clone(),
310                        socket,
311                        scmp_handlers,
312                    );
313                    Ok(Arc::new(async_udp_socket) as Arc<dyn AsyncUdpUnderlaySocket + 'static>)
314                }
315                None => {
316                    Err(
317                        crate::scionstack::ScionSocketBindError::NoUnderlayAvailable(
318                            bind_addr
319                                .map(|addr| addr.isd_asn().isd())
320                                .unwrap_or(Isd::WILDCARD),
321                        ),
322                    )
323                }
324            }
325        })
326    }
327
328    fn local_ases(&self) -> Vec<IsdAsn> {
329        let mut isd_ases: Vec<IsdAsn> = self.underlay_discovery.isd_ases().into_iter().collect();
330        isd_ases.sort();
331        isd_ases
332    }
333}
334
335#[cfg(windows)]
336fn set_exclusive_addr_use(sock: &Socket, enable: bool) -> std::io::Result<()> {
337    use std::{mem, os::windows::io::AsRawSocket};
338
339    use windows_sys::Win32::Networking::WinSock;
340
341    // Winsock expects an int/bool-ish value passed by pointer.
342    let val: u32 = if enable { 1 } else { 0 };
343
344    let rc = unsafe {
345        WinSock::setsockopt(
346            sock.as_raw_socket() as usize,
347            WinSock::SOL_SOCKET,
348            WinSock::SO_EXCLUSIVEADDRUSE,
349            &val as *const _ as *const _,
350            mem::size_of_val(&val) as _,
351        )
352    };
353
354    if rc == 0 {
355        Ok(())
356    } else {
357        Err(std::io::Error::last_os_error())
358    }
359}
360
361/// This is equivalent to tokio::net::UdpSocket::bind(addr) but with the exclusive address use set
362/// to true on windows.
363/// This is because on windows, by default, multiple sockets can bind to the same address:port
364/// if one binds to wildcard address.
365fn bind_udp_underlay_socket(
366    addr: net::SocketAddr,
367) -> Result<tokio::net::UdpSocket, ScionSocketBindError> {
368    let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))
369        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
370    socket
371        .set_nonblocking(true)
372        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
373    if addr.is_ipv6()
374        && let Err(e) = socket.set_only_v6(false)
375    {
376        tracing::debug!(%e, "unable to make socket dual-stack");
377    }
378
379    // XXX(uniquefine): on windows, we need to set the exclusive address use to true to
380    // prevent multiple sockets from binding to the same address.
381    #[cfg(windows)]
382    set_exclusive_addr_use(&socket, true).map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
383
384    socket.bind(&addr.into()).map_err(|e| {
385        match e.kind() {
386            std::io::ErrorKind::AddrInUse => ScionSocketBindError::PortAlreadyInUse(addr.port()),
387            std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::InvalidInput => {
388                ScionSocketBindError::InvalidBindAddress(
389                    InvalidBindAddressError::CannotBindToRequestedAddress(
390                        SocketAddr::from_std(IsdAsn::WILDCARD, addr),
391                        format!("Failed to bind socket: {e:#}").into(),
392                    ),
393                )
394            }
395            #[cfg(windows)]
396            // On windows, if a port is already in use the error returned is sometimes
397            // code 10013 WSAEACCES.
398            // see https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
399            std::io::ErrorKind::PermissionDenied => {
400                ScionSocketBindError::PortAlreadyInUse(addr.port())
401            }
402            _ => ScionSocketBindError::Other(Box::new(e)),
403        }
404    })?;
405
406    tokio::net::UdpSocket::from_std(std::net::UdpSocket::from(socket))
407        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))
408}
409
410// XXX(dsd): This function exists to avoid unnecessary vec-allocations when
411// dealing with the scion-proto API.
412//
413// # Arguments
414// * `packet`: the packet to be serialized
415// * `temp_buf`: a temporary buffer that is used for internal packet assembly
416// * `target_buf`: the buffer that will contain the final result
417#[inline]
418pub(crate) fn wire_encode<W, const N: usize>(
419    packet: &W,
420    temp_buf: &mut Packet,
421    target_buf: &mut Packet,
422) -> Result<(), W::Error>
423where
424    W: WireEncodeVec<N>,
425{
426    temp_buf.truncate(0);
427    let parts = packet.encode_with(temp_buf.buf_mut())?;
428
429    let mut n = 0;
430    parts.iter().for_each(|x| {
431        target_buf.as_mut()[n..(n + x.len())].copy_from_slice(x);
432        n += x.len();
433    });
434    target_buf.truncate(n);
435    Ok(())
436}
437
438/// Returns the local source IP address that can reach the given destination address.
439pub(crate) async fn source_ip_towards(dst: net::SocketAddr) -> Option<net::IpAddr> {
440    let bind_addr = match dst.ip() {
441        net::IpAddr::V4(_) => net::Ipv4Addr::UNSPECIFIED.into(),
442        net::IpAddr::V6(_) => net::Ipv6Addr::UNSPECIFIED.into(),
443    };
444    if let Ok(socket) = tokio::net::UdpSocket::bind(net::SocketAddr::new(bind_addr, 0)).await
445        && socket.connect(dst).await.is_ok()
446        && let Ok(addr) = socket.local_addr()
447    {
448        return Some(addr.ip());
449    }
450    None
451}