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::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((packet.udp().payload().len(), 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((packet.udp().payload().len(), 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    /// ### Params
303    /// * `path_manager`: The path manager this socket will query for paths to the destination.
304    /// * `connect_timeout`: The timeout for the initial path lookup when connecting to a remote
305    ///   address.
306    pub fn into_path_aware<P: PathManager>(
307        self,
308        path_manager: Arc<P>,
309        connect_timeout: Duration,
310    ) -> UdpScionSocket<P> {
311        UdpScionSocket::new(self, path_manager, connect_timeout, Subscribers::default())
312    }
313}
314
315/// A SCMP SCION socket.
316pub struct ScmpScionSocket {
317    inner: Box<dyn UnderlaySocket>,
318    /// The local SCION address the socket is bound to.
319    local_addr: ScionSocketIpAddr,
320    /// The SNAP data plane the socket is connected to (if a SNAP underlay is used).
321    snap_data_plane: Option<net::SocketAddr>,
322}
323
324impl ScmpScionSocket {
325    pub(crate) fn new(bound: BoundUnderlaySocket) -> Self {
326        Self {
327            inner: bound.socket,
328            local_addr: bound.local_addr,
329            snap_data_plane: bound.snap_data_plane,
330        }
331    }
332}
333
334impl ScmpScionSocket {
335    /// Send a SCMP message to the destination via the given path.
336    pub async fn send_to_via(
337        &self,
338        message: ScmpMessage,
339        destination: ScionAddr,
340        path: &ScionPath,
341    ) -> Result<(), ScionSocketSendError> {
342        let packet = ScionScmpPacket::new(
343            self.local_addr.scion_ip_addr().into(),
344            destination,
345            path.dp_path().to_model(),
346            message,
347        )
348        .try_encode_to_owned_view()
349        .map_err(|e| {
350            ScionSocketSendError::InvalidPacket(format!("error encoding packet: {e}").into())
351        })?
352        .into_raw();
353        self.inner.send(&packet).await
354    }
355
356    /// Receive a SCMP message with the sender and path.
357    #[allow(clippy::type_complexity)]
358    pub async fn recv_from_with_path(
359        &self,
360    ) -> Result<(Box<ScmpPayloadView>, ScionAddr, ScionPath), ScionSocketReceiveError> {
361        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
362        loop {
363            let n = self.inner.recv(&mut scratch).await?;
364            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
365                .expect("underlay recv returns a decoded packet");
366            let packet = match packet.try_as_scmp() {
367                Ok(packet) => packet,
368                Err(e) => {
369                    tracing::debug!(error = %e, "Received invalid SCMP packet, dropping");
370                    continue;
371                }
372            };
373
374            let src_addr = match packet.src_scion_addr() {
375                Ok(source) => source,
376                Err(e) => {
377                    tracing::debug!(error = %e, "Failed to decode packet source address, skipping");
378                    continue;
379                }
380            };
381
382            let path = ScionPath::new(
383                packet.header().src_ia(),
384                packet.header().dst_ia(),
385                packet.header().path().to_owned_view(),
386                None,
387                None,
388            );
389
390            return Ok((packet.scmp().to_boxed(), src_addr, path));
391        }
392    }
393
394    /// Receive a SCMP message with the sender.
395    pub async fn recv_from(
396        &self,
397    ) -> Result<(Box<ScmpPayloadView>, ScionAddr), ScionSocketReceiveError> {
398        let mut scratch = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
399        loop {
400            let n = self.inner.recv(&mut scratch).await?;
401            let (packet, _rest) = ScionRawPacketView::try_from_slice(&scratch[..n])
402                .expect("underlay recv returns a decoded packet");
403            let packet = match packet.try_as_scmp() {
404                Ok(packet) => packet,
405                Err(e) => {
406                    tracing::debug!(error = %e, "Received invalid SCMP packet, skipping");
407                    continue;
408                }
409            };
410            let src_addr = match packet.src_scion_addr() {
411                Ok(source) => source,
412                Err(e) => {
413                    tracing::debug!(error = %e, "Failed to decode packet source address, skipping");
414                    continue;
415                }
416            };
417            return Ok((packet.scmp().to_boxed(), src_addr));
418        }
419    }
420
421    /// Return the local socket address.
422    pub fn local_addr(&self) -> ScionSocketIpAddr {
423        self.local_addr
424    }
425
426    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
427    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
428        self.snap_data_plane
429    }
430}
431
432/// A raw SCION socket.
433pub struct RawScionSocket {
434    inner: Box<dyn UnderlaySocket>,
435    /// The local SCION address the socket is bound to.
436    local_addr: ScionSocketIpAddr,
437    /// The SNAP data plane the socket is connected to (if a SNAP underlay is used).
438    snap_data_plane: Option<net::SocketAddr>,
439}
440
441impl RawScionSocket {
442    pub(crate) fn new(bound: BoundUnderlaySocket) -> Self {
443        Self {
444            inner: bound.socket,
445            local_addr: bound.local_addr,
446            snap_data_plane: bound.snap_data_plane,
447        }
448    }
449}
450
451impl RawScionSocket {
452    /// Send a raw SCION packet.
453    pub async fn send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
454        self.inner.send(packet).await
455    }
456
457    /// Receive a raw SCION packet.
458    pub async fn recv(&self) -> Result<Box<ScionRawPacketView>, ScionSocketReceiveError> {
459        let mut buf = vec![0u8; MAX_UNDERLAY_PACKET_SIZE];
460        let n = self.inner.recv(&mut buf).await?;
461        let (view, _rest) = ScionRawPacketView::try_from_slice(&buf[..n])
462            .expect("underlay recv returns a decoded packet");
463        Ok(view.to_boxed())
464    }
465
466    /// Return the local socket address.
467    pub fn local_addr(&self) -> ScionSocketIpAddr {
468        self.local_addr
469    }
470
471    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
472    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
473        self.snap_data_plane
474    }
475}
476
477/// A trait for receiving socket send errors.
478pub trait SendErrorReceiver: Send + Sync {
479    /// Reports an error when sending a packet.
480    /// This function must return immediately and not block.
481    fn report_send_error(&self, error: &ScionSocketSendError);
482}
483
484/// A path aware UDP socket generic over the path manager.
485///
486/// The `P` type parameter is a **deferred extension point** for custom path management. Today the
487/// only way to obtain a socket is through [`ScionStack`](crate::ScionStack), which always yields a
488/// `UdpScionSocket<MultiPathManager>`; a public constructor for a caller-supplied `P` will be added
489/// once the path-manager trait surface is finalized. The parameter is kept now so that addition is
490/// not itself a breaking change.
491pub struct UdpScionSocket<P: PathManager = MultiPathManager> {
492    socket: PathUnawareUdpScionSocket,
493    pather: Arc<P>,
494    connect_timeout: Duration,
495    remote_addr: Option<ScionSocketIpAddr>,
496    send_error_receivers: Subscribers<dyn SendErrorReceiver>,
497}
498
499// Intentionally shows only the addresses; the path manager and receivers are not `Debug`.
500#[allow(clippy::missing_fields_in_debug)]
501impl<P: PathManager> std::fmt::Debug for UdpScionSocket<P> {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        f.debug_struct("UdpScionSocket")
504            .field("local_addr", &self.socket.local_addr())
505            .field("remote_addr", &self.remote_addr)
506            .finish()
507    }
508}
509
510impl<P: PathManager> UdpScionSocket<P> {
511    /// Creates a new path aware UDP SCION socket.
512    pub(crate) fn new(
513        socket: PathUnawareUdpScionSocket,
514        pather: Arc<P>,
515        connect_timeout: Duration,
516        send_error_receivers: Subscribers<dyn SendErrorReceiver>,
517    ) -> Self {
518        Self {
519            socket,
520            pather,
521            connect_timeout,
522            remote_addr: None,
523            send_error_receivers,
524        }
525    }
526
527    /// Connects the socket to a remote address.
528    ///
529    /// Ensures a Path to the Destination exists, returns an error if not.
530    ///
531    /// Timeouts after configured `connect_timeout`
532    pub async fn connect(
533        self,
534        remote_addr: ScionSocketIpAddr,
535    ) -> Result<Self, ScionSocketConnectError> {
536        // Check that a path exists to destination
537        let _path = self
538            .pather
539            .path_timeout(
540                self.socket.local_addr().isd_asn(),
541                remote_addr.isd_asn(),
542                SystemTime::now(),
543                self.connect_timeout,
544            )
545            .await?;
546
547        Ok(Self {
548            remote_addr: Some(remote_addr),
549            ..self
550        })
551    }
552
553    /// Send a datagram to the connected remote address.
554    ///
555    /// # Cancel safety
556    ///
557    /// This method is cancel-safe. If the future is dropped before completion, the packet may
558    /// be silently lost, but no socket state is corrupted and the socket remains usable.
559    pub async fn send(&self, payload: &[u8]) -> Result<(), ScionSocketSendError> {
560        if let Some(remote_addr) = self.remote_addr {
561            self.send_to(payload, remote_addr).await
562        } else {
563            Err(ScionSocketSendError::NotConnected)
564        }
565    }
566
567    /// Send a datagram to the specified destination.
568    ///
569    /// # Cancel safety
570    ///
571    /// This method is cancel-safe. It has two await points: the path lookup and the actual send.
572    /// If the future is dropped at either point, no socket state is corrupted and the socket
573    /// remains usable. A packet dropped mid-send is silently lost, which is normal for UDP.
574    pub async fn send_to(
575        &self,
576        payload: &[u8],
577        destination: ScionSocketIpAddr,
578    ) -> Result<(), ScionSocketSendError> {
579        let path = &self
580            .pather
581            .path_wait(
582                self.socket.local_addr().isd_asn(),
583                destination.isd_asn(),
584                SystemTime::now(),
585            )
586            .await?;
587        self.socket.send_to_via(payload, destination, path).await
588    }
589
590    /// Send a datagram to the specified destination via the specified path.
591    ///
592    /// # Cancel safety
593    ///
594    /// This method is cancel-safe. If the future is dropped before completion, the packet may
595    /// be silently lost, but no socket state is corrupted and the socket remains usable.
596    pub async fn send_to_via(
597        &self,
598        payload: &[u8],
599        destination: ScionSocketIpAddr,
600        path: &ScionPath,
601    ) -> Result<(), ScionSocketSendError> {
602        self.socket
603            .send_to_via(payload, destination, path)
604            .await
605            .inspect_err(|e| {
606                self.send_error_receivers
607                    .for_each(|receiver| receiver.report_send_error(e));
608            })
609    }
610
611    /// Receive a datagram from any address, along with the sender address and path.
612    ///
613    /// # Cancel safety
614    ///
615    /// This method is cancel-safe. The only await point is the inner underlay receive. If the
616    /// future is dropped while waiting for a packet, no packet data is consumed and `buffer` is
617    /// left unmodified.
618    ///
619    /// Path registration via the path manager runs synchronously within the same `poll`
620    /// invocation that delivers the received data, so it cannot be independently cancelled.
621    pub async fn recv_from_with_path(
622        &self,
623        buffer: &mut [u8],
624    ) -> Result<(usize, ScionSocketIpAddr, ScionPath), ScionSocketReceiveError> {
625        let (len, sender_addr, path): (usize, ScionSocketIpAddr, ScionPath) =
626            self.socket.recv_from_with_path(buffer).await?;
627
628        match path.clone().try_into_reversed() {
629            Ok(reversed_path) => {
630                // Register the path for future use
631                self.pather.register_path(
632                    self.socket.local_addr().isd_asn(),
633                    sender_addr.isd_asn(),
634                    SystemTime::now(),
635                    reversed_path,
636                );
637            }
638            Err((_, e)) => {
639                tracing::trace!(error = ?e, "Failed to reverse path for registration");
640            }
641        }
642
643        tracing::trace!(
644            src = %self.socket.local_addr(),
645            dst = %sender_addr,
646            "Registered reverse path",
647        );
648
649        Ok((len, sender_addr, path))
650    }
651
652    /// Receive a datagram from the connected remote address and write it into the provided buffer.
653    ///
654    /// The path of the received packet is used to register a reverse path with the path manager,
655    /// but is not returned to the caller. Use [`recv_from_with_path`](Self::recv_from_with_path)
656    /// if the path is needed.
657    ///
658    /// # Cancel safety
659    ///
660    /// This method is cancel-safe. If the future is dropped while waiting for a packet, no
661    /// packet is consumed and `buffer` is left unmodified. The contents of `buffer` are only
662    /// valid after the method returns `Ok`.
663    pub async fn recv_from(
664        &self,
665        buffer: &mut [u8],
666    ) -> Result<(usize, ScionSocketIpAddr), ScionSocketReceiveError> {
667        let (len, sender_addr, _) = self.recv_from_with_path(buffer).await?;
668        Ok((len, sender_addr))
669    }
670
671    /// Receive a datagram from the connected remote address.
672    ///
673    /// Datagrams from other addresses are silently discarded.
674    ///
675    /// # Cancel safety
676    ///
677    /// This method is cancel-safe. If the future is dropped while waiting for a packet, no
678    /// packet is permanently lost — the underlying receive is cancel-safe and an undelivered
679    /// packet remains available for the next call. Note that packets from other senders are
680    /// discarded during filtering; those discarded packets are not recoverable regardless of
681    /// cancellation. The contents of `buffer` are only valid after the method returns `Ok(n)`.
682    pub async fn recv(&self, buffer: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
683        if self.remote_addr.is_none() {
684            return Err(ScionSocketReceiveError::NotConnected);
685        }
686        loop {
687            let (len, sender_addr) = self.recv_from(buffer).await?;
688
689            // Check if the sender address matches the connected remote address if one is set.
690            match self.remote_addr {
691                Some(remote_addr) => {
692                    if sender_addr == remote_addr {
693                        return Ok(len);
694                    }
695                }
696                None => return Err(ScionSocketReceiveError::NotConnected),
697            }
698        }
699    }
700
701    /// Returns the local socket address.
702    pub fn local_addr(&self) -> ScionSocketIpAddr {
703        self.socket.local_addr()
704    }
705
706    /// The SNAP data plane the socket is connected to (if SNAP underlay is used).
707    pub fn snap_data_plane(&self) -> Option<net::SocketAddr> {
708        self.socket.snap_data_plane()
709    }
710}
711
712// Allow using `UdpScionSocket` as a `GenericScionUdpSocket` for compatibility with QUIC and HTTP/3
713// implementations.
714#[async_trait::async_trait]
715impl<P: PathManager + Sync + Send + 'static> GenericScionUdpSocket for UdpScionSocket<P> {
716    /// Asynchronously sends a Datagram to the specified destination address.
717    async fn send_to(
718        &self,
719        payload: &[u8],
720        destination: ScionSocketIpAddr,
721    ) -> Result<(), BoxedSocketError> {
722        self.send_to(payload, destination)
723            .await
724            .map_err(|e| Box::new(e) as BoxedSocketError)
725    }
726
727    /// Asynchronously receives a Datagram, writing it into the provided buffer, and returns the
728    /// number of bytes read and the source address.
729    async fn recv_from(
730        &self,
731        buf: &mut [u8],
732    ) -> Result<(usize, ScionSocketIpAddr), BoxedSocketError> {
733        self.recv_from(buf)
734            .await
735            .map_err(|e| Box::new(e) as BoxedSocketError)
736    }
737
738    /// Returns the local socket address of this socket.
739    fn local_addr(&self) -> ScionSocketIpAddr {
740        self.local_addr()
741    }
742}
743
744#[cfg(test)]
745mod cancel_safety_tests {
746    //! Unit tests verifying that all async methods on [`UdpScionSocket`] and
747    //! [`PathUnawareUdpScionSocket`] are cancel-safe.
748    //!
749    //! The tests use two hand-rolled test doubles rather than the real underlay and path manager:
750    //!
751    //! - [`ManualUnderlaySocket`]: backed by a bounded `tokio::sync::mpsc` channel. Injecting
752    //!   packets is done via the paired `Sender`. The `recv` future is backed by
753    //!   `tokio::sync::mpsc::Receiver::recv()`, which IS cancel-safe (the message stays in the
754    //!   channel if the future is dropped before returning `Ready`).
755    //!
756    //! - [`ImmediatePathManager`]: always returns a local (empty) path immediately, so tests do not
757    //!   depend on any background task.
758    //!
759    //! ## What these tests verify
760    //!
761    //! The tests verify that dropping a future at realistically reachable await points (the inner
762    //! underlay `recv`) leaves no corrupted socket state and that unconsumed packets remain
763    //! available for the next caller. They also verify that the wrong-sender filtering loop in
764    //! [`UdpScionSocket::recv`] can be safely cancelled mid-iteration.
765    //!
766    //! Because all processing steps after the underlay `recv` resolves run synchronously within
767    //! the same `poll()` invocation, there is no intermediate await point between "data received"
768    //! and "data returned" that could be independently cancelled. The tests therefore focus on
769    //! the cancel points that actually exist at runtime.
770
771    use std::{
772        io,
773        net::Ipv4Addr,
774        sync::{Arc, Mutex},
775        time::SystemTime,
776    };
777
778    use async_trait::async_trait;
779    use sciparse::{
780        identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
781        util::test_builder::{TestPathBuilder, TestPathContext},
782    };
783
784    use super::*;
785    use crate::{
786        internal::Subscribers,
787        path::manager::traits::{PathWaitError, SyncPathManager},
788        stack::{ScionSocketReceiveError, ScionSocketSendError, UnderlaySocket},
789    };
790
791    struct ManualUnderlaySocket {
792        rx: tokio::sync::Mutex<tokio::sync::mpsc::Receiver<Box<ScionRawPacketView>>>,
793        /// A packet staged by `readable` and not yet returned by `try_recv` (see the SNAP
794        /// underlay for the same pattern).
795        peeked: tokio::sync::Mutex<Option<Box<ScionRawPacketView>>>,
796    }
797
798    impl ManualUnderlaySocket {
799        fn new() -> (Self, tokio::sync::mpsc::Sender<Box<ScionRawPacketView>>) {
800            // Use a large bounded channel so tests never block on send.
801            let (inject_tx, recv_rx) = tokio::sync::mpsc::channel::<Box<ScionRawPacketView>>(64);
802            let socket = Self {
803                rx: tokio::sync::Mutex::new(recv_rx),
804                peeked: tokio::sync::Mutex::new(None),
805            };
806            (socket, inject_tx)
807        }
808    }
809
810    #[async_trait]
811    impl UnderlaySocket for ManualUnderlaySocket {
812        fn try_send(&self, _packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
813            Ok(())
814        }
815
816        async fn writeable(&self) {}
817
818        fn try_recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
819            let would_block =
820                || ScionSocketReceiveError::IoError(io::Error::from(io::ErrorKind::WouldBlock));
821
822            let packet: Box<ScionRawPacketView> = {
823                let Ok(mut peeked) = self.peeked.try_lock() else {
824                    return Err(would_block());
825                };
826                match peeked.take() {
827                    Some(packet) => packet,
828                    None => {
829                        let Ok(mut rx) = self.rx.try_lock() else {
830                            return Err(would_block());
831                        };
832                        match rx.try_recv() {
833                            Ok(packet) => packet,
834                            Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
835                                return Err(would_block());
836                            }
837                            Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
838                                return Err(ScionSocketReceiveError::IoError(io::Error::other(
839                                    "channel closed",
840                                )));
841                            }
842                        }
843                    }
844                }
845            };
846
847            let bytes = packet.as_slice();
848            let n = bytes.len();
849            buf[..n].copy_from_slice(bytes);
850            Ok(n)
851        }
852
853        async fn readable(&self) {
854            let mut peeked = self.peeked.lock().await;
855            if peeked.is_some() {
856                return;
857            }
858            // `tokio::sync::mpsc::Receiver::recv` is cancel-safe: if this future is dropped before
859            // a message arrives, the message stays in the channel.
860            if let Some(packet) = self.rx.lock().await.recv().await {
861                *peeked = Some(packet);
862            }
863        }
864    }
865
866    #[derive(Default)]
867    struct ImmediatePathManager {
868        registered_paths: Mutex<Vec<ScionPath>>,
869    }
870
871    impl SyncPathManager for ImmediatePathManager {
872        fn register_path(&self, _src: IsdAsn, _dst: IsdAsn, _now: SystemTime, path: ScionPath) {
873            self.registered_paths.lock().expect("poisoned").push(path);
874        }
875
876        fn try_cached_path(
877            &self,
878            src: IsdAsn,
879            _dst: IsdAsn,
880            _now: SystemTime,
881        ) -> io::Result<Option<ScionPath>> {
882            Ok(Some(
883                ScionPath::local(src).expect("src is not a wildcard IA"),
884            ))
885        }
886    }
887
888    impl PathManager for ImmediatePathManager {
889        fn path_wait(
890            &self,
891            src: IsdAsn,
892            _dst: IsdAsn,
893            _now: SystemTime,
894        ) -> impl std::future::Future<Output = Result<ScionPath, PathWaitError>> + Send + '_
895        {
896            async move { Ok(ScionPath::local(src).expect("src is not a wildcard IA")) }
897        }
898    }
899
900    const LOCAL_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(1));
901    const REMOTE_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(2));
902    const OTHER_ISD_ASN: IsdAsn = IsdAsn::new(Isd(1), Asn(3));
903
904    fn local_addr() -> ScionSocketIpAddr {
905        ScionSocketIpAddr::new(LOCAL_ISD_ASN, Ipv4Addr::LOCALHOST.into(), 8080)
906    }
907
908    fn remote_addr() -> ScionSocketIpAddr {
909        ScionSocketIpAddr::new(REMOTE_ISD_ASN, Ipv4Addr::new(127, 0, 0, 2).into(), 9090)
910    }
911
912    fn other_addr() -> ScionSocketIpAddr {
913        ScionSocketIpAddr::new(OTHER_ISD_ASN, Ipv4Addr::new(127, 0, 0, 3).into(), 7070)
914    }
915
916    /// Build a [`TestPathContext`] carrying a path from `src` to `dst`.
917    fn test_path_ctx(src: ScionAddr, dst: ScionAddr) -> TestPathContext {
918        TestPathBuilder::new(src, dst)
919            .using_info_timestamp(1_000_000)
920            .up()
921            .add_hop(0, 1)
922            .add_hop(1, 0)
923            .build(1_000_000)
924    }
925
926    /// Create a valid [`ScionPacketRaw`] that looks like a UDP packet from `src` to `dst`
927    /// with `payload`.
928    fn make_udp_raw(
929        src: ScionSocketIpAddr,
930        dst: ScionSocketIpAddr,
931        payload: &[u8],
932    ) -> Box<ScionRawPacketView> {
933        let ctx = test_path_ctx(src.scion_addr(), dst.scion_addr());
934        ctx.scion_packet_udp(payload, src.port(), dst.port())
935            .try_encode_to_owned_view()
936            .expect("should encode")
937            .into()
938    }
939
940    /// Build a connected [`UdpScionSocket`] backed by the test doubles.
941    /// Returns the socket, the packet injector, and the path manager.
942    fn build_socket() -> (
943        UdpScionSocket<ImmediatePathManager>,
944        tokio::sync::mpsc::Sender<Box<ScionRawPacketView>>,
945        Arc<ImmediatePathManager>,
946    ) {
947        let (underlay, inject_tx) = ManualUnderlaySocket::new();
948        let pather = Arc::new(ImmediatePathManager::default());
949        let path_unaware = PathUnawareUdpScionSocket::new(
950            BoundUnderlaySocket {
951                socket: Box::new(underlay),
952                local_addr: local_addr(),
953                snap_data_plane: None,
954            },
955            vec![], // no SCMP handlers needed
956        );
957        let socket = UdpScionSocket::new(
958            path_unaware,
959            pather.clone(),
960            std::time::Duration::from_secs(5),
961            Subscribers::new(),
962        );
963        (socket, inject_tx, pather)
964    }
965
966    // ─── Tests ─────────────────────────────────────────────────────────────────
967
968    /// Dropping a [`recv_from_with_path`] future while it is pending (waiting in the channel)
969    /// must not consume the packet. The next call must receive that packet.
970    ///
971    /// This verifies that the underlay's `recv` future is cancel-safe: the message stays in the
972    /// channel when the outer future is dropped before returning `Ready`.
973    #[tokio::test]
974    async fn recv_from_with_path_cancel_while_pending_does_not_lose_packet() {
975        let (socket, inject_tx, _pather) = build_socket();
976
977        // Poll once — returns Pending because the channel is empty.
978        {
979            let mut buf = [0u8; 64];
980            let mut fut = std::pin::pin!(socket.recv_from_with_path(&mut buf));
981            let waker = futures::task::noop_waker();
982            let mut cx = std::task::Context::from_waker(&waker);
983            // The future must be Pending (no packet injected yet).
984            assert!(fut.as_mut().poll(&mut cx).is_pending());
985            // Drop `fut` here — the future is cancelled while pending.
986        }
987
988        // Inject the packet AFTER the first future was dropped.
989        let payload = b"cancel-safe";
990        inject_tx
991            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
992            .unwrap();
993
994        // The packet must be available to the next future.
995        let mut buf2 = vec![0u8; 64];
996        let (len, sender, _path) = socket.recv_from_with_path(&mut buf2).await.unwrap();
997
998        assert_eq!(len, payload.len());
999        assert_eq!(&buf2[..len], payload);
1000        assert_eq!(sender, remote_addr());
1001    }
1002
1003    /// `recv` (connected socket) correctly filters wrong-sender packets and returns
1004    /// the packet from the connected remote address.
1005    #[tokio::test]
1006    async fn recv_filters_wrong_sender_and_delivers_correct_packet() {
1007        let (mut socket, inject_tx, _pather) = build_socket();
1008        // Connect to remote_addr.
1009        socket.remote_addr = Some(remote_addr());
1010
1011        // Inject wrong-sender packet first, then correct-sender packet.
1012        inject_tx
1013            .try_send(make_udp_raw(other_addr(), local_addr(), b"wrong"))
1014            .unwrap();
1015        inject_tx
1016            .try_send(make_udp_raw(remote_addr(), local_addr(), b"correct"))
1017            .unwrap();
1018
1019        let mut buf = [0u8; 64];
1020        let len = socket.recv(&mut buf).await.unwrap();
1021        assert_eq!(&buf[..len], b"correct");
1022    }
1023
1024    /// After cancelling `recv` mid-filtering (a wrong-sender packet was consumed),
1025    /// the socket must still be usable and must deliver subsequent correct-sender packets.
1026    #[tokio::test]
1027    async fn recv_cancel_during_filtering_socket_remains_usable() {
1028        let (mut socket, inject_tx, _pather) = build_socket();
1029        socket.remote_addr = Some(remote_addr());
1030
1031        // Inject only a wrong-sender packet — `recv` will consume it and loop back
1032        // to await the next packet (Pending at that point).
1033        inject_tx
1034            .try_send(make_udp_raw(other_addr(), local_addr(), b"wrong"))
1035            .unwrap();
1036
1037        // Poll once with a noop waker: recv processes the wrong-sender packet, finds it does not
1038        // match the connected address, and loops back to yield on the inner recv (Pending).
1039        // No Tokio runtime involvement is needed here — the channel already holds the packet.
1040        {
1041            let mut filter_buf = [0u8; 64];
1042            let mut fut = std::pin::pin!(socket.recv(&mut filter_buf));
1043            let waker = futures::task::noop_waker();
1044            let mut cx = std::task::Context::from_waker(&waker);
1045            assert!(
1046                fut.as_mut().poll(&mut cx).is_pending(),
1047                "recv must be Pending after consuming wrong-sender packet"
1048            );
1049            // Drop the future here — the wrong-sender packet has been consumed and discarded.
1050        }
1051
1052        // Now inject a correct-sender packet and verify the socket is still usable.
1053        inject_tx
1054            .try_send(make_udp_raw(remote_addr(), local_addr(), b"after-cancel"))
1055            .unwrap();
1056
1057        let mut buf = [0u8; 64];
1058        let len = socket.recv(&mut buf).await.unwrap();
1059        assert_eq!(&buf[..len], b"after-cancel");
1060    }
1061
1062    /// Buffer contents are only valid after a successful `Ok` return; after a
1063    /// cancel and retry the buffer must contain the correct data from the retry.
1064    #[tokio::test]
1065    async fn recv_from_buffer_valid_only_after_ok() {
1066        let (socket, inject_tx, _pather) = build_socket();
1067
1068        // Pre-fill buffer with sentinel bytes.
1069        let mut buf = [0xFFu8; 64];
1070
1071        // Cancel while pending (no packet).
1072        {
1073            let mut fut = std::pin::pin!(socket.recv_from(&mut buf));
1074            let waker = futures::task::noop_waker();
1075            let mut cx = std::task::Context::from_waker(&waker);
1076            assert!(fut.as_mut().poll(&mut cx).is_pending());
1077        }
1078
1079        // Inject a packet with known payload.
1080        let payload = b"real-data";
1081        inject_tx
1082            .try_send(make_udp_raw(remote_addr(), local_addr(), payload))
1083            .unwrap();
1084
1085        let (len, _sender) = socket.recv_from(&mut buf).await.unwrap();
1086        assert_eq!(len, payload.len());
1087        assert_eq!(
1088            &buf[..len],
1089            payload,
1090            "buffer must contain the real payload after Ok return"
1091        );
1092    }
1093}