-
Notifications
You must be signed in to change notification settings - Fork 2
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.
- 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.
| 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 |
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:
-
tlscertfilemay be the cert directory or the real cert+key file. When it names a file, the directory is derived from it andagent.pemis soft-linked to that file — so you can point straight at an externally managed certificate without renaming it.tlscafileis likewise soft-linked tocafile.pem. - The WSS-named files are then soft-linked from the TLS ones (
wss.pemfromagent.pem,ca-bundle.crtfromcafile.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.
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.
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.
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.
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.
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.
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.
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.
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).
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
createDataChannelbetween two browsers reaches end to end. The far leg negotiates its own SCTP; GABPBX proxies the DCEPOPEN/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-sizeis honored per RFC 8841 §6 (absent → the 64 KiB default; explicit0→ 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.
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 answereda=recvonly,inactiveis mirrored back, and a resume (sendrecv) restoressendrecv— 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 plianda=rtcp-fb:<pt> ccm firon 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.
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=fmtpis carried across.profile-level-idandpacketization-mode(RFC 6184 §8.2.2) are matched and relayed; apacketization-modemismatch 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
INFOpicture_fast_update(RFC 5168) ⇄ RTCP PLI/FIR — so the newly bridged video repaints promptly. TheINFOanswer is bound to its own transaction (NUTAG_WITH_THIS), which also removed a spurious ~32-second call teardown (Timer-F on an unansweredINFO).
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.
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=ulawA 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).
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=noon a peer disables DTLS for it entirely.
-
sip show peer <name>andsip show settingsreport the WebRTC state of a peer and the global default. -
sip set debug sdp onis 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 ontraces the ICE responder — each authenticated check, the latch and nomination decisions, and any dropped or fingerprint-failed record — andsip set debug fork ontraces 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_sofiacan stream every message as HEP to a Homer/sipcapture server withsip_capture_address(read it withsngrep -H udp:HOST:PORT) or write it to a plain-text file withsip_capture_file; the same can be toggled at runtime withsip 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=yesif you want an AMIPeerStatus/ BLF /regextenunregister to fire on each such close, or leave the defaultnoto keep routine reconnects quiet. - A reverse proxy (for example nginx) terminating browser HTTPS/WSS in front of
chan_sofiais a common deployment; route the WebSocket to thewssbindportlistener. When debugging the WSS leg, capture on the loopback the proxy forwards to. - After editing
sofia.confas root, restore ownership so GABPBX can read it (chown gabpbx:gabpbx /etc/gabpbx/sofia.conf).
- Full (non-lite) ICE with candidate gathering and TURN — GABPBX is the ICE-lite controlled agent by design.
SIP — chan_sofia
Subsystems
Realtime & data
Operations