Skip to main content

scion_stack/underlays/
udp.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//! UDP underlay socket.
15use std::{
16    io,
17    net::{self},
18    sync::Arc,
19    task::{Poll, ready},
20};
21
22use anyhow::Context;
23use bytes::BytesMut;
24use futures::future::BoxFuture;
25use scion_proto::{
26    address::{IsdAsn, SocketAddr},
27    packet::{
28        ByEndpoint, PacketClassification, ScionPacketRaw, ScionPacketUdp, classify_scion_packet,
29    },
30    path::{DataPlanePath, Path, PathInterface},
31    scmp::SCMP_PROTOCOL_NUMBER,
32    wire_encoding::{WireDecode as _, WireEncodeVec as _},
33};
34use tokio::{io::ReadBuf, net::UdpSocket};
35
36use crate::{
37    scionstack::{
38        AsyncUdpUnderlaySocket, ScionSocketSendError, UnderlaySocket, scmp_handler::ScmpHandler,
39        udp_polling::UdpPollHelper,
40    },
41    underlays::{discovery::UnderlayDiscovery, source_ip_towards},
42};
43
44const UDP_DATAGRAM_BUFFER_SIZE: usize = 65535;
45
46/// Local IP resolver.
47#[async_trait::async_trait]
48pub trait LocalIpResolver: Send + Sync {
49    /// Returns the local IP addresses of the host.
50    async fn local_ips(&self) -> Vec<net::IpAddr>;
51}
52
53#[async_trait::async_trait]
54impl LocalIpResolver for Vec<net::IpAddr> {
55    async fn local_ips(&self) -> Vec<net::IpAddr> {
56        self.clone()
57    }
58}
59
60// XXX(uniquefine): This should use impl ToSocketAddrs as argument and
61// try to connect to all addresses.
62pub(crate) struct TargetAddrLocalIpResolver {
63    api_socket_address: net::SocketAddr,
64}
65
66impl TargetAddrLocalIpResolver {
67    pub fn new(api_address: url::Url) -> anyhow::Result<Self> {
68        let socket_addr = api_address
69            .socket_addrs(|| None)
70            .context("invalid api address")?
71            .first()
72            .ok_or(anyhow::anyhow!("failed to resolve api socket address"))?
73            .to_owned();
74        Ok(Self {
75            api_socket_address: socket_addr,
76        })
77    }
78}
79
80#[async_trait::async_trait]
81impl LocalIpResolver for TargetAddrLocalIpResolver {
82    /// Binds to Ipv4 and Ipv6 unspecified addresses and returns the local addresses
83    /// that can reach the endhost API.
84    async fn local_ips(&self) -> Vec<net::IpAddr> {
85        match source_ip_towards(self.api_socket_address).await {
86            Some(ip) => vec![ip],
87            None => vec![],
88        }
89    }
90}
91
92/// A UDP underlay socket.
93pub struct UdpUnderlaySocket {
94    pub(crate) socket: UdpSocket,
95    pub(crate) bind_addr: SocketAddr,
96    pub(crate) underlay_discovery: Arc<dyn UnderlayDiscovery>,
97}
98
99impl UdpUnderlaySocket {
100    pub(crate) fn new(
101        socket: UdpSocket,
102        bind_addr: SocketAddr,
103        underlay_discovery: Arc<dyn UnderlayDiscovery>,
104    ) -> Self {
105        Self {
106            socket,
107            bind_addr,
108            underlay_discovery,
109        }
110    }
111
112    /// Dispatch a packet to the local AS network.
113    fn resolve_local_dispatch_addr(
114        &self,
115        packet: &ScionPacketRaw,
116    ) -> Result<net::SocketAddr, ScionSocketSendError> {
117        let dst_addr = packet
118            .headers
119            .address
120            .destination()
121            .ok_or(crate::scionstack::ScionSocketSendError::InvalidPacket(
122                "Packet to local endhost has no destination address".into(),
123            ))?
124            .local_address()
125            .ok_or(crate::scionstack::ScionSocketSendError::InvalidPacket(
126                "Cannot forward packet to local service address".into(),
127            ))?;
128        let classification = classify_scion_packet(packet.clone()).map_err(|e| {
129            crate::scionstack::ScionSocketSendError::InvalidPacket(
130                format!("Cannot classify packet to local endhost: {e:#}").into(),
131            )
132        })?;
133        let dst_port = match classification {
134            PacketClassification::Udp(udp_packet) => udp_packet.dst_port(),
135            PacketClassification::ScmpWithDestination(port, _) => port,
136            PacketClassification::ScmpWithoutDestination(_) | PacketClassification::Other(_) => {
137                return Err(crate::scionstack::ScionSocketSendError::InvalidPacket(
138                    "Cannot deduce port for packet to local endhost".into(),
139                ));
140            }
141        };
142        Ok(net::SocketAddr::new(dst_addr, dst_port))
143    }
144
145    fn try_dispatch_local(&self, packet: ScionPacketRaw) -> Result<(), ScionSocketSendError> {
146        let dst_addr = self.resolve_local_dispatch_addr(&packet)?;
147        let packet_bytes = packet.encode_to_bytes_vec().concat();
148        self.socket
149            .try_send_to(&packet_bytes, dst_addr)
150            .map_err(|e| {
151                Self::map_send_io_error(e, packet.headers.address.ia.source, 0, dst_addr)
152            })?;
153        Ok(())
154    }
155
156    /// Dispatch a packet to the local AS network.
157    async fn dispatch_local(
158        &self,
159        packet: ScionPacketRaw,
160    ) -> Result<(), crate::scionstack::ScionSocketSendError> {
161        let dst_addr = self.resolve_local_dispatch_addr(&packet)?;
162        let packet_bytes = packet.encode_to_bytes_vec().concat();
163        self.socket
164            .send_to(&packet_bytes, dst_addr)
165            .await
166            .map_err(|e| {
167                Self::map_send_io_error(e, packet.headers.address.ia.source, 0, dst_addr)
168            })?;
169        Ok(())
170    }
171
172    /// Map a UDP send error to a SCION socket send error.
173    fn map_send_io_error(
174        e: io::Error,
175        src: IsdAsn,
176        interface_id: u16,
177        next_hop: net::SocketAddr,
178    ) -> ScionSocketSendError {
179        use std::io::ErrorKind::*;
180        match e.kind() {
181            HostUnreachable | NetworkUnreachable => {
182                ScionSocketSendError::UnderlayNextHopUnreachable {
183                    isd_as: src,
184                    interface_id,
185                    address: Some(next_hop),
186                    msg: e.to_string(),
187                }
188            }
189            ConnectionAborted | ConnectionReset | BrokenPipe => ScionSocketSendError::Closed,
190            _ => ScionSocketSendError::IoError(e),
191        }
192    }
193}
194
195impl UnderlaySocket for UdpUnderlaySocket {
196    fn send<'a>(
197        &'a self,
198        packet: ScionPacketRaw,
199    ) -> BoxFuture<'a, Result<(), ScionSocketSendError>> {
200        let source_ia = packet.headers.address.ia.source;
201        if packet.headers.address.ia.destination == source_ia {
202            return Box::pin(async move {
203                self.dispatch_local(packet).await?;
204                Ok(())
205            });
206        }
207
208        // Extract the source IA and next hop from the packet.
209        let interface_id = if let DataPlanePath::Standard(standard_path) = &packet.headers.path
210            && let Some(interface_id) = standard_path.iter_interfaces().next()
211        {
212            interface_id
213        } else {
214            return Box::pin(async move {
215                Err(ScionSocketSendError::InvalidPacket(
216                    "Path does not contain first hop.".into(),
217                ))
218            });
219        };
220
221        let next_hop = match self
222            .underlay_discovery
223            .resolve_udp_underlay_next_hop(PathInterface {
224                isd_asn: source_ia,
225                id: interface_id.get(),
226            })
227            .ok_or(ScionSocketSendError::UnderlayNextHopUnreachable {
228                isd_as: source_ia,
229                interface_id: interface_id.get(),
230                address: None,
231                msg: "next hop not found".to_string(),
232            }) {
233            Ok(next_hop) => next_hop,
234            Err(e) => {
235                return Box::pin(async move { Err(e) });
236            }
237        };
238
239        let packet_bytes = packet.encode_to_bytes_vec().concat();
240        Box::pin(async move {
241            self.socket
242                .send_to(&packet_bytes, next_hop)
243                .await
244                .map_err(|e| {
245                    use std::io::ErrorKind::*;
246                    match e.kind() {
247                        HostUnreachable | NetworkUnreachable => {
248                            ScionSocketSendError::UnderlayNextHopUnreachable {
249                                isd_as: source_ia,
250                                interface_id: interface_id.get(),
251                                address: Some(next_hop),
252                                msg: e.to_string(),
253                            }
254                        }
255                        ConnectionAborted | ConnectionReset | BrokenPipe => {
256                            ScionSocketSendError::Closed
257                        }
258                        _ => ScionSocketSendError::IoError(e),
259                    }
260                })?;
261            Ok(())
262        })
263    }
264
265    /// Try to send a raw packet immediately. Takes a ScionPacketRaw because it needs to read the
266    /// path to resolve the underlay next hop.
267    fn try_send(&self, packet: ScionPacketRaw) -> Result<(), ScionSocketSendError> {
268        let source_ia = packet.headers.address.ia.source;
269        if packet.headers.address.ia.destination == source_ia {
270            return self.try_dispatch_local(packet);
271        }
272
273        // Extract the source IA and next hop from the packet.
274        let interface_id = if let DataPlanePath::Standard(standard_path) = &packet.headers.path
275            && let Some(interface_id) = standard_path.iter_interfaces().next()
276        {
277            interface_id
278        } else {
279            return Err(ScionSocketSendError::InvalidPacket(
280                "Path does not contain first hop.".into(),
281            ));
282        };
283
284        let next_hop = match self
285            .underlay_discovery
286            .resolve_udp_underlay_next_hop(PathInterface {
287                isd_asn: source_ia,
288                id: interface_id.get(),
289            })
290            .ok_or(ScionSocketSendError::UnderlayNextHopUnreachable {
291                isd_as: source_ia,
292                interface_id: interface_id.get(),
293                address: None,
294                msg: "next hop not found".to_string(),
295            }) {
296            Ok(next_hop) => next_hop,
297            Err(e) => {
298                return Err(e);
299            }
300        };
301
302        self.socket
303            .try_send_to(&packet.encode_to_bytes_vec().concat(), next_hop)
304            .map_err(|e| Self::map_send_io_error(e, source_ia, interface_id.get(), next_hop))?;
305        Ok(())
306    }
307
308    fn recv<'a>(
309        &'a self,
310    ) -> BoxFuture<'a, Result<ScionPacketRaw, crate::scionstack::ScionSocketReceiveError>> {
311        Box::pin(async move {
312            let mut buf = [0u8; UDP_DATAGRAM_BUFFER_SIZE];
313            loop {
314                let (n, _) = self.socket.recv_from(&mut buf).await?;
315                let packet = match ScionPacketRaw::decode(&mut BytesMut::from(&buf[..n])) {
316                    Ok(packet) => packet,
317                    Err(e) => {
318                        tracing::error!(error = %e, "Failed to decode SCION packet");
319                        continue;
320                    }
321                };
322
323                // Drop packets that are not addressed to this socket.
324                let dst = packet.headers.address.destination();
325                if let Some(dst) = dst
326                    && dst != self.bind_addr.scion_address()
327                {
328                    tracing::debug!(destination = ?dst, assigned_addr = %self.bind_addr.scion_address(), "Packet destination does not match assigned address, skipping");
329                    continue;
330                }
331                return Ok(packet);
332            }
333        })
334    }
335
336    fn local_addr(&self) -> scion_proto::address::SocketAddr {
337        self.bind_addr
338    }
339
340    fn snap_data_plane(&self) -> Option<net::SocketAddr> {
341        None
342    }
343}
344
345/// An async UDP underlay socket.
346pub struct UdpAsyncUdpUnderlaySocket {
347    local_addr: SocketAddr,
348    discovery: Arc<dyn UnderlayDiscovery>,
349    inner: UdpSocket,
350    scmp_handlers: Vec<Box<dyn ScmpHandler>>,
351}
352
353impl UdpAsyncUdpUnderlaySocket {
354    pub(crate) fn new(
355        local_addr: SocketAddr,
356        discovery: Arc<dyn UnderlayDiscovery>,
357        inner: UdpSocket,
358        scmp_handlers: Vec<Box<dyn ScmpHandler>>,
359    ) -> Self {
360        Self {
361            local_addr,
362            discovery,
363            inner,
364            scmp_handlers,
365        }
366    }
367
368    /// Dispatch a packet to the local AS network.
369    fn try_dispatch_local(&self, packet: ScionPacketRaw) -> io::Result<()> {
370        let dst_addr = packet
371            .headers
372            .address
373            .destination()
374            .ok_or(io::Error::new(
375                io::ErrorKind::InvalidInput,
376                "Packet to local endhost has no destination address".to_string(),
377            ))?
378            .local_address()
379            .ok_or(io::Error::new(
380                io::ErrorKind::InvalidInput,
381                "Cannot forward packet with service address".to_string(),
382            ))?;
383        let classification = classify_scion_packet(packet.clone()).map_err(|e| {
384            io::Error::new(
385                io::ErrorKind::InvalidInput,
386                format!("Cannot classify packet to local endhost: {e:#}"),
387            )
388        })?;
389        let dst_port = match classification {
390            PacketClassification::Udp(udp_packet) => udp_packet.dst_port(),
391            PacketClassification::ScmpWithDestination(port, _) => port,
392            PacketClassification::ScmpWithoutDestination(_) | PacketClassification::Other(_) => {
393                return Err(io::Error::new(
394                    io::ErrorKind::InvalidInput,
395                    "Cannot deduce port for packet to local endhost",
396                ));
397            }
398        };
399        let packet_bytes = packet.encode_to_bytes_vec().concat();
400        let dst_addr = net::SocketAddr::new(dst_addr, dst_port);
401        self.inner.try_send_to(&packet_bytes, dst_addr)?;
402        Ok(())
403    }
404}
405
406impl AsyncUdpUnderlaySocket for UdpAsyncUdpUnderlaySocket {
407    fn create_io_poller(
408        self: Arc<Self>,
409    ) -> std::pin::Pin<Box<dyn crate::scionstack::udp_polling::UdpPoller>> {
410        Box::pin(UdpPollHelper::new(move || {
411            let self_clone = self.clone();
412            async move { self_clone.inner.writable().await }
413        }))
414    }
415
416    fn try_send(&self, packet: ScionPacketRaw) -> Result<(), std::io::Error> {
417        let source_ia = packet.headers.address.ia.source;
418        if packet.headers.address.ia.destination == source_ia {
419            return self.try_dispatch_local(packet);
420        }
421
422        // Extract the source IA and next hop from the packet.
423        let interface_id = if let DataPlanePath::Standard(standard_path) = &packet.headers.path
424            && let Some(interface_id) = standard_path.iter_interfaces().next()
425        {
426            interface_id
427        } else {
428            return Err(std::io::Error::new(
429                std::io::ErrorKind::InvalidInput,
430                "Path does not contain first hop.".to_string(),
431            ));
432        };
433
434        let next_hop = self
435            .discovery
436            .resolve_udp_underlay_next_hop(PathInterface {
437                isd_asn: source_ia,
438                id: interface_id.get(),
439            })
440            .ok_or(std::io::Error::new(
441                std::io::ErrorKind::InvalidInput,
442                "could not resolve next hop",
443            ))?;
444
445        let packet_bytes = packet.encode_to_bytes_vec().concat();
446        // Ignore all errors except for WouldBlock. The sender should try to
447        // retransmit.
448        match self.inner.try_send_to(&packet_bytes, next_hop) {
449            Ok(_) => Ok(()),
450            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Err(e),
451            Err(e) => {
452                tracing::warn!(err = ?e, "Error sending packet");
453                Ok(())
454            }
455        }?;
456        Ok(())
457    }
458
459    fn poll_recv_from_with_path(
460        &self,
461        cx: &mut std::task::Context,
462    ) -> Poll<std::io::Result<(SocketAddr, bytes::Bytes, scion_proto::path::Path)>> {
463        loop {
464            let mut raw_buf = [0u8; UDP_DATAGRAM_BUFFER_SIZE];
465            let mut buf = ReadBuf::new(&mut raw_buf);
466            let _ = ready!(self.inner.poll_recv_from(cx, &mut buf))?;
467
468            let packet = match ScionPacketRaw::decode(&mut BytesMut::from(buf.initialized())) {
469                Ok(packet) => packet,
470                Err(e) => {
471                    tracing::trace!(error = %e, "Received non SCION packet, dropping");
472                    continue;
473                }
474            };
475            // Handle SCMP packets.
476            if packet.headers.common.next_header == SCMP_PROTOCOL_NUMBER {
477                tracing::debug!("SCMP packet received, forwarding to SCMP handlers");
478                for handler in &self.scmp_handlers {
479                    if let Some(reply) = handler.handle(packet.clone())
480                        && let Err(e) = self.try_send(reply)
481                    {
482                        tracing::warn!(error = %e, "failed to send SCMP reply");
483                    }
484                }
485                continue;
486            };
487
488            let fallible = || {
489                let src = packet
490                    .headers
491                    .address
492                    .source()
493                    .context("reading source address")?;
494                let dst = packet
495                    .headers
496                    .address
497                    .destination()
498                    .context("reading destination address")?;
499
500                // Drop packets that are not addressed to this socket.
501                if dst != self.local_addr.scion_address() {
502                    anyhow::bail!(
503                        "Packet destination does not match assigned address, skipping (dst: {}, assigned: {})",
504                        dst,
505                        self.local_addr.scion_address()
506                    );
507                }
508
509                let path = Path::new(
510                    packet.headers.path.clone(),
511                    ByEndpoint {
512                        source: src.isd_asn(),
513                        destination: dst.isd_asn(),
514                    },
515                    None,
516                );
517
518                let packet: ScionPacketUdp = packet.try_into().context("parsing UDP packet")?;
519
520                anyhow::Ok((
521                    SocketAddr::new(src, packet.src_port()),
522                    packet.datagram.payload,
523                    path,
524                ))
525            };
526
527            match fallible() {
528                Ok(result) => return Poll::Ready(Ok(result)),
529                Err(e) => {
530                    tracing::warn!(error = %e, "Received invalid packet, skipping");
531                    continue;
532                }
533            }
534        }
535    }
536
537    fn local_addr(&self) -> SocketAddr {
538        self.local_addr
539    }
540
541    fn snap_data_plane(&self) -> Option<net::SocketAddr> {
542        None
543    }
544}