diff --git a/CHANGELOG.md b/CHANGELOG.md index a263326..5c5dabc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased + * Fix server auto-sensing DTLS version with fragmented ClientHello #87 * DTLS 1.2 DTLS 1.3, parser reject ApplicationData in epoch 0/plaintext #90 * DTLS 1.3 reject plaintext records with non-zero epoch #90 * Silently discard invalid records and process subsequent valid records #90 diff --git a/src/detect.rs b/src/auto.rs similarity index 83% rename from src/detect.rs rename to src/auto.rs index a661bd7..bfe6af2 100644 --- a/src/detect.rs +++ b/src/auto.rs @@ -1,14 +1,20 @@ /// Version detection and hybrid ClientHello for auto-sensing DTLS endpoints. /// -/// Both `client_hello_version` and `server_hello_version` do lightweight -/// parsing of the first record in a datagram, looking for the -/// `supported_versions` extension to decide between DTLS 1.2 and 1.3. +/// `server_hello_version` does lightweight parsing of the first record in a +/// datagram. It looks for a `HelloVerifyRequest` or a `ServerHello` with the +/// `supported_versions` extension to decide between DTLS 1.2 and 1.3 for the +/// client auto-sense path. /// /// [`HybridClientHello`] constructs a ClientHello compatible with both -/// DTLS 1.2 and 1.3 servers: it offers both version's cipher suites and +/// DTLS 1.2 and 1.3 servers: it offers both versions cipher suites and /// includes `supported_versions` with both versions. Once the server /// responds, the caller inspects the reply with `server_hello_version` /// and forks into the appropriate handshake path. +/// +/// Server-side auto-sense does not use lightweight detection. Instead, +/// it starts a DTLS 1.3 server which handles fragment reassembly natively +/// 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}; @@ -245,35 +251,6 @@ impl HybridClientHello { } } -/// Auto-sense server waiting for the first ClientHello to determine the DTLS version. -pub(crate) struct ServerPending { - config: Arc, - certificate: DtlsCertificate, - last_now: Instant, -} - -impl ServerPending { - pub fn new(config: Arc, certificate: DtlsCertificate, now: Instant) -> Self { - ServerPending { - config, - certificate, - last_now: now, - } - } - - pub fn handle_timeout(&mut self, now: Instant) { - self.last_now = now; - } - - pub fn poll_output<'a>(&self, _buf: &'a mut [u8]) -> Output<'a> { - Output::Timeout(self.last_now + Duration::from_secs(86400)) - } - - pub fn into_parts(self) -> (Arc, DtlsCertificate, Instant) { - (self.config, self.certificate, self.last_now) - } -} - /// Auto-sense client that sends a hybrid ClientHello and waits for the server's response /// to determine the DTLS version. pub(crate) struct ClientPending { @@ -373,117 +350,6 @@ pub(crate) enum DetectedVersion { Unknown, } -/// Detect DTLS version from a ClientHello packet. -/// -/// Returns `Dtls13` if the `supported_versions` extension contains -/// DTLS 1.3 (0xFEFC), otherwise `Dtls12`. -pub(crate) fn client_hello_version(packet: &[u8]) -> DetectedVersion { - match client_hello_version_inner(packet) { - Some(true) => DetectedVersion::Dtls13, - _ => DetectedVersion::Dtls12, - } -} - -fn client_hello_version_inner(packet: &[u8]) -> Option { - // Record header: content_type(1) + version(2) + epoch(2) + seq(6) + length(2) = 13 - if packet.len() < 13 { - return Some(false); - } - - // content_type must be 0x16 (Handshake) - if packet[0] != 0x16 { - return Some(false); - } - - let record_len = u16::from_be_bytes([packet[11], packet[12]]) as usize; - let record_body = packet.get(13..13 + record_len)?; - - // Handshake header: msg_type(1) + length(3) + message_seq(2) + - // fragment_offset(3) + fragment_length(3) = 12 - if record_body.len() < 12 { - return Some(false); - } - - // msg_type must be 1 (ClientHello) - if record_body[0] != 1 { - return Some(false); - } - - let fragment_len = ((record_body[9] as usize) << 16) - | ((record_body[10] as usize) << 8) - | (record_body[11] as usize); - let body = record_body.get(12..12 + fragment_len)?; - - // ClientHello body: - // client_version(2) + random(32) = 34 minimum before session_id - if body.len() < 34 { - return Some(false); - } - let mut pos = 34; - - // session_id: 1-byte length + data - let sid_len = *body.get(pos)? as usize; - pos += 1 + sid_len; - - // cookie: 1-byte length + data - let cookie_len = *body.get(pos)? as usize; - pos += 1 + cookie_len; - - // cipher_suites: 2-byte length + data - if pos + 2 > body.len() { - return Some(false); - } - let cs_len = u16::from_be_bytes([body[pos], body[pos + 1]]) as usize; - pos += 2 + cs_len; - - // compression_methods: 1-byte length + data - let cm_len = *body.get(pos)? as usize; - pos += 1 + cm_len; - - // extensions: 2-byte total length - if pos + 2 > body.len() { - return Some(false); - } - let ext_total_len = u16::from_be_bytes([body[pos], body[pos + 1]]) as usize; - pos += 2; - let ext_end = pos + ext_total_len; - if ext_end > body.len() { - return Some(false); - } - - // Walk extensions looking for supported_versions (0x002B) - while pos + 4 <= ext_end { - let ext_type = u16::from_be_bytes([body[pos], body[pos + 1]]); - let ext_len = u16::from_be_bytes([body[pos + 2], body[pos + 3]]) as usize; - pos += 4; - - if ext_type == 0x002B { - // supported_versions client format: 1-byte list length, then 2-byte versions - if ext_len < 1 { - return Some(false); - } - let list_len = body[pos] as usize; - let list_start = pos + 1; - if list_start + list_len > pos + ext_len { - return Some(false); - } - let mut i = list_start; - while i + 2 <= list_start + list_len { - let version = u16::from_be_bytes([body[i], body[i + 1]]); - if version == 0xFEFC { - return Some(true); - } - i += 2; - } - return Some(false); - } - - pos += ext_len; - } - - Some(false) -} - /// Detect DTLS version from a server response (ServerHello or HelloVerifyRequest). /// /// - HelloVerifyRequest (msg_type 3) → `Dtls12` diff --git a/src/dtls13/client.rs b/src/dtls13/client.rs index a615341..77e388f 100644 --- a/src/dtls13/client.rs +++ b/src/dtls13/client.rs @@ -170,7 +170,7 @@ impl Client { /// already sent on the wire by `ClientPending`, so no record is /// enqueued for output. pub(crate) fn new_from_hybrid( - hybrid: crate::detect::HybridClientHello, + hybrid: crate::auto::HybridClientHello, config: std::sync::Arc, certificate: crate::DtlsCertificate, now: Instant, @@ -210,7 +210,7 @@ impl Client { } pub fn into_server(self) -> Server { - Server::new_with_engine(self.engine, self.last_now) + Server::new_with_engine(self.engine, self.last_now, false) } pub(crate) fn state_name(&self) -> &'static str { diff --git a/src/dtls13/engine.rs b/src/dtls13/engine.rs index 13326dc..f22961c 100644 --- a/src/dtls13/engine.rs +++ b/src/dtls13/engine.rs @@ -27,7 +27,7 @@ use crate::dtls13::message::Sequence; use crate::timer::ExponentialBackoff; use crate::types::{HashAlgorithm, Random}; use crate::window::ReplayWindow; -use crate::{Config, Error, Output, SeededRng}; +use crate::{Config, DtlsCertificate, Error, Output, SeededRng}; const MAX_DEFRAGMENT_PACKETS: usize = 50; @@ -39,6 +39,9 @@ pub struct Engine { /// Configuration options. config: Arc, + /// Saved certificate + certificate: DtlsCertificate, + /// Seedable random number generator for deterministic testing rng: SeededRng, @@ -93,9 +96,6 @@ pub struct Engine { /// Whether the remote peer has enabled encryption peer_encryption_enabled: bool, - /// Certificate in DER format - certificate_der: Vec, - /// Signing key for CertificateVerify signing_key: Box, @@ -189,7 +189,7 @@ struct Entry { } impl Engine { - pub fn new(config: Arc, certificate: crate::DtlsCertificate) -> Self { + pub fn new(config: Arc, certificate: DtlsCertificate) -> Self { let mut rng = SeededRng::new(config.rng_seed()); let flight_backoff = @@ -206,6 +206,7 @@ impl Engine { Self { config, + certificate, rng, buffers_free: BufferPool::default(), sequence_epoch_0: Sequence::new(0), @@ -224,7 +225,6 @@ impl Engine { prev_app_send_seq: 0, app_recv_keys: ArrayVec::new(), peer_encryption_enabled: false, - certificate_der: certificate.certificate, signing_key, is_client: false, peer_handshake_seq_no: 0, @@ -246,6 +246,10 @@ impl Engine { } } + pub fn into_fallback(self) -> (Arc, DtlsCertificate) { + (self.config, self.certificate) + } + pub fn set_client(&mut self, is_client: bool) { self.is_client = is_client; } @@ -307,7 +311,7 @@ impl Engine { } pub fn certificate_der(&self) -> &[u8] { - &self.certificate_der + &self.certificate.certificate } pub fn signing_key(&mut self) -> &mut dyn SigningKey { diff --git a/src/dtls13/server.rs b/src/dtls13/server.rs index 1b8681c..c8368fa 100644 --- a/src/dtls13/server.rs +++ b/src/dtls13/server.rs @@ -74,6 +74,8 @@ const HRR_RANDOM: [u8; 32] = [ 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C, ]; +const MAX_RETAINED_CLIENT_HELLO: usize = 64; + /// DTLS 1.3 server pub struct Server { /// Current server state. @@ -135,6 +137,15 @@ pub struct Server { /// Whether we need to respond with our own KeyUpdate. pending_key_update_response: bool, + + /// When true, a ClientHello without DTLS 1.3 in `supported_versions` + /// returns [`Error::Dtls12Fallback`] instead of a security error. + /// Used by the auto-sense server path. + auto_mode: bool, + + /// Raw packets buffered during auto-sense so they can be replayed + /// to a DTLS 1.2 server on fallback. + retained_hello: VecDeque, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -156,10 +167,21 @@ impl Server { /// Create a new DTLS 1.3 server. pub fn new(config: Arc, certificate: DtlsCertificate, now: Instant) -> Server { let engine = Engine::new(config, certificate); - Self::new_with_engine(engine, now) + Self::new_with_engine(engine, now, false) } - pub(crate) fn new_with_engine(mut engine: Engine, now: Instant) -> Server { + /// Create a new DTLS 1.3 server in auto-sense mode. + /// + /// In auto-sense mode, if the ClientHello does not offer DTLS 1.3 + /// in `supported_versions`, the server returns [`Error::Dtls12Fallback`] + /// instead of a fatal security error, allowing the caller to switch + /// to a DTLS 1.2 server. + pub fn new_auto(config: Arc, certificate: DtlsCertificate, now: Instant) -> Server { + let engine = Engine::new(config, certificate); + Self::new_with_engine(engine, now, true) + } + + pub fn new_with_engine(mut engine: Engine, now: Instant, auto_mode: bool) -> Server { let cookie_secret = engine.random_arr(); Server { @@ -183,6 +205,8 @@ impl Server { hello_retry: false, cookie_secret, pending_key_update_response: false, + auto_mode, + retained_hello: VecDeque::with_capacity(10), } } @@ -190,13 +214,46 @@ impl Server { Client::new_with_engine(self.engine, self.last_now) } + /// Whether this server is in auto-sense mode. + pub fn is_auto_mode(&self) -> bool { + self.auto_mode + } + + /// Take all relevant config from this server instance. + /// + /// This is used in two cases: + /// + /// 1. Switching a server pending (auto-mode) to dtls12 server + /// 2. set_active(true), turning a server pending (auto-mode) to a ClientPending + pub fn into_parts(self) -> (Arc, DtlsCertificate, Instant, VecDeque) { + let (config, cert) = self.engine.into_fallback(); + (config, cert, self.last_now, self.retained_hello) + } + pub(crate) fn state_name(&self) -> &'static str { self.state.name() } pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> { + // In auto-sense mode, buffer raw packets while still waiting for + // the ClientHello so they can be replayed to Server12 on fallback. + if self.auto_mode && self.state == State::AwaitClientHello { + // Cap buffered fragments to prevent unbounded growth from malicious traffic + if self.retained_hello.len() >= MAX_RETAINED_CLIENT_HELLO { + return Err(Error::TooManyClientHelloFragments); + } + self.retained_hello.push_back(packet.to_buf()); + } + self.engine.parse_packet(packet)?; self.make_progress()?; + + // Once past AwaitClientHello, DTLS 1.3 is committed — free the buffer. + if self.auto_mode && self.state != State::AwaitClientHello { + self.retained_hello.clear(); + self.auto_mode = false; + } + Ok(()) } @@ -392,6 +449,9 @@ impl State { } if !supported_versions_ok { + if server.auto_mode { + return Err(Error::Dtls12Fallback); + } return Err(Error::SecurityError( "ClientHello must include DTLS 1.3 in supported_versions".to_string(), )); diff --git a/src/error.rs b/src/error.rs index ec8e21d..dce6ec5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,6 +36,18 @@ pub enum Error { /// resolved. Callers should buffer the data and retry once the /// handshake advances. HandshakePending, + /// If we are in auto-sense mode for a server and we received too + /// many client hello fragments that haven't made a packet. + TooManyClientHelloFragments, + /// The DTLS 1.3 server received a ClientHello that does not offer + /// DTLS 1.3 in `supported_versions`. In auto-sense mode the caller + /// should fall back to a DTLS 1.2 server and replay the buffered + /// packets. + /// + /// This value should never be seen outside dimpl. It's an internal + /// value to communicate from dtls13/server.rs to lib.rs + #[doc(hidden)] + Dtls12Fallback, } impl<'a> From>> for Error { @@ -69,6 +81,10 @@ impl std::fmt::Display for Error { Error::HandshakePending => { write!(f, "handshake pending: cannot send application data yet") } + Error::TooManyClientHelloFragments => write!(f, "too many client hello fragments"), + Error::Dtls12Fallback => { + write!(f, "dtls 1.2 fallback (internal)") + } } } } diff --git a/src/lib.rs b/src/lib.rs index 0ae25e6..ecbe4df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,9 +178,9 @@ mod dtls13; use dtls12::{Client as Client12, Server as Server12}; use dtls13::{Client as Client13, Server as Server13}; -use detect::{ClientPending, ServerPending}; +use auto::ClientPending; -mod detect; +mod auto; mod time_tricks; pub(crate) mod buffer; @@ -242,7 +242,6 @@ enum Inner { Server12(Server12), Client13(Client13), Server13(Server13), - ServerPending(ServerPending), ClientPending(ClientPending), } @@ -276,17 +275,19 @@ impl Dtls { /// Create a new DTLS instance that auto‑senses the version. /// - /// **Server role** (default): the instance stays in a pending state. - /// When the first ClientHello arrives it inspects the - /// `supported_versions` extension and creates either a DTLS 1.2 or - /// 1.3 server. + /// **Server role** (default): starts as a DTLS 1.3 server. If the + /// peer's ClientHello does not offer DTLS 1.3 in `supported_versions`, + /// the server automatically falls back to DTLS 1.2. This handles + /// fragmented ClientHellos (e.g. with post-quantum key shares) + /// correctly because the DTLS 1.3 engine performs full reassembly + /// before inspecting extensions. /// /// **Client role** ([`set_active(true)`](Self::set_active)): the /// instance sends a hybrid ClientHello compatible with both DTLS 1.2 /// and 1.3 servers and forks into the correct handshake once the /// server responds. pub fn new_auto(config: Arc, certificate: DtlsCertificate, now: Instant) -> Self { - let inner = Inner::ServerPending(ServerPending::new(config, certificate, now)); + let inner = Inner::Server13(Server13::new_auto(config, certificate, now)); Dtls { inner: Some(inner) } } @@ -297,8 +298,16 @@ impl Dtls { pub fn protocol_version(&self) -> Option { match self.inner.as_ref()? { Inner::Client12(_) | Inner::Server12(_) => Some(ProtocolVersion::DTLS1_2), - Inner::Client13(_) | Inner::Server13(_) => Some(ProtocolVersion::DTLS1_3), - Inner::ServerPending(_) | Inner::ClientPending(_) => None, + Inner::Client13(_) => Some(ProtocolVersion::DTLS1_3), + Inner::Server13(s) => { + // Still waiting for a complete ClientHello + if s.is_auto_mode() { + None + } else { + Some(ProtocolVersion::DTLS1_3) + } + } + Inner::ClientPending(_) => None, } } @@ -342,14 +351,15 @@ impl Dtls { self.inner = Some(Inner::Client12(s.into_client())); } Inner::Server13(s) => { - self.inner = Some(Inner::Client13(s.into_client())); - } - Inner::ServerPending(sp) => { - let (config, certificate, now) = sp.into_parts(); - // unwrap: ClientPending::new only fails on missing kx groups - let cp = ClientPending::new(config, certificate, now) - .expect("failed to build hybrid ClientHello"); - self.inner = Some(Inner::ClientPending(cp)); + if s.is_auto_mode() { + let (config, certificate, now, _) = s.into_parts(); + let cp = ClientPending::new(config, certificate, now) + .expect("failed to build hybrid ClientHello"); + self.inner = Some(Inner::ClientPending(cp)); + } else { + // Not auto mode, or already consumed — just convert + self.inner = Some(Inner::Client13(s.into_client())); + } } _ => unreachable!(), } @@ -360,85 +370,119 @@ impl Dtls { /// Process an incoming DTLS datagram. pub fn handle_packet(&mut self, packet: &[u8]) -> Result<(), Error> { - // Auto-sense server: resolve version on first ClientHello - if matches!(self.inner, Some(Inner::ServerPending(_))) { - let inner = self.inner.take().unwrap(); - let Inner::ServerPending(sp) = inner else { - unreachable!() - }; - let (config, certificate, now) = sp.into_parts(); - - let is_13 = matches!( - detect::client_hello_version(packet), - detect::DetectedVersion::Dtls13 - ); - self.inner = if is_13 { - Some(Inner::Server13(Server13::new(config, certificate, now))) - } else { - Some(Inner::Server12(Server12::new(config, certificate, now))) - }; - - // Arm the server's random + retransmit timers. - // inner is already set, so errors won't leave it as None. - self.handle_timeout(now)?; + // unwrap is ok. The inner is only Option to work around borrowing + // issues when doing auto-sensing of DTLS version. + let inner = self.inner.as_mut().unwrap(); + + // Auto-sense pending states handle the packet themselves + // (including replay to the newly created inner), so we + // must not fall through to the regular dispatch below. + if inner.is_pending() { + return self.handle_pending_auto(packet); } - // Auto-sense client: resolve version on first server response - if matches!(self.inner, Some(Inner::ClientPending(_))) { - let version = detect::server_hello_version(packet); - - // Check version before taking inner — returning an error - // while inner is None would leave us unable to poll/timeout. - if matches!(version, detect::DetectedVersion::Unknown) { - return Err(Error::UnexpectedMessage( - "Unrecognized response from server".to_string(), - )); - } + match self.inner.as_mut().unwrap() { + Inner::Client12(client) => client.handle_packet(packet), + Inner::Server12(server) => server.handle_packet(packet), + Inner::Client13(client) => client.handle_packet(packet), + Inner::Server13(server) => server.handle_packet(packet), + Inner::ClientPending(_) => unreachable!(), + } + } - // unwrap: guarded by the matches! check above - let inner = self.inner.take().unwrap(); - let Inner::ClientPending(cp) = inner else { - unreachable!() - }; - let (hybrid, config, certificate, now) = cp.into_parts(); - match version { - detect::DetectedVersion::Dtls12 => { - let mut client12 = Client12::new_from_hybrid( - hybrid.random, - &hybrid.handshake_fragment, - config, - certificate, - now, - )?; - // Feed the HVR to Client12 — it enters - // AwaitHelloVerifyRequest and processes the cookie. - if let Err(e) = client12.handle_packet(packet) { - self.inner = Some(Inner::Client12(client12)); - return Err(e); + fn handle_pending_auto(&mut self, packet: &[u8]) -> Result<(), Error> { + match self.inner.as_mut().unwrap() { + Inner::ClientPending(_) => self.handle_pending_auto_client(packet), + Inner::Server13(server) if server.is_auto_mode() => { + match server.handle_packet(packet) { + Ok(()) => Ok(()), + Err(Error::Dtls12Fallback | Error::ParseError(_) | Error::ParseIncomplete) => { + // We detected a DTLS12 ClientHello, or the very + // first packet failed to parse in the + // DTLS 1.3 message parser (e.g. a pure DTLS 1.2 + // ClientHello with no 1.3 cipher suites). Fall + // back to 1.2. Later parse errors (corrupted + // fragments of a 1.3 CH) are not caught here. + self.handle_pending_auto_server() } + Err(e) => Err(e), + } + } + _ => unreachable!(), + } + } + + fn handle_pending_auto_client(&mut self, packet: &[u8]) -> Result<(), Error> { + // Auto-sense client: resolve version on first server response + let version = auto::server_hello_version(packet); + + // Check version before taking inner — returning an error + // while inner is None would leave us unable to poll/timeout. + if matches!(version, auto::DetectedVersion::Unknown) { + return Err(Error::UnexpectedMessage( + "Unrecognized response from server".to_string(), + )); + } + + // unwrap: guarded by the matches! check above + let inner = self.inner.take().unwrap(); + let Inner::ClientPending(cp) = inner else { + unreachable!() + }; + let (hybrid, config, certificate, now) = cp.into_parts(); + match version { + auto::DetectedVersion::Dtls12 => { + let mut client12 = Client12::new_from_hybrid( + hybrid.random, + &hybrid.handshake_fragment, + config, + certificate, + now, + )?; + // Feed the HVR to Client12 — it enters + // AwaitHelloVerifyRequest and processes the cookie. + if let Err(e) = client12.handle_packet(packet) { self.inner = Some(Inner::Client12(client12)); - return Ok(()); + return Err(e); } - detect::DetectedVersion::Dtls13 => { - let mut client13 = Client13::new_from_hybrid(hybrid, config, certificate, now)?; - if let Err(e) = client13.handle_packet(packet) { - self.inner = Some(Inner::Client13(client13)); - return Err(e); - } + self.inner = Some(Inner::Client12(client12)); + Ok(()) + } + auto::DetectedVersion::Dtls13 => { + let mut client13 = Client13::new_from_hybrid(hybrid, config, certificate, now)?; + if let Err(e) = client13.handle_packet(packet) { self.inner = Some(Inner::Client13(client13)); - return Ok(()); + return Err(e); } - detect::DetectedVersion::Unknown => unreachable!(), + self.inner = Some(Inner::Client13(client13)); + Ok(()) } + auto::DetectedVersion::Unknown => unreachable!(), } + } - match self.inner.as_mut().unwrap() { - Inner::Client12(client) => client.handle_packet(packet), - Inner::Server12(server) => server.handle_packet(packet), - Inner::Client13(client) => client.handle_packet(packet), - Inner::Server13(server) => server.handle_packet(packet), - Inner::ServerPending(_) | Inner::ClientPending(_) => unreachable!(), + /// Fall back from DTLS 1.3 auto-sense to a DTLS 1.2 server, replaying + /// all buffered packets from the Server13. + fn handle_pending_auto_server(&mut self) -> Result<(), Error> { + // Take buffered packets and last_now from the Server13 before replacing it. + + // unwrap: is ok, because we can only be here if the inner is a Server13. + let server = match self.inner.take().unwrap() { + Inner::Server13(server) => server, + _ => unreachable!(), + }; + + let (config, cert, now, buffered) = server.into_parts(); + + let mut server12 = Server12::new(config, cert, now); + server12.handle_timeout(now)?; + + self.inner = Some(Inner::Server12(server12)); + + for p in &buffered { + self.handle_packet(p)?; } + Ok(()) } /// Poll for pending output from the DTLS engine. @@ -448,7 +492,6 @@ impl Dtls { Inner::Server12(server) => server.poll_output(buf), Inner::Client13(client) => client.poll_output(buf), Inner::Server13(server) => server.poll_output(buf), - Inner::ServerPending(sp) => sp.poll_output(buf), Inner::ClientPending(cp) => cp.poll_output(buf), } } @@ -460,10 +503,6 @@ impl Dtls { Inner::Server12(server) => server.handle_timeout(now), Inner::Client13(client) => client.handle_timeout(now), Inner::Server13(server) => server.handle_timeout(now), - Inner::ServerPending(sp) => { - sp.handle_timeout(now); - Ok(()) - } Inner::ClientPending(cp) => cp.handle_timeout(now), } } @@ -474,12 +513,29 @@ impl Dtls { /// yet been resolved (auto-sense pending). Callers should buffer /// the data externally and retry after the handshake progresses. pub fn send_application_data(&mut self, data: &[u8]) -> Result<(), Error> { - match self.inner.as_mut().unwrap() { + // unwrap is ok, we only have an Option to deal with pending auto. + let inner = self.inner.as_mut().unwrap(); + + if inner.is_pending() { + return Err(Error::HandshakePending); + } + + match inner { Inner::Client12(client) => client.send_application_data(data), Inner::Server12(server) => server.send_application_data(data), Inner::Client13(client) => client.send_application_data(data), Inner::Server13(server) => server.send_application_data(data), - Inner::ServerPending(_) | Inner::ClientPending(_) => Err(Error::HandshakePending), + Inner::ClientPending(_) => Err(Error::HandshakePending), + } + } +} + +impl Inner { + fn is_pending(&self) -> bool { + match self { + Inner::Server13(v) => v.is_auto_mode(), + Inner::ClientPending(_) => true, + _ => false, } } } @@ -491,7 +547,6 @@ impl fmt::Debug for Dtls { Some(Inner::Server12(s)) => ("Server12", s.state_name()), Some(Inner::Client13(c)) => ("Client13", c.state_name()), Some(Inner::Server13(s)) => ("Server13", s.state_name()), - Some(Inner::ServerPending(_)) => ("ServerPending", ""), Some(Inner::ClientPending(_)) => ("ClientPending", ""), None => ("None", ""), }; @@ -671,7 +726,13 @@ mod test { #[test] fn test_protocol_version_auto_pending() { let dtls = new_instance_auto(); - // Auto-sense instance before negotiation should return None assert_eq!(dtls.protocol_version(), None); } + + #[test] + fn test_auto_server_send_application_data_pending() { + let mut dtls = new_instance_auto(); + let err = dtls.send_application_data(b"early data").unwrap_err(); + assert!(matches!(err, Error::HandshakePending)); + } } diff --git a/tests/auto/common.rs b/tests/auto/common.rs index 35a9d59..2eea05b 100644 --- a/tests/auto/common.rs +++ b/tests/auto/common.rs @@ -76,3 +76,13 @@ pub fn no_cookie_config() -> Arc { .expect("Failed to build config"), ) } + +/// Create a config with a small MTU to force ClientHello fragmentation. +pub fn small_mtu_config(mtu: usize) -> Arc { + Arc::new( + Config::builder() + .mtu(mtu) + .build() + .expect("Failed to build config"), + ) +} diff --git a/tests/auto/cross_matrix.rs b/tests/auto/cross_matrix.rs new file mode 100644 index 0000000..cc9f1e2 --- /dev/null +++ b/tests/auto/cross_matrix.rs @@ -0,0 +1,441 @@ +//! Cross-version matrix tests for DTLS auto-sense. +//! +//! Tests every combination of (client_version × server_version × mtu) to +//! verify that auto-sense, explicit 1.2, and explicit 1.3 all interoperate +//! correctly, including with fragmented ClientHellos. +//! +//! Matrix (expected outcome): +//! +//! | Client \ Server | auto | 1.2 | 1.3 | +//! |-----------------|--------|--------|--------| +//! | auto | 1.3 | 1.2 | 1.3 | +//! | 1.2 | 1.2 | 1.2 | FAIL | +//! | 1.3 | 1.3 | FAIL | 1.3 | +//! +//! Each passing combination is tested at normal MTU and at small MTU (200) +//! to exercise fragmented handshake messages. + +#![cfg(feature = "rcgen")] + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use dimpl::certificate::generate_self_signed_certificate; +use dimpl::{Config, Dtls, ProtocolVersion, SrtpProfile}; + +use crate::common::*; + +// ── Helpers ──────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy)] +enum Ver { + Auto, + V12, + V13, +} + +fn make_endpoint(ver: Ver, config: Arc, active: bool) -> Dtls { + let cert = generate_self_signed_certificate().unwrap(); + let now = Instant::now(); + let mut d = match ver { + Ver::Auto => Dtls::new_auto(config, cert, now), + Ver::V12 => Dtls::new_12(config, cert, now), + Ver::V13 => Dtls::new_13(config, cert, now), + }; + if active { + d.set_active(true); + } + d +} + +fn cfg(mtu: usize) -> Arc { + Arc::new(Config::builder().mtu(mtu).build().unwrap()) +} + +/// Drive a handshake to completion. Returns `None` if it fails to connect +/// within the iteration limit (expected for incompatible combinations). +fn try_handshake( + client: &mut Dtls, + server: &mut Dtls, +) -> Option<(ProtocolVersion, ProtocolVersion)> { + let mut now = Instant::now(); + let mut cc = false; + let mut sc = false; + + for _ in 0..80 { + if let Err(_) = client.handle_timeout(now) { + return None; + } + if let Err(_) = server.handle_timeout(now) { + return None; + } + + let co = drain_outputs(client); + let so = drain_outputs(server); + + if co.connected { + cc = true; + } + if so.connected { + sc = true; + } + + // Deliver packets, ignoring errors (incompatible version → errors are expected) + for p in &co.packets { + let _ = server.handle_packet(p); + } + for p in &so.packets { + let _ = client.handle_packet(p); + } + + if cc && sc { + return Some(( + client.protocol_version().unwrap(), + server.protocol_version().unwrap(), + )); + } + + now += Duration::from_millis(10); + } + + None +} + +/// Helper: successful handshake + verify versions + data exchange. +fn assert_connects(client_ver: Ver, server_ver: Ver, mtu: usize, expected_proto: ProtocolVersion) { + let _ = env_logger::try_init(); + + let client_cfg = cfg(mtu); + let server_cfg = cfg(mtu); + + let mut client = make_endpoint(client_ver, client_cfg, true); + let mut server = make_endpoint(server_ver, server_cfg, false); + + let result = try_handshake(&mut client, &mut server); + let (cv, sv) = result.unwrap_or_else(|| { + panic!( + "{:?} client (mtu={}) → {:?} server should connect as {:?}", + client_ver, mtu, server_ver, expected_proto + ) + }); + + assert_eq!( + cv, expected_proto, + "{:?} client version mismatch (mtu={})", + client_ver, mtu + ); + assert_eq!( + sv, expected_proto, + "{:?} server version mismatch (mtu={})", + server_ver, mtu + ); + + // Verify bidirectional data exchange. + let msg_c = b"from client"; + let msg_s = b"from server"; + let mut now = Instant::now() + Duration::from_millis(500); + + client.send_application_data(msg_c).unwrap(); + server.send_application_data(msg_s).unwrap(); + + for _ in 0..20 { + client.handle_timeout(now).unwrap(); + server.handle_timeout(now).unwrap(); + + let co = drain_outputs(&mut client); + let so = drain_outputs(&mut server); + + deliver_packets(&co.packets, &mut server); + deliver_packets(&so.packets, &mut client); + + let co2 = drain_outputs(&mut client); + let so2 = drain_outputs(&mut server); + + if so2.app_data.iter().any(|d| d == msg_c) && co2.app_data.iter().any(|d| d == msg_s) { + return; // success + } + + now += Duration::from_millis(10); + } + + panic!( + "{:?} client → {:?} server (mtu={}): data exchange failed", + client_ver, server_ver, mtu + ); +} + +/// Helper: verify that an incompatible combination does NOT connect. +fn assert_fails(client_ver: Ver, server_ver: Ver, mtu: usize) { + let _ = env_logger::try_init(); + + let client_cfg = cfg(mtu); + let server_cfg = cfg(mtu); + + let mut client = make_endpoint(client_ver, client_cfg, true); + let mut server = make_endpoint(server_ver, server_cfg, false); + + let result = try_handshake(&mut client, &mut server); + assert!( + result.is_none(), + "{:?} client (mtu={}) → {:?} server should NOT connect, but got {:?}", + client_ver, + mtu, + server_ver, + result + ); +} + +// ── Normal MTU (1150) ────────────────────────────────────────────────── + +const NORMAL: usize = 1150; + +// auto × auto → 1.3 + +#[test] +fn cross_auto_auto_normal() { + assert_connects(Ver::Auto, Ver::Auto, NORMAL, ProtocolVersion::DTLS1_3); +} + +// auto × 1.2 → 1.2 + +#[test] +fn cross_auto_v12_normal() { + assert_connects(Ver::Auto, Ver::V12, NORMAL, ProtocolVersion::DTLS1_2); +} + +// auto × 1.3 → 1.3 + +#[test] +fn cross_auto_v13_normal() { + assert_connects(Ver::Auto, Ver::V13, NORMAL, ProtocolVersion::DTLS1_3); +} + +// 1.2 × auto → 1.2 + +#[test] +fn cross_v12_auto_normal() { + assert_connects(Ver::V12, Ver::Auto, NORMAL, ProtocolVersion::DTLS1_2); +} + +// 1.2 × 1.2 → 1.2 + +#[test] +fn cross_v12_v12_normal() { + assert_connects(Ver::V12, Ver::V12, NORMAL, ProtocolVersion::DTLS1_2); +} + +// 1.2 × 1.3 → FAIL + +#[test] +fn cross_v12_v13_normal() { + assert_fails(Ver::V12, Ver::V13, NORMAL); +} + +// 1.3 × auto → 1.3 + +#[test] +fn cross_v13_auto_normal() { + assert_connects(Ver::V13, Ver::Auto, NORMAL, ProtocolVersion::DTLS1_3); +} + +// 1.3 × 1.2 → FAIL + +#[test] +fn cross_v13_v12_normal() { + assert_fails(Ver::V13, Ver::V12, NORMAL); +} + +// 1.3 × 1.3 → 1.3 + +#[test] +fn cross_v13_v13_normal() { + assert_connects(Ver::V13, Ver::V13, NORMAL, ProtocolVersion::DTLS1_3); +} + +// ── Small MTU (200) — fragmented ClientHello ─────────────────────────── + +const FRAG: usize = 200; + +// auto × auto → 1.3 + +#[test] +fn cross_auto_auto_frag() { + assert_connects(Ver::Auto, Ver::Auto, FRAG, ProtocolVersion::DTLS1_3); +} + +// auto × 1.2 → 1.2 + +#[test] +fn cross_auto_v12_frag() { + assert_connects(Ver::Auto, Ver::V12, FRAG, ProtocolVersion::DTLS1_2); +} + +// auto × 1.3 → 1.3 + +#[test] +fn cross_auto_v13_frag() { + assert_connects(Ver::Auto, Ver::V13, FRAG, ProtocolVersion::DTLS1_3); +} + +// 1.2 × auto → 1.2 + +#[test] +fn cross_v12_auto_frag() { + assert_connects(Ver::V12, Ver::Auto, FRAG, ProtocolVersion::DTLS1_2); +} + +// 1.2 × 1.2 → 1.2 + +#[test] +fn cross_v12_v12_frag() { + assert_connects(Ver::V12, Ver::V12, FRAG, ProtocolVersion::DTLS1_2); +} + +// 1.2 × 1.3 → FAIL + +#[test] +fn cross_v12_v13_frag() { + assert_fails(Ver::V12, Ver::V13, FRAG); +} + +// 1.3 × auto → 1.3 + +#[test] +fn cross_v13_auto_frag() { + assert_connects(Ver::V13, Ver::Auto, FRAG, ProtocolVersion::DTLS1_3); +} + +// 1.3 × 1.2 → FAIL + +#[test] +fn cross_v13_v12_frag() { + assert_fails(Ver::V13, Ver::V12, FRAG); +} + +// 1.3 × 1.3 → 1.3 + +#[test] +fn cross_v13_v13_frag() { + assert_connects(Ver::V13, Ver::V13, FRAG, ProtocolVersion::DTLS1_3); +} + +// ── Very small MTU (100) — heavy fragmentation ──────────────────────── + +const HEAVY: usize = 150; + +#[test] +fn cross_auto_auto_heavy() { + assert_connects(Ver::Auto, Ver::Auto, HEAVY, ProtocolVersion::DTLS1_3); +} + +#[test] +fn cross_v13_auto_heavy() { + assert_connects(Ver::V13, Ver::Auto, HEAVY, ProtocolVersion::DTLS1_3); +} + +#[test] +fn cross_v13_v13_heavy() { + assert_connects(Ver::V13, Ver::V13, HEAVY, ProtocolVersion::DTLS1_3); +} + +#[test] +fn cross_v12_auto_heavy() { + assert_connects(Ver::V12, Ver::Auto, HEAVY, ProtocolVersion::DTLS1_2); +} + +#[test] +fn cross_v12_v12_heavy() { + assert_connects(Ver::V12, Ver::V12, HEAVY, ProtocolVersion::DTLS1_2); +} + +#[test] +fn cross_auto_v13_heavy() { + assert_connects(Ver::Auto, Ver::V13, HEAVY, ProtocolVersion::DTLS1_3); +} + +#[test] +fn cross_auto_v12_heavy() { + assert_connects(Ver::Auto, Ver::V12, HEAVY, ProtocolVersion::DTLS1_2); +} + +// ── Keying material tests (fragmented) ───────────────────────────────── + +fn assert_keying_material(client_ver: Ver, server_ver: Ver, mtu: usize) { + let _ = env_logger::try_init(); + + let client_cfg = cfg(mtu); + let server_cfg = cfg(mtu); + + let mut client = make_endpoint(client_ver, client_cfg, true); + let mut server = make_endpoint(server_ver, server_cfg, false); + + let mut now = Instant::now(); + let mut client_km: Option<(Vec, SrtpProfile)> = None; + let mut server_km: Option<(Vec, SrtpProfile)> = None; + + for _ in 0..80 { + client.handle_timeout(now).unwrap(); + server.handle_timeout(now).unwrap(); + + let co = drain_outputs(&mut client); + let so = drain_outputs(&mut server); + + if let Some(km) = co.keying_material { + client_km = Some(km); + } + if let Some(km) = so.keying_material { + server_km = Some(km); + } + + deliver_packets(&co.packets, &mut server); + deliver_packets(&so.packets, &mut client); + + if client_km.is_some() && server_km.is_some() { + break; + } + + now += Duration::from_millis(10); + } + + let ckm = client_km.expect("Client should have keying material"); + let skm = server_km.expect("Server should have keying material"); + + assert_eq!(ckm.0, skm.0, "Keying material should match"); + assert_eq!(ckm.1, skm.1, "SRTP profile should match"); + assert!(!ckm.0.is_empty()); +} + +#[test] +fn keying_auto_auto_frag() { + assert_keying_material(Ver::Auto, Ver::Auto, FRAG); +} + +#[test] +fn keying_v13_auto_frag() { + assert_keying_material(Ver::V13, Ver::Auto, FRAG); +} + +#[test] +fn keying_v12_auto_frag() { + assert_keying_material(Ver::V12, Ver::Auto, FRAG); +} + +#[test] +fn keying_auto_v13_frag() { + assert_keying_material(Ver::Auto, Ver::V13, FRAG); +} + +#[test] +fn keying_auto_v12_frag() { + assert_keying_material(Ver::Auto, Ver::V12, FRAG); +} + +#[test] +fn keying_v13_v13_frag() { + assert_keying_material(Ver::V13, Ver::V13, FRAG); +} + +#[test] +fn keying_v12_v12_frag() { + assert_keying_material(Ver::V12, Ver::V12, FRAG); +} diff --git a/tests/auto/main.rs b/tests/auto/main.rs index 856fe46..195d12b 100644 --- a/tests/auto/main.rs +++ b/tests/auto/main.rs @@ -1,2 +1,4 @@ mod common; +mod cross_matrix; mod handshake; +mod server_fallback; diff --git a/tests/auto/server_fallback.rs b/tests/auto/server_fallback.rs new file mode 100644 index 0000000..d4e9f97 --- /dev/null +++ b/tests/auto/server_fallback.rs @@ -0,0 +1,691 @@ +//! Auto-sense server fallback tests. +//! +//! Tests the `Dtls::new_auto()` server path where the server starts as +//! DTLS 1.3 and falls back to DTLS 1.2 when the client doesn't offer 1.3. +//! Also tests that DTLS 1.3 clients connect without fallback. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use dimpl::{Dtls, Error, Output, ProtocolVersion}; + +use crate::common::*; + +/// Helper: run a handshake loop between client and server, return +/// (client_connected, server_connected, client_version, server_version). +fn run_handshake( + client: &mut Dtls, + server: &mut Dtls, +) -> (bool, bool, Option, Option) { + let mut now = Instant::now(); + let mut client_connected = false; + let mut server_connected = false; + + for _ in 0..80 { + client.handle_timeout(now).expect("client timeout"); + server.handle_timeout(now).expect("server timeout"); + + let client_out = drain_outputs(client); + let server_out = drain_outputs(server); + + if client_out.connected { + client_connected = true; + } + if server_out.connected { + server_connected = true; + } + + deliver_packets(&client_out.packets, server); + deliver_packets(&server_out.packets, client); + + if client_connected && server_connected { + break; + } + + now += Duration::from_millis(10); + } + + ( + client_connected, + server_connected, + client.protocol_version(), + server.protocol_version(), + ) +} + +// ============================================================================ +// Auto server + explicit DTLS 1.3 client → DTLS 1.3 (no fallback) +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_dtls13_client() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut client = Dtls::new_13(Arc::clone(&config), client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect"); + assert!(sc, "Server should connect"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_3)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +// ============================================================================ +// Auto server + explicit DTLS 1.2 client → DTLS 1.2 (fallback) +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_send_application_data_pending() { + use dimpl::certificate::generate_self_signed_certificate; + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut client = Dtls::new_12(Arc::clone(&config), client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, Instant::now()); + + let err = server + .send_application_data(b"early auto-server data") + .unwrap_err(); + assert!(matches!(err, Error::HandshakePending)); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect after pending send"); + assert!(sc, "Server should connect after pending send"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_2)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_2)); +} + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_protocol_version_pending() { + use dimpl::certificate::generate_self_signed_certificate; + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut client = Dtls::new_12(Arc::clone(&config), client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, Instant::now()); + + assert_eq!(server.protocol_version(), None); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect after pending protocol check"); + assert!(sc, "Server should connect after pending protocol check"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_2)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_2)); +} + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_dtls12_client() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut client = Dtls::new_12(Arc::clone(&config), client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect"); + assert!(sc, "Server should connect"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_2)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_2)); +} + +// ============================================================================ +// Auto server + auto client → DTLS 1.3 +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_auto_client() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut client = Dtls::new_auto(Arc::clone(&config), client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect"); + assert!(sc, "Server should connect"); + // Both auto: should negotiate DTLS 1.3 + assert_eq!(cv, Some(ProtocolVersion::DTLS1_3)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +// ============================================================================ +// Auto server + DTLS 1.2 client (no cookie) → fallback +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_dtls12_client_no_cookie() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let client_config = default_config(); + let server_config = no_cookie_config(); + + let mut client = Dtls::new_12(client_config, client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect"); + assert!(sc, "Server should connect"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_2)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_2)); +} + +// ============================================================================ +// Auto server + DTLS 1.3 client (no cookie) → DTLS 1.3 +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_dtls13_client_no_cookie() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let client_config = default_config(); + let server_config = no_cookie_config(); + + let mut client = Dtls::new_13(client_config, client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect"); + assert!(sc, "Server should connect"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_3)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +// ============================================================================ +// Auto server + DTLS 1.2 client → fallback, then exchange application data +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_dtls12_fallback_application_data() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut now = Instant::now(); + + let mut client = Dtls::new_12(Arc::clone(&config), client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, now); + + // Complete handshake + let (cc, sc, _, _) = run_handshake(&mut client, &mut server); + assert!(cc && sc, "Handshake should complete"); + + // Send data client → server + let msg = b"hello from dtls12 client"; + client.send_application_data(msg).expect("client send"); + now = Instant::now() + Duration::from_millis(100); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + deliver_packets(&client_out.packets, &mut server); + server.handle_timeout(now).expect("server timeout"); + let server_out = drain_outputs(&mut server); + assert!( + server_out.app_data.iter().any(|d| d == msg), + "Server should receive client's application data" + ); + + // Send data server → client + let reply = b"hello from auto server"; + server.send_application_data(reply).expect("server send"); + now += Duration::from_millis(10); + server.handle_timeout(now).expect("server timeout"); + let server_out = drain_outputs(&mut server); + deliver_packets(&server_out.packets, &mut client); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + assert!( + client_out.app_data.iter().any(|d| d == reply), + "Client should receive server's application data" + ); +} + +// ============================================================================ +// Auto server + DTLS 1.3 client → no fallback, exchange application data +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_dtls13_application_data() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut now = Instant::now(); + + let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, now); + + // Complete handshake + let (cc, sc, _, _) = run_handshake(&mut client, &mut server); + assert!(cc && sc, "Handshake should complete"); + + // Send data client → server + let msg = b"hello from dtls13 client"; + client.send_application_data(msg).expect("client send"); + now = Instant::now() + Duration::from_millis(100); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + deliver_packets(&client_out.packets, &mut server); + server.handle_timeout(now).expect("server timeout"); + let server_out = drain_outputs(&mut server); + assert!( + server_out.app_data.iter().any(|d| d == msg), + "Server should receive client's application data" + ); + + // Send data server → client + let reply = b"hello from auto server (1.3)"; + server.send_application_data(reply).expect("server send"); + now += Duration::from_millis(10); + server.handle_timeout(now).expect("server timeout"); + let server_out = drain_outputs(&mut server); + deliver_packets(&server_out.packets, &mut client); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + assert!( + client_out.app_data.iter().any(|d| d == reply), + "Client should receive server's application data" + ); +} + +// ============================================================================ +// Auto server + DTLS 1.2 client → keying material matches +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_dtls12_fallback_keying_material() { + use dimpl::SrtpProfile; + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut now = Instant::now(); + + let mut client = Dtls::new_12(Arc::clone(&config), client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, now); + + let mut client_km: Option<(Vec, SrtpProfile)> = None; + let mut server_km: Option<(Vec, SrtpProfile)> = None; + + for _ in 0..80 { + client.handle_timeout(now).expect("client timeout"); + server.handle_timeout(now).expect("server timeout"); + + let client_out = drain_outputs(&mut client); + let server_out = drain_outputs(&mut server); + + if let Some(km) = client_out.keying_material { + client_km = Some(km); + } + if let Some(km) = server_out.keying_material { + server_km = Some(km); + } + + deliver_packets(&client_out.packets, &mut server); + deliver_packets(&server_out.packets, &mut client); + + if client_km.is_some() && server_km.is_some() { + break; + } + + now += Duration::from_millis(10); + } + + let client_km = client_km.expect("Client should have keying material"); + let server_km = server_km.expect("Server should have keying material"); + + assert_eq!(client_km.0, server_km.0, "Keying material should match"); + assert_eq!(client_km.1, server_km.1, "SRTP profile should match"); + assert!( + !client_km.0.is_empty(), + "Keying material should not be empty" + ); +} + +// ============================================================================ +// Auto server + DTLS 1.3 client → keying material matches +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_dtls13_keying_material() { + use dimpl::SrtpProfile; + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut now = Instant::now(); + + let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(config, server_cert, now); + + let mut client_km: Option<(Vec, SrtpProfile)> = None; + let mut server_km: Option<(Vec, SrtpProfile)> = None; + + for _ in 0..80 { + client.handle_timeout(now).expect("client timeout"); + server.handle_timeout(now).expect("server timeout"); + + let client_out = drain_outputs(&mut client); + let server_out = drain_outputs(&mut server); + + if let Some(km) = client_out.keying_material { + client_km = Some(km); + } + if let Some(km) = server_out.keying_material { + server_km = Some(km); + } + + deliver_packets(&client_out.packets, &mut server); + deliver_packets(&server_out.packets, &mut client); + + if client_km.is_some() && server_km.is_some() { + break; + } + + now += Duration::from_millis(10); + } + + let client_km = client_km.expect("Client should have keying material"); + let server_km = server_km.expect("Server should have keying material"); + + assert_eq!(client_km.0, server_km.0, "Keying material should match"); + assert_eq!(client_km.1, server_km.1, "SRTP profile should match"); + assert!( + !client_km.0.is_empty(), + "Keying material should not be empty" + ); +} + +// ============================================================================ +// Auto server set_active(true) creates ClientPending +// ============================================================================ + +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_set_active_creates_client_pending() { + use dimpl::certificate::generate_self_signed_certificate; + + let cert = generate_self_signed_certificate().unwrap(); + let config = default_config(); + + let mut dtls = Dtls::new_auto(config, cert, Instant::now()); + assert!(!dtls.is_active()); + + dtls.set_active(true); + assert!(dtls.is_active()); + + // Should be able to produce a hybrid ClientHello + dtls.handle_timeout(Instant::now()).unwrap(); + let mut buf = vec![0u8; 2048]; + let output = dtls.poll_output(&mut buf); + assert!( + matches!(output, Output::Packet(_)), + "Should send hybrid ClientHello" + ); +} + +// ============================================================================ +// Fragmented ClientHello tests — small MTU forces multi-fragment CH +// ============================================================================ + +/// DTLS 1.3 client with small MTU → fragmented ClientHello → auto server connects as 1.3. +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_fragmented_dtls13_client_hello() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + + // MTU=200 forces the ClientHello to be fragmented across multiple records + let client_config = small_mtu_config(200); + let server_config = default_config(); + + let mut client = Dtls::new_13(client_config, client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, Instant::now()); + + let (cc, sc, cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect with fragmented CH"); + assert!(sc, "Server should connect with fragmented CH"); + assert_eq!(cv, Some(ProtocolVersion::DTLS1_3)); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +/// DTLS 1.3 client with very small MTU (100 bytes) → heavily fragmented CH → auto server 1.3. +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_with_heavily_fragmented_dtls13_client_hello() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + + // MTU=100 — heavy fragmentation (CH will be ~5-6 fragments) + let client_config = small_mtu_config(100); + let server_config = default_config(); + + let mut client = Dtls::new_13(client_config, client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, Instant::now()); + + let (cc, sc, _cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect with heavily fragmented CH"); + assert!(sc, "Server should connect with heavily fragmented CH"); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +/// Both client and server with small MTU → fragmented CH → auto server 1.3 + data exchange. +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_fragmented_ch_application_data() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + + let client_config = small_mtu_config(200); + let server_config = default_config(); + + let mut now = Instant::now(); + let mut client = Dtls::new_13(client_config, client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, now); + + // Complete handshake + let (cc, sc, _, _) = run_handshake(&mut client, &mut server); + assert!(cc && sc, "Handshake should complete with fragmented CH"); + + // Send data client → server + let msg = b"data after fragmented handshake"; + client.send_application_data(msg).expect("client send"); + now = Instant::now() + Duration::from_millis(100); + client.handle_timeout(now).expect("client timeout"); + let client_out = drain_outputs(&mut client); + deliver_packets(&client_out.packets, &mut server); + server.handle_timeout(now).expect("server timeout"); + let server_out = drain_outputs(&mut server); + assert!( + server_out.app_data.iter().any(|d| d == msg), + "Server should receive data after fragmented CH handshake" + ); +} + +/// Fragmented DTLS 1.3 ClientHello with no-cookie config → auto server 1.3. +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_fragmented_ch_no_cookie() { + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + + let client_config = small_mtu_config(200); + let server_config = no_cookie_config(); + + let mut client = Dtls::new_13(client_config, client_cert, Instant::now()); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, Instant::now()); + + let (cc, sc, _cv, sv) = run_handshake(&mut client, &mut server); + + assert!(cc, "Client should connect with fragmented CH, no cookie"); + assert!(sc, "Server should connect with fragmented CH, no cookie"); + assert_eq!(sv, Some(ProtocolVersion::DTLS1_3)); +} + +/// Fragmented DTLS 1.3 ClientHello → keying material matches between client and auto server. +#[test] +#[cfg(feature = "rcgen")] +fn auto_server_fragmented_ch_keying_material() { + use dimpl::SrtpProfile; + use dimpl::certificate::generate_self_signed_certificate; + + let _ = env_logger::try_init(); + + let client_cert = generate_self_signed_certificate().unwrap(); + let server_cert = generate_self_signed_certificate().unwrap(); + + let client_config = small_mtu_config(200); + let server_config = default_config(); + + let mut now = Instant::now(); + let mut client = Dtls::new_13(client_config, client_cert, now); + client.set_active(true); + + let mut server = Dtls::new_auto(server_config, server_cert, now); + + let mut client_km: Option<(Vec, SrtpProfile)> = None; + let mut server_km: Option<(Vec, SrtpProfile)> = None; + + for _ in 0..80 { + client.handle_timeout(now).expect("client timeout"); + server.handle_timeout(now).expect("server timeout"); + + let client_out = drain_outputs(&mut client); + let server_out = drain_outputs(&mut server); + + if let Some(km) = client_out.keying_material { + client_km = Some(km); + } + if let Some(km) = server_out.keying_material { + server_km = Some(km); + } + + deliver_packets(&client_out.packets, &mut server); + deliver_packets(&server_out.packets, &mut client); + + if client_km.is_some() && server_km.is_some() { + break; + } + + now += Duration::from_millis(10); + } + + let client_km = client_km.expect("Client should have keying material"); + let server_km = server_km.expect("Server should have keying material"); + + assert_eq!(client_km.0, server_km.0, "Keying material should match"); + assert_eq!(client_km.1, server_km.1, "SRTP profile should match"); + assert!( + !client_km.0.is_empty(), + "Keying material should not be empty" + ); +}