From 9027afa88ea888aff870bf5335b28d07e303ed32 Mon Sep 17 00:00:00 2001 From: Ronen Ulanovsky Date: Sun, 24 May 2026 08:40:05 +0300 Subject: [PATCH] dtls: defer replay commits until datagram parse succeeds --- CHANGELOG.md | 1 + src/dtls12/incoming.rs | 110 ++++++++++++++++++++----- src/dtls13/engine.rs | 11 ++- src/dtls13/incoming.rs | 164 ++++++++++++++++++++++++++++++++----- tests/dtls12/edge.rs | 105 ++++++++++++++++++++++++ tests/dtls12/retransmit.rs | 52 ++++++++++++ tests/dtls13/edge.rs | 105 ++++++++++++++++++++++++ 7 files changed, 504 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7e672a..b4d80e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased + * Fix malformed datagrams consuming DTLS replay-window state #121 * Stop DTLS 1.2 flight resends once the peer handshake is confirmed #125 * Drop plaintext DTLS 1.3 ACKs and alerts after peer encryption #118 * Replace pending DTLS 1.2 handshake output on resend #116 diff --git a/src/dtls12/incoming.rs b/src/dtls12/incoming.rs index da13eade..ea656426 100644 --- a/src/dtls12/incoming.rs +++ b/src/dtls12/incoming.rs @@ -8,6 +8,7 @@ use crate::Error; use crate::buffer::{Buf, TmpBuf}; use crate::crypto::{Aad, Nonce}; use crate::dtls12::message::{ContentType, DTLSRecord, Dtls12CipherSuite, Handshake, Sequence}; +use crate::window::ReplayWindow; /// Holds both the UDP packet and the parsed result of that packet. pub struct Incoming { @@ -67,12 +68,15 @@ pub struct Records { } impl Records { - pub fn parse( + fn parse( mut packet: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, ) -> Result { let mut parsed_records: ArrayVec = ArrayVec::new(); + let mut replay_updates: ArrayVec = ArrayVec::new(); + let mut authenticated_content_types: ArrayVec = ArrayVec::new(); + let mut pending_replay = ReplayWindow::new(); // Find record boundaries and copy each record ONCE from the packet while !packet.is_empty() { @@ -91,14 +95,34 @@ impl Records { // This is the ONLY copy: packet -> record buffer let record_slice = &packet[..record_end]; match Record::parse(record_slice, decrypt, cs) { - Ok(record) => { - if let Some(record) = record { + Ok(parsed) => { + if let Some(sequence) = parsed.replay_sequence { + if !pending_replay.check(sequence.sequence_number) { + trace!("Discarding duplicate rec in same datagram"); + packet = &packet[record_end..]; + continue; + } + } + + if let Some(record) = parsed.record { if parsed_records.try_push(record).is_err() { return Err(Error::TooManyRecords); } - } else { + } else if parsed.replay_sequence.is_none() { trace!("Discarding replayed rec"); } + + if let Some(sequence) = parsed.replay_sequence { + pending_replay.update(sequence.sequence_number); + if replay_updates.try_push(sequence).is_err() { + return Err(Error::TooManyRecords); + } + if let Some(content_type) = parsed.authenticated_content_type { + authenticated_content_types + .try_push(content_type) + .expect("authenticated records cannot exceed replay updates"); + } + } } Err(e) => return Err(e), } @@ -106,6 +130,17 @@ impl Records { packet = &packet[record_end..]; } + // Commit replay state and authenticated-record notifications only after + // the whole UDP datagram has parsed successfully. A malformed trailing + // record must not consume replay state or publish side effects for an + // earlier authenticated record in the same datagram. + for sequence in replay_updates { + decrypt.replay_update(sequence); + } + for content_type in authenticated_content_types { + decrypt.note_decrypted_record(content_type); + } + let mut records = ArrayVec::new(); for record in parsed_records { if let Some(record) = decrypt.classify_record(record)? { @@ -134,14 +169,20 @@ pub struct Record { parsed: Box, } +struct RecordParse { + record: Option, + replay_sequence: Option, + authenticated_content_type: Option, +} + impl Record { /// The first parse pass only parses the DTLSRecord header which is unencrypted. /// Copies record data from UDP packet ONCE into a pooled buffer. - pub fn parse( + fn parse( record_slice: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + ) -> Result { // ONLY COPY: UDP packet slice -> pooled buffer let mut buffer = Buf::new(); buffer.extend_from_slice(record_slice); @@ -151,7 +192,11 @@ impl Record { // RFC 6347 §4.1.2.7: Invalid records SHOULD be silently discarded. // This includes epoch 0 records with invalid ContentType. trace!("Discarding record: parse failed: {}", e); - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + authenticated_content_type: None, + }); } }; let parsed = Box::new(parsed); @@ -162,7 +207,11 @@ impl Record { // packet loss, we can end up seeing epoch 1 records before we can decrypt them. let is_epoch_0 = record.record().sequence.epoch == 0; if is_epoch_0 || !decrypt.is_peer_encryption_enabled() { - return Ok(Some(record)); + return Ok(RecordParse { + record: Some(record), + replay_sequence: None, + authenticated_content_type: None, + }); } // We need to decrypt the record and redo the parsing. @@ -172,12 +221,20 @@ impl Record { // Anti-replay check (read-only, does not update window) if !decrypt.replay_check(sequence) { - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + authenticated_content_type: None, + }); } let explicit_nonce_len = decrypt.explicit_nonce_len(); if (dtls.length as usize) < decrypt.min_protected_fragment_len() { - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + authenticated_content_type: None, + }); } // Get a reference to the buffer @@ -204,29 +261,38 @@ impl Record { } trace!("Discarding record: decrypt failed: {}", e); - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + authenticated_content_type: None, + }); } buffer.len() }; - // Decryption succeeded — now commit the replay window update. - // RFC 6347 §4.1.2.6: "The receive window is updated only if the - // MAC verification succeeds." - decrypt.replay_update(sequence); - - // The record is now authenticated. Tell the handler so it can act on a - // confirmed-genuine record (e.g. mark the peer past its handshake). - decrypt.note_decrypted_record(content_type); - // Update the length of the record. buffer[11] = (new_len >> 8) as u8; buffer[12] = new_len as u8; - let parsed = ParsedRecord::parse(&buffer, cs, explicit_nonce_len)?; + let parsed = match ParsedRecord::parse(&buffer, cs, explicit_nonce_len) { + Ok(parsed) => parsed, + Err(e) => { + trace!("Discarding authenticated record: parse failed: {}", e); + return Ok(RecordParse { + record: None, + replay_sequence: Some(sequence), + authenticated_content_type: Some(content_type), + }); + } + }; let parsed = Box::new(parsed); - Ok(Some(Record { buffer, parsed })) + Ok(RecordParse { + record: Some(Record { buffer, parsed }), + replay_sequence: Some(sequence), + authenticated_content_type: Some(content_type), + }) } pub fn record(&self) -> &DTLSRecord { diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index a4916816..06b9b02e 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -2352,7 +2352,13 @@ impl RecordHandler for Engine { epoch_bits } - fn resolve_sequence(&self, epoch: u16, seq_bits: u64, s_flag: bool) -> u64 { + fn resolve_sequence( + &self, + epoch: u16, + seq_bits: u64, + s_flag: bool, + expected_override: Option, + ) -> u64 { let expected = if epoch == 2 { self.hs_expected_recv_seq } else { @@ -2362,6 +2368,9 @@ impl RecordHandler for Engine { .map(|e| e.expected_recv_seq) .unwrap_or(0) }; + let expected = expected_override + .map(|override_expected| expected.max(override_expected)) + .unwrap_or(expected); let bits: u32 = if s_flag { 16 } else { 8 }; reconstruct_sequence(seq_bits, expected, bits) diff --git a/src/dtls13/incoming.rs b/src/dtls13/incoming.rs index 240cc830..1e83ecd1 100644 --- a/src/dtls13/incoming.rs +++ b/src/dtls13/incoming.rs @@ -7,6 +7,7 @@ use std::fmt; use crate::Error; use crate::buffer::{Buf, TmpBuf}; use crate::dtls13::message::{ContentType, Dtls13CipherSuite, Dtls13Record, Handshake, Sequence}; +use crate::window::ReplayWindow; /// Holds both the UDP packet and the parsed result of that packet. pub struct Incoming { @@ -72,6 +73,9 @@ impl Records { cs: Option, ) -> Result { let mut parsed_records: ArrayVec = ArrayVec::new(); + let mut replay_updates: ArrayVec = ArrayVec::new(); + let mut pending_replay: ArrayVec<(u16, ReplayWindow), 16> = ArrayVec::new(); + let mut pending_expected: ArrayVec<(u16, u64), 16> = ArrayVec::new(); // Find record boundaries and copy each record ONCE from the packet while !packet.is_empty() { @@ -129,15 +133,31 @@ impl Records { // This is the ONLY copy: packet -> record buffer let record_slice = &packet[..record_end]; - match Record::parse(record_slice, decrypt, cs) { - Ok(record) => { - if let Some(record) = record { + match Record::parse(record_slice, decrypt, cs, &pending_expected) { + Ok(parsed) => { + if let Some(sequence) = parsed.replay_sequence { + if !pending_replay_check(&pending_replay, sequence) { + trace!("Discarding duplicate rec in same datagram"); + packet = &packet[record_end..]; + continue; + } + } + + if let Some(record) = parsed.record { if parsed_records.try_push(record).is_err() { return Err(Error::TooManyRecords); } - } else { + } else if parsed.replay_sequence.is_none() { trace!("Discarding replayed rec"); } + + if let Some(sequence) = parsed.replay_sequence { + pending_replay_update(&mut pending_replay, sequence)?; + pending_expected_update(&mut pending_expected, sequence)?; + if replay_updates.try_push(sequence).is_err() { + return Err(Error::TooManyRecords); + } + } } Err(e) => return Err(e), } @@ -145,6 +165,13 @@ impl Records { packet = &packet[record_end..]; } + // Commit replay state only after the whole UDP datagram has parsed + // successfully. A malformed trailing record must not consume + // replay state for an earlier authenticated record in the same datagram. + for sequence in replay_updates { + decrypt.replay_update(sequence); + } + let mut records = ArrayVec::new(); for record in parsed_records { if let Some(record) = decrypt.classify_record(record)? { @@ -158,6 +185,62 @@ impl Records { } } +fn pending_replay_check(pending_replay: &ArrayVec<(u16, ReplayWindow), 16>, seq: Sequence) -> bool { + match pending_replay.iter().find(|(epoch, _)| *epoch == seq.epoch) { + Some((_, window)) => window.check(seq.sequence_number), + None => true, + } +} + +fn pending_replay_update( + pending_replay: &mut ArrayVec<(u16, ReplayWindow), 16>, + seq: Sequence, +) -> Result<(), Error> { + if let Some((_, window)) = pending_replay + .iter_mut() + .find(|(epoch, _)| *epoch == seq.epoch) + { + window.update(seq.sequence_number); + return Ok(()); + } + + let mut window = ReplayWindow::new(); + window.update(seq.sequence_number); + pending_replay + .try_push((seq.epoch, window)) + .map_err(|_| Error::TooManyRecords) +} + +fn pending_expected_override( + pending_expected: &ArrayVec<(u16, u64), 16>, + epoch: u16, +) -> Option { + pending_expected + .iter() + .find(|(candidate_epoch, _)| *candidate_epoch == epoch) + .map(|(_, expected)| *expected) +} + +fn pending_expected_update( + pending_expected: &mut ArrayVec<(u16, u64), 16>, + seq: Sequence, +) -> Result<(), Error> { + let next = seq.sequence_number + 1; + if let Some((_, expected)) = pending_expected + .iter_mut() + .find(|(epoch, _)| *epoch == seq.epoch) + { + if next > *expected { + *expected = next; + } + return Ok(()); + } + + pending_expected + .try_push((seq.epoch, next)) + .map_err(|_| Error::TooManyRecords) +} + impl Deref for Records { type Target = [Record]; @@ -173,14 +256,20 @@ pub struct Record { parsed: Box, } +struct RecordParse { + record: Option, + replay_sequence: Option, +} + impl Record { /// The first parse pass only parses the record header which is unencrypted. /// Copies record data from UDP packet ONCE into a pooled buffer. - pub fn parse( + fn parse( record_slice: &[u8], decrypt: &mut dyn RecordHandler, cs: Option, - ) -> Result, Error> { + pending_expected: &ArrayVec<(u16, u64), 16>, + ) -> Result { // ONLY COPY: UDP packet slice -> pooled buffer let mut buffer = Buf::new(); buffer.extend_from_slice(record_slice); @@ -218,7 +307,10 @@ impl Record { Ok(p) => p, Err(e) => { trace!("Discarding record: parse failed: {}", e); - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + }); } }; let parsed = Box::new(parsed); @@ -226,7 +318,10 @@ impl Record { // Plaintext records (epoch 0) are not encrypted if !is_ciphertext || !decrypt.is_peer_encryption_enabled() { - return Ok(Some(record)); + return Ok(RecordParse { + record: Some(record), + replay_sequence: None, + }); } // Resolve the full epoch from the 2-bit value in the unified header @@ -236,7 +331,12 @@ impl Record { // Resolve the full sequence number from the (now decrypted) partial value let seq_bits = record.record().sequence.sequence_number; let s_flag = record_slice[0] & 0b0000_1000 != 0; - let full_seq = decrypt.resolve_sequence(full_epoch, seq_bits, s_flag); + let full_seq = decrypt.resolve_sequence( + full_epoch, + seq_bits, + s_flag, + pending_expected_override(pending_expected, full_epoch), + ); let full_sequence = Sequence { epoch: full_epoch, @@ -245,7 +345,10 @@ impl Record { // Anti-replay check (read-only, does not update window) if !decrypt.replay_check(full_sequence) { - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + }); } // Save the raw header bytes for AAD before mutating the buffer. @@ -257,7 +360,10 @@ impl Record { // so decryption would necessarily fail. Catching it here keeps the // cipher impls' bounds-checking from being the only line of defence. if record.buffer.len() - header_end < decrypt.min_protected_fragment_len() { - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + }); } let mut header_buf = [0u8; 5]; header_buf[..header_end].copy_from_slice(&record.buffer[..header_end]); @@ -277,25 +383,26 @@ impl Record { Ok(()) => {} Err(e) => { trace!("Discarding ciphertext record: decryption failed: {}", e); - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: None, + }); } } buffer.len() }; - // Decryption succeeded — now commit the replay window update. - // RFC 9147 §4.5.1: "The window MUST NOT be updated due to a received - // record until that record has been deprotected successfully." - decrypt.replay_update(full_sequence); - // Recover inner content type from DTLSInnerPlaintext let decrypted = &buffer[header_end..header_end + new_len]; let (inner_content_type, content_len) = match recover_inner_content_type(decrypted) { Ok(v) => v, Err(e) => { trace!("Discarding record: invalid inner content type: {}", e); - return Ok(None); + return Ok(RecordParse { + record: None, + replay_sequence: Some(full_sequence), + }); } }; @@ -311,7 +418,10 @@ impl Record { ); let parsed = Box::new(parsed); - Ok(Some(Record { buffer, parsed })) + Ok(RecordParse { + record: Some(Record { buffer, parsed }), + replay_sequence: Some(full_sequence), + }) } pub fn record(&self) -> &Dtls13Record { @@ -407,7 +517,13 @@ pub trait RecordHandler { fn classify_record(&mut self, record: Record) -> Result, Error>; fn is_peer_encryption_enabled(&self) -> bool; fn resolve_epoch(&self, epoch_bits: u8) -> u16; - fn resolve_sequence(&self, epoch: u16, seq_bits: u64, s_flag: bool) -> u64; + fn resolve_sequence( + &self, + epoch: u16, + seq_bits: u64, + s_flag: bool, + expected_override: Option, + ) -> u64; fn replay_check(&self, seq: Sequence) -> bool; fn replay_update(&mut self, seq: Sequence); @@ -556,7 +672,13 @@ mod tests { panic!("resolve_epoch should not be called when peer encryption is disabled"); } - fn resolve_sequence(&self, _epoch: u16, _seq_bits: u64, _s_flag: bool) -> u64 { + fn resolve_sequence( + &self, + _epoch: u16, + _seq_bits: u64, + _s_flag: bool, + _expected_override: Option, + ) -> u64 { panic!("resolve_sequence should not be called when peer encryption is disabled"); } diff --git a/tests/dtls12/edge.rs b/tests/dtls12/edge.rs index 7640007c..6dcd02b5 100644 --- a/tests/dtls12/edge.rs +++ b/tests/dtls12/edge.rs @@ -680,6 +680,78 @@ fn dtls12_bad_encrypted_prefix_does_not_drop_valid_tail() { ); } +#[test] +#[cfg(feature = "rcgen")] +fn dtls12_malformed_trailing_record_does_not_consume_replay_window() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_12_pair(now); + + client + .send_application_data(b"replay-atomic") + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + let valid_packet = client_out + .packets + .first() + .expect("application data packet") + .clone(); + + let mut malformed_packet = valid_packet.clone(); + malformed_packet.push(0xff); + + let err = server + .handle_packet(&malformed_packet) + .expect_err("trailing partial record must reject the datagram"); + assert!( + matches!(err, dimpl::Error::ParseIncomplete), + "expected ParseIncomplete, got {err:?}" + ); + + let server_out = drain_outputs(&mut server); + assert!( + server_out.app_data.is_empty(), + "malformed datagram must not deliver application data" + ); + + server + .handle_packet(&valid_packet) + .expect("valid packet must still pass replay checks"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"replay-atomic".to_vec()]); +} + +#[test] +#[cfg(feature = "rcgen")] +fn dtls12_same_datagram_duplicate_encrypted_record_delivers_once() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_12_pair(now); + + client + .send_application_data(b"duplicate-once") + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + let valid_packet = client_out + .packets + .first() + .expect("application data packet") + .clone(); + + let mut duplicate_datagram = valid_packet.clone(); + duplicate_datagram.extend_from_slice(&valid_packet); + + server + .handle_packet(&duplicate_datagram) + .expect("duplicate datagram should parse"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"duplicate-once".to_vec()]); +} + #[test] #[cfg(feature = "rcgen")] fn dtls12_relabelled_encrypted_handshake_failure_is_not_silently_discarded() { @@ -767,6 +839,39 @@ fn dtls12_relabelled_encrypted_handshake_failure_is_not_silently_discarded() { ); } +#[test] +#[cfg(feature = "rcgen")] +fn dtls12_same_datagram_window_shift_drops_now_too_old_record() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_12_pair(now); + + let mut packets = Vec::new(); + for i in 0..66 { + client + .send_application_data(format!("msg-{i}").as_bytes()) + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let out = drain_outputs(&mut client); + let packet = out + .packets + .first() + .expect("application data packet") + .clone(); + packets.push(packet); + } + + let mut shifted_datagram = packets[65].clone(); + shifted_datagram.extend_from_slice(&packets[0]); + + server + .handle_packet(&shifted_datagram) + .expect("window-shift datagram should parse"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"msg-65".to_vec()]); +} + #[test] #[cfg(feature = "rcgen")] fn dtls12_app_data_after_close_notify_is_ignored() { diff --git a/tests/dtls12/retransmit.rs b/tests/dtls12/retransmit.rs index eb7a6445..003f9968 100644 --- a/tests/dtls12/retransmit.rs +++ b/tests/dtls12/retransmit.rs @@ -1124,6 +1124,58 @@ fn dtls12_stale_client_hello_after_peer_confirmed_triggers_no_resend() { ); } +#[test] +#[cfg(feature = "rcgen")] +fn dtls12_malformed_appdata_datagram_does_not_confirm_peer() { + let _ = env_logger::try_init(); + + const RX_QUEUE_LIMIT: usize = 8; + let FinalFlightResend { + mut client, + mut server, + f6_resend, + stale_epoch0_handshake, + .. + } = prepare_server_final_flight_resend(RX_QUEUE_LIMIT); + + deliver_packets(&f6_resend, &mut client); + assert!( + drain_outputs(&mut client).connected, + "client should connect once it receives flight 6" + ); + + client + .send_application_data(b"malformed-confirmation") + .expect("client sends application data"); + let client_app = collect_packets(&mut client); + assert!(!client_app.is_empty(), "client should emit app-data packet"); + + let mut malformed_app = client_app.first().expect("client app-data packet").clone(); + malformed_app.push(0xff); + + let err = server + .handle_packet(&malformed_app) + .expect_err("trailing partial record must reject the datagram"); + assert!( + matches!(err, dimpl::Error::ParseIncomplete), + "expected ParseIncomplete, got {err:?}" + ); + assert!( + drain_outputs(&mut server).app_data.is_empty(), + "malformed datagram must not deliver application data" + ); + + server + .handle_packet(&stale_epoch0_handshake) + .expect("stale ClientHello must be tolerated"); + let resend = collect_packets(&mut server); + + assert!( + !resend.is_empty(), + "malformed authenticated app-data datagram must not confirm the peer" + ); +} + #[cfg(feature = "rcgen")] fn forged_epoch1_app_data() -> Vec { // A DTLS 1.2 record header advertising epoch-1 ApplicationData with a diff --git a/tests/dtls13/edge.rs b/tests/dtls13/edge.rs index 5db61b60..e6133844 100644 --- a/tests/dtls13/edge.rs +++ b/tests/dtls13/edge.rs @@ -1466,6 +1466,111 @@ fn dtls13_mixed_datagram_valid_first_then_bogus() { ); } +#[test] +#[cfg(feature = "rcgen")] +fn dtls13_malformed_trailing_record_does_not_consume_replay_window() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_13_pair(now); + + client + .send_application_data(b"replay-atomic") + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + let valid_packet = client_out + .packets + .first() + .expect("application data packet") + .clone(); + + let mut malformed_packet = valid_packet.clone(); + malformed_packet.push(0xff); + + let err = server + .handle_packet(&malformed_packet) + .expect_err("trailing partial record must reject the datagram"); + assert!( + matches!(err, dimpl::Error::ParseIncomplete), + "expected ParseIncomplete, got {err:?}" + ); + + let server_out = drain_outputs(&mut server); + assert!( + server_out.app_data.is_empty(), + "malformed datagram must not deliver application data" + ); + + server + .handle_packet(&valid_packet) + .expect("valid packet must still pass replay checks"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"replay-atomic".to_vec()]); +} + +#[test] +#[cfg(feature = "rcgen")] +fn dtls13_same_datagram_duplicate_encrypted_record_delivers_once() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_13_pair(now); + + client + .send_application_data(b"duplicate-once") + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + let valid_packet = client_out + .packets + .first() + .expect("application data packet") + .clone(); + + let mut duplicate_datagram = valid_packet.clone(); + duplicate_datagram.extend_from_slice(&valid_packet); + + server + .handle_packet(&duplicate_datagram) + .expect("duplicate datagram should parse"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"duplicate-once".to_vec()]); +} + +#[test] +#[cfg(feature = "rcgen")] +fn dtls13_same_datagram_window_shift_drops_now_too_old_record() { + let _ = env_logger::try_init(); + let now = Instant::now(); + let (mut client, mut server, now) = setup_connected_13_pair(now); + + let mut packets = Vec::new(); + for i in 0..66 { + client + .send_application_data(format!("msg-{i}").as_bytes()) + .expect("send application data"); + client.handle_timeout(now).expect("client timeout"); + let out = drain_outputs(&mut client); + let packet = out + .packets + .first() + .expect("application data packet") + .clone(); + packets.push(packet); + } + + let mut shifted_datagram = packets[65].clone(); + shifted_datagram.extend_from_slice(&packets[0]); + + server + .handle_packet(&shifted_datagram) + .expect("window-shift datagram should parse"); + let server_out = drain_outputs(&mut server); + + assert_eq!(server_out.app_data, vec![b"msg-65".to_vec()]); +} + #[test] #[cfg(feature = "rcgen")] fn dtls13_half_close_send_then_close() {