Skip to main content

snap_tun/
server.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//! The server of the SNAPtun protocol.
15//!
16//! As the underlying protocol is symmetric (both peers can act as
17//! initiator/responders that establish a session), technically, there is no
18//! server. The term "server" here just refers to and endpoint that manages
19//! multiple peers.
20
21use std::{
22    collections::{HashMap, VecDeque},
23    net::SocketAddr,
24    sync::Arc,
25    time::Instant,
26};
27
28use ana_gotatun::{
29    noise::{Tunn, TunnResult, handshake::parse_handshake_anon, rate_limiter::RateLimiter},
30    packet::{Packet, WgKind},
31    x25519,
32};
33
34/// The [SnapTunServer] manages one [Tunn] per remote socket address.
35///
36/// The main structural difference between WireGuard (R) and snaptun-ng is that
37/// there is a one-to-one relation between a remote socket address (of the
38/// initiator) and a tunnel. The [SnapTunServer] manages that relation.
39///
40/// ## Scaling
41///
42/// The main methods [SnapTunServer::handle_incoming_packet],
43/// [SnapTunServer::handle_outgoing_packet], and
44/// [SnapTunServer::update_timers] all require an exclusive reference to the
45/// internal state. The reason is that processing both, incoming and outgoing
46/// packets requires access to the session state.
47///
48/// One simple way to achieve load distribution across different cores/threads
49/// is to shard over multiple [SnapTunServer]-instances based on a hash of the
50/// remote socket address.
51///
52/// ## Future improvements
53///
54/// * Separate incoming and outgoing code paths and optimistically lock the session state.
55///
56/// ## How to use
57///
58/// The [SnapTunServer] is i/o-free; i.e. it only manages state. The following
59/// is a pseudo-code like description of the simplest i/o-layer integration:
60///
61/// ```text
62/// let mut server = SnapTunServer::new(/*...*/);
63/// let mut send_to_network = VecDequeue::new();
64/// let mut current_sockaddr = ;
65/// loop {
66///   switch {
67///     (network_packet, sockaddr) = network_socket => {
68///       server.handle_incoming_packet(/*...*/);
69///       /* dispatch packets to tunnel if necessary */
70///     }
71///     tunnel_packet = tunnel_socket => {
72///       server.handle_outgoing_packet(/*...*/);
73///     }
74///     timer = tick(250ms) => {
75///       server.update_timers();
76///     }
77///   }
78///   // dispatch packets to network
79///   for p in send_to_network {
80///     network_socket.send(sockaddr, p);
81///   }
82/// }
83/// ```
84pub struct SnapTunServer<T: SnapTunAuthorization> {
85    static_private: x25519::StaticSecret,
86    static_public: x25519::PublicKey,
87    active_tunnels: HashMap<SocketAddr, ActiveTunnel>,
88    rate_limiter: Arc<RateLimiter>,
89    authz: Arc<T>,
90}
91
92struct ActiveTunnel {
93    peer_static: x25519::PublicKey,
94    tunn: Tunn,
95}
96
97/// Packet-processing output for callers that also need the resolved tunnel session.
98///
99/// This is an opt-in extension of the original `TunnResult`-based API so the
100/// dataplane can reuse the resolved session without forcing existing snap-tun
101/// callers to change their control flow.
102pub enum HandleIncomingPacketResult<S> {
103    /// The result returned by the underlying WireGuard tunnel state machine
104    /// when no tunneled SCION payload was forwarded.
105    Result {
106        /// The result returned by the underlying WireGuard tunnel state
107        /// machine.
108        result: TunnResult,
109    },
110    /// A forwarded tunneled SCION payload together with the processing
111    /// metadata captured while the tunnel entry was already borrowed.
112    Forwarded {
113        /// The packet forwarded to the caller.
114        packet: Packet,
115        /// The timestamp captured after rate-limiter verification.
116        processed_at: Instant,
117        /// The active session data resolved while processing the packet.
118        session_data: Arc<S>,
119    },
120}
121
122impl<S> HandleIncomingPacketResult<S> {
123    /// Converts the result back into the underlying WireGuard tunnel result.
124    pub fn into_result(self) -> TunnResult {
125        match self {
126            HandleIncomingPacketResult::Result { result } => result,
127            HandleIncomingPacketResult::Forwarded { packet, .. } => {
128                TunnResult::WriteToTunnel(packet)
129            }
130        }
131    }
132}
133
134/// Packet-processing output for callers that need the active session once an
135/// outbound payload is accepted into the tunnel pipeline.
136pub struct HandleOutgoingPacketResult<S> {
137    /// The WireGuard packet to send to the network, if the tunnel emitted one
138    /// immediately.
139    pub network_packet: Option<WgKind>,
140    /// The timestamp captured for the authorization check that gated this
141    /// outgoing packet.
142    pub processed_at: Instant,
143    /// The session data resolved for the tunneled SCION payload.
144    pub session_data: Arc<S>,
145}
146
147impl<S> HandleOutgoingPacketResult<S> {
148    /// Converts the result into the immediate network packet, if one exists.
149    pub fn into_packet(self) -> Option<WgKind> {
150        self.network_packet
151    }
152}
153
154impl<T: SnapTunAuthorization> SnapTunServer<T> {
155    // The caller captures this timestamp after rate-limiter verification and
156    // reuses it for both authorization and forwarded-packet metadata.
157    fn incoming_packet_result(
158        result: TunnResult,
159        session_data: Arc<T::SessionData>,
160        now: Instant,
161    ) -> HandleIncomingPacketResult<T::SessionData> {
162        match result {
163            TunnResult::WriteToTunnel(packet) => {
164                HandleIncomingPacketResult::Forwarded {
165                    packet,
166                    processed_at: now,
167                    session_data,
168                }
169            }
170            result => HandleIncomingPacketResult::Result { result },
171        }
172    }
173
174    fn outgoing_packet_result(
175        network_packet: Option<WgKind>,
176        now: Instant,
177        session_data: Arc<T::SessionData>,
178    ) -> HandleOutgoingPacketResult<T::SessionData> {
179        HandleOutgoingPacketResult {
180            network_packet,
181            processed_at: now,
182            session_data,
183        }
184    }
185
186    /// Creates a new [SnapTunServer] instance.
187    pub fn new(
188        static_private: x25519::StaticSecret,
189        rate_limiter: Arc<RateLimiter>,
190        authz: Arc<T>,
191    ) -> Self {
192        let static_public = x25519::PublicKey::from(&static_private);
193        Self {
194            static_private,
195            static_public,
196            active_tunnels: Default::default(),
197            rate_limiter,
198            authz,
199        }
200    }
201
202    /// Handle incoming packet for a tunnel assocated with remote socket address
203    /// `from`.
204    ///
205    /// This method _never_ returns [TunnResult::WriteToNetwork]. Instead,
206    /// it codifies the expected protocol behavior which is that, upon receiving
207    /// a packet from the remote, the queue of outgoing packets is completely
208    /// drained.
209    ///
210    /// If the rate limiter signals that the server is under load, at most one
211    /// packet is added to the queue.
212    ///
213    /// This compatibility wrapper preserves the original public API for callers
214    /// that only care about the tunnel result.
215    pub fn handle_incoming_packet(
216        &mut self,
217        packet: Packet,
218        from: SocketAddr,
219        send_to_network: &mut VecDeque<WgKind>,
220    ) -> TunnResult {
221        self.handle_incoming_packet_with_session(packet, from, send_to_network)
222            .into_result()
223    }
224
225    /// Handles an incoming packet and also returns the active session when one
226    /// was resolved while processing the packet.
227    ///
228    /// Callers on the dataplane hot path can use this to observe forwarded
229    /// packets without re-hashing `from` for a second active-tunnel lookup,
230    /// while existing callers can keep using [`SnapTunServer::handle_incoming_packet`].
231    #[tracing::instrument(skip_all, fields(remote = %from))]
232    pub fn handle_incoming_packet_with_session(
233        &mut self,
234        packet: Packet,
235        from: SocketAddr,
236        send_to_network: &mut VecDeque<WgKind>,
237    ) -> HandleIncomingPacketResult<T::SessionData> {
238        let parsed_packet = match self.rate_limiter.verify_packet(from.ip(), packet) {
239            Ok(p) => p,
240            Err(TunnResult::WriteToNetwork(c)) => {
241                tracing::debug!(remote = ?from, "rate limiter issued cookie reply");
242                send_to_network.push_back(c);
243                return HandleIncomingPacketResult::Result {
244                    result: TunnResult::Done,
245                };
246            }
247            Err(e) => {
248                tracing::debug!(remote = ?from, err = ?e, "rate limiter rejected packet");
249                return HandleIncomingPacketResult::Result { result: e };
250            }
251        };
252        // Capture one shared timestamp after rate-limiter verification so the
253        // authorization decision and any forwarded-packet metadata use the same
254        // instant without an extra clock read on the hot path.
255        let packet_now = Instant::now();
256
257        use std::collections::hash_map::Entry;
258
259        use ana_gotatun::noise::errors::WireGuardError;
260        match (self.active_tunnels.entry(from), parsed_packet) {
261            (Entry::Occupied(mut occupied_entry), p) => {
262                let active_tunnel = occupied_entry.get_mut();
263                // TODO(dsd): At the moment, this keeps a tunnel alive even
264                // though the processing might fail, but gives the authorization
265                // layer a chance to block incomding packets in case an identity
266                // is unauthorized.
267                //
268                // Will fix later.
269                let Some(session_data) = self
270                    .authz
271                    .is_authorized(packet_now, active_tunnel.peer_static.as_bytes())
272                else {
273                    tracing::debug!(remote = ?from, peer_static = ?active_tunnel.peer_static, "rejected packet from unauthorized peer");
274                    return HandleIncomingPacketResult::Result {
275                        result: TunnResult::Err(WireGuardError::UnexpectedPacket),
276                    };
277                };
278                let result = Self::handle_incoming_and_drain_queue(
279                    send_to_network,
280                    p,
281                    &mut active_tunnel.tunn,
282                );
283                Self::incoming_packet_result(result, session_data, packet_now)
284            }
285            (e, WgKind::HandshakeInit(wg_init)) => {
286                let peer = match parse_handshake_anon(
287                    &self.static_private,
288                    &self.static_public,
289                    &wg_init,
290                ) {
291                    Ok(v) => v,
292                    Err(e) => {
293                        tracing::debug!(remote = ?from, err = ?e, "failed to parse handshake init");
294                        return HandleIncomingPacketResult::Result {
295                            result: TunnResult::from(e),
296                        };
297                    }
298                };
299
300                // TODO(dsd): if the socket is occupied, and tunnel.identity !=
301                // peer.identity, then send a cookie and abort
302
303                // TODO(dsd): extend ana-gotatun::Tunn such that peer static
304                // identity can be retrieved
305                let Some(session_data) = self
306                    .authz
307                    .is_authorized(packet_now, &peer.peer_static_public)
308                else {
309                    tracing::debug!(remote = ?from, "rejected handshake from unauthorized peer");
310                    return HandleIncomingPacketResult::Result {
311                        result: TunnResult::Err(WireGuardError::UnexpectedPacket),
312                    };
313                };
314                tracing::debug!(remote = ?from, "accepted new handshake, inserting tunnel");
315                let peer_static = x25519::PublicKey::from(peer.peer_static_public);
316                let mut tunn = Tunn::new(
317                    self.static_private.clone(),
318                    peer_static,
319                    None,
320                    None,
321                    0,
322                    self.rate_limiter.clone(),
323                    from,
324                );
325                let res = Self::handle_incoming_and_drain_queue(
326                    send_to_network,
327                    WgKind::HandshakeInit(wg_init),
328                    &mut tunn,
329                );
330                // Derive the caller-visible forwarded-packet result before
331                // moving `session_data` into the active-tunnel entry below.
332                let handled = Self::incoming_packet_result(res, session_data.clone(), packet_now);
333                e.insert_entry(ActiveTunnel { peer_static, tunn });
334                handled
335            }
336            (_, _p) => {
337                tracing::debug!(remote = ?from, "received unexpected packet kind for new entry");
338                HandleIncomingPacketResult::Result {
339                    result: TunnResult::Err(WireGuardError::InvalidPacket),
340                }
341            }
342        }
343    }
344
345    /// Handles an outgoing packet sent through the tunnel identified by the
346    /// remote socket address `to`.
347    pub fn handle_outgoing_packet(&mut self, packet: Packet, to: SocketAddr) -> Option<WgKind> {
348        self.handle_outgoing_packet_with_session(packet, to)
349            .and_then(HandleOutgoingPacketResult::into_packet)
350    }
351
352    /// Handles an outgoing packet and returns the active tunnel session used to
353    /// admit the payload into the tunnel pipeline.
354    ///
355    /// This re-checks authorization on the outgoing path. If the active tunnel
356    /// no longer has current authorization, the packet is dropped and `None` is
357    /// returned even when the tunnel state itself still exists.
358    #[tracing::instrument(skip_all, fields(remote = %to))]
359    pub fn handle_outgoing_packet_with_session(
360        &mut self,
361        packet: Packet,
362        to: SocketAddr,
363    ) -> Option<HandleOutgoingPacketResult<T::SessionData>> {
364        let Some(active_tunnel) = self.active_tunnels.get_mut(&to) else {
365            tracing::error!(to=?to, "No tunnel for outgoing packet found.");
366            return None;
367        };
368        let packet_now = Instant::now();
369        let Some(session_data) = self
370            .authz
371            .is_authorized(packet_now, active_tunnel.peer_static.as_bytes())
372        else {
373            tracing::debug!(remote = ?to, peer_static = ?active_tunnel.peer_static, "dropping outgoing packet for unauthorized peer");
374            return None;
375        };
376        Some(Self::outgoing_packet_result(
377            active_tunnel
378                .tunn
379                .handle_outgoing_packet(packet.into_bytes()),
380            packet_now,
381            session_data,
382        ))
383    }
384
385    /// Update timers of all tunnels. Generate corresponding keepalive or
386    /// session handshake initializations.
387    ///
388    /// As a result of this call, all expired tunnels are removed. Note that
389    /// this is not the same as unauthorized tunnels.
390    ///
391    /// Callers are expected to invoke this periodically; it is also where the rate limiter's
392    /// under-load counter is reset. Without that reset the counter only ever grows and the
393    /// server eventually treats itself as permanently under load.
394    pub fn update_timers(&mut self) -> Vec<(SocketAddr, WgKind)> {
395        // Self-throttled to the rate limiter's own reset period, so calling this more often
396        // than once per second is harmless.
397        self.rate_limiter.try_reset_count();
398
399        let mut res = vec![];
400        self.active_tunnels.retain(|k, active_tunnel| {
401            match active_tunnel.tunn.update_timers() {
402                Ok(Some(wg)) => res.push((*k, wg)),
403                Ok(None) => {},
404                Err(e) => tracing::error!(err=?e, remote_sockaddr=?k, "error when updating timers on tunnel"),
405            }
406
407            !active_tunnel.tunn.is_expired()
408        });
409        res
410    }
411
412    fn handle_incoming_and_drain_queue(
413        q: &mut VecDeque<WgKind>,
414        p: WgKind,
415        tunn: &mut Tunn,
416    ) -> TunnResult {
417        let r = match tunn.handle_incoming_packet(p) {
418            TunnResult::WriteToNetwork(p) => {
419                q.push_back(p);
420                TunnResult::Done
421            }
422            // keep alive
423            TunnResult::WriteToTunnel(p) if p.is_empty() => TunnResult::Done,
424            r => r,
425        };
426        for p in tunn.get_queued_packets() {
427            q.push_back(p);
428        }
429        r
430    }
431}
432
433/// Authorization layer for the snaptun server.
434pub trait SnapTunAuthorization: Send + Sync {
435    /// Immutable session data that downstream dataplane consumers may read.
436    type SessionData: Clone + Send + Sync + 'static;
437
438    /// Returns the current session data iff the peer is allowed to send traffic.
439    fn is_authorized(&self, now: Instant, identity: &[u8; 32]) -> Option<Arc<Self::SessionData>>;
440}
441
442#[cfg(test)]
443mod tests {
444    use std::{
445        collections::{HashMap, VecDeque},
446        net::SocketAddr,
447        sync::{Arc, Mutex},
448        time::Duration,
449    };
450
451    use ana_gotatun::{
452        noise::{Tunn, TunnResult, rate_limiter::RateLimiter},
453        packet::{IpNextProtocol, Packet, WgKind},
454        x25519,
455    };
456    use zerocopy::IntoBytes;
457
458    use crate::{
459        scion_packet::{Scion, ScionHeader},
460        server::{
461            HandleIncomingPacketResult, HandleOutgoingPacketResult, SnapTunAuthorization,
462            SnapTunServer,
463        },
464    };
465
466    type ResultT = Result<(), Box<dyn std::error::Error>>;
467
468    struct TrivialAuthz;
469
470    impl SnapTunAuthorization for TrivialAuthz {
471        type SessionData = ();
472
473        fn is_authorized(
474            &self,
475            _now: std::time::Instant,
476            _ident: &[u8; 32],
477        ) -> Option<Arc<Self::SessionData>> {
478            Some(Arc::new(()))
479        }
480    }
481
482    #[derive(Debug, Clone, PartialEq, Eq)]
483    struct MutableSessionData {
484        jti: &'static str,
485        pssid: &'static str,
486        tags: Vec<(&'static str, &'static str)>,
487    }
488
489    #[derive(Default)]
490    struct MutableAuthz {
491        sessions: Mutex<HashMap<[u8; 32], Arc<MutableSessionData>>>,
492    }
493
494    impl MutableAuthz {
495        fn set_session_data(&self, identity: [u8; 32], session_data: MutableSessionData) {
496            self.sessions
497                .lock()
498                .unwrap()
499                .insert(identity, Arc::new(session_data));
500        }
501    }
502
503    impl SnapTunAuthorization for MutableAuthz {
504        type SessionData = MutableSessionData;
505
506        fn is_authorized(
507            &self,
508            _now: std::time::Instant,
509            ident: &[u8; 32],
510        ) -> Option<Arc<Self::SessionData>> {
511            self.sessions.lock().unwrap().get(ident).cloned()
512        }
513    }
514
515    fn test_packet<const N: usize>(payload: [u8; N]) -> Packet {
516        let packet = Scion {
517            header: ScionHeader::new(
518                0,
519                0xAA,
520                0xABCDE,
521                payload.len() as _,
522                IpNextProtocol::Udp,
523                7,
524                0x0123_4567_89AB_CDEF,
525                0xFEDC_BA98_7654_3210,
526            ),
527            payload,
528        };
529        Packet::copy_from(packet.as_bytes())
530    }
531
532    fn establish_tunnel<T: SnapTunAuthorization>(
533        snaptun_server: &mut SnapTunServer<T>,
534        tunn_client: &mut Tunn,
535        packet: &Packet,
536        sockaddr_client: SocketAddr,
537        send_to_network: &mut VecDeque<WgKind>,
538    ) {
539        let Some(WgKind::HandshakeInit(hs_init)) =
540            tunn_client.handle_outgoing_packet(Packet::copy_from(packet))
541        else {
542            panic!("expected handshake init")
543        };
544
545        snaptun_server.handle_incoming_packet(
546            Packet::copy_from(hs_init.as_bytes()),
547            sockaddr_client,
548            send_to_network,
549        );
550        dispatch_one(tunn_client, send_to_network);
551    }
552
553    #[test]
554    fn connect_with_multiple_clients() -> ResultT {
555        let sockaddr_client0: SocketAddr = "192.168.1.1:1234".parse().unwrap();
556        let static_client0 = x25519::StaticSecret::from([0u8; 32]);
557        let sockaddr_client1: SocketAddr = "192.168.1.2:4321".parse().unwrap();
558        let static_client1 = x25519::StaticSecret::from([1u8; 32]);
559        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
560        let static_server = x25519::StaticSecret::from([2u8; 32]);
561        let static_server_public = x25519::PublicKey::from(&static_server);
562
563        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
564        let mut snaptun_server =
565            SnapTunServer::new(static_server, rate_limiter.clone(), Arc::new(TrivialAuthz));
566
567        let mut send_to_network = VecDeque::<WgKind>::new();
568
569        let test_packet0 = test_packet([b'T', b'E', b'S', b'T', b'0']);
570        let test_packet1 = test_packet([b'T', b'E', b'S', b'T', b'1']);
571
572        let mut tunn_client0 = Tunn::new(
573            static_client0,
574            static_server_public,
575            None,
576            None,
577            0,
578            rate_limiter.clone(),
579            sockaddr_server,
580        );
581
582        let mut tunn_client1 = Tunn::new(
583            static_client1,
584            static_server_public,
585            None,
586            None,
587            0,
588            rate_limiter,
589            sockaddr_server,
590        );
591
592        /* handshake 0 */
593        establish_tunnel(
594            &mut snaptun_server,
595            &mut tunn_client0,
596            &test_packet0,
597            sockaddr_client0,
598            &mut send_to_network,
599        );
600        assert_eq!(
601            tunn_client0.get_initiator_remote_sockaddr(),
602            Some(sockaddr_client0)
603        );
604
605        /* handshake 1 */
606        establish_tunnel(
607            &mut snaptun_server,
608            &mut tunn_client1,
609            &test_packet1,
610            sockaddr_client1,
611            &mut send_to_network,
612        );
613        assert_eq!(
614            tunn_client1.get_initiator_remote_sockaddr(),
615            Some(sockaddr_client1)
616        );
617
618        /* send C0 -> S */
619        let Some(WgKind::Data(p)) = tunn_client0.get_queued_packets().next() else {
620            panic!("expected packet to be queued");
621        };
622
623        let TunnResult::WriteToTunnel(p) = snaptun_server.handle_incoming_packet(
624            Packet::copy_from(p.as_bytes()),
625            sockaddr_client0,
626            &mut send_to_network,
627        ) else {
628            panic!("Expected packet to be processed")
629        };
630        assert_eq!(p.as_bytes(), test_packet0.as_bytes());
631
632        /* send C1 -> S */
633        // before we can send a packet to client1, we need to send a packet from
634        // client1 so the server starts using the session.
635        let Some(WgKind::Data(p1)) = tunn_client1.get_queued_packets().next() else {
636            panic!("expected packet to be queued");
637        };
638
639        let TunnResult::WriteToTunnel(p1) = snaptun_server.handle_incoming_packet(
640            Packet::copy_from(p1.as_bytes()),
641            sockaddr_client1,
642            &mut send_to_network,
643        ) else {
644            panic!("expected packet to be received on server side");
645        };
646        assert_eq!(p1.as_bytes(), test_packet1.as_bytes());
647
648        /* send S -> C1 */
649        let res = snaptun_server.handle_outgoing_packet(p, sockaddr_client1);
650        let Some(p @ WgKind::Data(_)) = res else {
651            panic!("expected packet to be sent back to client")
652        };
653
654        let TunnResult::WriteToTunnel(p) = tunn_client1.handle_incoming_packet(p) else {
655            panic!("expected packet to be sent back to client")
656        };
657
658        assert_eq!(p.as_bytes(), test_packet0.as_bytes());
659
660        Ok(())
661    }
662
663    #[test]
664    fn outgoing_packet_with_session_returns_active_session() {
665        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
666        let static_client = x25519::StaticSecret::from([0u8; 32]);
667        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
668        let static_server = x25519::StaticSecret::from([2u8; 32]);
669        let static_server_public = x25519::PublicKey::from(&static_server);
670
671        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
672        let mut snaptun_server =
673            SnapTunServer::new(static_server, rate_limiter.clone(), Arc::new(TrivialAuthz));
674        let mut send_to_network = VecDeque::<WgKind>::new();
675
676        let test_packet = test_packet([b'T', b'E', b'S', b'T']);
677
678        let mut tunn_client = Tunn::new(
679            static_client,
680            static_server_public,
681            None,
682            None,
683            0,
684            rate_limiter,
685            sockaddr_server,
686        );
687
688        establish_tunnel(
689            &mut snaptun_server,
690            &mut tunn_client,
691            &test_packet,
692            sockaddr_client,
693            &mut send_to_network,
694        );
695
696        let Some(WgKind::Data(client_data)) = tunn_client.get_queued_packets().next() else {
697            panic!("expected packet to be queued");
698        };
699        let TunnResult::WriteToTunnel(server_plaintext) = snaptun_server.handle_incoming_packet(
700            Packet::copy_from(client_data.as_bytes()),
701            sockaddr_client,
702            &mut send_to_network,
703        ) else {
704            panic!("expected packet to be processed")
705        };
706
707        let handled = snaptun_server
708            .handle_outgoing_packet_with_session(server_plaintext, sockaddr_client)
709            .expect("expected packet to be encapsulated");
710        let HandleOutgoingPacketResult {
711            network_packet: Some(WgKind::Data(encapsulated)),
712            processed_at: _,
713            session_data,
714        } = handled
715        else {
716            panic!("expected encapsulated data packet")
717        };
718        assert_eq!(session_data.as_ref(), &());
719
720        let TunnResult::WriteToTunnel(plaintext) =
721            tunn_client.handle_incoming_packet(WgKind::Data(encapsulated))
722        else {
723            panic!("expected packet to be delivered back to client")
724        };
725        assert_eq!(plaintext.as_bytes(), test_packet.as_bytes());
726    }
727
728    #[test]
729    fn established_tunnel_refreshes_session_data_for_later_packets() {
730        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
731        let static_client = x25519::StaticSecret::from([0u8; 32]);
732        let client_identity = x25519::PublicKey::from(&static_client);
733        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
734        let static_server = x25519::StaticSecret::from([2u8; 32]);
735        let static_server_public = x25519::PublicKey::from(&static_server);
736        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
737        let authz = Arc::new(MutableAuthz::default());
738        let original_session = MutableSessionData {
739            jti: "original-jti",
740            pssid: "original-pssid",
741            tags: vec![("subject_id", "subject-1"), ("scope", "basic")],
742        };
743        authz.set_session_data(*client_identity.as_bytes(), original_session);
744
745        let mut snaptun_server =
746            SnapTunServer::new(static_server, rate_limiter.clone(), authz.clone());
747        let mut send_to_network = VecDeque::<WgKind>::new();
748        let test_packet = test_packet([b'T', b'E', b'S', b'T']);
749
750        let mut tunn_client = Tunn::new(
751            static_client,
752            static_server_public,
753            None,
754            None,
755            0,
756            rate_limiter,
757            sockaddr_server,
758        );
759
760        establish_tunnel(
761            &mut snaptun_server,
762            &mut tunn_client,
763            &test_packet,
764            sockaddr_client,
765            &mut send_to_network,
766        );
767
768        let refreshed_session = MutableSessionData {
769            jti: "refreshed-jti",
770            pssid: "refreshed-pssid",
771            tags: vec![("subject_id", "subject-2"), ("scope", "premium")],
772        };
773        authz.set_session_data(*client_identity.as_bytes(), refreshed_session.clone());
774
775        let Some(WgKind::Data(client_data)) = tunn_client.get_queued_packets().next() else {
776            panic!("expected packet to be queued");
777        };
778        let HandleIncomingPacketResult::Forwarded {
779            packet: server_plaintext,
780            session_data,
781            ..
782        } = snaptun_server.handle_incoming_packet_with_session(
783            Packet::copy_from(client_data.as_bytes()),
784            sockaddr_client,
785            &mut send_to_network,
786        )
787        else {
788            panic!("expected forwarded packet with refreshed session data")
789        };
790        assert_eq!(session_data.as_ref(), &refreshed_session);
791
792        let Some(HandleOutgoingPacketResult {
793            network_packet: Some(WgKind::Data(encapsulated)),
794            processed_at: _,
795            session_data,
796        }) = snaptun_server.handle_outgoing_packet_with_session(server_plaintext, sockaddr_client)
797        else {
798            panic!("expected encapsulated data packet with refreshed session data")
799        };
800        assert_eq!(session_data.as_ref(), &refreshed_session);
801
802        let TunnResult::WriteToTunnel(plaintext) =
803            tunn_client.handle_incoming_packet(WgKind::Data(encapsulated))
804        else {
805            panic!("expected packet to be delivered back to client")
806        };
807        assert_eq!(plaintext.as_bytes(), test_packet.as_bytes());
808    }
809
810    #[test]
811    fn outgoing_packet_with_session_returns_none_without_tunnel() {
812        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
813        let static_server = x25519::StaticSecret::from([2u8; 32]);
814        let static_server_public = x25519::PublicKey::from(&static_server);
815        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
816        let mut snaptun_server =
817            SnapTunServer::new(static_server, rate_limiter, Arc::new(TrivialAuthz));
818
819        let payload = [b'T', b'E', b'S', b'T'];
820        let test_packet = Scion {
821            header: ScionHeader::new(
822                0,
823                0xAA,
824                0xABCDE,
825                payload.len() as _,
826                IpNextProtocol::Udp,
827                7,
828                0x0123_4567_89AB_CDEF,
829                0xFEDC_BA98_7654_3210,
830            ),
831            payload,
832        };
833
834        assert!(
835            snaptun_server
836                .handle_outgoing_packet_with_session(
837                    Packet::copy_from(test_packet.as_bytes()),
838                    sockaddr_client
839                )
840                .is_none()
841        );
842    }
843
844    /// The rate limiter counts every handshake it verifies and demands a cookie once its limit
845    /// is reached, so the counter has to be reset periodically.
846    ///
847    /// [`Tunn::update_timers`] resets the shared limiter, which covers a server that has
848    /// tunnels to iterate over. This pins the case with no tunnel to piggyback on: handshakes
849    /// from unauthorized peers are counted but establish nothing, so without the reset in
850    /// [`SnapTunServer::update_timers`] the counter only ever grows and an idle server keeps
851    /// demanding cookies from every peer.
852    #[test]
853    fn update_timers_resets_the_rate_limiter_without_active_tunnels() {
854        let static_server = x25519::StaticSecret::from([2u8; 32]);
855        let static_server_public = x25519::PublicKey::from(&static_server);
856        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
857
858        // A limit of one means the second handshake the server verifies is already under load.
859        // The empty `MutableAuthz` authorizes nobody, so no handshake creates a tunnel.
860        let mut snaptun_server = SnapTunServer::new(
861            static_server,
862            Arc::new(RateLimiter::new(&static_server_public, 1)),
863            Arc::new(MutableAuthz::default()),
864        );
865        // The clients get their own limiter so that only server-side handshakes are counted
866        // against the limit under test.
867        let client_rate_limiter = Arc::new(RateLimiter::new(&static_server_public, u64::MAX));
868
869        let handshake_from_new_client = |server: &mut SnapTunServer<MutableAuthz>, index: u8| {
870            let mut client = Tunn::new(
871                x25519::StaticSecret::from([index; 32]),
872                static_server_public,
873                None,
874                None,
875                0,
876                client_rate_limiter.clone(),
877                sockaddr_server,
878            );
879            let Some(WgKind::HandshakeInit(hs_init)) =
880                client.handle_outgoing_packet(test_packet([index]))
881            else {
882                panic!("expected handshake init")
883            };
884
885            // Each client uses its own address so every handshake is a fresh one rather than
886            // a repeat handshake on an existing tunnel.
887            let mut send_to_network = VecDeque::<WgKind>::new();
888            server.handle_incoming_packet(
889                Packet::copy_from(hs_init.as_bytes()),
890                SocketAddr::new("192.168.1.1".parse().unwrap(), 1234 + u16::from(index)),
891                &mut send_to_network,
892            );
893            send_to_network.pop_front()
894        };
895
896        // Below the limit the handshake reaches the authorization check, which rejects it
897        // without a reply. At the limit the rate limiter answers with a cookie first.
898        assert!(
899            handshake_from_new_client(&mut snaptun_server, 1).is_none(),
900            "the first handshake is below the limit and must reach authorization"
901        );
902        assert!(
903            matches!(
904                handshake_from_new_client(&mut snaptun_server, 2),
905                Some(WgKind::CookieReply(_))
906            ),
907            "the second handshake reaches the limit and must be answered with a cookie"
908        );
909        assert!(
910            snaptun_server.active_tunnels.is_empty(),
911            "no tunnel may exist, otherwise its timers would reset the limiter"
912        );
913
914        // The rate limiter resets against the monotonic clock, which neither tokio's paused
915        // time nor a mocked instant reaches from here, so this waits out the reset period.
916        std::thread::sleep(Duration::from_millis(1_100));
917        snaptun_server.update_timers();
918
919        assert!(
920            handshake_from_new_client(&mut snaptun_server, 3).is_none(),
921            "after the reset period an idle server must stop demanding cookies"
922        );
923    }
924
925    fn dispatch_one(tunn: &mut Tunn, packets: &mut VecDeque<WgKind>) -> TunnResult {
926        if let Some(packet) = packets.pop_front() {
927            return tunn.handle_incoming_packet(packet);
928        }
929        TunnResult::Done
930    }
931}