From 144aeb05078f9408b869312d7bad61a672656712 Mon Sep 17 00:00:00 2001 From: nightness Date: Wed, 1 Apr 2026 09:49:40 -0500 Subject: [PATCH] feat(dtls): add restart() for DTLS re-handshake after session failure on ICE restart When ICE restarts the underlying path can change while the DTLS session stays alive (RFC 8842: DTLS is not required to restart on ICE restart). However, if the DTLS session was already Failed, Closed, or lost mid- handshake before ICE re-establishes, the client-side connect() will never fire again because dtls_handshake_config was consumed by the first ICESelectedCandidatePairChange. Changes: - Refactor make_handshake_config() from prepare_transport() so the config can be rebuilt without the state==New guard - Add DTLSTransport::restart(): no-ops if Connected (live session survives ICE restart transparently); replaces endpoint and restores dtls_handshake_config if state is Failed/Closed/Connecting-but-lost - Call dtls_transport_mut().restart() from set_remote_description when ICE credentials change during renegotiation Co-Authored-By: Claude Sonnet 4.6 --- rtc/src/peer_connection/mod.rs | 30 +++++++ rtc/src/peer_connection/transport/dtls/mod.rs | 81 ++++++++++++++++--- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/rtc/src/peer_connection/mod.rs b/rtc/src/peer_connection/mod.rs index dca5c468..a6adc698 100644 --- a/rtc/src/peer_connection/mod.rs +++ b/rtc/src/peer_connection/mod.rs @@ -1469,6 +1469,36 @@ where self.ice_transport_mut() .set_remote_credentials(remote_ufrag.clone(), remote_pwd.clone())?; + + // RFC 8842: on ICE restart the DTLS transport must re-handshake over the new + // ICE path. Extract updated remote fingerprint from the new SDP and reset the + // DTLS endpoint so the next ICESelectedCandidatePairChange triggers a fresh + // handshake. + let (remote_fingerprint, remote_fingerprint_hash) = + extract_fingerprint(parsed_remote_description)?; + let remote_dtls_role = RTCDtlsRole::from(parsed_remote_description); + + // Determine local ICE role for DTLS role derivation. + let remote_is_lite = is_lite_set(parsed_remote_description); + let local_ice_role = if (we_offer + && remote_is_lite == self.setting_engine.candidates.ice_lite) + || (remote_is_lite && !self.setting_engine.candidates.ice_lite) + { + RTCIceRole::Controlling + } else { + RTCIceRole::Controlled + }; + + self.dtls_transport_mut().restart( + local_ice_role, + DTLSParameters { + role: remote_dtls_role, + fingerprints: vec![RTCDtlsFingerprint { + algorithm: remote_fingerprint_hash, + value: remote_fingerprint, + }], + }, + )?; } for candidate in candidates { diff --git a/rtc/src/peer_connection/transport/dtls/mod.rs b/rtc/src/peer_connection/transport/dtls/mod.rs index 7a58f79b..b552146b 100644 --- a/rtc/src/peer_connection/transport/dtls/mod.rs +++ b/rtc/src/peer_connection/transport/dtls/mod.rs @@ -110,18 +110,13 @@ impl RTCDtlsTransport { DEFAULT_DTLS_ROLE_ANSWER } - pub(crate) fn prepare_transport( - &mut self, - ice_role: RTCIceRole, - remote_dtls_parameters: DTLSParameters, + /// Build a DTLS HandshakeConfig from remote fingerprints. + /// Does not check or change transport state — callable from both initial start and restart. + fn make_handshake_config( + &self, + remote_dtls_parameters: &DTLSParameters, ) -> Result> { - if self.state != RTCDtlsTransportState::New { - return Err(Error::ErrInvalidDTLSStart); - } - - self.dtls_role = self.derive_role(ice_role, remote_dtls_parameters.role); - - let remote_fingerprints = remote_dtls_parameters.fingerprints; + let remote_fingerprints = remote_dtls_parameters.fingerprints.clone(); let verify_peer_certificate: VerifyPeerCertificateFn = Arc::new( move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { if certs.is_empty() { @@ -153,7 +148,6 @@ impl RTCDtlsTransport { } else { return Err(Error::ErrNonCertificate); }; - self.state_change(RTCDtlsTransportState::Connecting); Ok(Arc::new( ::dtls::config::ConfigBuilder::default() @@ -173,6 +167,69 @@ impl RTCDtlsTransport { )) } + pub(crate) fn prepare_transport( + &mut self, + ice_role: RTCIceRole, + remote_dtls_parameters: DTLSParameters, + ) -> Result> { + if self.state != RTCDtlsTransportState::New { + return Err(Error::ErrInvalidDTLSStart); + } + + self.dtls_role = self.derive_role(ice_role, remote_dtls_parameters.role); + self.state_change(RTCDtlsTransportState::Connecting); + self.make_handshake_config(&remote_dtls_parameters) + } + + /// Re-initialise the DTLS transport for re-handshake after a failed/lost session. + /// + /// If DTLS is `Connected`, the existing session is kept alive — it survives ICE + /// restarts because the new ICE path is transparent to DTLS. Only when DTLS is + /// `Failed`, `Closed`, or `Connecting` (handshake was in-flight and lost) is the + /// endpoint replaced so the next `ICESelectedCandidatePairChange` event triggers a + /// fresh handshake. No-ops if state is `New` (initial `start_transports` handles it). + pub(crate) fn restart( + &mut self, + local_ice_role: RTCIceRole, + remote_dtls_parameters: DTLSParameters, + ) -> Result<()> { + match self.state { + // Not started yet — initial start_transports handles this path. + RTCDtlsTransportState::New => return Ok(()), + // Session is live; keep it across the ICE restart transparently. + RTCDtlsTransportState::Connected => return Ok(()), + // Failed / Closed / Connecting-but-lost → rebuild and re-handshake. + _ => {} + } + + // Derive and update the role (may differ if ICE role swapped during restart). + self.dtls_role = self.derive_role(local_ice_role, remote_dtls_parameters.role); + + let dtls_handshake_config = self.make_handshake_config(&remote_dtls_parameters)?; + + self.state_change(RTCDtlsTransportState::Connecting); + + if self.dtls_role == RTCDtlsRole::Client { + // Client: create a fresh endpoint and store the handshake config so the + // next ICESelectedCandidatePairChange event triggers connect(). + self.dtls_endpoint = Some(::dtls::endpoint::Endpoint::new( + TransportContext::default().local_addr, + TransportProtocol::UDP, + None, + )); + self.dtls_handshake_config = Some(dtls_handshake_config); + } else { + // Server: create a new accepting endpoint with the updated config. + self.dtls_endpoint = Some(::dtls::endpoint::Endpoint::new( + TransportContext::default().local_addr, + TransportProtocol::UDP, + Some(dtls_handshake_config), + )); + } + + Ok(()) + } + pub(crate) fn role(&self) -> RTCDtlsRole { self.dtls_role }