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                        return Err(SnapTunnelDriverError::WireguardError(e));
351                    }
352                    TunnResult::WriteToNetwork(p) => {
353                        if let Err(error) = self
354                            .sender
355                            .try_queue_packet(to_bytes(p), self.dataplane_address)
356                        {
357                            match error {
358                                QueuePacketError::Full { packet, target } => {
359                                    Self::try_flush(
360                                        &self.underlay_socket,
361                                        &mut self.sender,
362                                        &self.discarded_datagrams,
363                                    );
364                                    if self.sender.try_queue_packet(packet, target).is_err() {
365                                        tracing::debug!(?target, "dropping outbound packet because batched sender remains full");
366                                    }
367                                }
368                                QueuePacketError::PacketTooLarge {
369                                    packet_len,
370                                    max_packet_size,
371                                    ..
372                                } => {
373                                    return Err(SnapTunnelDriverError::SendIoError(io::Error::new(
374                                        io::ErrorKind::InvalidInput,
375                                        format!(
376                                            "outbound packet length {packet_len} exceeds batched sender max of {max_packet_size}"
377                                        ),
378                                    )));
379                                }
380                            }
381                        }
382                        for queued in self.tunn.lock().unwrap().get_queued_packets() {
383                            if let Err(error) = self
384                                .sender
385                                .try_queue_packet(to_bytes(queued), self.dataplane_address)
386                            {
387                                match error {
388                                    QueuePacketError::Full { packet, target } => {
389                                        Self::try_flush(
390                                            &self.underlay_socket,
391                                            &mut self.sender,
392                                            &self.discarded_datagrams,
393                                        );
394                                        if self.sender.try_queue_packet(packet, target).is_err() {
395                                            tracing::debug!(?target, "dropping queued outbound packet because batched sender remains full");
396                                        }
397                                    }
398                                    QueuePacketError::PacketTooLarge {
399                                        packet_len,
400                                        max_packet_size,
401                                        ..
402                                    } => {
403                                        return Err(SnapTunnelDriverError::SendIoError(io::Error::new(
404                                            io::ErrorKind::InvalidInput,
405                                            format!(
406                                                "queued outbound packet length {packet_len} exceeds batched sender max of {max_packet_size}"
407                                            ),
408                                        )));
409                                    }
410                                }
411                            }
412                        }
413                    }
414                    TunnResult::WriteToTunnel(mut p) => {
415                        let buf = p.buf_mut().to_owned();
416                        if !buf.is_empty() {
417                            match self.packet_sender.try_send(buf) {
418                                Ok(()) => {}
419                                Err(async_channel::TrySendError::Full(_)) => {
420                                    tracing::debug!("receive channel is full, dropping packet");
421                                }
422                                Err(_) => {
423                                    return Err(SnapTunnelDriverError::ReceiveQueueClosed);
424                                }
425                            }
426                        }
427                    }
428                }
429                Ok(())
430            }) => {
431                match recv {
432                    Ok(()) => {
433                        Self::flush(
434                            &self.underlay_socket,
435                            &mut self.sender,
436                            &self.discarded_datagrams,
437                        )
438                        .await?;
439                    }
440                    Err(RecvBatchError::Io(e)) => {
441                        return Err(SnapTunnelDriverError::ReceiveIoError(e));
442                    }
443                    Err(RecvBatchError::Handler(e)) => {
444                        return Err(e);
445                    }
446                }
447            }
448        }
449        Ok(())
450    }
451
452    fn create_tunn(
453        static_private: x25519::StaticSecret,
454        peer_public: x25519::PublicKey,
455        dataplane_address: SocketAddr,
456        persistent_keepalive_seconds: Option<u16>,
457    ) -> Tunn {
458        let local_public = x25519::PublicKey::from(&static_private);
459        Tunn::new(
460            static_private,
461            peer_public,
462            None,
463            persistent_keepalive_seconds,
464            0,
465            Arc::new(RateLimiter::new(&local_public, HANDSHAKE_RATE_LIMIT)),
466            dataplane_address,
467        )
468    }
469}
470
471/// What the driver does after one re-handshake attempt.
472#[derive(Debug, PartialEq, Eq)]
473enum Recovery {
474    /// The session is back on the same local address.
475    Recovered,
476    /// Try again after a backoff.
477    Retry,
478    /// Stop the driver.
479    Stop(StopReason),
480}
481
482/// Why the driver stops instead of trying the re-handshake again.
483#[derive(Debug, PartialEq, Eq)]
484enum StopReason {
485    /// The data plane assigned a different local address. The data plane derives the address from
486    /// the source address it observes, so this socket now speaks for a different SCION address
487    /// than the one the application was given.
488    AddressChanged { new_addr: SocketAddr },
489    /// The failure answers the same way on every attempt.
490    PermanentError,
491    /// Transient failures lasted longer than [`REHANDSHAKE_BUDGET`].
492    BudgetExhausted,
493}
494
495/// Decides how the driver continues after a re-handshake attempt that took `elapsed` since the
496/// session expired.
497fn rehandshake_outcome(
498    expected_addr: SocketAddr,
499    result: &Result<SocketAddr, SnapTunnelDriverError>,
500    elapsed: Duration,
501) -> Recovery {
502    match result {
503        Ok(addr) if *addr == expected_addr => Recovery::Recovered,
504        Ok(addr) => Recovery::Stop(StopReason::AddressChanged { new_addr: *addr }),
505        Err(e) if !e.is_transient() => Recovery::Stop(StopReason::PermanentError),
506        Err(_) if elapsed >= REHANDSHAKE_BUDGET => Recovery::Stop(StopReason::BudgetExhausted),
507        Err(_) => Recovery::Retry,
508    }
509}
510
511/// Error when receiving a packet from the SNAP tunnel connection.
512#[derive(Debug, thiserror::Error)]
513pub enum SnapTunnelReceiveError {
514    /// The receive queue is closed because the tunnel is closed. See [`SnapTunnel::is_closed`].
515    #[error("receive queue closed")]
516    ReceiveQueueClosed,
517}
518
519type RecvFuture = Pin<Box<dyn Future<Output = Result<BytesMut, async_channel::RecvError>> + Send>>;
520
521/// A SNAP tunnel connection.
522///
523/// A background driver task runs the WireGuard session. Once the driver stops the tunnel is closed
524/// for good: receiving fails with [`SnapTunnelReceiveError::ReceiveQueueClosed`] and sending fails
525/// with an `io::ErrorKind::ConnectionReset` error. See [`SnapTunnel::is_closed`] for when that
526/// happens.
527pub struct SnapTunnel {
528    _guard: TunnelGuard,
529    tunn: Arc<Mutex<Tunn>>,
530    underlay_socket: Arc<tokio::net::UdpSocket>,
531    dataplane_address: SocketAddr,
532    local_sockaddr: SocketAddr,
533    receive_queue: async_channel::Receiver<BytesMut>,
534    /// Stored receive future for poll_recv. Protected by Mutex for interior mutability.
535    recv_future: Mutex<Option<RecvFuture>>,
536    /// Tasks that drives the SNAP tunnel.
537    /// Cancelled when the socket is dropped.
538    driver_task: JoinHandle<()>,
539    discarded_datagrams: Arc<AtomicU64>,
540    closed: Arc<AtomicBool>,
541}
542
543impl Drop for SnapTunnel {
544    fn drop(&mut self) {
545        self.driver_task.abort();
546    }
547}
548
549impl SnapTunnel {
550    /// Creates a new SNAP tunnel and waits for the handshake to complete.
551    ///
552    /// # Arguments
553    ///
554    /// * `static_private` - The client's static private key
555    /// * `peer_public` - The server's static public key (needed for handshake)
556    /// * `rate_limiter` - Rate limiter for the tunnel
557    /// * `underlay_socket` - UDP socket for sending/receiving packets
558    /// * `dataplane_address` - Address of the remote server
559    /// * `receive_queue_capacity` - Capacity of the receive queue
560    pub(super) async fn new(
561        guard: TunnelGuard,
562        static_private: x25519::StaticSecret,
563        peer_public: x25519::PublicKey,
564        underlay_socket: Arc<tokio::net::UdpSocket>,
565        dataplane_address: SocketAddr,
566        receive_queue_capacity: usize,
567        persistent_keepalive_seconds: Option<u16>,
568        pool: PacketBufPool<PACKET_BUF_POOL_SIZE>,
569    ) -> Result<Self, SnapTunnelDriverError> {
570        let (packet_sender, packet_receiver) = async_channel::bounded(receive_queue_capacity);
571        let mut driver = SnapTunnelDriver::new(
572            static_private,
573            peer_public,
574            underlay_socket.clone(),
575            dataplane_address,
576            persistent_keepalive_seconds,
577            packet_sender,
578            pool.clone(),
579        )?;
580        let socket_addr = driver.initiate_connection().await?;
581        Ok(Self {
582            _guard: guard,
583            tunn: driver.tunn.clone(),
584            discarded_datagrams: driver.discarded_datagrams.clone(),
585            closed: driver.closed.clone(),
586            underlay_socket,
587            dataplane_address,
588            local_sockaddr: socket_addr,
589            receive_queue: packet_receiver,
590            recv_future: Mutex::new(None),
591            driver_task: tokio::spawn(driver.main_loop()),
592        })
593    }
594
595    /// Send a packet to the remote server.
596    ///
597    /// While a handshake is in progress the packet is queued and sent when the handshake
598    /// completes. Fails with `io::ErrorKind::ConnectionReset` once the tunnel is closed.
599    #[instrument(name = "st-client", skip_all, fields(socket_addr= ?self.local_sockaddr, payload_len= packet.len()))]
600    pub async fn send(&self, packet: Packet) -> io::Result<()> {
601        if self.is_closed() {
602            return Err(closed_error());
603        }
604        let encapsulated_packet = self.tunn.lock().unwrap().handle_outgoing_packet(packet);
605        match encapsulated_packet {
606            Some(wg) => {
607                let bytes = match wg {
608                    WgKind::HandshakeInit(p) => p.into_bytes(),
609                    WgKind::HandshakeResp(p) => p.into_bytes(),
610                    WgKind::CookieReply(p) => p.into_bytes(),
611                    WgKind::Data(p) => p.into_bytes(),
612                };
613                tracing::trace!(dataplane_address=?self.dataplane_address, "sending packet");
614                self.underlay_socket
615                    .send_to(bytes.as_bytes(), self.dataplane_address)
616                    .await?;
617                Ok(())
618            }
619            None => {
620                // None is returned if a handshake is ongoing but not yet complete.
621                // In this case the packet is queued and will be sent when the handshake is
622                // complete.
623                tracing::trace!("handshake ongoing, queueing packet");
624                Ok(())
625            }
626        }
627    }
628
629    /// Try to send a packet to the remote server. Returns error of try_send_to.
630    ///
631    /// Fails with `io::ErrorKind::ConnectionReset` once the tunnel is closed.
632    #[instrument(name = "st-client", skip_all, fields(socket_addr= ?self.local_sockaddr, payload_len= packet.len()))]
633    pub fn try_send(&self, packet: Packet) -> io::Result<()> {
634        if self.is_closed() {
635            return Err(closed_error());
636        }
637        match self.tunn.lock().unwrap().handle_outgoing_packet(packet) {
638            Some(wg) => {
639                let bytes = match wg {
640                    WgKind::HandshakeInit(p) => p.into_bytes(),
641                    WgKind::HandshakeResp(p) => p.into_bytes(),
642                    WgKind::CookieReply(p) => p.into_bytes(),
643                    WgKind::Data(p) => p.into_bytes(),
644                };
645                tracing::trace!(dataplane_address=?self.dataplane_address, "trying to send packet");
646                self.underlay_socket
647                    .try_send_to(bytes.as_bytes(), self.dataplane_address)?;
648                Ok(())
649            }
650            None => {
651                // None is returned if a handshake is ongoing but not yet complete.
652                // In this case the packet is queued and will be sent when the handshake is
653                // complete.
654                Ok(())
655            }
656        }
657    }
658
659    /// Receive a packet from the remote server.
660    ///
661    /// Fails with [`SnapTunnelReceiveError::ReceiveQueueClosed`] once the tunnel is closed.
662    pub async fn recv(&self) -> Result<Bytes, SnapTunnelReceiveError> {
663        match self.receive_queue.recv().await {
664            Ok(packet) => Ok(packet.into()),
665            Err(_) => Err(SnapTunnelReceiveError::ReceiveQueueClosed),
666        }
667    }
668
669    /// Try to receive a packet from the remote server without blocking.
670    ///
671    /// Returns `Ok(None)` if no packet is currently available.
672    pub fn try_recv(&self) -> Result<Option<Bytes>, SnapTunnelReceiveError> {
673        match self.receive_queue.try_recv() {
674            Ok(packet) => Ok(Some(packet.into())),
675            Err(async_channel::TryRecvError::Empty) => Ok(None),
676            Err(async_channel::TryRecvError::Closed) => {
677                Err(SnapTunnelReceiveError::ReceiveQueueClosed)
678            }
679        }
680    }
681
682    /// Poll for a packet from the remote server.
683    pub fn poll_recv(
684        &self,
685        cx: &mut std::task::Context<'_>,
686    ) -> std::task::Poll<Result<Bytes, SnapTunnelReceiveError>> {
687        let mut fut_guard = self.recv_future.lock().expect("lock poisoned");
688
689        // Create future if it doesn't exist
690        if fut_guard.is_none() {
691            // Clone the receiver (cheap with async-channel) to avoid borrowing self
692            let receiver = self.receive_queue.clone();
693            *fut_guard = Some(Box::pin(async move { receiver.recv().await }));
694        }
695
696        // Poll the stored future
697        let fut = fut_guard.as_mut().expect("future cannot be none");
698        match fut.as_mut().poll(cx) {
699            std::task::Poll::Ready(Ok(packet)) => {
700                // Clear the future so a new one is created on next poll
701                *fut_guard = None;
702                std::task::Poll::Ready(Ok(packet.into()))
703            }
704            std::task::Poll::Ready(Err(_)) => {
705                tracing::trace!("receive queue closed, returning error");
706                *fut_guard = None;
707                std::task::Poll::Ready(Err(SnapTunnelReceiveError::ReceiveQueueClosed))
708            }
709            std::task::Poll::Pending => std::task::Poll::Pending,
710        }
711    }
712
713    /// Get the local socket address. Assigned by the remote server.
714    pub fn local_addr(&self) -> SocketAddr {
715        self.local_sockaddr
716    }
717
718    /// Whether the tunnel is closed. A closed tunnel delivers nothing in either direction.
719    ///
720    /// The tunnel closes when the driver stops. The driver stops when the consumer of the receive
721    /// queue is gone, when an expired session cannot be re-established within a fixed budget, when
722    /// a re-handshake fails permanently, or when a re-handshake assigns a different local address.
723    /// The last case happens after the local network changed, because the data plane derives the
724    /// address from the source address it observes. An application that wants to continue then
725    /// connects a new tunnel; see the rules on [`super::SnapTunEndpoint`].
726    pub fn is_closed(&self) -> bool {
727        self.closed.load(Ordering::Acquire)
728    }
729
730    /// Check if the socket is writable.
731    pub async fn writable(&self) -> io::Result<()> {
732        self.underlay_socket.writable().await
733    }
734
735    /// The data plane the tunnel is connected to.
736    pub fn data_plane_address(&self) -> SocketAddr {
737        self.dataplane_address
738    }
739
740    /// Total number of outbound datagrams the underlay socket refused since the tunnel was
741    /// created.
742    ///
743    /// A refused datagram is dropped rather than retried, because retrying one the socket will
744    /// never accept blocks every packet queued behind it. The tunnel therefore stays up when
745    /// sending fails persistently, for example because the interface went down or a firewall
746    /// answers `EPERM`, and this counter is what tells such a tunnel apart from a healthy one.
747    pub fn discarded_datagrams(&self) -> u64 {
748        self.discarded_datagrams.load(Ordering::Relaxed)
749    }
750}
751
752struct BackoffState {
753    last: Instant,
754    exp_backoff: ExponentialBackoff,
755    attempt: usize,
756}
757
758impl BackoffState {
759    fn new() -> Self {
760        Self {
761            last: Instant::now(),
762            exp_backoff: ExponentialBackoff::new(
763                5.0, 180.0, // max 3 mins
764                1.3, 0.5,
765            ),
766            attempt: 0,
767        }
768    }
769
770    /// The delay before the next attempt or `None` when the next attempt is already due.
771    fn next_delay(&mut self) -> Option<Duration> {
772        let now = Instant::now();
773        let until_next = (self.last + self.exp_backoff.duration(self.attempt as u32))
774            .checked_duration_since(now);
775        self.attempt += 1;
776        self.last = now;
777        until_next
778    }
779
780    fn backoff(&mut self) -> impl Future<Output = ()> {
781        let until_next = self.next_delay();
782        async move {
783            if let Some(d) = until_next {
784                tokio::time::sleep(d).await;
785            }
786        }
787    }
788}
789
790fn closed_error() -> io::Error {
791    io::Error::new(io::ErrorKind::ConnectionReset, "SNAP tunnel closed")
792}
793
794fn to_bytes(wg: WgKind) -> Packet<[u8]> {
795    match wg {
796        WgKind::HandshakeInit(p) => p.into_bytes(),
797        WgKind::HandshakeResp(p) => p.into_bytes(),
798        WgKind::CookieReply(p) => p.into_bytes(),
799        WgKind::Data(p) => p.into_bytes(),
800    }
801}
802
803#[cfg(test)]
804mod tests {
805    use super::*;
806
807    #[test]
808    fn driver_error_transient_classification() {
809        // The socket or the peer did not carry the handshake through, so a retry may.
810        assert!(SnapTunnelDriverError::SendIoError(io::Error::other("boom")).is_transient());
811        assert!(SnapTunnelDriverError::ReceiveIoError(io::Error::other("boom")).is_transient());
812        assert!(SnapTunnelDriverError::ConnectionExpired.is_transient());
813        // Handshake timing and packet ordering: a datagram that arrived late, twice, or for a
814        // session that has since rotated. None of them says anything about the next attempt.
815        for error in [
816            WireGuardError::NoCurrentSession,
817            WireGuardError::WrongIndex,
818            WireGuardError::UnexpectedPacket,
819            WireGuardError::InvalidCounter,
820            WireGuardError::DuplicateCounter,
821        ] {
822            assert!(
823                SnapTunnelDriverError::WireguardError(error).is_transient(),
824                "a handshake-timing condition was reported as permanent"
825            );
826        }
827        // A peer key that does not match, and a gone consumer, answer the same way on every
828        // attempt.
829        assert!(!SnapTunnelDriverError::WireguardError(WireGuardError::WrongKey).is_transient());
830        assert!(!SnapTunnelDriverError::ReceiveQueueClosed.is_transient());
831    }
832
833    fn addr(s: &str) -> SocketAddr {
834        s.parse().unwrap()
835    }
836
837    #[test]
838    fn rehandshake_recovers_on_the_same_address() {
839        let expected = addr("192.0.2.1:4000");
840        assert_eq!(
841            rehandshake_outcome(expected, &Ok(expected), Duration::ZERO),
842            Recovery::Recovered
843        );
844    }
845
846    #[test]
847    fn rehandshake_stops_when_the_address_changed() {
848        let expected = addr("192.0.2.1:4000");
849        let new_addr = addr("198.51.100.7:4000");
850        assert_eq!(
851            rehandshake_outcome(expected, &Ok(new_addr), Duration::ZERO),
852            Recovery::Stop(StopReason::AddressChanged { new_addr })
853        );
854    }
855
856    #[test]
857    fn rehandshake_retries_transient_errors_within_the_budget() {
858        let expected = addr("192.0.2.1:4000");
859        let expired = Err(SnapTunnelDriverError::ConnectionExpired);
860        assert_eq!(
861            rehandshake_outcome(expected, &expired, Duration::ZERO),
862            Recovery::Retry
863        );
864        assert_eq!(
865            rehandshake_outcome(
866                expected,
867                &expired,
868                REHANDSHAKE_BUDGET - Duration::from_secs(1)
869            ),
870            Recovery::Retry
871        );
872        let send_failed = Err(SnapTunnelDriverError::SendIoError(io::Error::other("down")));
873        assert_eq!(
874            rehandshake_outcome(expected, &send_failed, Duration::ZERO),
875            Recovery::Retry
876        );
877    }
878
879    #[test]
880    fn rehandshake_stops_when_the_budget_is_spent() {
881        let expected = addr("192.0.2.1:4000");
882        let expired = Err(SnapTunnelDriverError::ConnectionExpired);
883        assert_eq!(
884            rehandshake_outcome(expected, &expired, REHANDSHAKE_BUDGET),
885            Recovery::Stop(StopReason::BudgetExhausted)
886        );
887    }
888
889    #[test]
890    fn rehandshake_stops_on_a_permanent_error() {
891        let expected = addr("192.0.2.1:4000");
892        let gone = Err(SnapTunnelDriverError::ReceiveQueueClosed);
893        assert_eq!(
894            rehandshake_outcome(expected, &gone, Duration::ZERO),
895            Recovery::Stop(StopReason::PermanentError)
896        );
897    }
898
899    #[test]
900    fn backoff_delay_grows_across_attempts() {
901        // The jitter is at most 0.5 s and the base delay grows by at least 1.5 s per attempt, so
902        // consecutive delays of one state are strictly increasing.
903        let mut backoff = BackoffState::new();
904        let first = backoff.next_delay().expect("first attempt waits");
905        let second = backoff.next_delay().expect("second attempt waits");
906        let third = backoff.next_delay().expect("third attempt waits");
907        assert!(
908            first < second && second < third,
909            "{first:?} {second:?} {third:?}"
910        );
911    }
912}