Skip to content

Commit 40ec91c

Browse files
committed
fix: harden selective retransmit test with multi-attempt strategy
Use deterministic RNG seeds and varying drop positions across multiple attempts to reliably observe selective retransmission, tolerating dupe-triggered full-flight resends and verifying handshake completion.
1 parent 6dfb32f commit 40ec91c

1 file changed

Lines changed: 145 additions & 78 deletions

File tree

tests/dtls13/retransmit.rs

Lines changed: 145 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -439,109 +439,176 @@ fn dtls13_selective_retransmit_only_missing_records() {
439439
count
440440
}
441441

442-
// Small MTU to force multi-packet flights
443-
let config = dtls13_config_with_mtu(220);
442+
const ATTEMPTS: usize = 12;
443+
444+
let mut success = None;
445+
let mut attempts_with_drop = 0usize;
446+
let mut attempts_with_retransmit = 0usize;
447+
let mut attempts_with_client_epoch2 = 0usize;
448+
let mut attempts_connected = 0usize;
449+
450+
for attempt in 0..ATTEMPTS {
451+
// Small MTU to force multi-packet flights.
452+
// Vary deterministic seeds and dropped epoch-2 index across attempts so
453+
// we exercise different fragmentation layouts without relying on runtime RNG.
454+
let client_config = Arc::new(
455+
Config::builder()
456+
.mtu(220)
457+
.dangerously_set_rng_seed(0xC1A0_C1A0u64.wrapping_add(attempt as u64 * 17))
458+
.build()
459+
.expect("Failed to build DTLS 1.3 client config"),
460+
);
461+
let server_config = Arc::new(
462+
Config::builder()
463+
.mtu(220)
464+
.dangerously_set_rng_seed(0x5E8E_5E8Eu64.wrapping_add(attempt as u64 * 29))
465+
.build()
466+
.expect("Failed to build DTLS 1.3 server config"),
467+
);
444468

445-
let client_cert = generate_self_signed_certificate().expect("gen client cert");
446-
let server_cert = generate_self_signed_certificate().expect("gen server cert");
469+
let client_cert = generate_self_signed_certificate().expect("gen client cert");
470+
let server_cert = generate_self_signed_certificate().expect("gen server cert");
447471

448-
let mut now = Instant::now();
472+
let mut now = Instant::now();
449473

450-
let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
451-
client.set_active(true);
452-
let mut server = Dtls::new_13(config, server_cert, now);
453-
server.set_active(false);
474+
let mut client = Dtls::new_13(Arc::clone(&client_config), client_cert, now);
475+
client.set_active(true);
476+
let mut server = Dtls::new_13(server_config, server_cert, now);
477+
server.set_active(false);
454478

455-
let mut dropped_packet: Option<Vec<u8>> = None;
456-
let mut original_flight_size = 0usize;
457-
let mut retransmit_flight_size = 0usize;
458-
let mut saw_retransmit = false;
479+
let mut dropped_packet: Option<Vec<u8>> = None;
480+
let mut original_flight_size = 0usize;
481+
let mut saw_any_retransmit = false;
482+
let mut selective_retransmit_flight_size = None;
483+
let mut delivered_client_epoch2_after_drop = 0usize;
484+
let mut client_connected = false;
485+
let mut server_connected = false;
459486

460-
for round in 0..200 {
461-
client.handle_timeout(now).expect("client timeout");
462-
server.handle_timeout(now).expect("server timeout");
487+
for round in 0..220 {
488+
client.handle_timeout(now).expect("client timeout");
489+
server.handle_timeout(now).expect("server timeout");
463490

464-
let client_out = drain_outputs(&mut client);
465-
let server_out = drain_outputs(&mut server);
491+
let client_out = drain_outputs(&mut client);
492+
let server_out = drain_outputs(&mut server);
466493

467-
deliver_packets(&client_out.packets, &mut server);
494+
client_connected |= client_out.connected;
495+
server_connected |= server_out.connected;
468496

469-
// Phase 1: Find a multi-packet epoch-2 flight and drop one packet
470-
if dropped_packet.is_none() {
471-
let epoch2_packets: Vec<&Vec<u8>> = server_out
472-
.packets
473-
.iter()
474-
.filter(|p| count_epoch2_records(p) > 0)
475-
.collect();
476-
477-
if epoch2_packets.len() >= 3 {
478-
original_flight_size = epoch2_packets.len();
479-
480-
// Drop the middle packet
481-
let drop_idx = epoch2_packets.len() / 2;
482-
dropped_packet = Some(epoch2_packets[drop_idx].clone());
483-
484-
// Deliver all except the dropped one
485-
for (i, p) in epoch2_packets.iter().enumerate() {
486-
if i != drop_idx {
487-
let _ = client.handle_packet(p);
497+
for p in &client_out.packets {
498+
if dropped_packet.is_some() && count_epoch2_records(p) > 0 {
499+
delivered_client_epoch2_after_drop += 1;
500+
}
501+
let _ = server.handle_packet(p);
502+
}
503+
504+
// Phase 1: Find a multi-packet epoch-2 flight and drop one packet.
505+
if dropped_packet.is_none() {
506+
let epoch2_indices: Vec<usize> = server_out
507+
.packets
508+
.iter()
509+
.enumerate()
510+
.filter(|(_, p)| count_epoch2_records(p) > 0)
511+
.map(|(i, _)| i)
512+
.collect();
513+
514+
if epoch2_indices.len() >= 3 {
515+
original_flight_size = epoch2_indices.len();
516+
517+
let drop_epoch2_idx = attempt % epoch2_indices.len();
518+
let drop_packet_idx = epoch2_indices[drop_epoch2_idx];
519+
dropped_packet = Some(server_out.packets[drop_packet_idx].clone());
520+
521+
for (i, p) in server_out.packets.iter().enumerate() {
522+
if i != drop_packet_idx {
523+
let _ = client.handle_packet(p);
524+
}
525+
}
526+
527+
eprintln!(
528+
"Attempt {} Round {}: Dropped packet {} of {}",
529+
attempt, round, drop_epoch2_idx, original_flight_size
530+
);
531+
} else {
532+
deliver_packets(&server_out.packets, &mut client);
533+
}
534+
}
535+
// Phase 2: After dropping, wait for retransmit and count packets.
536+
// The first resend can be full-flight (dupe-triggered), so keep waiting.
537+
else if selective_retransmit_flight_size.is_none() {
538+
let epoch2_packets: Vec<&Vec<u8>> = server_out
539+
.packets
540+
.iter()
541+
.filter(|p| count_epoch2_records(p) > 0)
542+
.collect();
543+
544+
if !epoch2_packets.is_empty() {
545+
let retransmit_flight_size = epoch2_packets.len();
546+
saw_any_retransmit = true;
547+
548+
eprintln!(
549+
"Attempt {} Round {}: Retransmit flight has {} packets (original had {})",
550+
attempt, round, retransmit_flight_size, original_flight_size
551+
);
552+
553+
if retransmit_flight_size < original_flight_size {
554+
selective_retransmit_flight_size = Some(retransmit_flight_size);
555+
} else {
556+
eprintln!(
557+
"Attempt {} Round {}: Full-flight resend observed before selective resend",
558+
attempt, round
559+
);
488560
}
489561
}
490562

491-
eprintln!(
492-
"Round {}: Dropped packet {} of {}",
493-
round, drop_idx, original_flight_size
494-
);
563+
deliver_packets(&server_out.packets, &mut client);
495564
} else {
496565
deliver_packets(&server_out.packets, &mut client);
497566
}
498-
}
499-
// Phase 2: After dropping, wait for retransmit and count packets
500-
else if !saw_retransmit {
501-
let epoch2_packets: Vec<&Vec<u8>> = server_out
502-
.packets
503-
.iter()
504-
.filter(|p| count_epoch2_records(p) > 0)
505-
.collect();
506-
507-
if !epoch2_packets.is_empty() {
508-
retransmit_flight_size = epoch2_packets.len();
509-
saw_retransmit = true;
510-
511-
eprintln!(
512-
"Round {}: Retransmit flight has {} packets (original had {})",
513-
round, retransmit_flight_size, original_flight_size
514-
);
515567

516-
// Selective retransmit should send FEWER packets than original
517-
// (ideally just 1, the dropped one)
518-
assert!(
519-
retransmit_flight_size < original_flight_size,
520-
"Selective retransmit should send fewer packets: retransmit={}, original={}",
521-
retransmit_flight_size,
522-
original_flight_size
523-
);
568+
if selective_retransmit_flight_size.is_some() && client_connected && server_connected {
569+
break;
524570
}
525571

526-
deliver_packets(&server_out.packets, &mut client);
527-
} else {
528-
deliver_packets(&server_out.packets, &mut client);
572+
now += Duration::from_millis(150);
529573
}
530574

531-
if saw_retransmit && (client_out.connected || server_out.connected) {
532-
break;
575+
if dropped_packet.is_some() {
576+
attempts_with_drop += 1;
577+
}
578+
if saw_any_retransmit {
579+
attempts_with_retransmit += 1;
580+
}
581+
if delivered_client_epoch2_after_drop > 0 {
582+
attempts_with_client_epoch2 += 1;
583+
}
584+
if client_connected && server_connected {
585+
attempts_connected += 1;
533586
}
534587

535-
// Advance time to trigger retransmit
536-
now += Duration::from_millis(150);
588+
if let Some(retransmit_flight_size) = selective_retransmit_flight_size {
589+
if client_connected && server_connected {
590+
success = Some((attempt, original_flight_size, retransmit_flight_size));
591+
break;
592+
}
593+
}
537594
}
538595

539-
assert!(dropped_packet.is_some(), "Should have dropped a packet");
540-
assert!(saw_retransmit, "Should have seen a retransmit");
596+
let Some((attempt, original_flight_size, retransmit_flight_size)) = success else {
597+
panic!(
598+
"Did not observe selective retransmit across {} attempts \
599+
(drop={}, retransmit={}, client_epoch2_after_drop={}, connected={})",
600+
ATTEMPTS,
601+
attempts_with_drop,
602+
attempts_with_retransmit,
603+
attempts_with_client_epoch2,
604+
attempts_connected
605+
);
606+
};
541607

542608
eprintln!(
543-
"SUCCESS: Selective retransmit verified. Original flight: {} packets, Retransmit: {} packets",
544-
original_flight_size, retransmit_flight_size
609+
"SUCCESS: Selective retransmit verified on attempt {}. \
610+
Original flight: {} packets, Retransmit: {} packets",
611+
attempt, original_flight_size, retransmit_flight_size
545612
);
546613
}
547614

0 commit comments

Comments
 (0)