Skip to content
Germán Luis Aracil Boned edited this page Jul 3, 2026 · 10 revisions

WebRTC

GABPBX makes chan_sofia a WebRTC endpoint. A browser running a SIP-over-WebSocket library (for example SIP.js or JsSIP) can register, place a call and receive a call, with two-way audio, two-way video, and relayed DataChannels, secured by DTLS-SRTP. The implementation is self-contained: there is no pjproject and no libnice in the tree. The same forked RTP engine that carries plain SIP calls carries WebRTC, so non-WebRTC media is unchanged.

This page covers how it fits together and how to turn it on. The per-feature catalogue is Features; the security model is Security.

What works

  • Two WSS browsers can hold a real two-way audio call end to end.
  • A call placed to a wss-registered browser connects with two-way audio as well — i.e. GABPBX as the SDP offerer, not only browser-originated calls.
  • Two-way video (VP8 or H.264) rides on top of the audio call.
  • Opus passes through both-Opus calls without transcoding.
  • Two browsers can open a DataChannel and exchange text or binary messages relayed back-to-back through GABPBX.
  • Hold and resume work like on a desk phone (1.4.2): either side can hold — the held party hears music on hold — and on unhold audio and video both return by themselves, driven by RTCP keyframe feedback (PSFB PLI/FIR).
  • A call forked to an extension registered from both a browser (wss) and a desk phone (udp) carries full two-way media whichever contact answers (1.4.1).
  • Setup, teardown and NAT survival all route correctly back over the accepted WebSocket.

The standards in play

Concern Mechanism RFC Where
SIP transport SIP over secure WebSocket RFC 7118 channels/chan_sofia.c:11568-11660
Media keying DTLS-SRTP RFC 5763/5764 res/res_rtp_gabpbx.c:1001
Connectivity ICE-lite STUN responder RFC 8445 res/res_rtp_gabpbx.c:1265
One socket per stream rtcp-mux RFC 5761 channels/sofia/sofia_sdp.c:268
Multiplexed media BUNDLE + a=mid RFC 8843 channels/sofia/sofia_sdp.c:359
RTP stream identification MID header extension RFC 8285 channels/sofia/sofia_sdp.c
Packet demux STUN / DTLS / SRTP by first byte RFC 7983 res/res_rtp_gabpbx.c:1732
Audio codec Opus RFC 7587 channels/sofia/sofia_sdp.c:172
DataChannel transport SCTP over DTLS RFC 8831 channels/sofia/sofia_datachannel.c
DataChannel setup DCEP OPEN/ACK RFC 8832 channels/sofia/sofia_datachannel.c
DataChannel SDP m=application UDP/DTLS/SCTP RFC 8841 channels/sofia/sofia_sdp.c

How a WebRTC leg is built

1. SIP over WSS

The browser registers and signals over a secure WebSocket. chan_sofia opens a dedicated WSS listener from wssbindport. sofia-sip looks for fixed certificate filenames inside a cert directory (agent.pem/cafile.pem for TLS, wss.pem/ca-bundle.crt for WSS), so chan_sofia bridges the operator's real files to those names automatically:

  • tlscertfile may be the cert directory or the real cert+key file. When it names a file, the directory is derived from it and agent.pem is soft-linked to that file — so you can point straight at an externally managed certificate without renaming it. tlscafile is likewise soft-linked to cafile.pem.
  • The WSS-named files are then soft-linked from the TLS ones (wss.pem from agent.pem, ca-bundle.crt from cafile.pem). All links are created only when missing and never overwrite existing files.

Certificate immunity: if a configured TLS or WSS listener has no usable certificate, chan_sofia logs an error and disables only that listener — UDP, TCP and WS keep serving and the module still loads, instead of the whole driver failing. tlsenable/wsenable/wssenable (default on) explicitly turn a listener off even when its bind port is set.

A browser's Contact host is always a placeholder, so in-dialog ACK/BYE are routed back over the exact accepted WebSocket connection rather than to the Contact (channels/chan_sofia.c:542-589), and ;transport=wss is appended to the request-URI (mandatory for WSS per RFC 7118).

Flow-close (RFC 5626). A WSS binding is tied to its WebSocket connection. When that connection closes — a browser tab closing, a page refresh, or a dropped network — the registrar's connection watch detects the close and removes the binding promptly, instead of leaving a stale binding to linger until the SIP registration expires. The same applies to the other connection-oriented transports (WS, TLS, TCP). This matters for browser clients that reconnect often: a refresh no longer leaves a dead contact that absorbs forked INVITEs until the expiry runs out. The binding is always removed and routing state is always corrected on a flow close; the optional flowclose_emit_unregister option (peer or [general], default no) controls only whether the external unregister side-effects fire on that close — the AMI PeerStatus event, BLF/hint device-state, and regexten cleanup. Leaving it no keeps a routine reconnect quiet (a refresh does not flap the BLF lamp); set it to yes to get an explicit unregister event/state update each time a flow closes.

2. The media proto

A WebRTC offer/answer uses the UDP/TLS/RTP/SAVPF media transport (channels/sofia/sofia_sdp.c:380) instead of plain RTP/AVP. The SDP carries a=ice-lite, ICE ufrag/pwd/candidate, a=fingerprint:sha-256, a=setup, a=rtcp-mux, a=mid, and a session-level a=group:BUNDLE.

3. ICE-lite

GABPBX is permanently the controlled, lite ICE agent (res/res_rtp_gabpbx.c:1265, role AST_RTP_ICE_ROLE_CONTROLLED). It does not gather candidates or send checks; it answers the browser's STUN binding requests, learns the peer transport address from the first authenticated check, and latches the media send tuple on every authenticated check (re-affirmed when a pair is nominated with USE-CANDIDATE). The STUN responder is armed in sofia_webrtc_provision_offer before the offer leaves the wire, so the browser's earliest connectivity checks are never dropped into a dead window.

Immediate media latch (RFC 8445 §7.3.1 / RFC 7675). GABPBX latches the media return path on every integrity-authenticated connectivity check, not only on the nominated (USE-CANDIDATE) pair. Because the responder is armed before the offer leaves the wire, the very first authenticated STUN binding request from the browser fixes where media is sent, and each subsequent authenticated check re-affirms it. This removes the intermittent one-way audio and the multi-second gap before audio flowed that could occur while waiting for nomination, and it is what makes a call placed to a browser (GABPBX as offerer) reliably two-way rather than coming up deaf. Nomination is still honoured when it arrives, but it is no longer a precondition for media to flow.

Role-conflict repair (RFC 8445 §6.1.1 / §7.3.1.1 / §7.2.5.1). When GABPBX is the offerer, the browser answers the offer — and a full agent answering can default to the controlled role too. With both ends controlled, neither side nominates a candidate pair (no USE-CANDIDATE is ever sent), so no media pair is selected: the call comes up deaf and is then dropped. GABPBX detects this and, on receiving an authenticated binding request that also carries ICE-CONTROLLED, replies 487 Role Conflict. Per the RFC the full agent (the browser) then flips to the controlling role, re-runs its connectivity checks, and nominates a pair — at which point GABPBX latches it. GABPBX keeps answering every other check normally; only the controlled/controlled collision triggers the 487.

4. DTLS-SRTP

Keys are not in the SDP. After ICE connectivity, a DTLS handshake runs over the media path; SRTP keys are exported from it. The cert is ephemeral, self-signed, EC P-256, CN gabpbx. a=setup advertises the engine's real negotiated role so the two sides never both try to be the DTLS client. A dedicated retransmit scheduler honors DTLS exponential backoff. gabpbx_dtls engine at res/res_rtp_gabpbx.c:1001; key install at :1015-1071.

The trust anchor is the SDP a=fingerprint (RFC 8827/5763): the self-signed cert cannot chain-verify, so on handshake completion the engine demands a peer cert, requires a stored remote fingerprint, and does a hash-matched X509_digest + memcmp against the SDP fingerprint. Any absence or mismatch fails the call closed as a possible MITM (res/res_rtp_gabpbx.c:1097-1120).

Deferring DTLS until the answer's fingerprint arrives (RFC 5763 / RFC 8842). When GABPBX is the offerer it pre-arms DTLS at offer time, but the browser learns GABPBX's connection address from ICE and can send its DTLS ClientHello before its SIP 200 OK answer — which carries the a=fingerprint trust anchor — has been received and applied. Completing the handshake with no remote fingerprint yet installed would trip the fail-closed check above and tear the call down. The engine now defers inbound DTLS records non-fatally while no remote fingerprint is set: DTLS naturally retransmits, the answer's fingerprint is installed once the 200 OK is applied, and the handshake then completes and verifies as normal. The answerer path (GABPBX already holds the offer's fingerprint before any handshake) is unchanged.

5. One socket, RFC 7983 demux

With rtcp-mux, a single socket per stream carries STUN, DTLS and (S)RTP/RTCP interleaved. __rtp_recvfrom demuxes by the first byte (res/res_rtp_gabpbx.c:1732-1786): STUN to the ICE responder, DTLS records to the handshake, RTP to SRTP-unprotect, with muxed SRTCP dropped and a flood guard. This path is dormant unless DTLS is active, so plain RTP is untouched.

6. BUNDLE and m-line handling

Audio is the tagged BUNDLE transport. A browser typically offers audio + video

  • datachannel; the datachannel m-line is accepted and bundled on the audio DTLS (see §9 below), while any genuinely unsupported m-line is reflected back as a port-0 rejection in order (RFC 3264 §6) rather than 488-ing the whole session (channels/sofia/sofia_sdp.c). This is what lets a browser's audio+video+datachannel offer be answered cleanly.

7. Video on its own transport

Video does not ride the audio BUNDLE. The accepted/offered m=video is emitted on its own port with its own ICE/fingerprint/setup/candidate, a=rtcp-mux, and a distinct a=mid — a separate DTLS association from the audio (channels/sofia/sofia_sdp.c:386-491). GABPBX both accepts a browser's offered video (answerer) and offers video itself (offerer), so two browsers can see each other.

Because video is pre-armed at offer time like audio, the answer is handled defensively: if the peer rejects the offered video, the pre-armed video DTLS/ICE is torn down cleanly, and a failure to set up the video media instance never takes the audio call down with it — the call continues as audio-only instead of dropping.

7a. Video bundled on the audio transport (webrtc_video_bundle)

Modern browsers negotiate max-bundle (RFC 8843): all media on a single ICE/DTLS transport. webrtc_video_bundle (per peer or [general], default off, requires webrtc=yes) makes GABPBX carry video on the same ICE/DTLS transport as the audio instead of on its own port. Audio and video then share one connectivity check and one DTLS association; the two streams are separated by payload-type demux and tagged with the RTP MID header extension (RFC 8285) so each packet is attributed to its a=mid. Left at its default (off), video keeps its own transport exactly as described in §7, so existing deployments are byte-for-byte unchanged until the option is set.

8. Opus passthrough

A both-Opus call passes through with no transcoding. The three fixes that made browser-to-browser Opus clean:

  • codec selection now knows Opus,
  • the format rate returns 48 kHz, so the RTP TX timestamp advances +960 per 20 ms frame (not +160),
  • the RTP smoother no longer re-chunks Opus's variable-size frames (res/res_rtp_gabpbx.c:2840-2845).

Opus is offered with useinbandfec=1;usedtx=0 and channels=2 (channels/sofia/sofia_sdp.c:172-174).

9. DataChannel relay

GABPBX bridges WebRTC DataChannels back-to-back between two WebRTC legs. When a browser offers m=application UDP/DTLS/SCTP webrtc-datachannel, the channel is negotiated on the same BUNDLE'd DTLS transport as the audio (no separate association), and the SCTP association is carried by an optional usrsctp build.

  • Offer and answer, both ways. GABPBX accepts an inbound DataChannel offer and offers one to the bridged far leg, so a createDataChannel between two browsers reaches end to end. The far leg negotiates its own SCTP; GABPBX proxies the DCEP OPEN/ACK (RFC 8832) and relays each message preserving its PPID, so both text and binary channels cross.
  • One worker lane. The receive path runs on a single dedicated worker off the RTP instance lock; the relay reaches the far leg with a non-blocking channel trylock + reference and releases all channel locks before the SCTP send, so the two legs can never deadlock against each other.
  • Bounded. a=max-message-size is honored per RFC 8841 §6 (absent → the 64 KiB default; explicit 0 → unbounded, hard-capped); the negotiated stream count is capped to bound memory.
  • Clean teardown. When a leg ends, its SCTP socket is abortively closed (SO_LINGER 0) so no library timer outlives the channel.

It is build-gated by usrsctp (HAVE_USRSCTP) and opt-in per peer with datachannel=yes (inherited from [general], requires webrtc=yes). Without the usrsctp build, or with datachannel=no, an offered datachannel m-line is simply port-0 rejected and audio/video are unaffected. Implementation: channels/sofia/sofia_datachannel.c plus the engine DTLS-appdata seam in main/rtp_engine.c / res/res_rtp_gabpbx.c.

10. Hold, resume and keyframe feedback (1.4.2)

WebRTC stacks validate SDP strictly (JSEP, RFC 8829), so hold — a routine re-INVITE for a desk phone — needs the answer to be exactly right, and video needs an RTCP feedback loop to recover after it. chan_sofia implements the whole chain:

  • The answer direction mirrors the offer (RFC 3264 §6.1). A hold offer (a=sendonly) is answered a=recvonly, inactive is mirrored back, and a resume (sendrecv) restores sendrecv — per media stream, staged during SDP parsing and committed only after every reject gate passes, so a rejected re-INVITE leaves the negotiated direction untouched.
  • The answer mirrors every offered m-line (RFC 3264 §6). A mid-call re-INVITE swaps the offer/answer roles, so parsing and generation carry the current transaction role: the answer reproduces the offer's m-lines in order (audio, video, datachannel — declined ones at port 0) even on the leg GABPBX originally made the offer to. Browsers reject an answer with missing m-lines with 488; this is what made hold from the answered side fail.
  • Renegotiation is stable. The negotiated video payload type is preserved across re-INVITEs when the new offer still includes it (RFC 3264 §8.3.2), and the SDP stream identity (cname, msid) persists for the whole dialog — including across a fork-winner handover (RFC 7022, RFC 8830) — so the browser keeps its remote track instead of seeing a new stream mid-call.
  • Keyframe feedback (RFC 4585 / RFC 5104). When a receiver is (re)created — the unhold case — it requests a fresh keyframe with an RTCP PSFB PLI (or FIR). GABPBX advertises a=rtcp-fb:<pt> nack pli and a=rtcp-fb:<pt> ccm fir on WebRTC video sections, understands inbound PLI and FIR (surfaced from the rtcp-mux demultiplexer as a video-update indication), relays the indication across the bridge to the other call leg, and transmits an SRTCP-protected PLI to that leg's browser over the ICE-selected RTP tuple (rtcp-mux aware), targeting the learned remote video SSRC and throttled against keyframe-request storms. The far encoder emits a keyframe and video repaints — which is why unhold now restores video as instantly as audio.

Only implemented feedback is advertised (no REMB or transport-cc), plain non-WebRTC video sections gain no feedback lines, and both feedback directions fail closed if the SRTP context is unavailable.

11. Video across to a legacy SIP video phone (1.5.0)

The WebRTC video work above assumed both legs were browsers (H.264/VP8). A plain SIP video phone on RTP/AVP — one that offers, say, H.264 alongside H.263-1998 — exposed a passthrough gap: because chan_sofia does not transcode video, the answer sent back to that caller has to be the intersection of what it offered with what the far/bridge leg actually negotiated. Previously the caller-leg answer kept the caller's full offered video set (and could list a codec the browser cannot produce first), so the phone locked its video onto a payload type the WebRTC peer never sent, and video went black in both directions while audio was fine.

1.5.0 closes this for the passthrough case (RFC 3264 §6):

  • The answer is narrowed to the far leg's negotiated video. When the bridged peer answered with, e.g., H.264 only, the caller's answer lists H.264 only — codecs the far leg cannot produce are dropped, not just reordered, so the phone cannot lock onto an unsupported payload type. If there is no common video codec, video fails closed to audio rather than emitting a stream the peer will drop.
  • H.264 a=fmtp is carried across. profile-level-id and packetization-mode (RFC 6184 §8.2.2) are matched and relayed; a packetization-mode mismatch is disqualifying (FU-A vs Single-NAL are not interchangeable), while an absent profile is treated leniently.
  • No mid-call channel-state mutation. The intersection is applied only to the SDP being emitted; the channel's native-format set is left untouched, so the fix cannot ripple into a re-INVITE storm on the far leg.
  • Keyframes cross the bridge. A picture-loss indication from either transport is translated to the other — SIP INFO picture_fast_update (RFC 5168) ⇄ RTCP PLI/FIR — so the newly bridged video repaints promptly. The INFO answer is bound to its own transaction (NUTAG_WITH_THIS), which also removed a spurious ~32-second call teardown (Timer-F on an unanswered INFO).

The browser side is unchanged; this only affects how the non-WebRTC caller leg's answer is built. Browser-to-browser video (the 1.4.x behaviour) is unaffected.

Turning it on

WebRTC is opt-in per peer with webrtc=yes, which is inherited from [general] and can be overridden per peer (channels/chan_sofia.c:2181, :2591, :13173).

[general]
; cert DIR, or the real cert+key FILE (agent.pem is soft-linked to it).
; WSS names (wss.pem / ca-bundle.crt) are auto-linked from the TLS ones.
tlscertfile=/etc/gabpbx/keys/gabpbx.pem
tlscafile=/etc/gabpbx/keys/ca.crt

; secure WebSocket listener for browsers
wssbindaddr=0.0.0.0
wssbindport=8089
; tlsenable / wsenable / wssenable default to yes; set to no to disable a
; listener even when its bind port is set (e.g. a node with no certificate).

; default new peers to WebRTC (or set per peer)
webrtc=yes
; carry WebRTC video on the audio ICE/DTLS transport (max-bundle); needs webrtc=yes
webrtc_video_bundle=yes
; relay browser-to-browser DataChannels (needs a usrsctp build)
datachannel=yes

[browser-200]
type=peer
host=dynamic
defaultuser=browser-200
secret=change-this-secret
context=internal
webrtc=yes
disallow=all
allow=opus
allow=ulaw

A webrtc=yes peer fails the call rather than ever falling back to insecure media, and direct media is force-disabled on any DTLS/SRTP leg so keys are never bridged or disclosed (channels/chan_sofia.c:3129-3134).

The media profile follows the transport, not a global flag

webrtc=yes (per peer or inherited from [general]) is only the enable. The actual outbound media profile is always chosen by the target's real transport, so a global webrtc=yes never forces DTLS-SRTP onto a plain phone (which would otherwise reject it with 406/488 - RFC 3264 / RFC 5764):

target offered media
contact registered over ws/wss UDP/TLS/RTP/SAVPF (DTLS-SRTP)
contact over udp / tcp / tls RTP/AVP (or RTP/SAVP with encryption=yes)
static peer (host!=dynamic) by its configured transport=

Because the choice is made per contact, one account can be used from several phones with different technologies at the same time - a WebRTC browser over wss, a desk phone over udp and another over tls, all registered concurrently. A call to that account forks an INVITE to each contact and offers each the correct profile (DTLS-SRTP to the wss browser, plain RTP to the udp/tls phones) in the same call; inbound calls are answered in kind. No realtime schema change is needed

  • WebRTC is auto-detected from each contact's ws/wss registration. webrtc=no on a peer disables DTLS for it entirely.

Operational notes

  • sip show peer <name> and sip show settings report the WebRTC state of a peer and the global default.
  • sip set debug sdp on is a focused SDP/ICE tracer (off by default) for WebRTC negotiation. It dumps the generated offer SDP, the WebRTC offer-gate decision, the incoming answer SDP and the offerer answer-apply path — showing the signaling of a WSS/TLS session already decrypted, which a packet capture on the wire cannot give you. See Operations.
  • rtp set debug ice on traces the ICE responder — each authenticated check, the latch and nomination decisions, and any dropped or fingerprint-failed record — and sip set debug fork on traces a single INVITE forked across mixed transports (for example one account registered from a udp phone and a wss browser at the same time), which is the main window into a mixed UDP+WSS fork. Both are off by default.
  • To read the full decrypted SIP exchange (including SDP) off a live WSS/TLS session, chan_sofia can stream every message as HEP to a Homer/sipcapture server with sip_capture_address (read it with sngrep -H udp:HOST:PORT) or write it to a plain-text file with sip_capture_file; the same can be toggled at runtime with sip set debug capture <host:port> / sip set debug file <path>. See Operations for the capture facility.
  • A WSS browser that refreshes or reconnects has its old binding dropped promptly on the WebSocket close (RFC 5626 flow-close); set flowclose_emit_unregister=yes if you want an AMI PeerStatus / BLF / regexten unregister to fire on each such close, or leave the default no to keep routine reconnects quiet.
  • A reverse proxy (for example nginx) terminating browser HTTPS/WSS in front of chan_sofia is a common deployment; route the WebSocket to the wssbindport listener. When debugging the WSS leg, capture on the loopback the proxy forwards to.
  • After editing sofia.conf as root, restore ownership so GABPBX can read it (chown gabpbx:gabpbx /etc/gabpbx/sofia.conf).

Not in scope (yet)

  • Full (non-lite) ICE with candidate gathering and TURN — GABPBX is the ICE-lite controlled agent by design.

Clone this wiki locally