diff --git a/CHANGELOG.md b/CHANGELOG.md index 299abca..d27e0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased + * Fix DTLS 1.2 ClientHello retransmissions #160 + # 0.7.2 * Stop advertising unsupported RSA signatures in DTLS 1.2 CertificateRequest #157 diff --git a/src/dtls12/engine.rs b/src/dtls12/engine.rs index dd4b9c2..9310eb7 100644 --- a/src/dtls12/engine.rs +++ b/src/dtls12/engine.rs @@ -514,6 +514,13 @@ impl Engine { } match (self.connect_timeout, self.flight_timeout) { + // Keep this before the `(Armed, _)` arms. Starting a new flight resets its timer to + // `Unarmed`, but leaves the overall connection timer armed. If that mixed state + // returned the connection deadline, the caller would not drive `handle_timeout` to + // arm the flight timer until the whole handshake expired, so the flight would never + // be retransmitted. Returning `now` requests that immediate drive; the next poll sees + // both concrete deadlines and can return the earlier one. + (Timeout::Unarmed, _) | (_, Timeout::Unarmed) => now, (Timeout::Armed(c), Timeout::Armed(f)) => { if c < f { c @@ -523,8 +530,6 @@ impl Engine { } (Timeout::Armed(c), _) => c, (_, Timeout::Armed(f)) => f, - // Both Unarmed or mixed Unarmed/Disabled: return current time - // to trigger handle_timeout on the next cycle. _ => now, } } diff --git a/tests/dtls12/retransmit.rs b/tests/dtls12/retransmit.rs index eb7a644..b1c9dde 100644 --- a/tests/dtls12/retransmit.rs +++ b/tests/dtls12/retransmit.rs @@ -155,6 +155,50 @@ fn first_record_matching(datagrams: &[Vec], content_type: u8, epoch: u16) -> None } +#[test] +#[cfg(feature = "rcgen")] +fn dtls12_client_hello_retransmits_using_advertised_deadlines() { + use dimpl::certificate::generate_self_signed_certificate; + + let now = Instant::now(); + let certificate = generate_self_signed_certificate().expect("generate client certificate"); + let mut client = Dtls::new_12(Arc::new(Config::default()), certificate, now); + client.set_active(true); + + client.handle_timeout(now).expect("start client handshake"); + let initial_output = drain_outputs(&mut client); + assert!( + !initial_output.packets.is_empty(), + "client should emit ClientHello" + ); + + // Drop the initial flight and handle the timeout. + let arm_at = initial_output + .timeout + .expect("client should advertise a deadline"); + client + .handle_timeout(arm_at) + .expect("arm ClientHello retransmission"); + let after_timer_arming = drain_outputs(&mut client); + assert!( + after_timer_arming.packets.is_empty(), + "arming should not retransmit early" + ); + + let retransmit_at = after_timer_arming + .timeout + .expect("client should advertise a retransmission deadline"); + client + .handle_timeout(retransmit_at) + .expect("retransmit ClientHello"); + let retransmission_output = drain_outputs(&mut client); + + assert!( + !retransmission_output.packets.is_empty(), + "the dropped ClientHello flight was not retransmitted" + ); +} + #[test] #[cfg(feature = "rcgen")] fn dtls12_resends_each_flight_epoch_and_sequence_increase() {