@@ -294,6 +294,27 @@ fn is_dtls12_psk_only(config: &Config) -> bool {
294294 . is_some_and ( |first| first. is_psk ( ) && suites. all ( |s| s. is_psk ( ) ) )
295295}
296296
297+ /// Lightweight structural check: does this packet look like a ClientHello?
298+ ///
299+ /// Used by the auto-sense server to gate the DTLS 1.2 fallback on parse
300+ /// errors. A packet that fails to parse in the DTLS 1.3 engine should
301+ /// only trigger a downgrade if it at least claims to be a ClientHello —
302+ /// otherwise random/garbage traffic could force fallback.
303+ fn looks_like_client_hello ( packet : & [ u8 ] ) -> bool {
304+ // DTLS record header: content_type(1) + version(2) + epoch(2) + seq(6) + length(2) = 13
305+ if packet. len ( ) < 13 || packet[ 0 ] != 0x16 {
306+ return false ;
307+ }
308+ let record_len = u16:: from_be_bytes ( [ packet[ 11 ] , packet[ 12 ] ] ) as usize ;
309+ let Some ( record_body) = packet. get ( 13 ..13 + record_len) else {
310+ return false ;
311+ } ;
312+
313+ // Handshake header: msg_type(1) + length(3) + message_seq(2) +
314+ // fragment_offset(3) + fragment_length(3) = 12
315+ record_body. len ( ) >= 12 && record_body[ 0 ] == 0x01
316+ }
317+
297318/// Peek at a buffered DTLS 1.2 ClientHello to decide whether the auto-sense
298319/// server fallback should construct a PSK-mode Server12.
299320///
@@ -540,15 +561,25 @@ impl Dtls {
540561 match self . inner . as_mut ( ) . unwrap ( ) {
541562 Inner :: ClientPending ( _) => self . handle_pending_auto_client ( packet) ,
542563 Inner :: Server13 ( server) if server. is_auto_mode ( ) => {
564+ // Run the structural check unconditionally so the time
565+ // spent here does not leak which error branch the parser
566+ // took — same cost whether handle_packet returns Ok,
567+ // Dtls12Fallback, ParseError, or anything else.
568+ let is_ch_shaped = looks_like_client_hello ( packet) ;
543569 match server. handle_packet ( packet) {
544570 Ok ( ( ) ) => Ok ( ( ) ) ,
545- Err ( Error :: Dtls12Fallback | Error :: ParseError ( _) | Error :: ParseIncomplete ) => {
546- // We detected a DTLS12 ClientHello, or the very
547- // first packet failed to parse in the
548- // DTLS 1.3 message parser (e.g. a pure DTLS 1.2
549- // ClientHello with no 1.3 cipher suites). Fall
550- // back to 1.2. Later parse errors (corrupted
551- // fragments of a 1.3 CH) are not caught here.
571+ Err ( Error :: Dtls12Fallback ) => {
572+ // The 1.3 engine cleanly rejected a ClientHello
573+ // that did not offer DTLS 1.3 in supported_versions.
574+ self . handle_pending_auto_server ( )
575+ }
576+ Err ( Error :: ParseError ( _) | Error :: ParseIncomplete ) if is_ch_shaped => {
577+ // The packet is structurally a ClientHello but the
578+ // 1.3 parser couldn't handle it — fall back to 1.2,
579+ // which has a more permissive parser. Random/garbage
580+ // traffic that happens to error is not caught here,
581+ // so an off-path attacker cannot force a downgrade
582+ // by spraying malformed packets.
552583 self . handle_pending_auto_server ( )
553584 }
554585 Err ( e) => Err ( e) ,
@@ -984,4 +1015,88 @@ mod test {
9841015 let err = dtls. close ( ) . unwrap_err ( ) ;
9851016 assert ! ( matches!( err, Error :: HandshakePending ) ) ;
9861017 }
1018+
1019+ fn make_record ( content_type : u8 , body : & [ u8 ] ) -> Vec < u8 > {
1020+ let mut pkt = Vec :: with_capacity ( 13 + body. len ( ) ) ;
1021+ pkt. push ( content_type) ;
1022+ pkt. extend_from_slice ( & [ 0xFE , 0xFD ] ) ; // version
1023+ pkt. extend_from_slice ( & [ 0x00 , 0x00 ] ) ; // epoch
1024+ pkt. extend_from_slice ( & [ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ] ) ; // seq
1025+ pkt. extend_from_slice ( & ( body. len ( ) as u16 ) . to_be_bytes ( ) ) ;
1026+ pkt. extend_from_slice ( body) ;
1027+ pkt
1028+ }
1029+
1030+ fn make_handshake_body ( msg_type : u8 ) -> Vec < u8 > {
1031+ let mut body = Vec :: new ( ) ;
1032+ body. push ( msg_type) ;
1033+ body. extend_from_slice ( & [ 0x00 , 0x00 , 0x00 ] ) ; // length
1034+ body. extend_from_slice ( & [ 0x00 , 0x00 ] ) ; // message_seq
1035+ body. extend_from_slice ( & [ 0x00 , 0x00 , 0x00 ] ) ; // fragment_offset
1036+ body. extend_from_slice ( & [ 0x00 , 0x00 , 0x00 ] ) ; // fragment_length
1037+ body
1038+ }
1039+
1040+ #[ test]
1041+ fn looks_like_client_hello_accepts_handshake_with_ch_msg_type ( ) {
1042+ let body = make_handshake_body ( 0x01 ) ;
1043+ let pkt = make_record ( 0x16 , & body) ;
1044+ assert ! ( looks_like_client_hello( & pkt) ) ;
1045+ }
1046+
1047+ #[ test]
1048+ fn looks_like_client_hello_rejects_non_handshake_record ( ) {
1049+ let body = make_handshake_body ( 0x01 ) ;
1050+ let pkt = make_record ( 0x17 , & body) ; // ApplicationData
1051+ assert ! ( !looks_like_client_hello( & pkt) ) ;
1052+ }
1053+
1054+ #[ test]
1055+ fn looks_like_client_hello_rejects_other_handshake_msg_types ( ) {
1056+ // ServerHello, HelloVerifyRequest, Finished, etc.
1057+ for msg_type in [ 0x02 , 0x03 , 0x04 , 0x0B , 0x0E , 0x14 ] {
1058+ let body = make_handshake_body ( msg_type) ;
1059+ let pkt = make_record ( 0x16 , & body) ;
1060+ assert ! (
1061+ !looks_like_client_hello( & pkt) ,
1062+ "msg_type {:#x} should not look like a CH" ,
1063+ msg_type
1064+ ) ;
1065+ }
1066+ }
1067+
1068+ #[ test]
1069+ fn looks_like_client_hello_rejects_truncated_packets ( ) {
1070+ assert ! ( !looks_like_client_hello( & [ ] ) ) ;
1071+ assert ! ( !looks_like_client_hello( & [ 0x16 ; 12 ] ) ) ; // too short for record header
1072+ // Record header claims body length 100 but no body bytes follow.
1073+ let mut pkt = vec ! [ 0x16 , 0xFE , 0xFD , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ;
1074+ pkt. extend_from_slice ( & 100u16 . to_be_bytes ( ) ) ;
1075+ assert ! ( !looks_like_client_hello( & pkt) ) ;
1076+ }
1077+
1078+ #[ test]
1079+ fn looks_like_client_hello_rejects_short_handshake_body ( ) {
1080+ // Valid record header but handshake body too short (< 12 bytes).
1081+ let pkt = make_record ( 0x16 , & [ 0x01 , 0x00 , 0x00 ] ) ;
1082+ assert ! ( !looks_like_client_hello( & pkt) ) ;
1083+ }
1084+
1085+ #[ test]
1086+ fn auto_server_drops_garbage_without_falling_back ( ) {
1087+ let mut dtls = new_instance_auto ( ) ;
1088+ // Random non-handshake bytes — the 1.3 engine will error, but the
1089+ // auto-sense path must not downgrade to 1.2.
1090+ let garbage = [ 0xFF ; 64 ] ;
1091+ let _ = dtls. handle_packet ( & garbage) ;
1092+ // Inner must remain Server13 in auto-sense mode.
1093+ let still_pending = match & dtls. inner {
1094+ Some ( Inner :: Server13 ( s) ) => s. is_auto_mode ( ) ,
1095+ _ => false ,
1096+ } ;
1097+ assert ! (
1098+ still_pending,
1099+ "auto-sense server must not fall back to DTLS 1.2 on garbage input"
1100+ ) ;
1101+ }
9871102}
0 commit comments