Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Build
run: cargo +${{steps.toolchain.outputs.name}} build --no-default-features --features "${{ matrix.feature }}"
- name: Test
if: matrix.feature == 'aws-lc-rs' || matrix.feature == 'rust-crypto'
if: matrix.feature == 'rcgen' || matrix.feature == 'rust-crypto'
run: cargo +${{steps.toolchain.outputs.name}} test --no-default-features --features "${{ matrix.feature }}"

snowflake:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

* Start the handshake deadline at the first sent packet, not at construction #161
* Apply flight retry jitter as ±25% of the RTO instead of ±250ms #161

# 0.7.5

* Reject internally reordered DTLS 1.2 datagrams #169
Expand Down
67 changes: 37 additions & 30 deletions src/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/// and falls back to DTLS 1.2 via [`Error::Dtls12Fallback`] if the
/// reassembled ClientHello does not offer DTLS 1.3.
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Instant;

use arrayvec::ArrayVec;

Expand All @@ -28,6 +28,7 @@ use crate::dtls13::message::Random;
use crate::dtls13::message::SignatureAlgorithmsExtension;
use crate::dtls13::message::SupportedGroupsExtension;
use crate::dtls13::message::UseSrtpExtension;
use crate::timer::HandshakeTimers;
use crate::types::NamedGroup;
use crate::{Config, CryptoError, DtlsCertificate, Error, Output, SeededRng, TimeoutError};
// Extension type constants
Expand Down Expand Up @@ -269,10 +270,8 @@ pub(crate) struct ClientPending {
needs_send: bool,
/// Last time handle_timeout was called.
last_now: Instant,
/// When to retransmit the wire_packet.
retransmit_at: Option<Instant>,
/// How many retransmits have occurred.
retransmit_count: usize,
timers: HandshakeTimers,
rng: SeededRng,
}

impl ClientPending {
Expand All @@ -283,38 +282,32 @@ impl ClientPending {
) -> Result<Self, Error> {
let hybrid = HybridClientHello::new(&config)?;
let wire_packet = hybrid.wire_packet();
let mut rng = SeededRng::new(config.rng_seed());
let mut timers = HandshakeTimers::new(&config, &mut rng);
timers.begin_flight(&mut rng);
Ok(ClientPending {
hybrid,
config,
certificate,
wire_packet,
needs_send: true,
last_now: now,
retransmit_at: None,
retransmit_count: 0,
timers,
rng,
})
}

pub fn handle_timeout(&mut self, now: Instant) -> Result<(), Error> {
self.last_now = now;
// Arm initial retransmit timer on first call
if self.retransmit_at.is_none() {
self.retransmit_at = Some(now + Duration::from_secs(1));
return Ok(());
}
if let Some(deadline) = self.retransmit_at {
if now >= deadline {
if self.retransmit_count >= self.config.flight_retries() {
return Err(Error::Timeout(TimeoutError::HybridClientHello));
}
self.retransmit_count += 1;
self.needs_send = true;
// Exponential backoff: 2s, 4s, 8s, ...
let shift = self.retransmit_count.min(5) as u32;
let rto = Duration::from_secs(1u64 << shift);
self.retransmit_at = Some(now + rto);
}
}
self.needs_send |= self
.timers
.handle_timeout(now, &mut self.rng)
.map_err(|error| {
Error::Timeout(match error {
TimeoutError::Handshake => TimeoutError::HybridClientHello,
other => other,
})
})?;
Ok(())
}

Expand All @@ -327,16 +320,30 @@ impl ClientPending {
}
self.needs_send = false;
buf[..len].copy_from_slice(&self.wire_packet);
self.timers.start_handshake();
self.timers.flight_sent();
return Output::Packet(&buf[..len]);
}
let next = self
.retransmit_at
.unwrap_or(self.last_now + Duration::from_secs(1));
let next = self.timers.poll_timeout(self.last_now);
Output::Timeout(next)
}

pub fn into_parts(self) -> (HybridClientHello, Arc<Config>, DtlsCertificate, Instant) {
(self.hybrid, self.config, self.certificate, self.last_now)
pub fn into_parts(
self,
) -> (
HybridClientHello,
Arc<Config>,
DtlsCertificate,
Instant,
HandshakeTimers,
) {
(
self.hybrid,
self.config,
self.certificate,
self.last_now,
self.timers,
)
}
}

Expand Down
34 changes: 31 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ impl ConfigBuilder {

/// Set the time of first retry.
///
/// Every flight restarts with this value.
/// Doubled for every retry with a ±25% jitter.
/// Every flight, including a DTLS 1.3 KeyUpdate, restarts with this value.
/// Doubled for every retry with a ±25% jitter. Must be nonzero.
/// Defaults to 1 second.
pub fn flight_start_rto(mut self, rto: Duration) -> Self {
self.flight_start_rto = rto;
Expand All @@ -372,6 +372,10 @@ impl ConfigBuilder {

/// Set the max number of retries per flight.
///
/// Excludes the initial send; 0 disables retransmission. Timer-driven and
/// duplicate-triggered resends share this budget. Exhausting it fails the
/// handshake, which may happen before [`Self::handshake_timeout`]: with the
/// defaults an unanswered flight gives up after roughly 31 seconds.
/// Defaults to 4.
pub fn flight_retries(mut self, retries: usize) -> Self {
self.flight_retries = retries;
Expand All @@ -380,7 +384,11 @@ impl ConfigBuilder {

/// Set the timeout for the entire handshake, regardless of flights.
///
/// Defaults to 40 seconds.
/// Starts when a client emits its first ClientHello packet, or a server
/// accepts its first ClientHello fragment, and is one absolute deadline
/// across cookie exchanges, retransmissions and Auto version selection.
/// Idle time before that is free. It no longer applies once connected.
/// Must be nonzero. Defaults to 40 seconds.
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
self.handshake_timeout = timeout;
self
Expand Down Expand Up @@ -518,6 +526,13 @@ impl ConfigBuilder {
return Err(Error::ConfigError(ConfigError::AeadEncryptionLimitTooSmall));
}

if self.handshake_timeout.is_zero() {
return Err(Error::ConfigError(ConfigError::HandshakeTimeoutTooSmall));
}
if self.flight_start_rto.is_zero() {
return Err(Error::ConfigError(ConfigError::FlightStartRtoTooSmall));
}

// Validate cipher suite filters: at least one version must have suites.
// Mirror Config::dtls12_cipher_suites() by dropping PSK suites when no PSK
// is configured, so a PSK-only filter without a PSK resolver fails fast.
Expand Down Expand Up @@ -734,6 +749,19 @@ mod tests {
}
}

#[test]
fn rejects_zero_timing() {
let zero = Duration::ZERO;
assert_eq!(
Config::builder().handshake_timeout(zero).build().err(),
Some(Error::ConfigError(ConfigError::HandshakeTimeoutTooSmall))
);
assert_eq!(
Config::builder().flight_start_rto(zero).build().err(),
Some(Error::ConfigError(ConfigError::FlightStartRtoTooSmall))
);
}

#[test]
fn accepts_minimum_aead_limit() {
Config::builder()
Expand Down
15 changes: 6 additions & 9 deletions src/dtls12/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::dtls12::message::{CompressionMethod, ContentType, Cookie};
use crate::dtls12::message::{DigitallySigned, Dtls12CipherSuite};
use crate::dtls12::message::{ExtensionType, KeyExchangeAlgorithm, MessageType, ProtocolVersion};
use crate::dtls12::message::{Random, SessionId, SignatureAndHashAlgorithm, UseSrtpExtension};
use crate::timer::HandshakeTimers;
use crate::{Config, DtlsCertificate, Error, InternalError, KeyingMaterial, Output};

/// DTLS client
Expand Down Expand Up @@ -129,6 +130,7 @@ impl Client {
config: std::sync::Arc<Config>,
certificate: DtlsCertificate,
now: Instant,
timers: HandshakeTimers,
) -> Result<Client, Error> {
assert!(
!certificate.certificate.is_empty(),
Expand All @@ -149,15 +151,7 @@ impl Client {
};
let mut engine = Engine::new(config, auth);
engine.set_client(true);
// The hybrid ClientHello was sent with message_seq=0 outside this
// engine. Advance the counter so the with-cookie CH gets message_seq=1
// per RFC 6347 §4.2.2.
engine.set_next_handshake_seq_no(1);
// Inject the hybrid CH into the transcript so it matches the server's
// transcript when the server skips HelloVerifyRequest.
engine.transcript.extend_from_slice(handshake_fragment);
// Advance epoch-0 record sequence past the hybrid CH record.
engine.advance_epoch_0_sequence();
engine.inject_hybrid_client_hello(handshake_fragment, timers);
let extension_data = engine.pop_buffer();
let defragment_buffer = engine.pop_buffer();

Expand Down Expand Up @@ -212,6 +206,9 @@ impl Client {
}

pub fn poll_output<'a>(&mut self, buf: &'a mut [u8]) -> Output<'a> {
if self.state == State::SendClientHello {
return Output::Timeout(self.last_now);
}
if let Some(event) = self.local_events.pop_front() {
return event.into_output(buf, &self.server_certificates);
}
Expand Down
Loading
Loading