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::PacketBufPool;
19use reqwest_connect_rpc::token_source::TokenSource;
20use sciparse::{address::ip_socket_addr::ScionSocketIpAddr, identifier::isd_asn::IsdAsn};
21use snap_tun::client::{PACKET_BUF_POOL_SIZE, SnapTunEndpoint};
22use socket2::{Domain, Protocol, Socket, Type};
23use tokio::net::UdpSocket;
24use url::Url;
25use x25519_dalek::StaticSecret;
26
27use crate::{
28    stack::{
29        BoundUnderlaySocket, DynUnderlayStack, InvalidBindAddressError, ScionSocketBindError,
30        SnapConnectionError, UnderlaySocket, builder::PreferredUnderlay,
31    },
32    underlays::{
33        discovery::{UnderlayDiscovery, UnderlayInfo},
34        udp::{OutboundIpResolver, UdpUnderlaySocket},
35    },
36};
37
38pub mod discovery;
39pub mod snap;
40pub mod udp;
41
42/// Configuration needed to create a SNAP socket(s).
43pub(crate) struct SnapSocketConfig {
44    /// Custom CRPC client for reaching the SNAP control plane.
45    pub crpc_client: Option<reqwest::Client>,
46    /// Source for SNAP token. If this is None, no SNAP sockets
47    /// can be bound.
48    pub snap_token_source: Option<Arc<dyn TokenSource>>,
49}
50
51/// Underlay stack.
52pub(crate) struct UnderlayStack {
53    preferred_underlay: PreferredUnderlay,
54    underlay_discovery: Arc<dyn UnderlayDiscovery>,
55    /// Resolver for the outbound IP address for UDP underlay sockets.
56    outbound_ip_resolver: Arc<dyn OutboundIpResolver>,
57    snap_socket_config: SnapSocketConfig,
58    snap_tunnel_manager: Option<SnapTunEndpoint>,
59    pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
60}
61
62impl UnderlayStack {
63    /// Creates a new underlay stack.
64    pub fn new(
65        preferred_underlay: PreferredUnderlay,
66        underlay_discovery: Arc<dyn UnderlayDiscovery>,
67        outbound_ip_resolver: Arc<dyn OutboundIpResolver>,
68        static_identity: StaticSecret,
69        default_snap_socket_config: SnapSocketConfig,
70    ) -> Self {
71        let snap_tunnel_manager = default_snap_socket_config
72            .snap_token_source
73            .as_ref()
74            .map(|token_source| SnapTunEndpoint::new(token_source.clone(), static_identity));
75        Self {
76            preferred_underlay,
77            underlay_discovery,
78            outbound_ip_resolver,
79            snap_socket_config: default_snap_socket_config,
80            snap_tunnel_manager,
81            pool: PacketBufPool::new(64),
82        }
83    }
84
85    /// Selects the first underlay that matches the requested isd as. If available, the preferred
86    /// underlay type is returned.
87    ///
88    /// XXX(uniquefine): We only use the ISD-AS to select the underlay, the bind address is ignored.
89    /// In the unlikely case that user requests a specific IP, but a wildcard ISD-AS, it could in
90    /// theory happen that we select the wrong underlay.
91    fn select_underlay(&self, requested_isd_as: IsdAsn) -> Option<(IsdAsn, UnderlayInfo)> {
92        let underlays = self.underlay_discovery.underlays(requested_isd_as);
93        match self.preferred_underlay {
94            PreferredUnderlay::Snap => {
95                if let Some(underlay) = underlays
96                    .iter()
97                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Snap(_)))
98                {
99                    return Some(underlay.clone());
100                }
101            }
102            PreferredUnderlay::Udp => {
103                if let Some(underlay) = underlays
104                    .iter()
105                    .find(|(_, underlay)| matches!(underlay, UnderlayInfo::Udp(_)))
106                {
107                    return Some(underlay.clone());
108                }
109            }
110        }
111        underlays.into_iter().next()
112    }
113
114    async fn bind_snap_socket(
115        &self,
116        requested_addr: Option<ScionSocketIpAddr>,
117        isd_as: IsdAsn,
118        cp_url: Url,
119    ) -> Result<snap::SnapUnderlaySocket, ScionSocketBindError> {
120        let (Some(token_source), Some(snap_tunnel_manager)) = (
121            self.snap_socket_config.snap_token_source.as_ref(),
122            self.snap_tunnel_manager.as_ref(),
123        ) else {
124            return Err(ScionSocketBindError::SnapConnectionError(
125                SnapConnectionError::SnapTokenSourceMissing,
126            ))?;
127        };
128
129        let local_addr = match requested_addr {
130            Some(addr) => addr.socket_addr(),
131            None => {
132                if let Some(cp_addr) = cp_url
133                    .socket_addrs(|| None)
134                    .ok()
135                    .and_then(|addrs| addrs.first().copied())
136                    && let Some(ip) = outbound_ip_towards(cp_addr).await
137                {
138                    Ok(net::SocketAddr::new(ip, 0))
139                } else {
140                    Err(ScionSocketBindError::InvalidBindAddress(
141                        InvalidBindAddressError::NoLocalIpAddressFound,
142                    ))
143                }?
144            }
145        };
146
147        let bind_addr = ScionSocketIpAddr::new(isd_as, local_addr.ip(), local_addr.port());
148
149        let udp_socket = bind_udp_underlay_socket(local_addr)?;
150
151        let socket = snap::SnapUnderlaySocket::new(
152            bind_addr,
153            cp_url,
154            udp_socket,
155            snap_tunnel_manager,
156            token_source.clone(),
157            1024,
158            self.pool.clone(),
159            self.snap_socket_config.crpc_client.clone(),
160        )
161        .await?;
162
163        let assigned_addr = socket.local_addr();
164
165        // If the requested address is specified but does not match the assigned address, return an
166        // error.
167        if let Some(requested_addr) = requested_addr
168                // IsdAsn mismatch
169                && requested_addr.isd_asn().matches(assigned_addr.isd_asn())
170                // IP mismatch. Note, that both addresses will have ip addresses.
171                && let requested_socket_addr = requested_addr.socket_addr()
172                && let assigned_socket_addr = assigned_addr.socket_addr()
173                && ((!requested_socket_addr.ip().is_unspecified() && assigned_socket_addr.ip() != requested_socket_addr.ip())
174                // Port mismatch
175                || (requested_socket_addr.port() != 0 && assigned_socket_addr.port() != requested_socket_addr.port()))
176        {
177            // IsdAsns must match
178
179            return Err(crate::stack::ScionSocketBindError::InvalidBindAddress(
180                crate::stack::InvalidBindAddressError::AddressMismatch {
181                    assigned_addr: ScionSocketIpAddr::new(
182                        bind_addr.isd_asn(),
183                        requested_socket_addr.ip(),
184                        requested_socket_addr.port(),
185                    ),
186                    bind_addr,
187                },
188            ));
189        }
190
191        Ok(socket)
192    }
193
194    /// Resolves the bind address for a UDP socket. If an override address is provided, it is used.
195    ///
196    /// Otherwise tries to determine which interface's ip address can reach the endhost api and uses
197    /// that as the bind address
198    async fn resolve_udp_bind_addr(
199        &self,
200        isd_as: IsdAsn,
201        override_addr: Option<ScionSocketIpAddr>,
202    ) -> Result<ScionSocketIpAddr, ScionSocketBindError> {
203        if let Some(addr) = override_addr {
204            return Ok(addr);
205        }
206
207        // No override address provided, try to determine the local IP address that can reach the
208        // control plane.
209        let local_address = *self
210            .outbound_ip_resolver
211            .outbound_ips()
212            .await
213            .first()
214            .ok_or(ScionSocketBindError::InvalidBindAddress(
215                InvalidBindAddressError::NoLocalIpAddressFound,
216            ))?;
217
218        Ok(ScionSocketIpAddr::new(isd_as, local_address, 0))
219    }
220
221    async fn bind_udp_socket(
222        &self,
223        isd_as: IsdAsn,
224        bind_addr: Option<ScionSocketIpAddr>,
225    ) -> Result<(ScionSocketIpAddr, UdpSocket), ScionSocketBindError> {
226        //TODO: the bind address could be a wildcard, in which case it should be handled the same
227        // as a None? So this should probably be either disallowed, or the Option removed
228        let bind_addr = self.resolve_udp_bind_addr(isd_as, bind_addr).await?;
229        let local_addr: net::SocketAddr = bind_addr.socket_addr();
230
231        // Bind the udp socket
232        let socket = bind_udp_underlay_socket(local_addr)?;
233        let local_addr = socket.local_addr().map_err(|e| {
234            ScionSocketBindError::Other(
235                anyhow::anyhow!("failed to get local address: {e}").into_boxed_dyn_error(),
236            )
237        })?;
238
239        // We use the extracted local address to avoid mismatches between the requested bind address
240        // and the actual bind address.
241        let bind_addr =
242            ScionSocketIpAddr::new(bind_addr.isd_asn(), local_addr.ip(), local_addr.port());
243
244        Ok((bind_addr, socket))
245    }
246}
247
248impl DynUnderlayStack for UnderlayStack {
249    fn bind_socket(
250        &self,
251        _kind: crate::stack::SocketKind,
252        bind_addr: Option<ScionSocketIpAddr>,
253    ) -> futures::future::BoxFuture<'_, Result<BoundUnderlaySocket, ScionSocketBindError>> {
254        Box::pin(async move {
255            let requested_isd_as = bind_addr.map_or(IsdAsn::WILDCARD, |addr| addr.isd_asn());
256            match self.select_underlay(requested_isd_as) {
257                Some((isd_as, UnderlayInfo::Snap(cp_url))) => {
258                    let socket = self.bind_snap_socket(bind_addr, isd_as, cp_url).await?;
259                    Ok(BoundUnderlaySocket {
260                        local_addr: socket.local_addr(),
261                        snap_data_plane: socket.snap_data_plane(),
262                        socket: Box::new(socket) as Box<dyn UnderlaySocket>,
263                    })
264                }
265                Some((isd_as, UnderlayInfo::Udp(_))) => {
266                    let (bind_addr, socket) = self.bind_udp_socket(isd_as, bind_addr).await?;
267                    Ok(BoundUnderlaySocket {
268                        local_addr: bind_addr,
269                        snap_data_plane: None,
270                        socket: Box::new(UdpUnderlaySocket::new(
271                            socket,
272                            bind_addr,
273                            self.underlay_discovery.clone(),
274                        )) as Box<dyn UnderlaySocket>,
275                    })
276                }
277                None => {
278                    Err(ScionSocketBindError::NoUnderlayAvailable(
279                        requested_isd_as.isd(),
280                    ))
281                }
282            }
283        })
284    }
285
286    fn local_ases(&self) -> Vec<IsdAsn> {
287        let mut isd_ases: Vec<IsdAsn> = self.underlay_discovery.isd_ases().into_iter().collect();
288        isd_ases.sort();
289        isd_ases
290    }
291}
292
293#[cfg(windows)]
294fn set_exclusive_addr_use(sock: &Socket, enable: bool) -> std::io::Result<()> {
295    use std::{mem, os::windows::io::AsRawSocket};
296
297    use windows_sys::Win32::Networking::WinSock;
298
299    // Winsock expects an int/bool-ish value passed by pointer.
300    let val: u32 = if enable { 1 } else { 0 };
301
302    let rc = unsafe {
303        WinSock::setsockopt(
304            sock.as_raw_socket() as usize,
305            WinSock::SOL_SOCKET,
306            WinSock::SO_EXCLUSIVEADDRUSE,
307            &val as *const _ as *const _,
308            mem::size_of_val(&val) as _,
309        )
310    };
311
312    if rc == 0 {
313        Ok(())
314    } else {
315        Err(std::io::Error::last_os_error())
316    }
317}
318
319/// This is equivalent to `tokio::net::UdpSocket::bind(addr)` but with the exclusive address use set
320/// to true on windows.
321/// This is because on windows, by default, multiple sockets can bind to the same address:port
322/// if one binds to wildcard address.
323fn bind_udp_underlay_socket(
324    addr: net::SocketAddr,
325) -> Result<tokio::net::UdpSocket, ScionSocketBindError> {
326    let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))
327        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
328    socket
329        .set_nonblocking(true)
330        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
331    if addr.is_ipv6()
332        && let Err(e) = socket.set_only_v6(false)
333    {
334        tracing::debug!(%e, "unable to make socket dual-stack");
335    }
336
337    // XXX(uniquefine): on windows, we need to set the exclusive address use to true to
338    // prevent multiple sockets from binding to the same address.
339    #[cfg(windows)]
340    set_exclusive_addr_use(&socket, true).map_err(|e| ScionSocketBindError::Other(Box::new(e)))?;
341
342    socket.bind(&addr.into()).map_err(|e| {
343        match e.kind() {
344            std::io::ErrorKind::AddrInUse => ScionSocketBindError::PortAlreadyInUse(addr.port()),
345            std::io::ErrorKind::AddrNotAvailable | std::io::ErrorKind::InvalidInput => {
346                ScionSocketBindError::InvalidBindAddress(
347                    InvalidBindAddressError::CannotBindToRequestedAddress(
348                        ScionSocketIpAddr::new(IsdAsn::WILDCARD, addr.ip(), addr.port()),
349                        format!("Failed to bind socket: {e:#}").into(),
350                    ),
351                )
352            }
353            #[cfg(windows)]
354            // On windows, if a port is already in use the error returned is sometimes
355            // code 10013 WSAEACCES.
356            // see https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
357            std::io::ErrorKind::PermissionDenied => {
358                ScionSocketBindError::PortAlreadyInUse(addr.port())
359            }
360            _ => ScionSocketBindError::Other(Box::new(e)),
361        }
362    })?;
363
364    tokio::net::UdpSocket::from_std(std::net::UdpSocket::from(socket))
365        .map_err(|e| ScionSocketBindError::Other(Box::new(e)))
366}
367
368/// Returns the outbound IP address that can reach the given destination address.
369pub(crate) async fn outbound_ip_towards(dst: net::SocketAddr) -> Option<net::IpAddr> {
370    let bind_addr = match dst.ip() {
371        net::IpAddr::V4(_) => net::Ipv4Addr::UNSPECIFIED.into(),
372        net::IpAddr::V6(_) => net::Ipv6Addr::UNSPECIFIED.into(),
373    };
374    if let Ok(socket) = tokio::net::UdpSocket::bind(net::SocketAddr::new(bind_addr, 0)).await
375        && socket.connect(dst).await.is_ok()
376        && let Ok(addr) = socket.local_addr()
377    {
378        return Some(addr.ip());
379    }
380    None
381}