Skip to main content

snap_tun/client/
tunnel.rs

1// Copyright 2026 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
15use std::{
16    future::Future,
17    io,
18    net::SocketAddr,
19    pin::Pin,
20    sync::{
21        Arc, Mutex,
22        atomic::{AtomicBool, AtomicU64, Ordering},
23    },
24    time::{Duration, Instant},
25};
26
27use ana_gotatun::{
28    noise::{Tunn, TunnResult, errors::WireGuardError, rate_limiter::RateLimiter},
29    packet::{Packet, PacketBufPool, WgKind},
30    x25519::{self},
31};
32use bytes::{Bytes, BytesMut};
33use scion_sdk_utils::backoff::ExponentialBackoff;
34use tokio::{select, task::JoinHandle, time::Interval};
35use tracing::instrument;
36use zerocopy::IntoBytes as _;
37
38use super::{PACKET_BUF_POOL_SIZE, TunnelGuard};
39use crate::udp_batch::{QueuePacketError, RecvBatchError, UdpBatchReceiver, UdpBatchSender};
40
41const HANDSHAKE_RATE_LIMIT: u64 = 20;
42const RECEIVE_BATCH_SIZE: usize = 64;
43
44/// How long the driver tries to re-establish an expired session before it stops.
45const REHANDSHAKE_BUDGET: Duration = Duration::from_secs(600);
46
47/// Error when sending or receiving packets on the SNAP tunnel.
48#[derive(Debug, thiserror::Error)]
49pub enum SnapTunnelDriverError {
50    /// I/O error when sending packets on the underlay socket.
51    #[error("send i/o error: {0}")]
52    SendIoError(#[from] std::io::Error),
53    /// I/O error when receiving packets on the underlay socket.
54    #[error("receive i/o error: {0}")]
55    ReceiveIoError(std::io::Error),
56    /// Receive queue closed.
57    #[error("receive queue closed")]
58    ReceiveQueueClosed,
59    /// Connection expired.
60    #[error("connection expired")]
61    ConnectionExpired,
62    /// Error receiving a Wireguard packet.
63    /// This will never be WireGuardError::ConnectionExpired.
64    #[error("error receiving a Wireguard packet: {0:?}")]
65    WireguardError(WireGuardError),
66}
67
68impl SnapTunnelDriverError {
69    /// Returns whether the failure is transient, so that a retry may help.
70    ///
71    /// Prefer this over matching the variants: a new variant would silently fall into a caller's
72    /// wildcard arm.
73    #[must_use]
74    pub fn is_transient(&self) -> bool {
75        match self {
76            // The underlay socket could not carry the datagram, which is a condition of the local
77            // network or of the route to the data plane.
78            Self::SendIoError(_) | Self::ReceiveIoError(_) => true,
79            // The peer did not complete the handshake within the WireGuard time window, so a new
80            // handshake may still succeed.
81            Self::ConnectionExpired => true,
82            // The consumer of the tunnel is gone; there is nothing left to deliver packets to.
83            Self::ReceiveQueueClosed => false,
84            // A WireGuard failure describes either the datagram or the configuration, and only
85            // the second survives a retry. Ordering, duplication, corruption and replay are all
86            // properties of one datagram that a fresh handshake, or simply the next packet,
87            // leaves behind. A peer key that does not match, a poisoned lock, and a buffer this
88            // side sized too small answer the same way every time.
89            //
90            // Spelled out rather than reached through a wildcard: `WireGuardError` is not
91            // `#[non_exhaustive]`, so naming every variant makes an `ana-gotatun` upgrade a
92            // compile error here instead of a silent reclassification.
93            Self::WireguardError(error) => {
94                match error {
95                    WireGuardError::NoCurrentSession
96                    | WireGuardError::WrongIndex
97                    | WireGuardError::UnexpectedPacket
98                    | WireGuardError::WrongPacketType
99                    | WireGuardError::IncorrectPacketLength
100                    | WireGuardError::InvalidPacket
101                    | WireGuardError::InvalidCounter
102                    | WireGuardError::DuplicateCounter
103                    | WireGuardError::InvalidMac
104                    | WireGuardError::InvalidAeadTag
105                    | WireGuardError::InvalidTai64nTimestamp
106                    | WireGuardError::WrongTai64nTimestamp
107                    | WireGuardError::ConnectionExpired => true,
108                    WireGuardError::WrongKey
109                    | WireGuardError::LockFailed
110                    | WireGuardError::DestinationBufferTooSmall => false,
111                }
112            }
113        }
114    }
115}
116
117struct SnapTunnelDriver {
118    pub tunn: Arc<Mutex<Tunn>>,
119    pub static_private: x25519::StaticSecret,
120    pub peer_public: x25519::PublicKey,
121    pub underlay_socket: Arc<tokio::net::UdpSocket>,
122    pub dataplane_address: SocketAddr,
123    pub persistent_keepalive_seconds: Option<u16>,
124    pub update_timers_interval: Interval,
125    pub packet_sender: async_channel::Sender<BytesMut>,
126    pub local_sockaddr: Option<SocketAddr>,
127    pub pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
128    pub receiver: UdpBatchReceiver<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>,
129    pub sender: UdpBatchSender<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>,
130    /// Shared with the [`SnapTunnel`] handle, which exposes it to the application.
131    pub discarded_datagrams: Arc<AtomicU64>,
132    /// Set when the driver stops. Shared with the [`SnapTunnel`] handle.
133    pub closed: Arc<AtomicBool>,
134}
135
136impl SnapTunnelDriver {
137    fn new(
138        static_private: x25519::StaticSecret,
139        peer_public: x25519::PublicKey,
140        underlay_socket: Arc<tokio::net::UdpSocket>,
141        dataplane_address: SocketAddr,
142        persistent_keepalive_seconds: Option<u16>,
143        packet_sender: async_channel::Sender<BytesMut>,
144        pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
145    ) -> io::Result<Self> {
146        let update_timers_interval = tokio::time::interval_at(
147            tokio::time::Instant::now() + Duration::from_millis(250),
148            Duration::from_millis(250),
149        );
150        let receiver = UdpBatchReceiver::<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>::new(
151            underlay_socket.as_ref(),
152            &pool,
153        )?;
154        let sender = UdpBatchSender::<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>::new(
155            underlay_socket.as_ref(),
156        )?;
157        Ok(Self {
158            tunn: Arc::new(Mutex::new(Self::create_tunn(
159                static_private.clone(),
160                peer_public,
161                dataplane_address,
162                persistent_keepalive_seconds,
163            ))),
164            static_private,
165            peer_public,
166            underlay_socket,
167            dataplane_address,
168            persistent_keepalive_seconds,
169            update_timers_interval,
170            packet_sender,
171            local_sockaddr: None,
172            receiver,
173            sender,
174            pool,
175            discarded_datagrams: Arc::new(AtomicU64::new(0)),
176            closed: Arc::new(AtomicBool::new(false)),
177        })
178    }
179
180    /// Flushes the send queue as far as the socket allows right now.
181    ///
182    /// Back pressure needs no handling here: the datagrams stay queued and the next flush
183    /// picks them up.
184    ///
185    /// Takes the fields it needs one by one instead of `&mut self` so that it stays callable
186    /// from the receive closure, which borrows the driver field by field.
187    fn try_flush(
188        socket: &tokio::net::UdpSocket,
189        sender: &mut UdpBatchSender<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>,
190        discarded_datagrams: &AtomicU64,
191    ) {
192        let _ = sender.try_flush_best_effort(socket);
193        Self::account_discarded_datagrams(sender, discarded_datagrams);
194    }
195
196    /// Flushes the send queue, waiting for the socket to become writable.
197    async fn flush(
198        socket: &tokio::net::UdpSocket,
199        sender: &mut UdpBatchSender<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>,
200        discarded_datagrams: &AtomicU64,
201    ) -> io::Result<()> {
202        let result = sender.flush(socket).await;
203        Self::account_discarded_datagrams(sender, discarded_datagrams);
204        result
205    }
206
207    /// Publishes the datagrams the sender had to discard to the shared counter.
208    ///
209    /// Every flush goes through [`Self::try_flush`] or [`Self::flush`] so that no discard
210    /// escapes this accounting.
211    fn account_discarded_datagrams(
212        sender: &mut UdpBatchSender<RECEIVE_BATCH_SIZE, PACKET_BUF_POOL_SIZE>,
213        discarded_datagrams: &AtomicU64,
214    ) {
215        let discarded = sender.take_discarded_datagrams();
216        if discarded > 0 {
217            discarded_datagrams.fetch_add(discarded, Ordering::Relaxed);
218        }
219    }
220
221    #[instrument(name = "st-client", skip(self), fields(socket_addr= ?self.local_sockaddr))]
222    async fn initiate_connection(&mut self) -> Result<SocketAddr, SnapTunnelDriverError> {
223        let handshake_init = self.tunn.lock().unwrap().format_handshake_initiation(false);
224        if let Some(wg_init) = handshake_init
225            && let Err(e) = self
226                .underlay_socket
227                .send_to(
228                    to_bytes(WgKind::HandshakeInit(wg_init)).as_bytes(),
229                    self.dataplane_address,
230                )
231                .await
232        {
233            return Err(SnapTunnelDriverError::SendIoError(e));
234        }
235        // Drive the tunnel until any error occurs or the handshake is completed.
236        loop {
237            self.drive_once().await?;
238            if let Some(sockaddr) = self.tunn.lock().unwrap().get_initiator_remote_sockaddr() {
239                if self.local_sockaddr.is_none() {
240                    self.local_sockaddr = Some(sockaddr);
241                }
242                tracing::debug!(local_addr=?sockaddr, "handshake completed, local address assigned");
243                return Ok(sockaddr);
244            }
245        }
246    }
247
248    /// Drives the tunnel until it stops, then marks the tunnel closed.
249    #[instrument(name = "st-client", skip(self), fields(socket_addr= ?self.local_sockaddr))]
250    async fn main_loop(mut self) {
251        self.run().await;
252        self.closed.store(true, Ordering::Release);
253        tracing::info!("snap tunnel driver stopped");
254    }
255
256    /// Drives the tunnel. Returns when the consumer of the receive queue is gone or when an
257    /// expired session cannot be re-established on this socket.
258    async fn run(&mut self) {
259        let local_sockaddr = self
260            .local_sockaddr
261            .expect("local address must be set before main_loop()");
262        loop {
263            match self.drive_once().await {
264                Err(SnapTunnelDriverError::ReceiveQueueClosed) => {
265                    tracing::info!("receive queue closed, snap tunnel driver shutting down");
266                    return;
267                }
268                Err(SnapTunnelDriverError::ConnectionExpired) => {
269                    if !self.rehandshake(local_sockaddr).await {
270                        return;
271                    }
272                }
273                Err(ref e) => tracing::error!(err=?e, "error driving tunnel"),
274                _ => {}
275            }
276        }
277    }
278
279    /// Re-establishes an expired session with a fresh handshake.
280    ///
281    /// Returns `true` when the session is back on the same local address. Returns `false` when the
282    /// driver must stop: the data plane assigned a different address, the failure is permanent, or
283    /// [`REHANDSHAKE_BUDGET`] is spent.
284    async fn rehandshake(&mut self, local_sockaddr: SocketAddr) -> bool {
285        let started = Instant::now();
286        let mut backoff = BackoffState::new();
287        loop {
288            *self.tunn.lock().expect("poison") = Self::create_tunn(
289                self.static_private.clone(),
290                self.peer_public,
291                self.dataplane_address,
292                self.persistent_keepalive_seconds,
293            );
294            let result = self.initiate_connection().await;
295            match rehandshake_outcome(local_sockaddr, &result, started.elapsed()) {
296                Recovery::Recovered => return true,
297                Recovery::Retry => {
298                    tracing::warn!(?result, "re-handshake failed, retrying");
299                }
300                Recovery::Stop(reason) => {
301                    tracing::error!(
302                        ?reason,
303                        ?result,
304                        "cannot re-establish snap tunnel, stopping driver"
305                    );
306                    return false;
307                }
308            }
309            backoff.backoff().await;
310        }
311    }
312
313    /// Drives the tunnel once. Returns Ok(()) if no error occured in the drive, otherwise returns
314    /// the error. This method is called periodically by the main loop to update the timers and
315    /// receive packets.
316    async fn drive_once(&mut self) -> Result<(), SnapTunnelDriverError> {
317        select! {
318            // bias to ensure that high receive load cannot starve the timer
319            biased;
320            _ = self.update_timers_interval.tick() => {
321                let p = match self.tunn.lock().unwrap().update_timers() {
322                    Ok(Some(wg)) => { Some(wg) },
323                    Ok(None) => None,
324                    Err(WireGuardError::ConnectionExpired) => {
325                        return Err(SnapTunnelDriverError::ConnectionExpired);
326                    }
327                    Err(e) => {
328                        // At the time of writing, update_timers does not return any error
329                        // other than ConnectionExpired.
330                        tracing::error!(err=?e, "unexpected error updating timers on tunnel");
331                        None
332                    }
333                };
334                if let Some(wg) = p && let Err(e) = self.underlay_socket.send_to(to_bytes(wg).as_bytes(), self.dataplane_address).await {
335                    return Err(SnapTunnelDriverError::SendIoError(e));
336                }
337            },
338            recv = self.receiver.recv_batch(&self.underlay_socket, &self.pool, |buf, sender_addr| {
339                if sender_addr != self.dataplane_address {
340                    return Ok(());
341                }
342                let Ok(wg) = buf.try_into_wg() else {
343                    tracing::debug!("received packet that is not a valid WireGuard packet, ignoring");
344                    return Ok(());
345                };
346                let result = self.tunn.lock().unwrap().handle_incoming_packet(wg);
347                match result {
348                    TunnResult::Done => {}
349                    TunnResult::Err(e) => {
350                        let error = SnapTunnelDriverError::WireguardError(e);
351                        // Drop only this datagram. An error stops the rest of the batch.
352                        if error.is_transient() {
353                            tracing::debug!(err=?error, "dropping invalid WireGuard packet");
354                            return Ok(());
355                        }
356                        return Err(error);
357                    }
358                    TunnResult::WriteToNetwork(p) => {
359                        if let Err(error) = self
360                            .sender
361                            .try_queue_packet(to_bytes(p), self.dataplane_address)
362                        {
363                            match error {
364                                QueuePacketError::Full { packet, target } => {
365                                    Self::try_flush(
366                                        &self.underlay_socket,
367                                        &mut self.sender,
368                                        &self.discarded_datagrams,
369                                    );
370                                    if self.sender.try_queue_packet(packet, target).is_err() {
371                                        tracing::debug!(?target, "dropping outbound packet because batched sender remains full");
372                                    }
373                                }
374                                QueuePacketError::PacketTooLarge {
375                                    packet_len,
376                                    max_packet_size,
377                                    ..
378                                } => {
379                                    return Err(SnapTunnelDriverError::SendIoError(io::Error::new(
380                                        io::ErrorKind::InvalidInput,
381                                        format!(
382                                            "outbound packet length {packet_len} exceeds batched sender max of {max_packet_size}"
383                                        ),
384                                    )));
385                                }
386                            }
387                        }
388                        for queued in self.tunn.lock().unwrap().get_queued_packets() {
389                            if let Err(error) = self
390                                .sender
391                                .try_queue_packet(to_bytes(queued), self.dataplane_address)
392                            {
393                                match error {
394                                    QueuePacketError::Full { packet, target } => {
395                                        Self::try_flush(
396                                            &self.underlay_socket,
397                                            &mut self.sender,
398                                            &self.discarded_datagrams,
399                                        );
400                                        if self.sender.try_queue_packet(packet, target).is_err() {
401                                            tracing::debug!(?target, "dropping queued outbound packet because batched sender remains full");
402                                        }
403                                    }
404                                    QueuePacketError::PacketTooLarge {
405                                        packet_len,
406                                        max_packet_size,
407                                        ..
408                                    } => {
409                                        return Err(SnapTunnelDriverError::SendIoError(io::Error::new(
410                                            io::ErrorKind::InvalidInput,
411                                            format!(
412                                                "queued outbound packet length {packet_len} exceeds batched sender max of {max_packet_size}"
413                                            ),
414                                        )));
415                                    }
416                                }
417                            }
418                        }
419                    }
420                    TunnResult::WriteToTunnel(mut p) => {
421                        let buf = p.buf_mut().to_owned();
422                        if !buf.is_empty() {
423                            match self.packet_sender.try_send(buf) {
424                                Ok(()) => {}
425                                Err(async_channel::TrySendError::Full(_)) => {
426                                    tracing::debug!("receive channel is full, dropping packet");
427                                }
428                                Err(_) => {
429                                    return Err(SnapTunnelDriverError::ReceiveQueueClosed);
430                                }
431                            }
432                        }
433                    }
434                }
435                Ok(())
436            }) => {
437                match recv {
438                    Ok(()) => {
439                        Self::flush(
440                            &self.underlay_socket,
441                            &mut self.sender,
442                            &self.discarded_datagrams,
443                        )
444                        .await?;
445                    }
446                    Err(RecvBatchError::Io(e)) => {
447                        return Err(SnapTunnelDriverError::ReceiveIoError(e));
448                    }
449                    Err(RecvBatchError::Handler(e)) => {
450                        return Err(e);
451                    }
452                }
453            }
454        }
455        Ok(())
456    }
457
458    fn create_tunn(
459        static_private: x25519::StaticSecret,
460        peer_public: x25519::PublicKey,
461        dataplane_address: SocketAddr,
462        persistent_keepalive_seconds: Option<u16>,
463    ) -> Tunn {
464        let local_public = x25519::PublicKey::from(&static_private);
465        Tunn::new(
466            static_private,
467            peer_public,
468            None,
469            persistent_keepalive_seconds,
470            0,
471            Arc::new(RateLimiter::new(&local_public, HANDSHAKE_RATE_LIMIT)),
472            dataplane_address,
473        )
474    }
475}
476
477/// What the driver does after one re-handshake attempt.
478#[derive(Debug, PartialEq, Eq)]
479enum Recovery {
480    /// The session is back on the same local address.
481    Recovered,
482    /// Try again after a backoff.
483    Retry,
484    /// Stop the driver.
485    Stop(StopReason),
486}
487
488/// Why the driver stops instead of trying the re-handshake again.
489#[derive(Debug, PartialEq, Eq)]
490enum StopReason {
491    /// The data plane assigned a different local address. The data plane derives the address from
492    /// the source address it observes, so this socket now speaks for a different SCION address
493    /// than the one the application was given.
494    AddressChanged { new_addr: SocketAddr },
495    /// The failure answers the same way on every attempt.
496    PermanentError,
497    /// Transient failures lasted longer than [`REHANDSHAKE_BUDGET`].
498    BudgetExhausted,
499}
500
501/// Decides how the driver continues after a re-handshake attempt that took `elapsed` since the
502/// session expired.
503fn rehandshake_outcome(
504    expected_addr: SocketAddr,
505    result: &Result<SocketAddr, SnapTunnelDriverError>,
506    elapsed: Duration,
507) -> Recovery {
508    match result {
509        Ok(addr) if *addr == expected_addr => Recovery::Recovered,
510        Ok(addr) => Recovery::Stop(StopReason::AddressChanged { new_addr: *addr }),
511        Err(e) if !e.is_transient() => Recovery::Stop(StopReason::PermanentError),
512        Err(_) if elapsed >= REHANDSHAKE_BUDGET => Recovery::Stop(StopReason::BudgetExhausted),
513        Err(_) => Recovery::Retry,
514    }
515}
516
517/// Error when receiving a packet from the SNAP tunnel connection.
518#[derive(Debug, thiserror::Error)]
519pub enum SnapTunnelReceiveError {
520    /// The receive queue is closed because the tunnel is closed. See [`SnapTunnel::is_closed`].
521    #[error("receive queue closed")]
522    ReceiveQueueClosed,
523}
524
525type RecvFuture = Pin<Box<dyn Future<Output = Result<BytesMut, async_channel::RecvError>> + Send>>;
526
527/// A SNAP tunnel connection.
528///
529/// A background driver task runs the WireGuard session. Once the driver stops the tunnel is closed
530/// for good: receiving fails with [`SnapTunnelReceiveError::ReceiveQueueClosed`] and sending fails
531/// with an `io::ErrorKind::ConnectionReset` error. See [`SnapTunnel::is_closed`] for when that
532/// happens.
533pub struct SnapTunnel {
534    _guard: TunnelGuard,
535    tunn: Arc<Mutex<Tunn>>,
536    underlay_socket: Arc<tokio::net::UdpSocket>,
537    dataplane_address: SocketAddr,
538    local_sockaddr: SocketAddr,
539    receive_queue: async_channel::Receiver<BytesMut>,
540    /// Stored receive future for poll_recv. Protected by Mutex for interior mutability.
541    recv_future: Mutex<Option<RecvFuture>>,
542    /// Tasks that drives the SNAP tunnel.
543    /// Cancelled when the socket is dropped.
544    driver_task: JoinHandle<()>,
545    discarded_datagrams: Arc<AtomicU64>,
546    closed: Arc<AtomicBool>,
547}
548
549impl Drop for SnapTunnel {
550    fn drop(&mut self) {
551        self.driver_task.abort();
552    }
553}
554
555impl SnapTunnel {
556    /// Creates a new SNAP tunnel and waits for the handshake to complete.
557    ///
558    /// # Arguments
559    ///
560    /// * `static_private` - The client's static private key
561    /// * `peer_public` - The server's static public key (needed for handshake)
562    /// * `rate_limiter` - Rate limiter for the tunnel
563    /// * `underlay_socket` - UDP socket for sending/receiving packets
564    /// * `dataplane_address` - Address of the remote server
565    /// * `receive_queue_capacity` - Capacity of the receive queue
566    pub(super) async fn new(
567        guard: TunnelGuard,
568        static_private: x25519::StaticSecret,
569        peer_public: x25519::PublicKey,
570        underlay_socket: Arc<tokio::net::UdpSocket>,
571        dataplane_address: SocketAddr,
572        receive_queue_capacity: usize,
573        persistent_keepalive_seconds: Option<u16>,
574        pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
575    ) -> Result<Self, SnapTunnelDriverError> {
576        let (packet_sender, packet_receiver) = async_channel::bounded(receive_queue_capacity);
577        let mut driver = SnapTunnelDriver::new(
578            static_private,
579            peer_public,
580            underlay_socket.clone(),
581            dataplane_address,
582            persistent_keepalive_seconds,
583            packet_sender,
584            pool.clone(),
585        )?;
586        let socket_addr = driver.initiate_connection().await?;
587        Ok(Self {
588            _guard: guard,
589            tunn: driver.tunn.clone(),
590            discarded_datagrams: driver.discarded_datagrams.clone(),
591            closed: driver.closed.clone(),
592            underlay_socket,
593            dataplane_address,
594            local_sockaddr: socket_addr,
595            receive_queue: packet_receiver,
596            recv_future: Mutex::new(None),
597            driver_task: tokio::spawn(driver.main_loop()),
598        })
599    }
600
601    /// Send a packet to the remote server.
602    ///
603    /// While a handshake is in progress the packet is queued and sent when the handshake
604    /// completes. Fails with `io::ErrorKind::ConnectionReset` once the tunnel is closed.
605    #[instrument(name = "st-client", skip_all, fields(socket_addr= ?self.local_sockaddr, payload_len= packet.len()))]
606    pub async fn send(&self, packet: Packet) -> io::Result<()> {
607        if self.is_closed() {
608            return Err(closed_error());
609        }
610        let encapsulated_packet = self.tunn.lock().unwrap().handle_outgoing_packet(packet);
611        match encapsulated_packet {
612            Some(wg) => {
613                let bytes = match wg {
614                    WgKind::HandshakeInit(p) => p.into_bytes(),
615                    WgKind::HandshakeResp(p) => p.into_bytes(),
616                    WgKind::CookieReply(p) => p.into_bytes(),
617                    WgKind::Data(p) => p.into_bytes(),
618                };
619                tracing::trace!(dataplane_address=?self.dataplane_address, "sending packet");
620                self.underlay_socket
621                    .send_to(bytes.as_bytes(), self.dataplane_address)
622                    .await?;
623                Ok(())
624            }
625            None => {
626                // None is returned if a handshake is ongoing but not yet complete.
627                // In this case the packet is queued and will be sent when the handshake is
628                // complete.
629                tracing::trace!("handshake ongoing, queueing packet");
630                Ok(())
631            }
632        }
633    }
634
635    /// Try to send a packet to the remote server. Returns error of try_send_to.
636    ///
637    /// Fails with `io::ErrorKind::ConnectionReset` once the tunnel is closed.
638    #[instrument(name = "st-client", skip_all, fields(socket_addr= ?self.local_sockaddr, payload_len= packet.len()))]
639    pub fn try_send(&self, packet: Packet) -> io::Result<()> {
640        if self.is_closed() {
641            return Err(closed_error());
642        }
643        match self.tunn.lock().unwrap().handle_outgoing_packet(packet) {
644            Some(wg) => {
645                let bytes = match wg {
646                    WgKind::HandshakeInit(p) => p.into_bytes(),
647                    WgKind::HandshakeResp(p) => p.into_bytes(),
648                    WgKind::CookieReply(p) => p.into_bytes(),
649                    WgKind::Data(p) => p.into_bytes(),
650                };
651                tracing::trace!(dataplane_address=?self.dataplane_address, "trying to send packet");
652                self.underlay_socket
653                    .try_send_to(bytes.as_bytes(), self.dataplane_address)?;
654                Ok(())
655            }
656            None => {
657                // None is returned if a handshake is ongoing but not yet complete.
658                // In this case the packet is queued and will be sent when the handshake is
659                // complete.
660                Ok(())
661            }
662        }
663    }
664
665    /// Receive a packet from the remote server.
666    ///
667    /// Fails with [`SnapTunnelReceiveError::ReceiveQueueClosed`] once the tunnel is closed.
668    pub async fn recv(&self) -> Result<Bytes, SnapTunnelReceiveError> {
669        match self.receive_queue.recv().await {
670            Ok(packet) => Ok(packet.into()),
671            Err(_) => Err(SnapTunnelReceiveError::ReceiveQueueClosed),
672        }
673    }
674
675    /// Try to receive a packet from the remote server without blocking.
676    ///
677    /// Returns `Ok(None)` if no packet is currently available.
678    pub fn try_recv(&self) -> Result<Option<Bytes>, SnapTunnelReceiveError> {
679        match self.receive_queue.try_recv() {
680            Ok(packet) => Ok(Some(packet.into())),
681            Err(async_channel::TryRecvError::Empty) => Ok(None),
682            Err(async_channel::TryRecvError::Closed) => {
683                Err(SnapTunnelReceiveError::ReceiveQueueClosed)
684            }
685        }
686    }
687
688    /// Poll for a packet from the remote server.
689    pub fn poll_recv(
690        &self,
691        cx: &mut std::task::Context<'_>,
692    ) -> std::task::Poll<Result<Bytes, SnapTunnelReceiveError>> {
693        let mut fut_guard = self.recv_future.lock().expect("lock poisoned");
694
695        // Create future if it doesn't exist
696        if fut_guard.is_none() {
697            // Clone the receiver (cheap with async-channel) to avoid borrowing self
698            let receiver = self.receive_queue.clone();
699            *fut_guard = Some(Box::pin(async move { receiver.recv().await }));
700        }
701
702        // Poll the stored future
703        let fut = fut_guard.as_mut().expect("future cannot be none");
704        match fut.as_mut().poll(cx) {
705            std::task::Poll::Ready(Ok(packet)) => {
706                // Clear the future so a new one is created on next poll
707                *fut_guard = None;
708                std::task::Poll::Ready(Ok(packet.into()))
709            }
710            std::task::Poll::Ready(Err(_)) => {
711                tracing::trace!("receive queue closed, returning error");
712                *fut_guard = None;
713                std::task::Poll::Ready(Err(SnapTunnelReceiveError::ReceiveQueueClosed))
714            }
715            std::task::Poll::Pending => std::task::Poll::Pending,
716        }
717    }
718
719    /// Get the local socket address. Assigned by the remote server.
720    pub fn local_addr(&self) -> SocketAddr {
721        self.local_sockaddr
722    }
723
724    /// Whether the tunnel is closed. A closed tunnel delivers nothing in either direction.
725    ///
726    /// The tunnel closes when the driver stops. The driver stops when the consumer of the receive
727    /// queue is gone, when an expired session cannot be re-established within a fixed budget, when
728    /// a re-handshake fails permanently, or when a re-handshake assigns a different local address.
729    /// The last case happens after the local network changed, because the data plane derives the
730    /// address from the source address it observes. An application that wants to continue then
731    /// connects a new tunnel; see the rules on [`super::SnapTunEndpoint`].
732    pub fn is_closed(&self) -> bool {
733        self.closed.load(Ordering::Acquire)
734    }
735
736    /// Check if the socket is writable.
737    pub async fn writable(&self) -> io::Result<()> {
738        self.underlay_socket.writable().await
739    }
740
741    /// The data plane the tunnel is connected to.
742    pub fn data_plane_address(&self) -> SocketAddr {
743        self.dataplane_address
744    }
745
746    /// Total number of outbound datagrams the underlay socket refused since the tunnel was
747    /// created.
748    ///
749    /// A refused datagram is dropped rather than retried, because retrying one the socket will
750    /// never accept blocks every packet queued behind it. The tunnel therefore stays up when
751    /// sending fails persistently, for example because the interface went down or a firewall
752    /// answers `EPERM`, and this counter is what tells such a tunnel apart from a healthy one.
753    pub fn discarded_datagrams(&self) -> u64 {
754        self.discarded_datagrams.load(Ordering::Relaxed)
755    }
756}
757
758struct BackoffState {
759    last: Instant,
760    exp_backoff: ExponentialBackoff,
761    attempt: usize,
762}
763
764impl BackoffState {
765    fn new() -> Self {
766        Self {
767            last: Instant::now(),
768            exp_backoff: ExponentialBackoff::new(
769                5.0, 180.0, // max 3 mins
770                1.3, 0.5,
771            ),
772            attempt: 0,
773        }
774    }
775
776    /// The delay before the next attempt or `None` when the next attempt is already due.
777    fn next_delay(&mut self) -> Option<Duration> {
778        let now = Instant::now();
779        let until_next = (self.last + self.exp_backoff.duration(self.attempt as u32))
780            .checked_duration_since(now);
781        self.attempt += 1;
782        self.last = now;
783        until_next
784    }
785
786    fn backoff(&mut self) -> impl Future<Output = ()> {
787        let until_next = self.next_delay();
788        async move {
789            if let Some(d) = until_next {
790                tokio::time::sleep(d).await;
791            }
792        }
793    }
794}
795
796fn closed_error() -> io::Error {
797    io::Error::new(io::ErrorKind::ConnectionReset, "SNAP tunnel closed")
798}
799
800fn to_bytes(wg: WgKind) -> Packet<[u8]> {
801    match wg {
802        WgKind::HandshakeInit(p) => p.into_bytes(),
803        WgKind::HandshakeResp(p) => p.into_bytes(),
804        WgKind::CookieReply(p) => p.into_bytes(),
805        WgKind::Data(p) => p.into_bytes(),
806    }
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    #[test]
814    fn driver_error_transient_classification() {
815        // The socket or the peer did not carry the handshake through, so a retry may.
816        assert!(SnapTunnelDriverError::SendIoError(io::Error::other("boom")).is_transient());
817        assert!(SnapTunnelDriverError::ReceiveIoError(io::Error::other("boom")).is_transient());
818        assert!(SnapTunnelDriverError::ConnectionExpired.is_transient());
819        // Handshake timing and packet ordering: a datagram that arrived late, twice, or for a
820        // session that has since rotated. None of them says anything about the next attempt.
821        for error in [
822            WireGuardError::NoCurrentSession,
823            WireGuardError::WrongIndex,
824            WireGuardError::UnexpectedPacket,
825            WireGuardError::InvalidCounter,
826            WireGuardError::DuplicateCounter,
827        ] {
828            assert!(
829                SnapTunnelDriverError::WireguardError(error).is_transient(),
830                "a handshake-timing condition was reported as permanent"
831            );
832        }
833        // A peer key that does not match, and a gone consumer, answer the same way on every
834        // attempt.
835        assert!(!SnapTunnelDriverError::WireguardError(WireGuardError::WrongKey).is_transient());
836        assert!(!SnapTunnelDriverError::ReceiveQueueClosed.is_transient());
837    }
838
839    fn addr(s: &str) -> SocketAddr {
840        s.parse().unwrap()
841    }
842
843    #[test]
844    fn rehandshake_recovers_on_the_same_address() {
845        let expected = addr("192.0.2.1:4000");
846        assert_eq!(
847            rehandshake_outcome(expected, &Ok(expected), Duration::ZERO),
848            Recovery::Recovered
849        );
850    }
851
852    #[test]
853    fn rehandshake_stops_when_the_address_changed() {
854        let expected = addr("192.0.2.1:4000");
855        let new_addr = addr("198.51.100.7:4000");
856        assert_eq!(
857            rehandshake_outcome(expected, &Ok(new_addr), Duration::ZERO),
858            Recovery::Stop(StopReason::AddressChanged { new_addr })
859        );
860    }
861
862    #[test]
863    fn rehandshake_retries_transient_errors_within_the_budget() {
864        let expected = addr("192.0.2.1:4000");
865        let expired = Err(SnapTunnelDriverError::ConnectionExpired);
866        assert_eq!(
867            rehandshake_outcome(expected, &expired, Duration::ZERO),
868            Recovery::Retry
869        );
870        assert_eq!(
871            rehandshake_outcome(
872                expected,
873                &expired,
874                REHANDSHAKE_BUDGET - Duration::from_secs(1)
875            ),
876            Recovery::Retry
877        );
878        let send_failed = Err(SnapTunnelDriverError::SendIoError(io::Error::other("down")));
879        assert_eq!(
880            rehandshake_outcome(expected, &send_failed, Duration::ZERO),
881            Recovery::Retry
882        );
883    }
884
885    #[test]
886    fn rehandshake_stops_when_the_budget_is_spent() {
887        let expected = addr("192.0.2.1:4000");
888        let expired = Err(SnapTunnelDriverError::ConnectionExpired);
889        assert_eq!(
890            rehandshake_outcome(expected, &expired, REHANDSHAKE_BUDGET),
891            Recovery::Stop(StopReason::BudgetExhausted)
892        );
893    }
894
895    #[test]
896    fn rehandshake_stops_on_a_permanent_error() {
897        let expected = addr("192.0.2.1:4000");
898        let gone = Err(SnapTunnelDriverError::ReceiveQueueClosed);
899        assert_eq!(
900            rehandshake_outcome(expected, &gone, Duration::ZERO),
901            Recovery::Stop(StopReason::PermanentError)
902        );
903    }
904
905    #[test]
906    fn backoff_delay_grows_across_attempts() {
907        // The jitter is at most 0.5 s and the base delay grows by at least 1.5 s per attempt, so
908        // consecutive delays of one state are strictly increasing.
909        let mut backoff = BackoffState::new();
910        let first = backoff.next_delay().expect("first attempt waits");
911        let second = backoff.next_delay().expect("second attempt waits");
912        let third = backoff.next_delay().expect("third attempt waits");
913        assert!(
914            first < second && second < third,
915            "{first:?} {second:?} {third:?}"
916        );
917    }
918}