Skip to main content

corevpn_protocol/
session.rs

1//! Protocol Session Management
2
3use std::time::{Duration, Instant};
4
5use bytes::Bytes;
6
7use corevpn_crypto::{CipherSuite, KeyMaterial};
8
9use crate::{
10    KeyId, OpCode, Packet, DataPacket, DataChannel,
11    ReliableTransport, ReliableConfig, TlsRecordReassembler,
12    ProtocolError, Result,
13};
14use crate::packet::ControlPacketData;
15
16/// Protocol session state
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ProtocolState {
19    /// Initial state, waiting for client hello
20    Initial,
21    /// TLS handshake in progress
22    TlsHandshake,
23    /// Key exchange in progress
24    KeyExchange,
25    /// Authentication in progress
26    Authenticating,
27    /// Session fully established
28    Established,
29    /// Rekeying in progress
30    Rekeying,
31    /// Session terminated
32    Terminated,
33}
34
35/// Session ID type (8 bytes)
36pub type SessionIdBytes = [u8; 8];
37
38/// Replay window for tls-auth packet IDs
39/// Uses a 64-bit bitmap to track the last 64 packet IDs
40struct ReplayWindow {
41    /// Highest seen packet ID
42    highest: u32,
43    /// Bitmap of recently seen packets (relative to highest)
44    /// Bit 0 = highest, bit N = highest - N
45    bitmap: u64,
46}
47
48impl ReplayWindow {
49    /// Window size in packets (64 bits = 64 packet tracking)
50    const WINDOW_SIZE: u32 = 64;
51
52    fn new() -> Self {
53        Self {
54            highest: 0,
55            bitmap: 0,
56        }
57    }
58
59    /// Check if packet ID is valid (not replayed) and update window
60    ///
61    /// Returns true if the packet should be processed, false if it's a replay
62    /// or too old.
63    fn check_and_update(&mut self, packet_id: u32) -> bool {
64        // Packet ID 0 is invalid (counter starts at 1)
65        if packet_id == 0 {
66            return false;
67        }
68
69        if packet_id > self.highest {
70            // New highest packet - advance window
71            let shift = packet_id - self.highest;
72
73            if shift >= Self::WINDOW_SIZE {
74                // Packet is way ahead, clear entire window
75                self.bitmap = 1; // Only mark current packet
76            } else {
77                // Shift window and mark current packet
78                self.bitmap = (self.bitmap << shift) | 1;
79            }
80            self.highest = packet_id;
81            true
82        } else {
83            // Packet is at or before highest
84            let diff = self.highest - packet_id;
85
86            // Check if packet is within window
87            if diff >= Self::WINDOW_SIZE {
88                return false; // Too old
89            }
90
91            // Check if already seen using bit test
92            let mask = 1u64 << diff;
93            if self.bitmap & mask != 0 {
94                return false; // Replay detected
95            }
96
97            // Mark as seen
98            self.bitmap |= mask;
99            true
100        }
101    }
102
103    /// Reset the replay window (e.g., for key renegotiation)
104    fn reset(&mut self) {
105        self.highest = 0;
106        self.bitmap = 0;
107    }
108}
109
110/// Protocol session
111pub struct ProtocolSession {
112    /// Local session ID
113    local_session_id: SessionIdBytes,
114    /// Remote session ID
115    remote_session_id: Option<SessionIdBytes>,
116    /// Current protocol state
117    state: ProtocolState,
118    /// Current key ID
119    current_key_id: KeyId,
120    /// Reliable transport for control channel
121    reliable: ReliableTransport,
122    /// TLS record reassembler
123    tls_reassembler: TlsRecordReassembler,
124    /// Data channels (one per key ID)
125    data_channels: [Option<DataChannel>; 8],
126    /// Peer ID (for P_DATA_V2)
127    peer_id: Option<u32>,
128    /// Use tls-auth
129    use_tls_auth: bool,
130    /// tls-auth key
131    tls_auth_key: Option<corevpn_crypto::HmacAuth>,
132    /// Replay window for tls-auth packet IDs
133    replay_window: ReplayWindow,
134    /// Outgoing tls-auth packet ID counter (for replay protection)
135    tls_auth_packet_id: u32,
136    /// Session creation time
137    created_at: Instant,
138    /// Last activity time
139    last_activity: Instant,
140    /// Cipher suite to use
141    cipher_suite: CipherSuite,
142}
143
144impl ProtocolSession {
145    /// Create a new server-side session
146    pub fn new_server(cipher_suite: CipherSuite) -> Self {
147        Self {
148            local_session_id: corevpn_crypto::generate_session_id(),
149            remote_session_id: None,
150            state: ProtocolState::Initial,
151            current_key_id: KeyId::default(),
152            reliable: ReliableTransport::new(ReliableConfig::default()),
153            tls_reassembler: TlsRecordReassembler::new(65536),
154            data_channels: Default::default(),
155            peer_id: None,
156            use_tls_auth: false,
157            tls_auth_key: None,
158            replay_window: ReplayWindow::new(),
159            tls_auth_packet_id: 1, // OpenVPN packet IDs start at 1 (0 is invalid)
160            created_at: Instant::now(),
161            last_activity: Instant::now(),
162            cipher_suite,
163        }
164    }
165
166    /// Create a new client-side session
167    pub fn new_client(cipher_suite: CipherSuite) -> Self {
168        let mut session = Self::new_server(cipher_suite);
169        session.state = ProtocolState::Initial;
170        session
171    }
172
173    /// Get local session ID
174    pub fn local_session_id(&self) -> &SessionIdBytes {
175        &self.local_session_id
176    }
177
178    /// Get remote session ID
179    pub fn remote_session_id(&self) -> Option<&SessionIdBytes> {
180        self.remote_session_id.as_ref()
181    }
182
183    /// Get current state
184    pub fn state(&self) -> ProtocolState {
185        self.state
186    }
187
188    /// Set state
189    pub fn set_state(&mut self, state: ProtocolState) {
190        self.state = state;
191        self.last_activity = Instant::now();
192    }
193
194    /// Set remote session ID
195    pub fn set_remote_session_id(&mut self, id: SessionIdBytes) {
196        self.remote_session_id = Some(id);
197    }
198
199    /// Enable tls-auth
200    pub fn set_tls_auth(&mut self, key: corevpn_crypto::HmacAuth) {
201        self.use_tls_auth = true;
202        self.tls_auth_key = Some(key);
203    }
204
205    /// Update the cipher suite (e.g., after NCP cipher negotiation)
206    pub fn set_cipher_suite(&mut self, cipher_suite: CipherSuite) {
207        self.cipher_suite = cipher_suite;
208    }
209
210    /// Process incoming packet
211    pub fn process_packet(&mut self, data: &[u8]) -> Result<ProcessedPacket> {
212        self.last_activity = Instant::now();
213
214        // Verify HMAC if tls-auth enabled for control packets
215        if self.use_tls_auth {
216            if let Some(key) = &self.tls_auth_key {
217                if !data.is_empty() && OpCode::from_byte(data[0])?.is_control() {
218                    // OpenVPN tls-auth wire format:
219                    // [opcode(1)] [session_id(8)] [HMAC(32)] [pid(4)] [time(4)] [rest...]
220                    //
221                    // HMAC covers (via swap_hmac rearrangement):
222                    // [pid(4)] [time(4)] [opcode(1)] [session_id(8)] [rest...]
223                    // = data[41..49] ++ data[0..9] ++ data[49..]
224                    if data.len() < 49 {
225                        return Err(ProtocolError::PacketTooShort {
226                            expected: 49,
227                            got: data.len(),
228                        });
229                    }
230
231                    let mut hmac = [0u8; 32];
232                    hmac.copy_from_slice(&data[9..41]); // HMAC at offset 9
233
234                    // Build HMAC input: pid + time + opcode + session_id + rest
235                    let mut hmac_input = Vec::with_capacity(8 + 9 + data.len() - 49);
236                    hmac_input.extend_from_slice(&data[41..49]); // pid(4) + time(4)
237                    hmac_input.extend_from_slice(&data[0..9]);   // opcode(1) + session_id(8)
238                    hmac_input.extend_from_slice(&data[49..]);   // rest
239
240                    key.verify(&hmac_input, &hmac)?;
241                }
242            }
243        }
244
245        let packet = Packet::parse(data, self.use_tls_auth)?;
246
247        match packet {
248            Packet::Control(ctrl) => self.process_control_packet(ctrl),
249            Packet::Data(data_pkt) => self.process_data_packet(data_pkt),
250        }
251    }
252
253    fn process_control_packet(&mut self, ctrl: ControlPacketData) -> Result<ProcessedPacket> {
254        // Check replay protection for tls-auth packets
255        if self.use_tls_auth {
256            if let Some(packet_id) = ctrl.header.packet_id {
257                if !self.replay_window.check_and_update(packet_id) {
258                    return Err(ProtocolError::ReplayDetected);
259                }
260            }
261        }
262
263        // Process ACKs
264        if !ctrl.acks.is_empty() {
265            self.reliable.process_acks(&ctrl.acks);
266        }
267
268        // Handle different opcodes
269        match ctrl.header.opcode {
270            OpCode::HardResetClientV2 | OpCode::HardResetClientV3 => {
271                // Client initiating connection
272                // Security: Validate session ID - generate new one instead of accepting client's
273                // This prevents session fixation attacks
274                if let Some(remote_sid) = ctrl.header.session_id {
275                    // Validate session ID is not all zeros or obviously malicious
276                    if remote_sid == [0; 8] {
277                        return Err(ProtocolError::InvalidSessionId);
278                    }
279                    // Accept the session ID but we'll use our own for the response
280                    self.remote_session_id = Some(remote_sid);
281                }
282
283                // Queue ACK for the client's hard reset packet via the reliable
284                // transport so it will be included in our response
285                if let Some(packet_id) = ctrl.message_packet_id {
286                    let _ = self.reliable.receive(packet_id, Bytes::new())?;
287                }
288
289                self.state = ProtocolState::TlsHandshake;
290
291                Ok(ProcessedPacket::HardReset {
292                    session_id: self.local_session_id,
293                })
294            }
295            OpCode::HardResetServerV2 => {
296                // Server response to hard reset
297                if let Some(remote_sid) = ctrl.header.session_id {
298                    self.remote_session_id = Some(remote_sid);
299                }
300
301                // ACK the server's hard reset via reliable transport
302                if let Some(packet_id) = ctrl.message_packet_id {
303                    let _ = self.reliable.receive(packet_id, Bytes::new())?;
304                }
305
306                self.state = ProtocolState::TlsHandshake;
307
308                Ok(ProcessedPacket::HardResetAck)
309            }
310            OpCode::ControlV1 => {
311                // TLS data
312                if let Some(packet_id) = ctrl.message_packet_id {
313                    if let Some(data) = self.reliable.receive(packet_id, ctrl.payload.clone())? {
314                        self.tls_reassembler.add(&data)?;
315                        let records = self.tls_reassembler.extract_records();
316                        if !records.is_empty() {
317                            return Ok(ProcessedPacket::TlsData(records));
318                        }
319                    }
320                }
321                Ok(ProcessedPacket::None)
322            }
323            OpCode::AckV1 => {
324                // Pure ACK, already processed above
325                Ok(ProcessedPacket::None)
326            }
327            OpCode::SoftResetV1 => {
328                // Key renegotiation
329                self.state = ProtocolState::Rekeying;
330                Ok(ProcessedPacket::SoftReset)
331            }
332            _ => Err(ProtocolError::UnknownOpcode(ctrl.header.opcode as u8)),
333        }
334    }
335
336    fn process_data_packet(&mut self, data_pkt: crate::packet::DataPacketData) -> Result<ProcessedPacket> {
337        let packet = DataPacket {
338            key_id: data_pkt.header.key_id,
339            peer_id: data_pkt.peer_id,
340            payload: data_pkt.payload,
341        };
342
343        let key_id = packet.key_id.0 as usize;
344        if let Some(channel) = &mut self.data_channels[key_id] {
345            let decrypted = channel.decrypt(&packet)?;
346            Ok(ProcessedPacket::Data(decrypted))
347        } else {
348            Err(ProtocolError::KeyNotAvailable(packet.key_id.0))
349        }
350    }
351
352    /// Create a hard reset client packet (client initiating connection)
353    pub fn create_hard_reset_client(&mut self) -> Result<Bytes> {
354        let (packet_id, _) = self.reliable.send(Bytes::new())?;
355
356        let packet = crate::packet::ControlPacketData {
357            header: crate::PacketHeader {
358                opcode: OpCode::HardResetClientV2,
359                key_id: KeyId::default(),
360                session_id: Some(self.local_session_id),
361                hmac: None,
362                packet_id: None,
363                timestamp: None,
364            },
365            remote_session_id: None, // No remote session yet
366            acks: vec![],
367            message_packet_id: Some(packet_id),
368            payload: Bytes::new(),
369        };
370
371        let serialized = Packet::Control(packet).serialize();
372        Ok(self.maybe_wrap_tls_auth(serialized.freeze()))
373    }
374
375    /// Create a hard reset response packet
376    pub fn create_hard_reset_response(&mut self) -> Result<Bytes> {
377        // Register with reliable transport to get a message_packet_id.
378        // OpenVPN requires all control packets (including hard resets) to
379        // carry a message_packet_id for the reliable transport layer.
380        let (packet_id, _) = self.reliable.send(Bytes::new())?;
381
382        let packet = crate::packet::ControlPacketData {
383            header: crate::PacketHeader {
384                opcode: OpCode::HardResetServerV2,
385                key_id: KeyId::default(),
386                session_id: Some(self.local_session_id),
387                hmac: None,
388                packet_id: None,
389                timestamp: None,
390            },
391            remote_session_id: self.remote_session_id,
392            acks: self.reliable.get_acks(),
393            message_packet_id: Some(packet_id),
394            payload: Bytes::new(),
395        };
396
397        let serialized = Packet::Control(packet).serialize();
398        Ok(self.maybe_wrap_tls_auth(serialized.freeze()))
399    }
400
401    /// Maximum payload size for a single control channel packet.
402    /// OpenVPN splits TLS data across multiple control packets to stay within
403    /// the link MTU. We use 1100 bytes to leave room for:
404    /// - 1 byte opcode + 8 bytes session_id + 1 byte ack_count
405    /// - up to ~12 bytes ACKs + remote_session_id
406    /// - 4 bytes message_packet_id
407    /// - 20 bytes HMAC (if tls-auth)
408    /// - 8 bytes packet_id + timestamp (if tls-auth)
409    /// - 20 bytes IP header + 8 bytes UDP header
410    /// Total overhead ~82 bytes, so 1100 + 82 = 1182 < 1500 MTU
411    const MAX_CONTROL_PAYLOAD: usize = 1100;
412
413    /// Create control packets with TLS data, splitting across multiple
414    /// packets if the data exceeds the maximum control channel payload size.
415    /// This prevents oversized UDP datagrams that would require IP fragmentation.
416    pub fn create_control_packets(&mut self, tls_data: Bytes) -> Result<Vec<Bytes>> {
417        let mut packets = Vec::new();
418
419        if tls_data.len() <= Self::MAX_CONTROL_PAYLOAD {
420            // Data fits in a single packet
421            packets.push(self.create_single_control_packet(tls_data)?);
422        } else {
423            // Split data across multiple packets
424            let mut offset = 0;
425            while offset < tls_data.len() {
426                let end = std::cmp::min(offset + Self::MAX_CONTROL_PAYLOAD, tls_data.len());
427                let chunk = tls_data.slice(offset..end);
428                packets.push(self.create_single_control_packet(chunk)?);
429                offset = end;
430            }
431        }
432
433        Ok(packets)
434    }
435
436    /// Create a single control packet with TLS data (internal helper)
437    fn create_single_control_packet(&mut self, tls_data: Bytes) -> Result<Bytes> {
438        let (packet_id, _) = self.reliable.send(tls_data.clone())?;
439
440        let packet = crate::packet::ControlPacketData {
441            header: crate::PacketHeader {
442                opcode: OpCode::ControlV1,
443                key_id: self.current_key_id,
444                session_id: Some(self.local_session_id),
445                hmac: None,
446                packet_id: None,
447                timestamp: None,
448            },
449            remote_session_id: self.remote_session_id,
450            acks: self.reliable.get_acks(),
451            message_packet_id: Some(packet_id),
452            payload: tls_data,
453        };
454
455        let serialized = Packet::Control(packet).serialize();
456        Ok(self.maybe_wrap_tls_auth(serialized.freeze()))
457    }
458
459    /// Create a control packet with TLS data (convenience for single packet)
460    pub fn create_control_packet(&mut self, tls_data: Bytes) -> Result<Bytes> {
461        self.create_single_control_packet(tls_data)
462    }
463
464    /// Create an ACK packet
465    pub fn create_ack_packet(&mut self) -> Option<Bytes> {
466        let acks = self.reliable.get_acks();
467        if acks.is_empty() {
468            return None;
469        }
470
471        let packet = crate::packet::ControlPacketData {
472            header: crate::PacketHeader {
473                opcode: OpCode::AckV1,
474                key_id: self.current_key_id,
475                session_id: Some(self.local_session_id),
476                hmac: None,
477                packet_id: None,
478                timestamp: None,
479            },
480            remote_session_id: self.remote_session_id,
481            acks,
482            message_packet_id: None,
483            payload: Bytes::new(),
484        };
485
486        self.reliable.ack_sent();
487        let serialized = Packet::Control(packet).serialize();
488        Some(self.maybe_wrap_tls_auth(serialized.freeze()))
489    }
490
491    /// Install data channel keys
492    pub fn install_keys(&mut self, key_material: &KeyMaterial, is_server: bool) {
493        let key_id = self.current_key_id;
494        let idx = key_id.0 as usize;
495
496        let (encrypt_key, decrypt_key) = if is_server {
497            (
498                key_material.server_data_key(self.cipher_suite),
499                key_material.client_data_key(self.cipher_suite),
500            )
501        } else {
502            (
503                key_material.client_data_key(self.cipher_suite),
504                key_material.server_data_key(self.cipher_suite),
505            )
506        };
507
508        // Use V2 data packets only if we have a negotiated peer_id.
509        // When peer_id is None, both sides use P_DATA_V1 where the
510        // opcode is NOT included in the AEAD AAD.
511        let use_v2 = self.peer_id.is_some();
512        self.data_channels[idx] = Some(DataChannel::new(
513            key_id,
514            encrypt_key,
515            decrypt_key,
516            use_v2,
517            self.peer_id,
518        ));
519    }
520
521    /// Encrypt data for transmission
522    pub fn encrypt_data(&mut self, data: &[u8]) -> Result<Bytes> {
523        let idx = self.current_key_id.0 as usize;
524        if let Some(channel) = &mut self.data_channels[idx] {
525            let packet = channel.encrypt(data)?;
526            Ok(packet.serialize().freeze())
527        } else {
528            Err(ProtocolError::KeyNotAvailable(self.current_key_id.0))
529        }
530    }
531
532    /// Get packets needing retransmission
533    pub fn get_retransmits(&mut self) -> Vec<Bytes> {
534        self.reliable
535            .get_retransmits()
536            .into_iter()
537            .map(|(id, data)| {
538                // Rebuild packet with same ID
539                let packet = crate::packet::ControlPacketData {
540                    header: crate::PacketHeader {
541                        opcode: OpCode::ControlV1,
542                        key_id: self.current_key_id,
543                        session_id: Some(self.local_session_id),
544                        hmac: None,
545                        packet_id: None,
546                        timestamp: None,
547                    },
548                    remote_session_id: self.remote_session_id,
549                    acks: vec![],
550                    message_packet_id: Some(id),
551                    payload: data,
552                };
553                let serialized = Packet::Control(packet).serialize();
554                self.maybe_wrap_tls_auth(serialized.freeze())
555            })
556            .collect()
557    }
558
559    /// Check if we should send an ACK
560    pub fn should_send_ack(&self) -> bool {
561        self.reliable.should_send_ack()
562    }
563
564    /// Get next timeout
565    pub fn next_timeout(&self) -> Option<Duration> {
566        self.reliable.next_timeout()
567    }
568
569    /// Check if session is established
570    pub fn is_established(&self) -> bool {
571        self.state == ProtocolState::Established
572    }
573
574    /// Get session duration
575    pub fn duration(&self) -> Duration {
576        self.created_at.elapsed()
577    }
578
579    /// Get idle time
580    pub fn idle_time(&self) -> Duration {
581        self.last_activity.elapsed()
582    }
583
584    fn maybe_wrap_tls_auth(&mut self, data: Bytes) -> Bytes {
585        if self.use_tls_auth {
586            if let Some(key) = &self.tls_auth_key {
587                // OpenVPN tls-auth wire format:
588                // [opcode(1)] [session_id(8)] [HMAC(32)] [pid(4)] [time(4)] [rest...]
589                //
590                // Input `data` is a serialized packet: [opcode(1)] [session_id(8)] [rest...]
591                //
592                // HMAC is computed over (internal/swap format):
593                //   [pid(4)] [time(4)] [opcode(1)] [session_id(8)] [rest...]
594                // Which equals: pid_bytes + time_bytes + entire_input_data
595                //
596                // Wire output: data[0..9] + HMAC + pid_bytes + time_bytes + data[9..]
597
598                if data.len() < 9 {
599                    return data;
600                }
601
602                // Allocate outgoing packet ID
603                let packet_id = self.tls_auth_packet_id;
604                self.tls_auth_packet_id += 1;
605
606                // Current timestamp
607                let timestamp = std::time::SystemTime::now()
608                    .duration_since(std::time::UNIX_EPOCH)
609                    .unwrap_or_default()
610                    .as_secs() as u32;
611
612                let pid_bytes = packet_id.to_be_bytes();
613                let time_bytes = timestamp.to_be_bytes();
614
615                // Build HMAC input: pid + time + entire original packet
616                // (This matches OpenVPN's swap_hmac internal format)
617                let mut hmac_input = Vec::with_capacity(8 + data.len());
618                hmac_input.extend_from_slice(&pid_bytes);
619                hmac_input.extend_from_slice(&time_bytes);
620                hmac_input.extend_from_slice(&data); // opcode + session_id + rest
621
622                let hmac = key.authenticate(&hmac_input);
623
624                // Build wire format: [opcode+session_id] [HMAC] [pid] [time] [rest]
625                let mut output = Vec::with_capacity(data.len() + 32 + 8);
626                output.extend_from_slice(&data[0..9]);       // opcode(1) + session_id(8)
627                output.extend_from_slice(&hmac);              // HMAC(32)
628                output.extend_from_slice(&pid_bytes);         // pid(4)
629                output.extend_from_slice(&time_bytes);        // time(4)
630                output.extend_from_slice(&data[9..]);         // rest (ack_stuff, msg_pid, payload)
631
632                return Bytes::from(output);
633            }
634        }
635        data
636    }
637
638    /// Rotate to next key ID (for rekeying)
639    pub fn rotate_key(&mut self) {
640        self.current_key_id = self.current_key_id.next();
641        // Reset replay window on key rotation
642        self.replay_window.reset();
643    }
644}
645
646/// Result of processing a packet
647#[derive(Debug)]
648pub enum ProcessedPacket {
649    /// No action needed
650    None,
651    /// Hard reset from client
652    HardReset {
653        /// Session ID for the new connection
654        session_id: SessionIdBytes,
655    },
656    /// Hard reset acknowledged
657    HardResetAck,
658    /// TLS records to process
659    TlsData(Vec<Bytes>),
660    /// Decrypted data packet
661    Data(Bytes),
662    /// Soft reset (rekey)
663    SoftReset,
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_session_creation() {
672        let session = ProtocolSession::new_server(CipherSuite::ChaCha20Poly1305);
673        assert_eq!(session.state(), ProtocolState::Initial);
674        assert!(session.remote_session_id().is_none());
675    }
676
677    #[test]
678    fn test_hard_reset() {
679        let mut session = ProtocolSession::new_server(CipherSuite::ChaCha20Poly1305);
680
681        // Simulate receiving hard reset from client
682        let hard_reset = [
683            0x38, // opcode=7 (HardResetClientV2), key_id=0
684            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // session_id
685            0x00, // ack_count = 0
686            0x00, 0x00, 0x00, 0x00, // message_packet_id = 0
687        ];
688
689        let result = session.process_packet(&hard_reset).unwrap();
690        matches!(result, ProcessedPacket::HardReset { .. });
691        assert_eq!(session.state(), ProtocolState::TlsHandshake);
692    }
693
694    #[test]
695    fn test_hard_reset_response_has_packet_id() {
696        let mut session = ProtocolSession::new_server(CipherSuite::ChaCha20Poly1305);
697
698        // Process client hard reset first
699        let hard_reset = [
700            0x38, // opcode=7 (HardResetClientV2), key_id=0
701            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // session_id
702            0x00, // ack_count = 0
703            0x00, 0x00, 0x00, 0x00, // message_packet_id = 0
704        ];
705        session.process_packet(&hard_reset).unwrap();
706
707        // Create response and verify it contains message_packet_id
708        let response = session.create_hard_reset_response().unwrap();
709
710        // Response format (no tls-auth):
711        // [0]    opcode + key_id (HardResetServerV2 = 0x40)
712        // [1-8]  session_id (8 bytes)
713        // [9]    ack_count (should be 1 - ACK of client's packet 0)
714        // [10-13] ack_id[0] (4 bytes, value 0)
715        // [14-21] remote_session_id (8 bytes)
716        // [22-25] message_packet_id (4 bytes, value 0)
717        assert!(response.len() >= 26, "Response too short: {} bytes", response.len());
718
719        // Verify opcode is HardResetServerV2 (opcode=8, key_id=0 → 0x40)
720        assert_eq!(response[0], 0x40);
721
722        // Verify ack_count = 1 (ACK of client's hard reset)
723        assert_eq!(response[9], 1);
724
725        // Verify ACK'd packet_id = 0
726        let ack_id = u32::from_be_bytes(response[10..14].try_into().unwrap());
727        assert_eq!(ack_id, 0);
728
729        // Verify message_packet_id = 0 (first outgoing packet)
730        let msg_pid = u32::from_be_bytes(response[22..26].try_into().unwrap());
731        assert_eq!(msg_pid, 0);
732    }
733}