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