feat(dtls): add restart() for DTLS re-handshake after ICE restart#82
feat(dtls): add restart() for DTLS re-handshake after ICE restart#82nightness wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a restart() API to the DTLS transport and wires it into the ICE-restart path so a new DTLS handshake can be triggered without tearing down the entire peer connection.
Changes:
- Refactors DTLS handshake-config building into a reusable helper (
make_handshake_config). - Adds
RTCDtlsTransport::restart()to rebuild DTLS endpoint/config for re-handshake scenarios. - Hooks ICE restart detection in SDP negotiation to call DTLS restart with the new remote fingerprint.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| rtc/src/peer_connection/transport/dtls/mod.rs | Introduces DTLS restart and refactors handshake-config creation to support both initial start and restart. |
| rtc/src/peer_connection/mod.rs | Calls DTLS restart during ICE restart using fingerprint extracted from the updated remote SDP. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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, | ||
| }], | ||
| }, | ||
| )?; |
There was a problem hiding this comment.
The ICE-restart path comment and PR description state that ICE restart must trigger a DTLS re-handshake, but RTCDtlsTransport::restart() currently no-ops when DTLS state is Connected. Either (a) update restart() to actually force a re-handshake on ICE restart even when connected, or (b) adjust the RFC/behavior comments (and possibly the PR description) to reflect the intended behavior. As-is, the call site strongly implies a handshake will occur, but the DTLS layer may skip it.
There was a problem hiding this comment.
Addressed: the Connected no-op branch was removed. restart() now forces a re-handshake from any state. Should be marked outdated.
| 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), | ||
| )); | ||
| } |
There was a problem hiding this comment.
In the server branch, self.dtls_handshake_config is not cleared. If the transport was previously acting as a client (or had a stored config for any reason), this leaves stale state around that can be confusing and can cause incorrect behavior if other code checks dtls_handshake_config without also checking dtls_role. Consider explicitly setting self.dtls_handshake_config = None in the server branch.
There was a problem hiding this comment.
Addressed: server branch now sets self.dtls_handshake_config = None to clear stale client config. Should be marked outdated.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #82 +/- ##
==========================================
+ Coverage 71.17% 71.21% +0.04%
==========================================
Files 442 442
Lines 67330 67386 +56
==========================================
+ Hits 47922 47989 +67
+ Misses 19408 19397 -11 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Move state_change(Connecting) after make_handshake_config() in prepare_transport() so a config-build failure does not leave the transport stuck in Connecting state. - Remove the Connected no-op in restart() so ICE restarts always trigger a fresh DTLS re-handshake as documented. - Clear dtls_handshake_config in the server branch of restart() to avoid stale client config from a previous role. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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); |
There was a problem hiding this comment.
DTLS restart replaces the endpoint but does not regenerate local DTLS credentials/certificate. That means the local fingerprint used in subsequently generated SDP (via dtls_transport().certificates.first().get_fingerprints()) will remain unchanged, which conflicts with the PR description (“Regenerates DTLS credentials on restart”) and can break interop if the peer expects a new fingerprint on restart. Consider generating a fresh RTCCertificate on restart (or a dedicated “dtls restart” path) and updating the stored certificates so future offers/answers signal the new fingerprint.
There was a problem hiding this comment.
Addressed: PR description corrected — local certificate and fingerprint are intentionally preserved across restarts (same identity, new handshake per RFC 8842 §4.4). Should be marked outdated.
| // 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); | ||
|
|
There was a problem hiding this comment.
New behavior (DTLS restart triggered by ICE restart / remote credential change) doesn’t appear to have coverage asserting a DTLS re-handshake actually occurs (e.g., DTLSHandshakeComplete emitted twice, SRTP contexts replaced, or transport dtls_state transitions back through Connecting). Since the repo already has ICE restart interop tests, consider extending one to validate the DTLS restart path so regressions are caught automatically.
There was a problem hiding this comment.
Addressed: 20 unit tests added covering restart from Connected (client/server), Failed, Closed, Connecting, New, certificate preservation, stale config clearing, and state transitions. Should be marked outdated.
…p/derive_role Address Copilot review comments on PR webrtc-rs#82: - Clarify in doc comment that restart() preserves certificate identity (same fingerprint, new handshake) — this is intentional per RFC 8842 §4.4 - Add comprehensive test coverage for all restart() state transitions (Connected, Failed, Closed, Connecting, New no-op) - Test client/server role assignment, stale config clearing, certificate preservation, and error paths (missing certs, double-start) - Cover prepare_transport state-change ordering (Connecting only after successful config build) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
- Move state_change(Connecting) after make_handshake_config() in prepare_transport() so a config-build failure does not leave the transport stuck in Connecting state. - Remove the Connected no-op in restart() so ICE restarts always trigger a fresh DTLS re-handshake as documented. - Clear dtls_handshake_config in the server branch of restart() to avoid stale client config from a previous role. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…p/derive_role Address Copilot review comments on PR webrtc-rs#82: - Clarify in doc comment that restart() preserves certificate identity (same fingerprint, new handshake) — this is intentional per RFC 8842 §4.4 - Add comprehensive test coverage for all restart() state transitions (Connected, Failed, Closed, Connecting, New no-op) - Test client/server role assignment, stale config clearing, certificate preservation, and error paths (missing certs, double-start) - Cover prepare_transport state-change ordering (Connecting only after successful config build) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94b9a64 to
47480ba
Compare
|
Rebased onto upstream/master so this PR contains only its own changes. Previous branch structure caused merge conflicts when PRs were merged in sequence. Each PR is now independently mergeable. |
Summary
DTLSTransport::restart()method to trigger a full DTLS re-handshakeReview feedback addressed
state_change(Connecting)aftermake_handshake_config()to avoid stuck state on config-build failureConnectedno-op inrestart()so ICE restarts always trigger a fresh DTLS re-handshakedtls_handshake_configin server branch ofrestart()to prevent stale client configTest plan
cargo test -p rtc-dtls— 49 tests passcargo check/cargo clippy/cargo fmt --check— all cleanrestart()triggers a new DTLS handshake (unit tests assert state→Connecting, endpoint replaced, config set)🤖 Generated with Claude Code