use std::sync::Arc;
use std::time::{Duration, Instant};
use dimpl::{Config, Dtls};
use crate::common::*;
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_client_retransmits_on_timeout() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = dtls13_config();
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
client.handle_timeout(now).expect("client start");
client.handle_timeout(now).expect("client arm");
let initial_packets = collect_packets(&mut client);
assert!(
!initial_packets.is_empty(),
"Client should send ClientHello"
);
trigger_timeout(&mut client, &mut now);
let retransmit_packets = collect_packets(&mut client);
assert!(
!retransmit_packets.is_empty(),
"Client should retransmit on timeout"
);
assert_eq!(
initial_packets.len(),
retransmit_packets.len(),
"Retransmit should have same packet count"
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_handshake_completes_after_packet_loss() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = dtls13_config();
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut drop_next_client_packet = true;
for i in 0..60 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
if client_out.connected {
client_connected = true;
}
if server_out.connected {
server_connected = true;
}
if !client_out.packets.is_empty() && drop_next_client_packet {
drop_next_client_packet = false;
} else {
deliver_packets(&client_out.packets, &mut server);
}
deliver_packets(&server_out.packets, &mut client);
if client_connected && server_connected {
break;
}
if i % 5 == 4 {
now += Duration::from_secs(2);
} else {
now += Duration::from_millis(10);
}
}
assert!(
client_connected,
"Client should connect despite initial packet loss"
);
assert!(
server_connected,
"Server should connect despite initial packet loss"
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_handshake_completes_with_early_packet_loss() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = Arc::new(
Config::builder()
.flight_retries(8)
.build()
.expect("Failed to build DTLS 1.3 config"),
);
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut client_packets_to_drop = 2;
let mut server_packets_to_drop = 2;
for i in 0..60 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
if client_out.connected {
client_connected = true;
}
if server_out.connected {
server_connected = true;
}
for packet in &client_out.packets {
if client_packets_to_drop > 0 {
client_packets_to_drop -= 1;
} else {
let _ = server.handle_packet(packet);
}
}
for packet in &server_out.packets {
if server_packets_to_drop > 0 {
server_packets_to_drop -= 1;
} else {
let _ = client.handle_packet(packet);
}
}
if client_connected && server_connected {
break;
}
if i % 5 == 4 {
now += Duration::from_secs(2);
} else {
now += Duration::from_millis(10);
}
}
assert!(
client_connected,
"Client should connect despite early packet loss"
);
assert!(
server_connected,
"Server should connect despite early packet loss"
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_handles_bidirectional_packet_loss() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let config = dtls13_config_with_mtu(220);
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut dropped_client = 0;
let mut dropped_server = 0;
let mut total_client_packets = 0;
let mut total_server_packets = 0;
for round in 0..300 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
let is_loss_window = round < 5 || (8..13).contains(&round);
for (i, p) in client_out.packets.iter().enumerate() {
total_client_packets += 1;
if is_loss_window && i % 2 == 1 {
dropped_client += 1;
} else {
let _ = server.handle_packet(p);
}
}
for (i, p) in server_out.packets.iter().enumerate() {
total_server_packets += 1;
if is_loss_window && i % 2 == 0 && server_out.packets.len() > 1 {
dropped_server += 1;
} else {
let _ = client.handle_packet(p);
}
}
client_connected |= client_out.connected;
server_connected |= server_out.connected;
if client_connected && server_connected {
break;
}
now += Duration::from_millis(20);
}
assert!(
client_connected,
"Client should connect despite bidirectional loss"
);
assert!(
server_connected,
"Server should connect despite bidirectional loss"
);
assert!(
dropped_client > 0 || dropped_server > 0,
"Test should have dropped some packets"
);
eprintln!(
concat!(
"SUCCESS: Handshake completed with bidirectional loss. Dropped: ",
"client→server={}/{}, server→client={}/{}"
),
dropped_client, total_client_packets, dropped_server, total_server_packets
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_survives_random_packet_loss_pattern() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let config = dtls13_config_with_mtu(220);
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut total_dropped = 0;
let mut total_delivered = 0;
let should_drop = |round: usize, packet_idx: usize| -> bool {
round > 2 && round % 7 == 0 && packet_idx == 0
};
for round in 0..100 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
for (i, p) in client_out.packets.iter().enumerate() {
if !should_drop(round, i) || client_out.packets.len() == 1 {
let _ = server.handle_packet(p);
total_delivered += 1;
} else {
total_dropped += 1;
}
}
for (i, p) in server_out.packets.iter().enumerate() {
if !should_drop(round, i) || server_out.packets.len() == 1 {
let _ = client.handle_packet(p);
total_delivered += 1;
} else {
total_dropped += 1;
}
}
client_connected |= client_out.connected;
server_connected |= server_out.connected;
if client_connected && server_connected {
break;
}
now += Duration::from_millis(30);
}
assert!(client_connected, "Client should eventually connect");
assert!(server_connected, "Server should eventually connect");
let drop_rate = if total_dropped + total_delivered > 0 {
total_dropped as f64 / (total_dropped + total_delivered) as f64 * 100.0
} else {
0.0
};
eprintln!(
"SUCCESS: Handshake completed with controlled loss. Dropped: {}, Delivered: {}, Drop rate: {:.1}%",
total_dropped, total_delivered, drop_rate
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_selective_retransmit_only_missing_records() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
fn count_epoch2_records(packet: &[u8]) -> usize {
let mut i = 0usize;
let mut count = 0usize;
while i < packet.len() {
let b0 = packet[i];
if (b0 & 0b1110_0000) == 0b0010_0000 {
let c = (b0 & 0b0001_0000) != 0;
let s16 = (b0 & 0b0000_1000) != 0;
let l = (b0 & 0b0000_0100) != 0;
let epoch_bits = b0 & 0b0000_0011;
if c {
break;
}
let mut header_len = 1 + if s16 { 2 } else { 1 };
if l {
header_len += 2;
}
if i + header_len > packet.len() {
break;
}
let ciphertext_len = if l {
let off = i + 1 + if s16 { 2 } else { 1 };
u16::from_be_bytes([packet[off], packet[off + 1]]) as usize
} else {
packet.len() - (i + header_len)
};
if epoch_bits == 2 {
count += 1;
}
i += header_len.saturating_add(ciphertext_len);
continue;
}
if i + 13 > packet.len() {
break;
}
let len = u16::from_be_bytes([packet[i + 11], packet[i + 12]]) as usize;
i += 13 + len;
}
count
}
const ATTEMPTS: usize = 12;
let mut success = None;
let mut attempts_with_drop = 0usize;
let mut attempts_with_retransmit = 0usize;
let mut attempts_with_client_epoch2 = 0usize;
let mut attempts_connected = 0usize;
for attempt in 0..ATTEMPTS {
let client_config = Arc::new(
Config::builder()
.mtu(220)
.dangerously_set_rng_seed(0xC1A0_C1A0u64.wrapping_add(attempt as u64 * 17))
.build()
.expect("Failed to build DTLS 1.3 client config"),
);
let server_config = Arc::new(
Config::builder()
.mtu(220)
.dangerously_set_rng_seed(0x5E8E_5E8Eu64.wrapping_add(attempt as u64 * 29))
.build()
.expect("Failed to build DTLS 1.3 server config"),
);
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&client_config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(server_config, server_cert, now);
server.set_active(false);
let mut dropped_packet: Option<Vec<u8>> = None;
let mut original_flight_size = 0usize;
let mut saw_any_retransmit = false;
let mut selective_retransmit_flight_size = None;
let mut delivered_client_epoch2_after_drop = 0usize;
let mut client_connected = false;
let mut server_connected = false;
for round in 0..220 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
client_connected |= client_out.connected;
server_connected |= server_out.connected;
for p in &client_out.packets {
if dropped_packet.is_some() && count_epoch2_records(p) > 0 {
delivered_client_epoch2_after_drop += 1;
}
let _ = server.handle_packet(p);
}
if dropped_packet.is_none() {
let epoch2_indices: Vec<usize> = server_out
.packets
.iter()
.enumerate()
.filter(|(_, p)| count_epoch2_records(p) > 0)
.map(|(i, _)| i)
.collect();
if epoch2_indices.len() >= 3 {
original_flight_size = epoch2_indices.len();
let drop_epoch2_idx = attempt % epoch2_indices.len();
let drop_packet_idx = epoch2_indices[drop_epoch2_idx];
dropped_packet = Some(server_out.packets[drop_packet_idx].clone());
for (i, p) in server_out.packets.iter().enumerate() {
if i != drop_packet_idx {
let _ = client.handle_packet(p);
}
}
eprintln!(
"Attempt {} Round {}: Dropped packet {} of {}",
attempt, round, drop_epoch2_idx, original_flight_size
);
} else {
deliver_packets(&server_out.packets, &mut client);
}
}
else if selective_retransmit_flight_size.is_none() {
let epoch2_packets: Vec<&Vec<u8>> = server_out
.packets
.iter()
.filter(|p| count_epoch2_records(p) > 0)
.collect();
if !epoch2_packets.is_empty() {
let retransmit_flight_size = epoch2_packets.len();
saw_any_retransmit = true;
eprintln!(
"Attempt {} Round {}: Retransmit flight has {} packets (original had {})",
attempt, round, retransmit_flight_size, original_flight_size
);
if retransmit_flight_size < original_flight_size {
selective_retransmit_flight_size = Some(retransmit_flight_size);
} else {
eprintln!(
"Attempt {} Round {}: Full-flight resend observed before selective resend",
attempt, round
);
}
}
deliver_packets(&server_out.packets, &mut client);
} else {
deliver_packets(&server_out.packets, &mut client);
}
if selective_retransmit_flight_size.is_some() && client_connected && server_connected {
break;
}
now += Duration::from_millis(150);
}
if dropped_packet.is_some() {
attempts_with_drop += 1;
}
if saw_any_retransmit {
attempts_with_retransmit += 1;
}
if delivered_client_epoch2_after_drop > 0 {
attempts_with_client_epoch2 += 1;
}
if client_connected && server_connected {
attempts_connected += 1;
}
if let Some(retransmit_flight_size) = selective_retransmit_flight_size {
if client_connected && server_connected {
success = Some((attempt, original_flight_size, retransmit_flight_size));
break;
}
}
}
let Some((attempt, original_flight_size, retransmit_flight_size)) = success else {
panic!(
"Did not observe selective retransmit across {} attempts \
(drop={}, retransmit={}, client_epoch2_after_drop={}, connected={})",
ATTEMPTS,
attempts_with_drop,
attempts_with_retransmit,
attempts_with_client_epoch2,
attempts_connected
);
};
eprintln!(
"SUCCESS: Selective retransmit verified on attempt {}. \
Original flight: {} packets, Retransmit: {} packets",
attempt, original_flight_size, retransmit_flight_size
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_retransmit_exponential_backoff() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = Arc::new(
Config::builder()
.flight_retries(6)
.handshake_timeout(Duration::from_secs(300))
.build()
.expect("Failed to build config"),
);
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let _server = Dtls::new_13(config, server_cert, now);
client.handle_timeout(now).expect("client start");
client.handle_timeout(now).expect("client arm");
let _initial_packets = collect_packets(&mut client);
let mut timeouts: Vec<Duration> = Vec::new();
for _ in 0..6 {
let out = drain_outputs(&mut client);
let timeout_instant = out.timeout.expect("Should have a timeout scheduled");
let wait = timeout_instant.duration_since(now);
timeouts.push(wait);
now = timeout_instant;
client.handle_timeout(now).expect("client timeout");
let _retransmit = collect_packets(&mut client);
}
assert!(
timeouts.len() >= 4,
"Should have at least 4 timeout values, got {}",
timeouts.len()
);
for i in 1..timeouts.len() {
assert!(
timeouts[i] > timeouts[i - 1],
"Timeout {} ({:?}) should be larger than timeout {} ({:?})",
i,
timeouts[i],
i - 1,
timeouts[i - 1]
);
}
for i in 1..timeouts.len() {
let ratio = timeouts[i].as_secs_f64() / timeouts[i - 1].as_secs_f64();
assert!(
ratio > 1.4,
"Timeout ratio {}/{} = {:.2} should be > 1.4 (exponential backoff)",
i,
i - 1,
ratio
);
}
eprintln!(
"SUCCESS: Exponential backoff verified. Timeouts: {:?}",
timeouts
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_ack_loss_falls_back_to_timer() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = Arc::new(
Config::builder()
.flight_retries(8)
.build()
.expect("Failed to build config"),
);
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut client_final_flight_seen = false;
let mut ack_dropped = false;
for round in 0..100 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
client_connected |= client_out.connected;
server_connected |= server_out.connected;
deliver_packets(&client_out.packets, &mut server);
if client_final_flight_seen && !ack_dropped && !server_out.packets.is_empty() {
ack_dropped = true;
eprintln!("Round {}: Dropped server ACK", round);
} else {
deliver_packets(&server_out.packets, &mut client);
}
if !client_final_flight_seen && !client_out.packets.is_empty() && round > 2 {
client_final_flight_seen = true;
}
if client_connected && server_connected {
eprintln!(
"Round {}: Both connected (ack_dropped={})",
round, ack_dropped
);
break;
}
if round % 5 == 4 {
now += Duration::from_secs(2);
} else {
now += Duration::from_millis(10);
}
}
assert!(client_connected, "Client should connect despite ACK loss");
assert!(server_connected, "Server should connect despite ACK loss");
assert!(ack_dropped, "Test should have dropped an ACK packet");
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_retransmit_during_hrr_flow() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = Arc::new(
Config::builder()
.flight_retries(8)
.build()
.expect("Failed to build config"),
);
let mut now = Instant::now();
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let mut server = Dtls::new_13(config, server_cert, now);
server.set_active(false);
let mut client_connected = false;
let mut server_connected = false;
let mut client_flight_count = 0;
let mut dropped_server_hrr = false;
for round in 0..100 {
client.handle_timeout(now).expect("client timeout");
server.handle_timeout(now).expect("server timeout");
let client_out = drain_outputs(&mut client);
let server_out = drain_outputs(&mut server);
client_connected |= client_out.connected;
server_connected |= server_out.connected;
if !client_out.packets.is_empty() {
client_flight_count += 1;
}
deliver_packets(&client_out.packets, &mut server);
if !dropped_server_hrr && !server_out.packets.is_empty() && client_flight_count <= 1 {
dropped_server_hrr = true;
eprintln!("Round {}: Dropped server HRR response", round);
} else {
deliver_packets(&server_out.packets, &mut client);
}
if client_connected && server_connected {
break;
}
if round % 5 == 4 {
now += Duration::from_secs(2);
} else {
now += Duration::from_millis(10);
}
}
assert!(
client_connected,
"Client should connect despite HRR packet loss"
);
assert!(
server_connected,
"Server should connect despite HRR packet loss"
);
assert!(
dropped_server_hrr,
"Test should have dropped the HRR response"
);
}
#[test]
#[cfg(feature = "rcgen")]
fn dtls13_handshake_timeout_aborts() {
use dimpl::certificate::generate_self_signed_certificate;
let _ = env_logger::try_init();
let client_cert = generate_self_signed_certificate().expect("gen client cert");
let server_cert = generate_self_signed_certificate().expect("gen server cert");
let config = Arc::new(
Config::builder()
.handshake_timeout(Duration::from_secs(5))
.flight_retries(10) .flight_start_rto(Duration::from_millis(200))
.build()
.expect("Failed to build config"),
);
let mut now = Instant::now();
let start = now;
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
client.set_active(true);
let _server = Dtls::new_13(config, server_cert, now);
client.handle_timeout(now).expect("client start");
client.handle_timeout(now).expect("client arm");
let _ = collect_packets(&mut client);
let mut got_timeout_error = false;
for _ in 0..200 {
let out = drain_outputs(&mut client);
let Some(timeout_instant) = out.timeout else {
break;
};
now = timeout_instant;
match client.handle_timeout(now) {
Ok(()) => {
let _ = collect_packets(&mut client);
}
Err(e) => {
let msg = format!("{}", e);
assert!(
msg.contains("timeout"),
"Expected timeout error, got: {}",
msg
);
got_timeout_error = true;
break;
}
}
}
assert!(
got_timeout_error,
"Client should have aborted with a timeout error"
);
let elapsed = now.duration_since(start);
assert!(
elapsed <= Duration::from_secs(10),
"Timeout should have fired within a reasonable time, took {:?}",
elapsed
);
eprintln!(
"SUCCESS: Handshake aborted after {:?} with timeout error",
elapsed
);
}