Skip to content

[core] Retransmit the last DTLS flight when the peer repeats Finished - #3225

Open
renny678 wants to merge 1 commit into
signalwire:masterfrom
renny678:fix-dtls-last-flight-retransmit
Open

renny678 wants to merge 1 commit into
signalwire:masterfrom
renny678:fix-dtls-last-flight-retransmit

Conversation

@renny678

Copy link
Copy Markdown

Problem

WebRTC calls occasionally have no audio in either direction for the whole call. FreeSWITCH logs Fingerprint Verified, Activating audio Secure RTP SEND/RECV and DTLS state from SETUP to READY, while the browser's DTLS transport stays connecting until hangup and no RTP flows. In a load test with 1500 concurrent WebRTC calls, 5 of 9,052 calls were affected (FreeSWITCH 1.10.12; the code below is unchanged in 1.11.3 and master).

Root cause

do_dtls() writes every received DTLS record into the read BIO and then runs the handler for the current state. Once the handshake is done, dtls_state_ready() never calls SSL_read(), so records received in READY are never processed.

As DTLS server in an ordinary handshake, FreeSWITCH sends the last flight (ChangeCipherSpec + Finished). If that flight is lost, the browser retransmits its own last flight, and RFC 6347 section 4.2.4 requires us to answer it: "the node that transmits the last flight (the server in an ordinary handshake or the client in a resumed handshake) MUST respond to a retransmit of the peer's last flight with a retransmit of the last flight. This avoids deadlock conditions if the last flight gets lost."

OpenSSL does this in dtls1_read_bytes() ("If we are server, we may have a repeated FINISHED of the client here, then retransmit our CCS and FINISHED."), but only when the application calls SSL_read().

A packet capture on the FreeSWITCH host showed the same sequence for 4 of the affected calls: the browser sends its Finished flight, FreeSWITCH sends its last flight once (it never reaches the browser), then the browser retransmits its Finished 10 times over about 51 seconds (backoff from 50 ms doubling up to about 25.6 s) and FreeSWITCH never answers.

Fix

In dtls_state_ready(), when a record was received, call SSL_read(). OpenSSL handles the repeated Finished and writes the retransmission into the write BIO, which do_dtls() already sends right after the state handler returns. No application data is expected on this association, so whatever SSL_read() returns is discarded; errors other than SSL_ERROR_WANT_READ and SSL_ERROR_ZERO_RETURN are logged.

For comparison, Janus (src/dtls.c) and Asterisk (res/res_rtp_asterisk.c) call SSL_read() right after each BIO_write() of a received record, whether or not the handshake is done.

Testing

FreeSWITCH 1.11.3 (do_dtls() and dtls_state_ready() are identical on master), Chromium WebRTC clients.

  • Targeted loss: on the browser host, an nftables rule dropped the first copy of FreeSWITCH's last flight on each browser port (UDP payload of 75 bytes starting with 0x14). 13 calls: every first copy was dropped, FreeSWITCH retransmitted the flight 50-80 ms later (as two records, a 14-byte ChangeCipherSpec and a 61-byte Finished), and all 13 calls had audio (at least 99.0% of RTP packets received). Without the patch, a lost last flight was never retransmitted (the capture above).
  • Load test with 1500 concurrent WebRTC calls after the patch: 9,013 calls, none without audio. This run did not happen to lose a last flight, so it only shows that nothing regressed.

We have been running this patch in production since 2026-10-01.

🤖 Generated with Claude Code

do_dtls() writes every received DTLS record into the read BIO, but once
the handshake is done dtls_state_ready() never calls SSL_read(). When our
last flight (ChangeCipherSpec + Finished) is lost, the browser
retransmits its Finished, nobody processes it, and OpenSSL never
retransmits our flight as RFC 6347 section 4.2.4 requires. The browser
stays in "connecting" and the call has no audio in either direction.

Call SSL_read() in dtls_state_ready() when a record was received, so
OpenSSL retransmits the last flight; do_dtls() then sends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant