Skip to content

feat(dtls): add restart() for DTLS re-handshake after ICE restart#82

Open
nightness wants to merge 3 commits into
webrtc-rs:masterfrom
Brainwires:feat/dtls-restart
Open

feat(dtls): add restart() for DTLS re-handshake after ICE restart#82
nightness wants to merge 3 commits into
webrtc-rs:masterfrom
Brainwires:feat/dtls-restart

Conversation

@nightness

@nightness nightness commented Apr 1, 2026

Copy link
Copy Markdown

Summary

  • Add DTLSTransport::restart() method to trigger a full DTLS re-handshake
  • Integrates with ICE restart detection in SDP negotiation
  • Allows recovery from DTLS session failure without tearing down the peer connection
  • Reuses the existing local certificate/fingerprint across restarts (same identity, new handshake per RFC 8842 §4.4)

Review feedback addressed

  • Move state_change(Connecting) after make_handshake_config() to avoid stuck state on config-build failure
  • Remove Connected no-op in restart() so ICE restarts always trigger a fresh DTLS re-handshake
  • Clear dtls_handshake_config in server branch of restart() to prevent stale client config
  • Clarify that fingerprint is intentionally preserved (not regenerated) — same identity, new handshake
  • Add 20 unit tests covering restart(), prepare_transport(), start(), stop(), and derive_role()

Test plan

  • cargo test -p rtc-dtls — 49 tests pass
  • cargo check / cargo clippy / cargo fmt --check — all clean
  • Verify that calling restart() triggers a new DTLS handshake (unit tests assert state→Connecting, endpoint replaced, config set)
  • Verify ICE restart path correctly invokes DTLS restart (restart() tested for all prior states: Connected, Failed, Closed, Connecting)
  • Verify certificate/fingerprint preservation across restarts
  • Verify error paths (missing certs, double-start, restart from New)

🤖 Generated with Claude Code

@rainliu rainliu requested a review from Copilot April 4, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread rtc/src/peer_connection/transport/dtls/mod.rs Outdated
Comment on lines +1473 to +1501
// 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,
}],
},
)?;

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: the Connected no-op branch was removed. restart() now forces a re-handshake from any state. Should be marked outdated.

Comment on lines +212 to +228
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),
));
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: server branch now sets self.dtls_handshake_config = None to clear stale client config. Should be marked outdated.

@codecov

codecov Bot commented Apr 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.12121% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.21%. Comparing base (9feb4a3) to head (144aeb0).

Files with missing lines Patch % Lines
rtc/src/peer_connection/transport/dtls/mod.rs 48.93% 24 Missing ⚠️
rtc/src/peer_connection/mod.rs 94.73% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

nightness added a commit to Brainwires/webrtc-rs-rtc that referenced this pull request Apr 8, 2026
- 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>
@nightness nightness requested a review from Copilot April 8, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +201 to +206
// 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);

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1473 to +1480
// 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);

Copilot AI Apr 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

nightness added a commit to Brainwires/webrtc-rs-rtc that referenced this pull request Apr 8, 2026
…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>
nightness and others added 3 commits April 10, 2026 00:16
… 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>
@nightness

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants