From 0bf6a2cee828236435f68af98e161d57bbfcae34 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Tue, 21 Apr 2026 18:23:15 +0700 Subject: [PATCH] fix(media-server): answer as DTLS server for inbound calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the new DTLS transport logging I could see the real failure mode for inbound calls: meta peer: DTLS state changed state=connecting meta peer: DTLS state changed state=FAILED (~2.4s later) Not a timeout — a fatal DTLS alert. Meta was rejecting our ClientHello because it hadn't yet seen our SDP answer (the answer flows to Meta via Rails' pre_accept_call / accept_call *after* create_session returns, so for ~1 second Meta has no fingerprint to verify us against). Pion treats the alert as fatal and closes the PC, which drops every later audio packet we tried to forward to Meta → Meta times out at 20s with error 138021. Flip the roles with SetAnsweringDTLSRole(DTLSRoleServer). Meta now answers its own offer's actpass as the DTLS client, and Meta only starts ClientHello after it has our answer (so our fingerprint is already known when we receive it). We've been idle until then and have everything we need to complete the handshake in one round trip. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../media-server/internal/peer/meta_peer.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/enterprise/media-server/internal/peer/meta_peer.go b/enterprise/media-server/internal/peer/meta_peer.go index 5592ad4b3..e8eb3ae88 100644 --- a/enterprise/media-server/internal/peer/meta_peer.go +++ b/enterprise/media-server/internal/peer/meta_peer.go @@ -48,12 +48,17 @@ func NewMetaPeer(cfg *config.Config, sdpOffer string, iceServers []webrtc.ICESer return nil, "", fmt.Errorf("set UDP port range: %w", err) } - // Meta's side finishes setting up our DTLS credentials only *after* Rails - // calls pre_accept_call / accept_call on the Graph API. For inbound calls - // that happens ~1 second after we've already begun the DTLS handshake, so - // our first ClientHello is dropped. Extend the handshake window to 30s and - // tighten the retransmission interval so a later retry lands once Meta is - // ready. + // For inbound WhatsApp calls we answer Meta's offer. Meta doesn't know our + // DTLS fingerprint until Rails delivers the answer via pre_accept_call / + // accept_call, which happens ~1 second *after* pion is ready to handshake. + // If we're the DTLS client (pion default when remote is actpass) we blast + // ClientHello at Meta before Meta is listening for us; Meta responds with a + // fatal alert and pion closes the PC. Flip the roles so *we* are the DTLS + // server: Meta becomes the client and only starts ClientHello after it has + // our fingerprint, so the handshake happens in the right order. + if err := se.SetAnsweringDTLSRole(webrtc.DTLSRoleServer); err != nil { + return nil, "", fmt.Errorf("set answering DTLS role: %w", err) + } se.SetDTLSConnectContextMaker(func() (context.Context, func()) { return context.WithTimeout(context.Background(), 30*time.Second) })