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    pub fn update_timers(&mut self) -> Vec<(SocketAddr, WgKind)> {
391        let mut res = vec![];
392        self.active_tunnels.retain(|k, active_tunnel| {
393            match active_tunnel.tunn.update_timers() {
394                Ok(Some(wg)) => res.push((*k, wg)),
395                Ok(None) => {},
396                Err(e) => tracing::error!(err=?e, remote_sockaddr=?k, "error when updating timers on tunnel"),
397            }
398
399            !active_tunnel.tunn.is_expired()
400        });
401        res
402    }
403
404    fn handle_incoming_and_drain_queue(
405        q: &mut VecDeque<WgKind>,
406        p: WgKind,
407        tunn: &mut Tunn,
408    ) -> TunnResult {
409        let r = match tunn.handle_incoming_packet(p) {
410            TunnResult::WriteToNetwork(p) => {
411                q.push_back(p);
412                TunnResult::Done
413            }
414            // keep alive
415            TunnResult::WriteToTunnel(p) if p.is_empty() => TunnResult::Done,
416            r => r,
417        };
418        for p in tunn.get_queued_packets() {
419            q.push_back(p);
420        }
421        r
422    }
423}
424
425/// Authorization layer for the snaptun server.
426pub trait SnapTunAuthorization: Send + Sync {
427    /// Immutable session data that downstream dataplane consumers may read.
428    type SessionData: Clone + Send + Sync + 'static;
429
430    /// Returns the current session data iff the peer is allowed to send traffic.
431    fn is_authorized(&self, now: Instant, identity: &[u8; 32]) -> Option<Arc<Self::SessionData>>;
432}
433
434#[cfg(test)]
435mod tests {
436    use std::{
437        collections::{HashMap, VecDeque},
438        net::SocketAddr,
439        sync::{Arc, Mutex},
440    };
441
442    use ana_gotatun::{
443        noise::{Tunn, TunnResult, rate_limiter::RateLimiter},
444        packet::{IpNextProtocol, Packet, WgKind},
445        x25519,
446    };
447    use zerocopy::IntoBytes;
448
449    use crate::{
450        scion_packet::{Scion, ScionHeader},
451        server::{
452            HandleIncomingPacketResult, HandleOutgoingPacketResult, SnapTunAuthorization,
453            SnapTunServer,
454        },
455    };
456
457    type ResultT = Result<(), Box<dyn std::error::Error>>;
458
459    struct TrivialAuthz;
460
461    impl SnapTunAuthorization for TrivialAuthz {
462        type SessionData = ();
463
464        fn is_authorized(
465            &self,
466            _now: std::time::Instant,
467            _ident: &[u8; 32],
468        ) -> Option<Arc<Self::SessionData>> {
469            Some(Arc::new(()))
470        }
471    }
472
473    #[derive(Debug, Clone, PartialEq, Eq)]
474    struct MutableSessionData {
475        jti: &'static str,
476        pssid: &'static str,
477        tags: Vec<(&'static str, &'static str)>,
478    }
479
480    #[derive(Default)]
481    struct MutableAuthz {
482        sessions: Mutex<HashMap<[u8; 32], Arc<MutableSessionData>>>,
483    }
484
485    impl MutableAuthz {
486        fn set_session_data(&self, identity: [u8; 32], session_data: MutableSessionData) {
487            self.sessions
488                .lock()
489                .unwrap()
490                .insert(identity, Arc::new(session_data));
491        }
492    }
493
494    impl SnapTunAuthorization for MutableAuthz {
495        type SessionData = MutableSessionData;
496
497        fn is_authorized(
498            &self,
499            _now: std::time::Instant,
500            ident: &[u8; 32],
501        ) -> Option<Arc<Self::SessionData>> {
502            self.sessions.lock().unwrap().get(ident).cloned()
503        }
504    }
505
506    fn test_packet<const N: usize>(payload: [u8; N]) -> Packet {
507        let packet = Scion {
508            header: ScionHeader::new(
509                0,
510                0xAA,
511                0xABCDE,
512                payload.len() as _,
513                IpNextProtocol::Udp,
514                7,
515                0x0123_4567_89AB_CDEF,
516                0xFEDC_BA98_7654_3210,
517            ),
518            payload,
519        };
520        Packet::copy_from(packet.as_bytes())
521    }
522
523    fn establish_tunnel<T: SnapTunAuthorization>(
524        snaptun_server: &mut SnapTunServer<T>,
525        tunn_client: &mut Tunn,
526        packet: &Packet,
527        sockaddr_client: SocketAddr,
528        send_to_network: &mut VecDeque<WgKind>,
529    ) {
530        let Some(WgKind::HandshakeInit(hs_init)) =
531            tunn_client.handle_outgoing_packet(Packet::copy_from(packet))
532        else {
533            panic!("expected handshake init")
534        };
535
536        snaptun_server.handle_incoming_packet(
537            Packet::copy_from(hs_init.as_bytes()),
538            sockaddr_client,
539            send_to_network,
540        );
541        dispatch_one(tunn_client, send_to_network);
542    }
543
544    #[test]
545    fn connect_with_multiple_clients() -> ResultT {
546        let sockaddr_client0: SocketAddr = "192.168.1.1:1234".parse().unwrap();
547        let static_client0 = x25519::StaticSecret::from([0u8; 32]);
548        let sockaddr_client1: SocketAddr = "192.168.1.2:4321".parse().unwrap();
549        let static_client1 = x25519::StaticSecret::from([1u8; 32]);
550        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
551        let static_server = x25519::StaticSecret::from([2u8; 32]);
552        let static_server_public = x25519::PublicKey::from(&static_server);
553
554        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
555        let mut snaptun_server =
556            SnapTunServer::new(static_server, rate_limiter.clone(), Arc::new(TrivialAuthz));
557
558        let mut send_to_network = VecDeque::<WgKind>::new();
559
560        let test_packet0 = test_packet([b'T', b'E', b'S', b'T', b'0']);
561        let test_packet1 = test_packet([b'T', b'E', b'S', b'T', b'1']);
562
563        let mut tunn_client0 = Tunn::new(
564            static_client0,
565            static_server_public,
566            None,
567            None,
568            0,
569            rate_limiter.clone(),
570            sockaddr_server,
571        );
572
573        let mut tunn_client1 = Tunn::new(
574            static_client1,
575            static_server_public,
576            None,
577            None,
578            0,
579            rate_limiter,
580            sockaddr_server,
581        );
582
583        /* handshake 0 */
584        establish_tunnel(
585            &mut snaptun_server,
586            &mut tunn_client0,
587            &test_packet0,
588            sockaddr_client0,
589            &mut send_to_network,
590        );
591        assert_eq!(
592            tunn_client0.get_initiator_remote_sockaddr(),
593            Some(sockaddr_client0)
594        );
595
596        /* handshake 1 */
597        establish_tunnel(
598            &mut snaptun_server,
599            &mut tunn_client1,
600            &test_packet1,
601            sockaddr_client1,
602            &mut send_to_network,
603        );
604        assert_eq!(
605            tunn_client1.get_initiator_remote_sockaddr(),
606            Some(sockaddr_client1)
607        );
608
609        /* send C0 -> S */
610        let Some(WgKind::Data(p)) = tunn_client0.get_queued_packets().next() else {
611            panic!("expected packet to be queued");
612        };
613
614        let TunnResult::WriteToTunnel(p) = snaptun_server.handle_incoming_packet(
615            Packet::copy_from(p.as_bytes()),
616            sockaddr_client0,
617            &mut send_to_network,
618        ) else {
619            panic!("Expected packet to be processed")
620        };
621        assert_eq!(p.as_bytes(), test_packet0.as_bytes());
622
623        /* send C1 -> S */
624        // before we can send a packet to client1, we need to send a packet from
625        // client1 so the server starts using the session.
626        let Some(WgKind::Data(p1)) = tunn_client1.get_queued_packets().next() else {
627            panic!("expected packet to be queued");
628        };
629
630        let TunnResult::WriteToTunnel(p1) = snaptun_server.handle_incoming_packet(
631            Packet::copy_from(p1.as_bytes()),
632            sockaddr_client1,
633            &mut send_to_network,
634        ) else {
635            panic!("expected packet to be received on server side");
636        };
637        assert_eq!(p1.as_bytes(), test_packet1.as_bytes());
638
639        /* send S -> C1 */
640        let res = snaptun_server.handle_outgoing_packet(p, sockaddr_client1);
641        let Some(p @ WgKind::Data(_)) = res else {
642            panic!("expected packet to be sent back to client")
643        };
644
645        let TunnResult::WriteToTunnel(p) = tunn_client1.handle_incoming_packet(p) else {
646            panic!("expected packet to be sent back to client")
647        };
648
649        assert_eq!(p.as_bytes(), test_packet0.as_bytes());
650
651        Ok(())
652    }
653
654    #[test]
655    fn outgoing_packet_with_session_returns_active_session() {
656        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
657        let static_client = x25519::StaticSecret::from([0u8; 32]);
658        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
659        let static_server = x25519::StaticSecret::from([2u8; 32]);
660        let static_server_public = x25519::PublicKey::from(&static_server);
661
662        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
663        let mut snaptun_server =
664            SnapTunServer::new(static_server, rate_limiter.clone(), Arc::new(TrivialAuthz));
665        let mut send_to_network = VecDeque::<WgKind>::new();
666
667        let test_packet = test_packet([b'T', b'E', b'S', b'T']);
668
669        let mut tunn_client = Tunn::new(
670            static_client,
671            static_server_public,
672            None,
673            None,
674            0,
675            rate_limiter,
676            sockaddr_server,
677        );
678
679        establish_tunnel(
680            &mut snaptun_server,
681            &mut tunn_client,
682            &test_packet,
683            sockaddr_client,
684            &mut send_to_network,
685        );
686
687        let Some(WgKind::Data(client_data)) = tunn_client.get_queued_packets().next() else {
688            panic!("expected packet to be queued");
689        };
690        let TunnResult::WriteToTunnel(server_plaintext) = snaptun_server.handle_incoming_packet(
691            Packet::copy_from(client_data.as_bytes()),
692            sockaddr_client,
693            &mut send_to_network,
694        ) else {
695            panic!("expected packet to be processed")
696        };
697
698        let handled = snaptun_server
699            .handle_outgoing_packet_with_session(server_plaintext, sockaddr_client)
700            .expect("expected packet to be encapsulated");
701        let HandleOutgoingPacketResult {
702            network_packet: Some(WgKind::Data(encapsulated)),
703            processed_at: _,
704            session_data,
705        } = handled
706        else {
707            panic!("expected encapsulated data packet")
708        };
709        assert_eq!(session_data.as_ref(), &());
710
711        let TunnResult::WriteToTunnel(plaintext) =
712            tunn_client.handle_incoming_packet(WgKind::Data(encapsulated))
713        else {
714            panic!("expected packet to be delivered back to client")
715        };
716        assert_eq!(plaintext.as_bytes(), test_packet.as_bytes());
717    }
718
719    #[test]
720    fn established_tunnel_refreshes_session_data_for_later_packets() {
721        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
722        let static_client = x25519::StaticSecret::from([0u8; 32]);
723        let client_identity = x25519::PublicKey::from(&static_client);
724        let sockaddr_server: SocketAddr = "10.0.0.1:5001".parse().unwrap();
725        let static_server = x25519::StaticSecret::from([2u8; 32]);
726        let static_server_public = x25519::PublicKey::from(&static_server);
727        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
728        let authz = Arc::new(MutableAuthz::default());
729        let original_session = MutableSessionData {
730            jti: "original-jti",
731            pssid: "original-pssid",
732            tags: vec![("subject_id", "subject-1"), ("scope", "basic")],
733        };
734        authz.set_session_data(*client_identity.as_bytes(), original_session);
735
736        let mut snaptun_server =
737            SnapTunServer::new(static_server, rate_limiter.clone(), authz.clone());
738        let mut send_to_network = VecDeque::<WgKind>::new();
739        let test_packet = test_packet([b'T', b'E', b'S', b'T']);
740
741        let mut tunn_client = Tunn::new(
742            static_client,
743            static_server_public,
744            None,
745            None,
746            0,
747            rate_limiter,
748            sockaddr_server,
749        );
750
751        establish_tunnel(
752            &mut snaptun_server,
753            &mut tunn_client,
754            &test_packet,
755            sockaddr_client,
756            &mut send_to_network,
757        );
758
759        let refreshed_session = MutableSessionData {
760            jti: "refreshed-jti",
761            pssid: "refreshed-pssid",
762            tags: vec![("subject_id", "subject-2"), ("scope", "premium")],
763        };
764        authz.set_session_data(*client_identity.as_bytes(), refreshed_session.clone());
765
766        let Some(WgKind::Data(client_data)) = tunn_client.get_queued_packets().next() else {
767            panic!("expected packet to be queued");
768        };
769        let HandleIncomingPacketResult::Forwarded {
770            packet: server_plaintext,
771            session_data,
772            ..
773        } = snaptun_server.handle_incoming_packet_with_session(
774            Packet::copy_from(client_data.as_bytes()),
775            sockaddr_client,
776            &mut send_to_network,
777        )
778        else {
779            panic!("expected forwarded packet with refreshed session data")
780        };
781        assert_eq!(session_data.as_ref(), &refreshed_session);
782
783        let Some(HandleOutgoingPacketResult {
784            network_packet: Some(WgKind::Data(encapsulated)),
785            processed_at: _,
786            session_data,
787        }) = snaptun_server.handle_outgoing_packet_with_session(server_plaintext, sockaddr_client)
788        else {
789            panic!("expected encapsulated data packet with refreshed session data")
790        };
791        assert_eq!(session_data.as_ref(), &refreshed_session);
792
793        let TunnResult::WriteToTunnel(plaintext) =
794            tunn_client.handle_incoming_packet(WgKind::Data(encapsulated))
795        else {
796            panic!("expected packet to be delivered back to client")
797        };
798        assert_eq!(plaintext.as_bytes(), test_packet.as_bytes());
799    }
800
801    #[test]
802    fn outgoing_packet_with_session_returns_none_without_tunnel() {
803        let sockaddr_client: SocketAddr = "192.168.1.1:1234".parse().unwrap();
804        let static_server = x25519::StaticSecret::from([2u8; 32]);
805        let static_server_public = x25519::PublicKey::from(&static_server);
806        let rate_limiter = Arc::new(RateLimiter::new(&static_server_public, 100));
807        let mut snaptun_server =
808            SnapTunServer::new(static_server, rate_limiter, Arc::new(TrivialAuthz));
809
810        let payload = [b'T', b'E', b'S', b'T'];
811        let test_packet = Scion {
812            header: ScionHeader::new(
813                0,
814                0xAA,
815                0xABCDE,
816                payload.len() as _,
817                IpNextProtocol::Udp,
818                7,
819                0x0123_4567_89AB_CDEF,
820                0xFEDC_BA98_7654_3210,
821            ),
822            payload,
823        };
824
825        assert!(
826            snaptun_server
827                .handle_outgoing_packet_with_session(
828                    Packet::copy_from(test_packet.as_bytes()),
829                    sockaddr_client
830                )
831                .is_none()
832        );
833    }
834
835    fn dispatch_one(tunn: &mut Tunn, packets: &mut VecDeque<WgKind>) -> TunnResult {
836        if let Some(packet) = packets.pop_front() {
837            return tunn.handle_incoming_packet(packet);
838        }
839        TunnResult::Done
840    }
841}