Skip to content

Commit 0b6103f

Browse files
committed
fix: handle fragmented ClientHello in auto-sense server
Firefox 148+ sends X25519MLKEM768 as its primary key_share, making the ClientHello too large for a single DTLS record. The supported_versions extension ends up in a later fragment, causing the auto-sense server to miss it and fall back to DTLS 1.2. Replace the lightweight packet-sniffing version detection (ServerPending) with a design that uses Server13 directly in auto mode. The DTLS 1.3 engine handles fragment reassembly natively. After reassembling the complete ClientHello: - If supported_versions contains DTLS 1.3: proceed as 1.3 (zero overhead) - If not: return Error::Dtls12Fallback, create Server12, replay packets Changes: - Add Error::Dtls12Fallback variant - Add auto_mode + auto_packets to Server13 - Remove ServerPending from Inner enum and detect.rs - new_auto() creates Server13::new_auto(), falls back to Server12 on Dtls12Fallback or ParseError - Server13 buffers raw packets during AwaitClientHello, frees on commit - 47 new tests: server_fallback (15) + cross_matrix (32) covering all version combinations at normal/fragmented/heavy MTU
1 parent 186f991 commit 0b6103f

9 files changed

Lines changed: 1348 additions & 198 deletions

File tree

src/detect.rs

Lines changed: 10 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
/// Version detection and hybrid ClientHello for auto-sensing DTLS endpoints.
22
///
3-
/// Both `client_hello_version` and `server_hello_version` do lightweight
4-
/// parsing of the first record in a datagram, looking for the
5-
/// `supported_versions` extension to decide between DTLS 1.2 and 1.3.
3+
/// `server_hello_version` does lightweight parsing of the first record in a
4+
/// datagram. It looks for a `HelloVerifyRequest` or a `ServerHello` with the
5+
/// `supported_versions` extension to decide between DTLS 1.2 and 1.3 for the
6+
/// client auto-sense path.
67
///
78
/// [`HybridClientHello`] constructs a ClientHello compatible with both
8-
/// DTLS 1.2 and 1.3 servers: it offers both version's cipher suites and
9+
/// DTLS 1.2 and 1.3 servers: it offers both versions cipher suites and
910
/// includes `supported_versions` with both versions. Once the server
1011
/// responds, the caller inspects the reply with `server_hello_version`
1112
/// and forks into the appropriate handshake path.
13+
///
14+
/// Server-side auto-sense does not use lightweight detection. Instead,
15+
/// it starts a DTLS 1.3 server which handles fragment reassembly natively
16+
/// and falls back to DTLS 1.2 via [`Error::Dtls12Fallback`] if the
17+
/// reassembled ClientHello does not offer DTLS 1.3.
1218
use std::sync::Arc;
1319
use std::time::{Duration, Instant};
1420

@@ -245,35 +251,6 @@ impl HybridClientHello {
245251
}
246252
}
247253

248-
/// Auto-sense server waiting for the first ClientHello to determine the DTLS version.
249-
pub(crate) struct ServerPending {
250-
config: Arc<Config>,
251-
certificate: DtlsCertificate,
252-
last_now: Instant,
253-
}
254-
255-
impl ServerPending {
256-
pub fn new(config: Arc<Config>, certificate: DtlsCertificate, now: Instant) -> Self {
257-
ServerPending {
258-
config,
259-
certificate,
260-
last_now: now,
261-
}
262-
}
263-
264-
pub fn handle_timeout(&mut self, now: Instant) {
265-
self.last_now = now;
266-
}
267-
268-
pub fn poll_output<'a>(&self, _buf: &'a mut [u8]) -> Output<'a> {
269-
Output::Timeout(self.last_now + Duration::from_secs(86400))
270-
}
271-
272-
pub fn into_parts(self) -> (Arc<Config>, DtlsCertificate, Instant) {
273-
(self.config, self.certificate, self.last_now)
274-
}
275-
}
276-
277254
/// Auto-sense client that sends a hybrid ClientHello and waits for the server's response
278255
/// to determine the DTLS version.
279256
pub(crate) struct ClientPending {
@@ -373,117 +350,6 @@ pub(crate) enum DetectedVersion {
373350
Unknown,
374351
}
375352

376-
/// Detect DTLS version from a ClientHello packet.
377-
///
378-
/// Returns `Dtls13` if the `supported_versions` extension contains
379-
/// DTLS 1.3 (0xFEFC), otherwise `Dtls12`.
380-
pub(crate) fn client_hello_version(packet: &[u8]) -> DetectedVersion {
381-
match client_hello_version_inner(packet) {
382-
Some(true) => DetectedVersion::Dtls13,
383-
_ => DetectedVersion::Dtls12,
384-
}
385-
}
386-
387-
fn client_hello_version_inner(packet: &[u8]) -> Option<bool> {
388-
// Record header: content_type(1) + version(2) + epoch(2) + seq(6) + length(2) = 13
389-
if packet.len() < 13 {
390-
return Some(false);
391-
}
392-
393-
// content_type must be 0x16 (Handshake)
394-
if packet[0] != 0x16 {
395-
return Some(false);
396-
}
397-
398-
let record_len = u16::from_be_bytes([packet[11], packet[12]]) as usize;
399-
let record_body = packet.get(13..13 + record_len)?;
400-
401-
// Handshake header: msg_type(1) + length(3) + message_seq(2) +
402-
// fragment_offset(3) + fragment_length(3) = 12
403-
if record_body.len() < 12 {
404-
return Some(false);
405-
}
406-
407-
// msg_type must be 1 (ClientHello)
408-
if record_body[0] != 1 {
409-
return Some(false);
410-
}
411-
412-
let fragment_len = ((record_body[9] as usize) << 16)
413-
| ((record_body[10] as usize) << 8)
414-
| (record_body[11] as usize);
415-
let body = record_body.get(12..12 + fragment_len)?;
416-
417-
// ClientHello body:
418-
// client_version(2) + random(32) = 34 minimum before session_id
419-
if body.len() < 34 {
420-
return Some(false);
421-
}
422-
let mut pos = 34;
423-
424-
// session_id: 1-byte length + data
425-
let sid_len = *body.get(pos)? as usize;
426-
pos += 1 + sid_len;
427-
428-
// cookie: 1-byte length + data
429-
let cookie_len = *body.get(pos)? as usize;
430-
pos += 1 + cookie_len;
431-
432-
// cipher_suites: 2-byte length + data
433-
if pos + 2 > body.len() {
434-
return Some(false);
435-
}
436-
let cs_len = u16::from_be_bytes([body[pos], body[pos + 1]]) as usize;
437-
pos += 2 + cs_len;
438-
439-
// compression_methods: 1-byte length + data
440-
let cm_len = *body.get(pos)? as usize;
441-
pos += 1 + cm_len;
442-
443-
// extensions: 2-byte total length
444-
if pos + 2 > body.len() {
445-
return Some(false);
446-
}
447-
let ext_total_len = u16::from_be_bytes([body[pos], body[pos + 1]]) as usize;
448-
pos += 2;
449-
let ext_end = pos + ext_total_len;
450-
if ext_end > body.len() {
451-
return Some(false);
452-
}
453-
454-
// Walk extensions looking for supported_versions (0x002B)
455-
while pos + 4 <= ext_end {
456-
let ext_type = u16::from_be_bytes([body[pos], body[pos + 1]]);
457-
let ext_len = u16::from_be_bytes([body[pos + 2], body[pos + 3]]) as usize;
458-
pos += 4;
459-
460-
if ext_type == 0x002B {
461-
// supported_versions client format: 1-byte list length, then 2-byte versions
462-
if ext_len < 1 {
463-
return Some(false);
464-
}
465-
let list_len = body[pos] as usize;
466-
let list_start = pos + 1;
467-
if list_start + list_len > pos + ext_len {
468-
return Some(false);
469-
}
470-
let mut i = list_start;
471-
while i + 2 <= list_start + list_len {
472-
let version = u16::from_be_bytes([body[i], body[i + 1]]);
473-
if version == 0xFEFC {
474-
return Some(true);
475-
}
476-
i += 2;
477-
}
478-
return Some(false);
479-
}
480-
481-
pos += ext_len;
482-
}
483-
484-
Some(false)
485-
}
486-
487353
/// Detect DTLS version from a server response (ServerHello or HelloVerifyRequest).
488354
///
489355
/// - HelloVerifyRequest (msg_type 3) → `Dtls12`

src/dtls13/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl Client {
210210
}
211211

212212
pub fn into_server(self) -> Server {
213-
Server::new_with_engine(self.engine, self.last_now)
213+
Server::new_with_engine(self.engine, self.last_now, false)
214214
}
215215

216216
pub(crate) fn state_name(&self) -> &'static str {

src/dtls13/server.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ pub struct Server {
135135

136136
/// Whether we need to respond with our own KeyUpdate.
137137
pending_key_update_response: bool,
138+
139+
/// When true, a ClientHello without DTLS 1.3 in `supported_versions`
140+
/// returns [`Error::Dtls12Fallback`] instead of a security error.
141+
/// Used by the auto-sense server path.
142+
auto_mode: bool,
143+
144+
/// Raw packets buffered during auto-sense so they can be replayed
145+
/// to a DTLS 1.2 server on fallback.
146+
auto_packets: Vec<Vec<u8>>,
138147
}
139148

140149
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -156,10 +165,21 @@ impl Server {
156165
/// Create a new DTLS 1.3 server.
157166
pub fn new(config: Arc<Config>, certificate: DtlsCertificate, now: Instant) -> Server {
158167
let engine = Engine::new(config, certificate);
159-
Self::new_with_engine(engine, now)
168+
Self::new_with_engine(engine, now, false)
169+
}
170+
171+
/// Create a new DTLS 1.3 server in auto-sense mode.
172+
///
173+
/// In auto-sense mode, if the ClientHello does not offer DTLS 1.3
174+
/// in `supported_versions`, the server returns [`Error::Dtls12Fallback`]
175+
/// instead of a fatal security error, allowing the caller to switch
176+
/// to a DTLS 1.2 server.
177+
pub fn new_auto(config: Arc<Config>, certificate: DtlsCertificate, now: Instant) -> Server {
178+
let engine = Engine::new(config, certificate);
179+
Self::new_with_engine(engine, now, true)
160180
}
161181

162-
pub(crate) fn new_with_engine(mut engine: Engine, now: Instant) -> Server {
182+
pub(crate) fn new_with_engine(mut engine: Engine, now: Instant, auto_mode: bool) -> Server {
163183
let cookie_secret = engine.random_arr();
164184

165185
Server {
@@ -183,23 +203,64 @@ impl Server {
183203
hello_retry: false,
184204
cookie_secret,
185205
pending_key_update_response: false,
206+
auto_mode,
207+
auto_packets: Vec::new(),
186208
}
187209
}
188210

189211
pub fn into_client(self) -> Client {
190212
Client::new_with_engine(self.engine, self.last_now)
191213
}
192214

215+
/// Whether this server is in auto-sense mode.
216+
pub fn is_auto_mode(&self) -> bool {
217+
self.auto_mode
218+
}
219+
220+
/// The last time instant seen by this server.
221+
pub fn last_now(&self) -> Instant {
222+
self.last_now
223+
}
224+
193225
pub(crate) fn state_name(&self) -> &'static str {
194226
self.state.name()
195227
}
196228

197229
pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> {
230+
// In auto-sense mode, buffer raw packets while still waiting for
231+
// the ClientHello so they can be replayed to Server12 on fallback.
232+
if self.auto_mode && self.state == State::AwaitClientHello {
233+
// Cap buffered fragments to prevent unbounded growth from malicious traffic
234+
if self.auto_packets.len() >= 64 {
235+
return Err(Error::SecurityError(
236+
"too many fragmented packets during auto-sense".to_string(),
237+
));
238+
}
239+
self.auto_packets.push(packet.to_vec());
240+
}
241+
198242
self.engine.parse_packet(packet)?;
199243
self.make_progress()?;
244+
245+
// Once past AwaitClientHello, DTLS 1.3 is committed — free the buffer.
246+
if self.auto_mode && self.state != State::AwaitClientHello {
247+
self.auto_packets.clear();
248+
self.auto_mode = false;
249+
}
250+
200251
Ok(())
201252
}
202253

254+
/// Take the buffered raw packets for DTLS 1.2 fallback replay.
255+
pub fn take_auto_packets(&mut self) -> Vec<Vec<u8>> {
256+
std::mem::take(&mut self.auto_packets)
257+
}
258+
259+
/// Number of buffered auto-sense packets.
260+
pub fn auto_packet_count(&self) -> usize {
261+
self.auto_packets.len()
262+
}
263+
203264
pub fn poll_output<'a>(&mut self, buf: &'a mut [u8]) -> Output<'a> {
204265
if let Some(event) = self.local_events.pop_front() {
205266
return event.into_output(buf, &self.client_certificates);
@@ -392,6 +453,9 @@ impl State {
392453
}
393454

394455
if !supported_versions_ok {
456+
if server.auto_mode {
457+
return Err(Error::Dtls12Fallback);
458+
}
395459
return Err(Error::SecurityError(
396460
"ClientHello must include DTLS 1.3 in supported_versions".to_string(),
397461
));

src/error.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ pub enum Error {
3636
/// resolved. Callers should buffer the data and retry once the
3737
/// handshake advances.
3838
HandshakePending,
39+
/// The DTLS 1.3 server received a ClientHello that does not offer
40+
/// DTLS 1.3 in `supported_versions`. In auto-sense mode the caller
41+
/// should fall back to a DTLS 1.2 server and replay the buffered
42+
/// packets.
43+
Dtls12Fallback,
3944
}
4045

4146
impl<'a> From<nom::Err<nom::error::Error<&'a [u8]>>> for Error {
@@ -69,6 +74,12 @@ impl std::fmt::Display for Error {
6974
Error::HandshakePending => {
7075
write!(f, "handshake pending: cannot send application data yet")
7176
}
77+
Error::Dtls12Fallback => {
78+
write!(
79+
f,
80+
"dtls 1.2 fallback required: ClientHello does not offer DTLS 1.3"
81+
)
82+
}
7283
}
7384
}
7485
}

0 commit comments

Comments
 (0)