@@ -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