Skip to main content

scion_stack/stack/
socket.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 socket types.
15
16use std::{
17    net::{self},
18    sync::Arc,
19    time::{Duration, SystemTime},
20};
21
22use scion_quic::socket::{BoxedSocketError, GenericScionUdpSocket};
23use sciparse::{
24    address::{addr::ScionAddr, ip_socket_addr::ScionSocketIpAddr},
25    core::{model::Model, view::View},
26    dataplane_path::view::ScionDpPathViewExt,
27    packet::{
28        model::{ScionScmpPacket, ScionUdpPacket},
29        view::ScionRawPacketView,
30    },
31    path::ScionPath,
32    payload::{
33        ProtocolNumber,
34        scmp::{model::ScmpMessage, view::ScmpPayloadView},
35    },
36};
37
38use super::{BoundUnderlaySocket, MAX_UNDERLAY_PACKET_SIZE, UnderlaySocket, UnderlaySocketExt};
39use crate::{
40    internal::Subscribers,
41    path::manager::{MultiPathManager, traits::PathManager},
42    stack::{
43        ScionSocketConnectError, ScionSocketReceiveError, ScionSocketSendError,
44        scmp_handler::{ScmpErrorHandler, ScmpHandler},
45    },
46};
47
48/// A path unaware UDP SCION socket.
49pub struct PathUnawareUdpScionSocket {
50    inner: Box<dyn UnderlaySocket>,
51    /// The local SCION address the socket is bound to.
52    local_addr: ScionSocketIpAddr,
53    /// The SNAP data plane the socket is connected to (if a SNAP underlay is used).
54    snap_data_plane: Option<net::SocketAddr>,
55    /// The SCMP handlers.
56    scmp_handlers: Vec<Box<dyn ScmpHandler>>,
57}
58
59// Intentionally shows only the local address; the inner socket/handlers are not `Debug`.
60#[allow(clippy::missing_fields_in_debug)]
61impl std::fmt::Debug for PathUnawareUdpScionSocket {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("PathUnawareUdpScionSocket")
64            .field("local_addr", &self.local_addr)
65            .finish()
66    }
67}
68
69impl PathUnawareUdpScionSocket {
70    pub(crate) fn new(
71        bound: BoundUnderlaySocket,
72        scmp_handlers: Vec<Box<dyn ScmpHandler>>,
73    ) -> Self {
74        Self {
75            inner: bound.socket,
76            local_addr: bound.local_addr,
77            snap_data_plane: bound.snap_data_plane,
78            scmp_handlers,
79        }
80    }
81
82    /// Send a SCION UDP datagram via the given path.
83    ///
84    /// # Cancel safety
85    ///
86    /// This method is cancel-safe. If the future is dropped before completion, the packet may
87    /// be silently lost, but no socket state is corrupted and the socket remains usable.
88    pub async fn send_to_via(
89        &self,
90        payload: &[u8],
91        destination: ScionSocketIpAddr,
92        path: &ScionPath,
93    ) -> Result<(), ScionSocketSendError> {
94        // TODO: Should look into a way to encode without cloning payload and parsing dp_path
95        let packet = ScionUdpPacket::new(
96            self.local_addr.into(),
97            destination.into(),
98            path.dp_path().to_model(),
99            payload.to_vec(),
100        )
101        .try_encode_to_owned_view()
102        .map_err(|e| {
103            ScionSocketSendError::InvalidPacket(format!("error encoding packet: {e}").into())
104        })?
105        .into_raw();
106
107        self.inner.send(&packet).await
108    }
109
110    /// Receive a SCION packet with the sender and path.
111    ///
112    /// # Cancel safety
113    ///
114    /// This method is cancel-safe. The only await point is the inner underlay receive. If the
115    /// future is dropped while waiting for a packet, no packet data is consumed and `buffer`
116    /// and `path_buffer` are left unmodified. If a packet has already been received (i.e., the
117    /// future is dropped after data has been written into the buffers), this cannot occur in
118    /// practice because those steps run synchronously within a single `poll` invocation.
119    #[allow(clippy::type_complexity)]
120    pub async fn recv_from_with_path(
121        &self,
122        buffer: &mut [u8],
123    ) -> Result<(usize, ScionSocketIpAddr, ScionPath), ScionSocketReceiveError> {
124        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
125        loop {
126            let n = self.inner.recv(&mut scratch).await?;
127            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
128                .expect("underlay recv returns a decoded packet");
129
130            match packet.header().next_header() {
131                ProtocolNumber::Udp => {}
132                ProtocolNumber::Scmp => {
133                    tracing::debug!("SCMP packet received, forwarding to SCMP handlers");
134                    for handler in &self.scmp_handlers {
135                        // Check if the handler wants to send a reply and send it
136                        let Some(reply) = handler.handle(packet) else {
137                            continue;
138                        };
139
140                        let reply = match reply.try_encode_to_owned_view() {
141                            Ok(reply) => reply,
142                            Err(e) => {
143                                tracing::warn!(error = %e, "failed to encode SCMP reply");
144                                continue;
145                            }
146                        };
147
148                        if let Err(e) = self.inner.try_send(&reply) {
149                            tracing::warn!(error = %e, "failed to send SCMP reply");
150                        }
151                    }
152                    continue;
153                }
154                next_header => {
155                    tracing::debug!(%next_header, "Packet with unexpected next layer protocol, skipping");
156                    continue;
157                }
158            }
159
160            let packet = match packet.try_as_udp() {
161                Ok(packet) => packet,
162                Err(e) => {
163                    tracing::debug!(error = %e, "Received invalid UDP packet, skipping");
164                    continue;
165                }
166            };
167            let src_addr = match packet.src_socket_addr() {
168                Ok(src_addr) => src_addr,
169                Err(err) => {
170                    tracing::debug!(
171                        %err,
172                        "Failed to decode packet source address, skipping"
173                    );
174                    continue;
175                }
176            };
177
178            tracing::trace!(
179                src = %src_addr,
180                length = packet.udp().payload().len(),
181                "received packet",
182            );
183
184            let Some(src_addr) = src_addr.try_to_scion_sock_ip_addr() else {
185                tracing::debug!("Received packet with non-IP source address, skipping");
186                continue;
187            };
188
189            let max_read = std::cmp::min(buffer.len(), packet.udp().payload().len());
190            buffer[..max_read].copy_from_slice(&packet.udp().payload()[..max_read]);
191
192            // Note, that we do not have the next hop address of the path.
193            // A socket that uses more than one tunnel will need to distinguish between
194            // packets received on different tunnels.
195            let path = ScionPath::new(
196                src_addr.isd_asn(),
197                packet.header().dst_ia(),
198                packet.header().path().to_owned_view(),
199                None,
200                None,
201            );
202
203            return Ok((max_read, src_addr, path));
204        }
205    }
206
207    /// Receive a SCION packet with the sender.
208    ///
209    /// # Cancel safety
210    ///
211    /// This method is cancel-safe. If the future is dropped while waiting for a packet, no
212    /// packet is consumed and `buffer` is left unmodified. The contents of `buffer` are only
213    /// valid after the method returns `Ok`.
214    pub async fn recv_from(
215        &self,
216        buffer: &mut [u8],
217    ) -> Result<(usize, ScionSocketIpAddr), ScionSocketReceiveError> {
218        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
219        loop {
220            let n = self.inner.recv(&mut scratch).await?;
221            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
222                .expect("underlay recv returns a decoded packet");
223
224            match packet.header().next_header() {
225                ProtocolNumber::Udp => {}
226                ProtocolNumber::Scmp => {
227                    tracing::debug!("SCMP packet received, forwarding to SCMP handlers");
228                    for handler in &self.scmp_handlers {
229                        // Check if the handler wants to send a reply and send it
230                        let Some(reply) = handler.handle(packet) else {
231                            continue;
232                        };
233
234                        let reply = match reply.try_encode_to_owned_view() {
235                            Ok(reply) => reply,
236                            Err(e) => {
237                                tracing::warn!(error = %e, "failed to encode SCMP reply");
238                                continue;
239                            }
240                        };
241
242                        if let Err(e) = self.inner.try_send(&reply) {
243                            tracing::warn!(error = %e, "failed to send SCMP reply");
244                        }
245                    }
246                    continue;
247                }
248                next_header => {
249                    tracing::debug!(%next_header, "Packet with unknown next layer protocol, skipping");
250                    continue;
251                }
252            }
253
254            let packet = match packet.try_as_udp() {
255                Ok(packet) => packet,
256                Err(e) => {
257                    tracing::debug!(error = %e, "Received invalid UDP packet, dropping");
258                    continue;
259                }
260            };
261
262            let src_addr = match packet.src_socket_addr() {
263                Ok(src_addr) => src_addr,
264                Err(err) => {
265                    tracing::debug!(%err, "Failed to decode packet source address, skipping");
266                    continue;
267                }
268            };
269
270            tracing::trace!(
271                src = %src_addr,
272                length = packet.udp().payload().len(),
273                buffer_size = buffer.len(),
274                "received packet",
275            );
276
277            let Some(src_addr) = src_addr.try_to_scion_sock_ip_addr() else {
278                tracing::debug!("Received packet with non-IP source address, skipping");
279                continue;
280            };
281
282            let max_read = std::cmp::min(buffer.len(), packet.udp().payload().len());
283            buffer[..max_read].copy_from_slice(&packet.udp().payload()[..max_read]);
284
285            return Ok((max_read, src_addr));
286        }
287    }
288
289    /// The local address the socket is bound to.
290    pub fn local_addr(&self) -> ScionSocketIpAddr {
291        self.local_addr
292    }
293
294    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
295    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
296        self.snap_data_plane
297    }
298
299    /// Converts the [`PathUnawareUdpScionSocket`] to a [`UdpScionSocket`] using the given
300    /// [`PathManager`]
301    ///
302    /// If the path manager supports it SCMP and send errors are forwarded to the
303    /// [`PathManager::scmp_error_receiver`] and [`PathManager::send_error_receiver`].
304    ///
305    /// ### Params
306    /// * `path_manager`: The path manager this socket will query for paths to the destination.
307    /// * `connect_timeout`: The timeout for the initial path lookup when connecting to a remote
308    ///   address.
309    pub fn into_path_aware<P: PathManager>(
310        mut self,
311        path_manager: Arc<P>,
312        connect_timeout: Duration,
313    ) -> UdpScionSocket<P> {
314        if let Some(receiver) = path_manager.clone().scmp_error_receiver() {
315            let scmp_error_receivers = Subscribers::new();
316            scmp_error_receivers.register(receiver);
317            self.scmp_handlers
318                .push(Box::new(ScmpErrorHandler::new(scmp_error_receivers)));
319        }
320        let send_error_receivers = Subscribers::new();
321        if let Some(receiver) = path_manager.clone().send_error_receiver() {
322            send_error_receivers.register(receiver);
323        }
324        UdpScionSocket::new(self, path_manager, connect_timeout, send_error_receivers)
325    }
326}
327
328/// A SCMP SCION socket.
329pub struct ScmpScionSocket {
330    inner: Box<dyn UnderlaySocket>,
331    /// The local SCION address the socket is bound to.
332    local_addr: ScionSocketIpAddr,
333    /// The SNAP data plane the socket is connected to (if a SNAP underlay is used).
334    snap_data_plane: Option<net::SocketAddr>,
335}
336
337impl ScmpScionSocket {
338    pub(crate) fn new(bound: BoundUnderlaySocket) -> Self {
339        Self {
340            inner: bound.socket,
341            local_addr: bound.local_addr,
342            snap_data_plane: bound.snap_data_plane,
343        }
344    }
345}
346
347impl ScmpScionSocket {
348    /// Send a SCMP message to the destination via the given path.
349    pub async fn send_to_via(
350        &self,
351        message: ScmpMessage,
352        destination: ScionAddr,
353        path: &ScionPath,
354    ) -> Result<(), ScionSocketSendError> {
355        let packet = ScionScmpPacket::new(
356            self.local_addr.scion_ip_addr().into(),
357            destination,
358            path.dp_path().to_model(),
359            message,
360        )
361        .try_encode_to_owned_view()
362        .map_err(|e| {
363            ScionSocketSendError::InvalidPacket(format!("error encoding packet: {e}").into())
364        })?
365        .into_raw();
366        self.inner.send(&packet).await
367    }
368
369    /// Receive a SCMP message with the sender and path.
370    #[allow(clippy::type_complexity)]
371    pub async fn recv_from_with_path(
372        &self,
373    ) -> Result<(Box<ScmpPayloadView>, ScionAddr, ScionPath), ScionSocketReceiveError> {
374        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
375        loop {
376            let n = self.inner.recv(&mut scratch).await?;
377            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
378                .expect("underlay recv returns a decoded packet");
379            let packet = match packet.try_as_scmp() {
380                Ok(packet) => packet,
381                Err(e) => {
382                    tracing::debug!(error = %e, "Received invalid SCMP packet, dropping");
383                    continue;
384                }
385            };
386
387            let src_addr = match packet.src_scion_addr() {
388                Ok(source) => source,
389                Err(e) => {
390                    tracing::debug!(error = %e, "Failed to decode packet source address, skipping");
391                    continue;
392                }
393            };
394
395            let path = ScionPath::new(
396                packet.header().src_ia(),
397                packet.header().dst_ia(),
398                packet.header().path().to_owned_view(),
399                None,
400                None,
401            );
402
403            return Ok((packet.scmp().to_boxed(), src_addr, path));
404        }
405    }
406
407    /// Receive a SCMP message with the sender.
408    pub async fn recv_from(
409        &self,
410    ) -> Result<(Box<ScmpPayloadView>, ScionAddr), ScionSocketReceiveError> {
411        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
412        loop {
413            let n = self.inner.recv(&mut scratch).await?;
414            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
415                .expect("underlay recv returns a decoded packet");
416            let packet = match packet.try_as_scmp() {
417                Ok(packet) => packet,
418                Err(e) => {
419                    tracing::debug!(error = %e, "Received invalid SCMP packet, skipping");
420                    continue;
421                }
422            };
423            let src_addr = match packet.src_scion_addr() {
424                Ok(source) => source,
425                Err(e) => {
426                    tracing::debug!(error = %e, "Failed to decode packet source address, skipping");
427                    continue;
428                }
429            };
430            return Ok((packet.scmp().to_boxed(), src_addr));
431        }
432    }
433
434    /// Return the local socket address.
435    pub fn local_addr(&self) -> ScionSocketIpAddr {
436        self.local_addr
437    }
438
439    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
440    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
441        self.snap_data_plane
442    }
443}
444
445/// A raw SCION socket.
446pub struct RawScionSocket {
447    inner: Box<dyn UnderlaySocket>,
448    /// The local SCION address the socket is bound to.
449    local_addr: ScionSocketIpAddr,
450    /// The SNAP data plane the socket is connected to (if a SNAP underlay is used).
451    snap_data_plane: Option<net::SocketAddr>,
452}
453
454impl RawScionSocket {
455    pub(crate) fn new(bound: BoundUnderlaySocket) -> Self {
456        Self {
457            inner: bound.socket,
458            local_addr: bound.local_addr,
459            snap_data_plane: bound.snap_data_plane,
460        }
461    }
462}
463
464impl RawScionSocket {
465    /// Send a raw SCION packet.
466    pub async fn send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
467        self.inner.send(packet).await
468    }
469
470    /// Receive a raw SCION packet.
471    pub async fn recv(&self) -> Result<Box<ScionRawPacketView>, ScionSocketReceiveError> {
472        let mut buf = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
473        let n = self.inner.recv(&mut buf).await?;
474        let (view, _rest) = ScionRawPacketView::try_from_slice(&buf[..n])
475            .expect("underlay recv returns a decoded packet");
476        Ok(view.to_boxed())
477    }
478
479    /// Return the local socket address.
480    pub fn local_addr(&self) -> ScionSocketIpAddr {
481        self.local_addr
482    }
483
484    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
485    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
486        self.snap_data_plane
487    }
488}
489
490/// A trait for receiving socket send errors.
491pub trait SendErrorReceiver: Send + Sync {
492    /// Reports an error when sending a packet.
493    /// This function must return immediately and not block.
494    fn report_send_error(&self, error: &ScionSocketSendError);
495}
496
497/// A path aware UDP socket generic over the path manager.
498///
499/// The `P` type parameter is a **deferred extension point** for custom path management. Today the
500/// only way to obtain a socket is through [`ScionStack`](crate::ScionStack), which always yields a
501/// `UdpScionSocket<MultiPathManager>`; a public constructor for a caller-supplied `P` will be added
502/// once the path-manager trait surface is finalized. The parameter is kept now so that addition is
503/// not itself a breaking change.
504pub struct UdpScionSocket<P: PathManager = MultiPathManager> {
505    socket: PathUnawareUdpScionSocket,
506    pather: Arc<P>,
507    connect_timeout: Duration,
508    remote_addr: Option<ScionSocketIpAddr>,
509    send_error_receivers: Subscribers<dyn SendErrorReceiver>,
510}
511
512// Intentionally shows only the addresses; the path manager and receivers are not `Debug`.
513#[allow(clippy::missing_fields_in_debug)]
514impl<P: PathManager> std::fmt::Debug for UdpScionSocket<P> {
515    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516        f.debug_struct("UdpScionSocket")
517            .field("local_addr", &self.socket.local_addr())
518            .field("remote_addr", &self.remote_addr)
519            .finish()
520    }
521}
522
523impl<P: PathManager> UdpScionSocket<P> {
524    /// Creates a new path aware UDP SCION socket.
525    pub(crate) fn new(
526        socket: PathUnawareUdpScionSocket,
527        pather: Arc<P>,
528        connect_timeout: Duration,
529        send_error_receivers: Subscribers<dyn SendErrorReceiver>,
530    ) -> Self {
531        Self {
532            socket,
533            pather,
534            connect_timeout,
535            remote_addr: None,
536            send_error_receivers,
537        }
538    }
539
540    /// Connects the socket to a remote address.
541    ///
542    /// Ensures a Path to the Destination exists, returns an error if not.
543    ///
544    /// Timeouts after configured `connect_timeout`
545    pub async fn connect(
546        self,
547        remote_addr: ScionSocketIpAddr,
548    ) -> Result<Self, ScionSocketConnectError> {
549        // Check that a path exists to destination
550        let _path = self
551            .pather
552            .path_timeout(
553                self.socket.local_addr().isd_asn(),
554                remote_addr.isd_asn(),
555                SystemTime::now(),
556                self.connect_timeout,
557            )
558            .await?;
559
560        Ok(Self {
561            remote_addr: Some(remote_addr),
562            ..self
563        })
564    }
565
566    /// Send a datagram to the connected remote address.
567    ///
568    /// # Cancel safety
569    ///
570    /// This method is cancel-safe. If the future is dropped before completion, the packet may
571    /// be silently lost, but no socket state is corrupted and the socket remains usable.
572    pub async fn send(&self, payload: &[u8]) -> Result<(), ScionSocketSendError> {
573        if let Some(remote_addr) = self.remote_addr {
574            self.send_to(payload, remote_addr).await
575        } else {
576            Err(ScionSocketSendError::NotConnected)
577        }
578    }
579
580    /// Send a datagram to the specified destination.
581    ///
582    /// # Cancel safety
583    ///
584    /// This method is cancel-safe. It has two await points: the path lookup and the actual send.
585    /// If the future is dropped at either point, no socket state is corrupted and the socket
586    /// remains usable. A packet dropped mid-send is silently lost, which is normal for UDP.
587    pub async fn send_to(
588        &self,
589        payload: &[u8],
590        destination: ScionSocketIpAddr,
591    ) -> Result<(), ScionSocketSendError> {
592        let path = &self
593            .pather
594            .path_wait(
595                self.socket.local_addr().isd_asn(),
596                destination.isd_asn(),
597                SystemTime::now(),
598            )
599            .await?;
600        self.socket.send_to_via(payload, destination, path).await
601    }
602
603    /// Send a datagram to the specified destination via the specified path.
604    ///
605    /// # Cancel safety
606    ///
607    /// This method is cancel-safe. If the future is dropped before completion, the packet may
608    /// be silently lost, but no socket state is corrupted and the socket remains usable.
609    pub async fn send_to_via(
610        &self,
611        payload: &[u8],
612        destination: ScionSocketIpAddr,
613        path: &ScionPath,
614    ) -> Result<(), ScionSocketSendError> {
615        self.socket
616            .send_to_via(payload, destination, path)
617            .await
618            .inspect_err(|e| {
619                self.send_error_receivers
620                    .for_each(|receiver| receiver.report_send_error(e));
621            })
622    }
623
624    /// Receive a datagram from any address, along with the sender address and path.
625    ///
626    /// # Cancel safety
627    ///
628    /// This method is cancel-safe. The only await point is the inner underlay receive. If the
629    /// future is dropped while waiting for a packet, no packet data is consumed and `buffer` is
630    /// left unmodified.
631    ///
632    /// Path registration via the path manager runs synchronously within the same `poll`
633    /// invocation that delivers the received data, so it cannot be independently cancelled.
634    pub async fn recv_from_with_path(
635        &self,
636        buffer: &mut [u8],
637    ) -> Result<(usize, ScionSocketIpAddr, ScionPath), ScionSocketReceiveError> {
638        let (len, sender_addr, path): (usize, ScionSocketIpAddr, ScionPath) =
639            self.socket.recv_from_with_path(buffer).await?;
640
641        match path.clone().try_into_reversed() {
642            Ok(reversed_path) => {
643                // Register the path for future use
644                self.pather.register_path(
645                    self.socket.local_addr().isd_asn(),
646                    sender_addr.isd_asn(),
647                    SystemTime::now(),
648                    reversed_path,
649                );
650            }
651            Err((_, e)) => {
652                tracing::trace!(error = ?e, "Failed to reverse path for registration");
653            }
654        }
655
656        tracing::trace!(
657            src = %self.socket.local_addr(),
658            dst = %sender_addr,
659            "Registered reverse path",
660        );
661
662        Ok((len, sender_addr, path))
663    }
664
665    /// Receive a datagram from the connected remote address and write it into the provided buffer.
666    ///
667    /// The path of the received packet is used to register a reverse path with the path manager,
668    /// but is not returned to the caller. Use [`recv_from_with_path`](Self::recv_from_with_path)
669    /// if the path is needed.
670    ///
671    /// # Cancel safety
672    ///
673    /// This method is cancel-safe. If the future is dropped while waiting for a packet, no
674    /// packet is consumed and `buffer` is left unmodified. The contents of `buffer` are only
675    /// valid after the method returns `Ok`.
676    pub async fn recv_from(
677        &self,
678        buffer: &mut [u8],
679    ) -> Result<(usize, ScionSocketIpAddr), ScionSocketReceiveError> {
680        let (len, sender_addr, _) = self.recv_from_with_path(buffer).await?;
681        Ok((len, sender_addr))
682    }
683
684    /// Receive a datagram from the connected remote address.
685    ///
686    /// Datagrams from other addresses are silently discarded.
687    ///
688    /// # Cancel safety
689    ///
690    /// This method is cancel-safe. If the future is dropped while waiting for a packet, no
691    /// packet is permanently lost — the underlying receive is cancel-safe and an undelivered
692    /// packet remains available for the next call. Note that packets from other senders are
693    /// discarded during filtering; those discarded packets are not recoverable regardless of
694    /// cancellation. The contents of `buffer` are only valid after the method returns `Ok(n)`.
695    pub async fn recv(&self, buffer: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
696        if self.remote_addr.is_none() {
697            return Err(ScionSocketReceiveError::NotConnected);
698        }
699        loop {
700            let (len, sender_addr) = self.recv_from(buffer).await?;
701
702            // Check if the sender address matches the connected remote address if one is set.
703            match self.remote_addr {
704                Some(remote_addr) => {
705                    if sender_addr == remote_addr {
706                        return Ok(len);
707                    }
708                }
709                None => return Err(ScionSocketReceiveError::NotConnected),
710            }
711        }
712    }
713
714    /// Returns the local socket address.
715    pub fn local_addr(&self) -> ScionSocketIpAddr {
716        self.socket.local_addr()
717    }
718
719    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
720    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
721        self.socket.snap_data_plane()
722    }
723}
724
725// Allow using `UdpScionSocket` as a `GenericScionUdpSocket` for compatibility with QUIC and HTTP/3
726// implementations.
727#[async_trait::async_trait]
728impl<P: PathManager + Sync + Send + 'static> GenericScionUdpSocket for UdpScionSocket<P> {
729    /// Asynchronously sends a Datagram to the specified destination address.
730    async fn send_to(
731        &self,
732        payload: &[u8],
733        destination: ScionSocketIpAddr,
734    ) -> Result<(), BoxedSocketError> {
735        self.send_to(payload, destination)
736            .await
737            .map_err(|e| Box::new(e) as BoxedSocketError)
738    }
739
740    /// Asynchronously receives a Datagram, writing it into the provided buffer, and returns the
741    /// number of bytes read and the source address.
742    async fn recv_from(
743        &self,
744        buf: &mut [u8],
745    ) -> Result<(usize, ScionSocketIpAddr), BoxedSocketError> {
746        self.recv_from(buf)
747            .await
748            .map_err(|e| Box::new(e) as BoxedSocketError)
749    }
750
751    /// Returns the local socket address of this socket.
752    fn local_addr(&self) -> ScionSocketIpAddr {
753        self.local_addr()
754    }
755}
756
757#[cfg(test)]
758mod cancel_safety_tests {
759    //! Unit tests verifying that all async methods on [`UdpScionSocket`] and
760    //! [`PathUnawareUdpScionSocket`] are cancel-safe.
761    //!
762    //! The tests use two hand-rolled test doubles rather than the real underlay and path manager:
763    //!
764    //! - [`ManualUnderlaySocket`]: backed by a bounded `tokio::sync::mpsc` channel. Injecting
765    //!   packets is done via the paired `Sender`. The `recv` future is backed by
766    //!   `tokio::sync::mpsc::Receiver::recv()`, which IS cancel-safe (the message stays in the
767    //!   channel if the future is dropped before returning `Ready`).
768    //!
769    //! - [`ImmediatePathManager`]: always returns a local (empty) path immediately, so tests do not
770    //!   depend on any background task.
771    //!
772    //! ## What these tests verify
773    //!
774    //! The tests verify that dropping a future at realistically reachable await points (the inner
775    //! underlay `recv`) leaves no corrupted socket state and that unconsumed packets remain
776    //! available for the next caller. They also verify that the wrong-sender filtering loop in
777    //! [`UdpScionSocket::recv`] can be safely cancelled mid-iteration.
778    //!
779    //! Because all processing steps after the underlay `recv` resolves run synchronously within
780    //! the same `poll()` invocation, there is no intermediate await point between "data received"
781    //! and "data returned" that could be independently cancelled. The tests therefore focus on
782    //! the cancel points that actually exist at runtime.
783
784    use std::{
785        io,
786        net::Ipv4Addr,
787        sync::{Arc, Mutex},
788        time::SystemTime,
789    };
790
791    use async_trait::async_trait;
792    use sciparse::{
793        identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
794        util::test_builder::{TestPathBuilder, TestPathContext},
795    };
796
797    use super::*;
798    use crate::{
799        internal::Subscribers,
800        path::manager::traits::{PathWaitError, SyncPathManager},
801        stack::{ScionSocketReceiveError, ScionSocketSendError, UnderlaySocket},
802    };
803
804    struct ManualUnderlaySocket {
805        rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Box<ScionRawPacketView>>>,
806        /// A packet staged by `readable` and not yet returned by `try_recv` (see the SNAP
807        /// underlay for the same pattern).
808        peeked: tokio::sync::Mutex<Option<Box<ScionRawPacketView>>>,
809    }
810
811    impl ManualUnderlaySocket {
812        fn new() -> (Self, tokio::sync::mpsc::Sender<Box<ScionRawPacketView>>) {
813            // Use a large bounded channel so tests never block on send.
814            let (inject_tx, recv_rx) = tokio::sync::mpsc::channel::<Box<ScionRawPacketView>>(64);
815            let socket = Self {
816                rx: tokio::sync::Mutex::new(recv_rx),
817                peeked: tokio::sync::Mutex::new(None),
818            };
819            (socket, inject_tx)
820        }
821    }
822
823    #[async_trait]
824    impl UnderlaySocket for ManualUnderlaySocket {
825        fn try_send(&self, _packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
826            Ok(())
827        }
828
829        async fn writeable(&self) {}
830
831        fn try_recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
832            let would_block =
833                || ScionSocketReceiveError::IoError(io::Error::from(io::ErrorKind::WouldBlock));
834
835            let packet: Box<ScionRawPacketView> = {
836                let Ok(mut peeked) = self.peeked.try_lock() else {
837                    return Err(would_block());
838                };
839                match peeked.take() {
840                    Some(packet) => packet,
841                    None => {
842                        let Ok(mut rx) = self.rx.try_lock() else {
843                            return Err(would_block());
844                        };
845                        match rx.try_recv() {
846                            Ok(packet) => packet,
847                            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
848                                return Err(would_block());
849                            }
850                            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
851                                return Err(ScionSocketReceiveError::IoError(io::Error::other(
852                                    "channel closed",
853                                )));
854                            }
855                        }
856                    }
857                }
858            };
859
860            let bytes = packet.as_slice();
861            let n = bytes.len();
862            buf[..n].copy_from_slice(bytes);
863            Ok(n)
864        }
865
866        async fn readable(&self) {
867            let mut peeked = self.peeked.lock().await;
868            if peeked.is_some() {
869                return;
870            }
871            // `tokio::sync::mpsc::Receiver::recv` is cancel-safe: if this future is dropped before
872            // a message arrives, the message stays in the channel.
873            if let Some(packet) = self.rx.lock().await.recv().await {
874                *peeked = Some(packet);
875            }
876        }
877    }
878
879    #[derive(Default)]
880    struct ImmediatePathManager {
881        registered_paths: Mutex<Vec<ScionPath>>,
882    }
883
884    impl SyncPathManager for ImmediatePathManager {
885        fn register_path(&self, _src: IsdAsn, _dst: IsdAsn, _now: SystemTime, path: ScionPath) {
886            self.registered_paths.lock().expect("poisoned").push(path);
887        }
888
889        fn try_cached_path(
890            &self,
891            src: IsdAsn,
892            _dst: IsdAsn,
893            _now: SystemTime,
894        ) -> io::Result<Option<ScionPath>> {
895            Ok(Some(
896                ScionPath::local(src).expect("src is not a wildcard IA"),
897            ))
898        }
899    }
900
901    impl PathManager for ImmediatePathManager {
902        fn path_wait(
903            &self,
904            src: IsdAsn,
905            _dst: IsdAsn,
906            _now: SystemTime,
907        ) -> impl std::future::Future<Output = Result<ScionPath, PathWaitError>> + Send + '_
908        {
909            async move { Ok(ScionPath::local(src).expect("src is not a wildcard IA")) }
910        }
911    }
912
913    const LOCAL_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(1));
914    const REMOTE_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(2));
915    const OTHER_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(3));
916
917    fn local_addr() -> ScionSocketIpAddr {
918        ScionSocketIpAddr::new(LOCAL_ISD_ASN, Ipv4Addr::LOCALHOST.into(), 8080)
919    }
920
921    fn remote_addr() -> ScionSocketIpAddr {
922        ScionSocketIpAddr::new(REMOTE_ISD_ASN, Ipv4Addr::new(127, 0, 0, 2).into(), 9090)
923    }
924
925    fn other_addr() -> ScionSocketIpAddr {
926        ScionSocketIpAddr::new(OTHER_ISD_ASN, Ipv4Addr::new(127, 0, 0, 3).into(), 7070)
927    }
928
929    /// Build a [`TestPathContext`] carrying a path from `src` to `dst`.
930    fn test_path_ctx(src: ScionAddr, dst: ScionAddr) -> TestPathContext {
931        TestPathBuilder::new(src, dst)
932            .using_info_timestamp(1_000_000)
933            .up()
934            .add_hop(0, 1)
935            .add_hop(1, 0)
936            .build(1_000_000)
937    }
938
939    /// Create a valid [`ScionPacketRaw`] that looks like a UDP packet from `src` to `dst`
940    /// with `payload`.
941    fn make_udp_raw(
942        src: ScionSocketIpAddr,
943        dst: ScionSocketIpAddr,
944        payload: &[u8],
945    ) -> Box<ScionRawPacketView> {
946        let ctx = test_path_ctx(src.scion_addr(), dst.scion_addr());
947        ctx.scion_packet_udp(payload, src.port(), dst.port())
948            .try_encode_to_owned_view()
949            .expect("should encode")
950            .into()
951    }
952
953    /// Build a connected [`UdpScionSocket`] backed by the test doubles.
954    /// Returns the socket, the packet injector, and the path manager.
955    fn build_socket() -> (
956        UdpScionSocket<ImmediatePathManager>,
957        tokio::sync::mpsc::Sender<Box<ScionRawPacketView>>,
958        Arc<ImmediatePathManager>,
959    ) {
960        let (underlay, inject_tx) = ManualUnderlaySocket::new();
961        let pather = Arc::new(ImmediatePathManager::default());
962        let path_unaware = PathUnawareUdpScionSocket::new(
963            BoundUnderlaySocket {
964                socket: Box::new(underlay),
965                local_addr: local_addr(),
966                snap_data_plane: None,
967            },
968            vec![], // no SCMP handlers needed
969        );
970        let socket = UdpScionSocket::new(
971            path_unaware,
972            pather.clone(),
973            std::time::Duration::from_secs(5),
974            Subscribers::new(),
975        );
976        (socket, inject_tx, pather)
977    }
978
979    /// Dropping a [`recv_from_with_path`] future while it is pending (waiting in the channel)
980    /// must not consume the packet. The next call must receive that packet.
981    ///
982    /// This verifies that the underlay's `recv` future is cancel-safe: the message stays in the
983    /// channel when the outer future is dropped before returning `Ready`.
984    #[tokio::test]
985    async fn recv_from_with_path_cancel_while_pending_does_not_lose_packet() {
986        let (socket, inject_tx, _pather) = build_socket();
987
988        // Poll once — returns Pending because the channel is empty.
989        {
990            let mut buf = [0u8; 64];
991            let mut fut = std::pin::pin!(socket.recv_from_with_path(&mut buf));
992            let waker = futures::task::noop_waker();
993            let mut cx = std::task::Context::from_waker(&waker);
994            // The future must be Pending (no packet injected yet).
995            assert!(fut.as_mut().poll(&mut cx).is_pending());
996            // Drop `fut` here — the future is cancelled while pending.
997        }
998
999        // Inject the packet AFTER the first future was dropped.
1000        let payload = b"cancel-safe";
1001        inject_tx
1002            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
1003            .unwrap();
1004
1005        // The packet must be available to the next future.
1006        let mut buf2 = vec![0u8; 64];
1007        let (len, sender, _path) = socket.recv_from_with_path(&mut buf2).await.unwrap();
1008
1009        assert_eq!(len, payload.len());
1010        assert_eq!(&buf2[..len], payload);
1011        assert_eq!(sender, remote_addr());
1012    }
1013
1014    /// `recv` (connected socket) correctly filters wrong-sender packets and returns
1015    /// the packet from the connected remote address.
1016    #[tokio::test]
1017    async fn recv_filters_wrong_sender_and_delivers_correct_packet() {
1018        let (mut socket, inject_tx, _pather) = build_socket();
1019        // Connect to remote_addr.
1020        socket.remote_addr = Some(remote_addr());
1021
1022        // Inject wrong-sender packet first, then correct-sender packet.
1023        inject_tx
1024            .try_send(make_udp_raw(other_addr(), local_addr(), b"wrong"))
1025            .unwrap();
1026        inject_tx
1027            .try_send(make_udp_raw(remote_addr(), local_addr(), b"correct"))
1028            .unwrap();
1029
1030        let mut buf = [0u8; 64];
1031        let len = socket.recv(&mut buf).await.unwrap();
1032        assert_eq!(&buf[..len], b"correct");
1033    }
1034
1035    /// After cancelling `recv` mid-filtering (a wrong-sender packet was consumed),
1036    /// the socket must still be usable and must deliver subsequent correct-sender packets.
1037    #[tokio::test]
1038    async fn recv_cancel_during_filtering_socket_remains_usable() {
1039        let (mut socket, inject_tx, _pather) = build_socket();
1040        socket.remote_addr = Some(remote_addr());
1041
1042        // Inject only a wrong-sender packet — `recv` will consume it and loop back
1043        // to await the next packet (Pending at that point).
1044        inject_tx
1045            .try_send(make_udp_raw(other_addr(), local_addr(), b"wrong"))
1046            .unwrap();
1047
1048        // Poll once with a noop waker: recv processes the wrong-sender packet, finds it does not
1049        // match the connected address, and loops back to yield on the inner recv (Pending).
1050        // No Tokio runtime involvement is needed here — the channel already holds the packet.
1051        {
1052            let mut filter_buf = [0u8; 64];
1053            let mut fut = std::pin::pin!(socket.recv(&mut filter_buf));
1054            let waker = futures::task::noop_waker();
1055            let mut cx = std::task::Context::from_waker(&waker);
1056            assert!(
1057                fut.as_mut().poll(&mut cx).is_pending(),
1058                "recv must be Pending after consuming wrong-sender packet"
1059            );
1060            // Drop the future here — the wrong-sender packet has been consumed and discarded.
1061        }
1062
1063        // Now inject a correct-sender packet and verify the socket is still usable.
1064        inject_tx
1065            .try_send(make_udp_raw(remote_addr(), local_addr(), b"after-cancel"))
1066            .unwrap();
1067
1068        let mut buf = [0u8; 64];
1069        let len = socket.recv(&mut buf).await.unwrap();
1070        assert_eq!(&buf[..len], b"after-cancel");
1071    }
1072
1073    /// Buffer contents are only valid after a successful `Ok` return; after a
1074    /// cancel and retry the buffer must contain the correct data from the retry.
1075    #[tokio::test]
1076    async fn recv_from_buffer_valid_only_after_ok() {
1077        let (socket, inject_tx, _pather) = build_socket();
1078
1079        // Pre-fill buffer with sentinel bytes.
1080        let mut buf = [0xFFu8; 64];
1081
1082        // Cancel while pending (no packet).
1083        {
1084            let mut fut = std::pin::pin!(socket.recv_from(&mut buf));
1085            let waker = futures::task::noop_waker();
1086            let mut cx = std::task::Context::from_waker(&waker);
1087            assert!(fut.as_mut().poll(&mut cx).is_pending());
1088        }
1089
1090        // Inject a packet with known payload.
1091        let payload = b"real-data";
1092        inject_tx
1093            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
1094            .unwrap();
1095
1096        let (len, _sender) = socket.recv_from(&mut buf).await.unwrap();
1097        assert_eq!(len, payload.len());
1098        assert_eq!(
1099            &buf[..len],
1100            payload,
1101            "buffer must contain the real payload after Ok return"
1102        );
1103    }
1104
1105    /// `recv_from` returns the number of bytes written if the buffer is too small.
1106    #[tokio::test]
1107    async fn recv_from_truncates_to_buffer_size() {
1108        let (socket, inject_tx, _pather) = build_socket();
1109        let payload = b"truncated-payload";
1110        inject_tx
1111            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
1112            .unwrap();
1113
1114        let mut buf = [0u8; 4];
1115        let (len, _sender) = socket.recv_from(&mut buf).await.unwrap();
1116        assert_eq!(len, buf.len());
1117        assert_eq!(&buf[..], &payload[..4]);
1118    }
1119
1120    /// `recv_from_with_path` returns the number of bytes written if the buffer is too small.
1121    #[tokio::test]
1122    async fn recv_from_with_path_truncates_to_buffer_size() {
1123        let (socket, inject_tx, _pather) = build_socket();
1124        let payload = b"truncated-payload";
1125        inject_tx
1126            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
1127            .unwrap();
1128
1129        let mut buf = [0u8; 4];
1130        let (len, _sender, _path) = socket.recv_from_with_path(&mut buf).await.unwrap();
1131        assert_eq!(len, buf.len());
1132        assert_eq!(&buf[..], &payload[..4]);
1133    }
1134}