WAI Extension: Telephony Bridge
Mirrored from the canonical text at commit c5e7133e ().
Status: Draft. Lets a caller on the telephone network join a room as an audio participant, and lets callers hear the room, through the ingest gateway of live-ingest: a bridge that answers calls from one configured peer (a SIP trunk or a PBX) and places calls to it. This text says how the bridge reads SIP messages, session descriptions and RTP, and what it holds each to; how it authenticates its peer and answers its peer’s challenges; how a call becomes a participant under a grant its operator’s policy names; how a caller’s sound becomes a room track with receipts and playout timing; how the room’s sound is mixed back to a caller in integers; and every reason a message, a description, a credential or a packet is refused for. It registers no capability and no record. No new transport: WAI defines none by charter. The reference implementation is
joule-moq-ingest(moduletelephony; the bridge behind itstelephonyfeature) injoule-code/joule-code-rs/crates/; the corpus islive-conformance/telephony/, and its second implementation islive-conformance/telephony_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision, each fetched from its publisher on 11 November 2026 (the RFC Editor’s plain-text RFCs; the ITU-T’s PDF of G.711): RFC 3261 §7, §8, §9, §12, §13, §14, §15, §17, §18, §19.3, §20, §21 and §22; RFC 3264 §5, §6 and §8; RFC 8866 §5 and §6; RFC 3550 §5.1; RFC 3551 §4 and §6; ITU-T Recommendation G.711 (Blue Book, 11/88) §1 to §4 and Tables 1 and 2; RFC 7587 §3.1.3, §4 and §7; RFC 4733 §2 and §3; RFC 3389 (for what is not done); RFC 7616 §3; RFC 2617 §3.2.2 and §3.5; RFC 8760 §2; RFC 1321; RFC 9639 §8.2 and §9. RFC 3262, RFC 4028, RFC 3515, RFC 3711 and RFC 4568 were read only for the option tags, methods and protocols they define, which this revision refuses (§3). A section number of one of those texts here is that revision’s.
1. Scope
In scope: reading and writing SIP messages (§4); digest authentication of the peer and to it (§5); reading session descriptions, answering an offer, and making one (§6); reading a call’s RTP, G.711, Opus and telephone events (§7); the bridge as a user agent: the order in which it answers a request, the form of its responses, its transactions and its dialogs (§8); a call into a room (§9); the room back to a call (§10); the record of a call (§11); and the reasons (§12).
Out of scope: a proxy, a registrar or a redirect server; more than one peer; video; and everything a room, a grant, a track, a receipt and a catalog are, which live-rooms, live-ingest and moq-streaming-format define and this text uses unchanged. WAI texts say nothing about how an operator identifies a person: a number, a PIN and the grants they map to are the operator’s configuration, and this text states only what the bridge does with them.
Relationship to the core spec: this extension names no payload format. Its objects are
wai.audio.flac and wai.audio.opus.packet envelopes as SPEC §5 frames them,
and its receipts are the first hop’s, as live-ingest §8 states them.
2. Terms
- Peer: the one SIP trunk or PBX the bridge is configured with.
- Call: one dialog (RFC 3261 §12) between the bridge and the peer, and its media.
- Caller: whoever the peer connects on the other side of a call. The bridge attests what it received from the peer, never who spoke.
- Law: one of G.711’s two encodings, A-law (PCMA) and µ-law (PCMU).
- Mix: the sound the bridge sends a call: the sum of the room tracks the call hears.
- Mark: something a reader says of a packet or an event that it kept, as distinct from a refusal.
3. Texts, and what this revision does not read or do
Every rule of §4 to §8 is a rule of a text the status note lists, or says that it is this extension’s own. Where this text and one of those disagree on a format, that text governs the format and this text is in error.
Not done, each refused by the rule its own text gives where it arrives, or stated:
| what | where it shows |
|---|---|
| SRTP (RFC 3711) and its keys in SDP (RFC 4568) | an offer whose audio is RTP/SAVP alone is answered 488, sdp_proto_not_carried (§6.2); the bridge offers RTP/AVP alone |
| reliable provisional responses (RFC 3262) | Require: 100rel is 420 with Unsupported: 100rel (§8.2 rule 9); Supported: 100rel is passed over and no provisional response is sent reliably |
| session timers (RFC 4028) | Require: timer is 420 with Unsupported: timer; Supported: timer is passed over and no Session-Expires is sent |
| REFER (RFC 3515), and UPDATE, INFO, PRACK, SUBSCRIBE, NOTIFY, MESSAGE, PUBLISH and REGISTER received | 405 with Allow (§8.2 rule 4) |
| forking | the bridge, placing a call, keeps the first dialog a 2xx makes, and acknowledges and ends every other (RFC 3261 §13.2.2.4); as a user agent server it sees no fork |
| a re-INVITE that changes more than the direction, the address or the port of the sound | 488, reinvite_codec_change, where it drops the call’s law; another change of formats is answered with the call’s law kept (§8.5) |
| the bridge sending a re-INVITE or UPDATE | not done: so 491 does not arise, and an INVITE is answered at once or after its ring (§8.2) |
| comfort noise (RFC 3389) and silence suppression by the bridge | not offered or answered; the bridge sends every 20 ms (§7.5) |
| a G.711 packet of another length than the answer’s packet time | refused, g711_ptime_length (§7.2), this extension’s rule |
| decoding Opus, and any loss concealment | not done: Opus is carried as it came (§7.3), a call’s track has no sound made for what did not arrive (§9.2), and the mix makes none (§10.2) |
| telephone events other than DTMF (RFC 4733 §3.2’s 0 to 15), and the bridge sending events | refused, dtmf_event_not_offered; the bridge sends none |
| a DTMF digit carried into the room | not done: digits are read for a PIN (§9.1) and counted, never published |
| SIP over WebSocket, SCTP, and SIP URIs resolved by DNS | not read: the bridge talks to its configured peer’s address |
-sess digest algorithms and qop=auth-int | not offered; refused, digest_algorithm and digest_qop (§5.3) |
| RTCP | not sent; ignored where it arrives |
4. SIP messages
4.1 The reader
A message is a start line, header fields, an empty line and a body (RFC 3261 §7). The reader holds every message, over any transport, to these rules, in this order; the first that fails is the message’s reason:
- A message of more than 65 535 octets is
sip_too_large. This bound is this extension’s. - The header ends at the first empty line (CRLF CRLF), and is UTF-8 text of lines ending in
CRLF: otherwise
sip_header_line. - The start line is a request line,
Method SP Request-URI SP SIP-Version, of a token, a URI with a colon and no space, and a version; or a status line,SIP-Version SP Status-Code SP Reason-Phrase, of three digits from 100 to 699 (§7.1, §7.2). Otherwisesip_start_line. A version other thanSIP/2.0(compared ignoring case) issip_version. - A line that begins with a space or a tab continues the line before it, the line break and
the leading white space taken as one space (§7.3.1); one with no line before it is
sip_header_line. Every other line isname ":" value, white space allowed before the colon, the name a token: otherwisesip_header_line. Values are trimmed of white space at both ends. - A compact name stands for its long name (§7.3.3, §20):
iCall-ID,mContact,eContent-Encoding,lContent-Length,cContent-Type,fFrom,sSubject,kSupported,tTo,vVia. Names are compared ignoring case. A name that is one of RFC 3261 §20’s header fields is kept as §20 spells it (CSeq,Call-ID,WWW-Authenticate); any other name as written. Field rows are kept in their order. Call-ID,Content-Length,CSeq,From,Max-ForwardsandTogiven in two rows aresip_header_value(§7.3.1: only a comma-separated list may be repeated).- A request without
To,From,CSeq,Call-ID,Max-ForwardsorVia, and a response withoutTo,From,CSeq,Call-IDorVia, issip_header_missing(§8.1.1). sip_header_value: aCSeqthat is not a decimal number below 2^31 and a method (§8.1.1.5), or, in a request, whose method is not the request’s; aMax-Forwardsthat is not one to three digits of at most 255; aCall-IDthat is empty or holds white space; aFromor aTothat is not a name-addr ([display] <URI>and parameters) or an addr-spec (a URI and parameters); a request whoseFromhas notagparameter (§8.1.1.3).- The top
Via(the first element of the firstViarow) reads asSIP/2.0/<transport> <sent-by>and parameters: otherwisesip_header_value. ItsbranchMUST begin withz9hG4bKand hold more after it (§8.1.1.7): otherwisesip_via_branch. A request of an RFC 2543 element, whose branch has no such prefix, is not read: this extension’s rule. - The body (§18.3): with a
Content-Length(one to nine digits, orsip_header_value), the body is that many octets after the empty line; fewer issip_body_short, and octets past them are discarded. With none, over UDP the body runs to the datagram’s end; over a stream it issip_content_length.
A list field’s elements are its rows’ values split at commas outside quoted strings and
angle brackets (§7.3.1). A field’s parameters are the ;name=value pairs after its value,
names compared ignoring case.
Over a stream (TCP, TLS), CRLFs before a start line are passed over, and a message ends
where its Content-Length says (§7.5).
4.2 The writer
The bridge writes a start line, each field row as Name: value with CRLF, a
Content-Length row last of the fields, an empty line and the body. It writes the long names
of §4.1 rule 5.
5. Digest authentication
5.1 The hashes
The bridge offers and checks SHA-256 (FIPS 180-4) and MD5 (RFC 1321) digests, written as
lowercase hexadecimal (RFC 8760 §2.2). MD5 is broken as a collision-resistant hash; it is here
because peers still answer with it, and the bridge offers SHA-256 first. The reference’s MD5
is its own, written from RFC 1321 and held to that RFC’s test suite (Appendix A.5). The -sess
forms are not offered.
The response is RFC 7616 §3.4.1 to §3.4.3’s, with "SIP/2.0" in place of HTTP (RFC 8760
§2.6): H(H(A1) ":" nonce ":" nc ":" cnonce ":" qop ":" H(A2)), A1 username ":" realm ":" password and A2 Method ":" uri; and, for a peer whose challenge carries no qop, RFC 2617
§3.2.2.1’s H(H(A1) ":" nonce ":" H(A2)).
5.2 The bridge’s challenge
Where the operator gives the bridge credentials to ask of its peer, a request outside a
dialog (§8.2 rule 3) without good credentials is answered 401 with one WWW-Authenticate row
for each algorithm the bridge offers, SHA-256 first (RFC 8760 §2.3), each
Digest realm="<realm>", nonce="<nonce>", algorithm=<algorithm>, qop="auth"
with , stale=true after it where the request’s nonce had expired. The rows of one response
share one nonce. The bridge MUST send qop (RFC 8760 §2.6 bullet 8) and offers auth
alone.
The reference’s nonces. A nonce is the first 16 octets, in hexadecimal, of SHA-256 over
the ASCII wai-telephony/nonce, a 32-octet secret, the count of nonces issued (this one
included) and the second of issue, each count eight octets big-endian. The secret is
SHA-256 over wai-telephony/nonce-secret and the bridge’s own 32-octet secret, which is
random at each start. The bridge keeps the newest 4096 nonces with the second each was
issued and the highest nonce count accepted under it. A bridge may make its nonces otherwise;
the corpus states the secret it ran with, so a second implementation reproduces them.
5.3 Checking credentials
The Authorization rows of a request are checked in this order; the first that fails is the
reason, and the request is answered 401 with a fresh challenge (§5.2), or 400 where the
reason says so:
- No row:
digest_missing. - The first row of scheme
Digestwhoserealmis the bridge’s. ADigestrow whose parameters do not read (RFC 7616 §3.4’sname=valuepairs, values quoted or tokens, no name twice) isdigest_malformed(400); rows of other schemes are passed over; no row of the bridge’s realm isdigest_realm. username,nonce,uriandresponsepresent: otherwisedigest_malformed(400).algorithm(MD5 where absent) one the bridge offered: otherwisedigest_algorithm.qoppresent andauth, with anncof eight hexadecimal digits and acnonce:qopabsent, or of another value, isdigest_qop;qop=authwithout them isdigest_malformed(400).nonceone the bridge issued and keeps: otherwisedigest_nonce_unknown. A response computed for a nonce of anyone else’s is refused here.- The nonce issued no more than the bridge’s nonce lifetime (300 seconds by default) ago:
otherwise
digest_nonce_stale, and the new challenge saysstale=true. uriequal, octet for octet, to the request’s Request-URI (RFC 7616 §3.4.6): otherwisedigest_uri_mismatch(400).responseequal (ignoring case) to §5.1’s for an account the bridge holds, of thatusername: otherwisedigest_response_mismatch.ncabove every nonce count accepted under that nonce: otherwisedigest_nc_replayed.
On success the count is kept. A request inside a dialog the peer opened is not challenged: the dialog is the peer’s, its identifiers are unguessable (§8.3), and the peer’s address is held (§8.1). ACK and CANCEL are never challenged (RFC 3261 §22.1).
Passwords are read from files the operator names and are never written to a log or a record. What a refusal says is its reason.
5.4 Answering a peer’s challenge
On a 401 or a 407 to a request it sent, the bridge answers the topmost challenge of an
algorithm it computes (RFC 8760 §2.4), ignoring others: with qop=auth, a fresh cnonce and
nc counting the requests under that nonce from 00000001, where auth is offered; in RFC
2617’s form otherwise. It resends the request with a new CSeq and Authorization (for 401)
or Proxy-Authorization (for 407). A second challenge to the request answered so is not
answered again: the request fails with the challenge’s status.
6. Session descriptions
6.1 Reading
A description is UTF-8 text of lines ending in CRLF or LF, each <letter>=<value>, the
letter lowercase (RFC 8866 §5); empty lines are passed over, and the last line’s ending may
be absent. Otherwise, and in each of these
cases, it is sdp_malformed: a first line other than v=0, or v= again; no o= of six
fields whose second and third are decimal numbers and fourth IN; no s=; no t= of two
decimal numbers; a c= that is not IN IP4 <address> or IN IP6 <address> (an IPv4 address
may carry a TTL after a slash) or whose address is not of its type; an m= of fewer than four
fields or a port that is not a decimal number below 65 536 (a /<count> after it is passed
over); an rtpmap that is not <type> <name>/<rate>[/<channels>] with a type of 0 to 127 and
decimal numbers; a ptime that is not a decimal number.
a=sendrecv, sendonly, recvonly and inactive give a direction, the media’s own over
the session’s, sendrecv where none is given (RFC 3264 §5.1). The connection address of a
media description is its own c=, or the session’s.
A payload type names a codec by its rtpmap, or, without one, by RFC 3551 §6’s static
types: 0 PCMU and 8 PCMA. The bridge knows PCMU/8000 and PCMA/8000 (one channel where
stated), opus/48000/2 (RFC 7587 §7) and telephone-event/<rate> (RFC 4733 §2.4), each
name compared ignoring case. Any other is not one it knows, and is passed over.
6.2 The answer
The bridge carries one audio stream a call. It receives G.711 and Opus, and sends G.711 alone: it holds no Opus encoder. So an offer is answered only where it names a law: the bridge both receives and sends that law.
The call’s stream is the first m=audio whose protocol is RTP/AVP, whose port is not 0 and
which names a law. Refused, in this order, each answered 488 (§8.2): no m=audio,
sdp_no_audio; no m=audio of RTP/AVP with a port, sdp_proto_not_carried; none of those
names a law, sdp_no_common_codec; the call’s stream has no connection address,
sdp_connection, or a multicast one, sdp_multicast.
The answer, as the bridge writes it (RFC 3264 §6):
v=0
o=- <session id> <version> IN IP4 <address>
s=-
c=IN IP4 <address>
t=<the offer's t= value>
(IP6 for an IPv6 address); then, for each media description of the offer in its order, the
call’s stream as
m=audio <port> RTP/AVP <types>
a=rtpmap:<type> <name>/<rate>[/<channels>] one a type, in the order of <types>
a=fmtp:<type> 0-15 after each telephone-event's rtpmap
a=ptime:20
a=<direction>
and every other as m=<media> 0 <protocol> <its formats as offered> (rejected, §6). The types
are the offer’s own numbers (RFC 3264 §6.1): first the offer’s first law; then Opus, where the
offer names it and the bridge receives Opus; then each telephone-event of the offer whose
clock rate is one of those codecs’, the first of each rate. The rtpmap is written PCMU/8000,
PCMA/8000, opus/48000/2 or telephone-event/<rate>. The law comes first though the offer
may list Opus first: the bridge sends the law, and a peer that sends what the answer lists
first (RFC 3264 §6.1) sends what it receives. The direction is the offer’s answered: sendonly
by recvonly, recvonly by sendonly, inactive by inactive, sendrecv by sendrecv. The bridge asks
for 20 ms packets.
What the answer settles for the call: where the peer receives (its connection address and the
stream’s port; 0.0.0.0 asks for nothing to be sent, RFC 3264 §8.4); the law the bridge
sends, under the offer’s type for it; every type the bridge receives; the bridge’s direction.
The bridge sends 20 ms packets whatever packet time the offer states: it does not follow RFC
3264 §6.1’s SHOULD there, and says so here.
The reference’s session identity. The o= session id is the first eight octets of
SHA-256 over wai-telephony/session, the bridge’s secret and the Call-ID, big-endian,
shifted right one bit; the version starts at 1 and rises by one with each new description of
the call.
6.3 The bridge’s offer, and the answer to it
The bridge offers, for a call it places and in the 2xx to an INVITE that carried no offer:
m=audio <port> RTP/AVP 0 8 101
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=ptime:20
a=sendrecv
after §6.2’s session lines (t=0 0). It reads the answer by §6.1 and takes the first
m=audio, refusing the call, in this order, where there is none (sdp_no_audio), it has port
0 (sdp_no_common_codec), it is of another protocol than RTP/AVP (sdp_proto_not_carried),
it has no connection address (sdp_connection) or a multicast one (sdp_multicast), or it
names no law under the offer’s type for it (sdp_no_common_codec). It sends the first law the
answer names under the offer’s type. It receives every format its offer named, whatever the
answer lists: RFC 3264 §7 lets an offerer stop listening for the others, and the bridge does
not. An answer that does not read is sdp_malformed; an empty body read as a description is
one.
7. RTP
7.1 The header, the source and the sequence
Each datagram on a call’s media port is read in this order; the first that fails is its reason:
- Its source address MUST be the connection address the call’s description states for
the peer: otherwise
rtp_source_not_stated. The source port is not held to the description’s, which says where the peer receives (RFC 3264 §6.1), not where it sends. - The header (RFC 3550 §5.1): fewer than 12 octets, or a CSRC list or a header extension
(§5.3.1) that runs past the end, is
rtp_short; a version other than 2 isrtp_version; with the padding bit set, a last octet of 0 or one past the payload isrtp_padding. A header extension and CSRCs are passed over. - The payload type MUST be one the call’s description has the bridge receive:
otherwise
rtp_payload_type. - The first packet read fixes the call’s SSRC; a packet of another is
ssrc_changed. - The format’s length: a G.711 payload of the answer’s packet time (§7.2,
g711_ptime_length), an Opus payload not empty (§7.3,opus_empty), a telephone-event payload of whole reports (§7.4,dtmf_length). - A sound packet of another codec than the call’s first sound packet is
rtp_codec_switch: a call’s track is of one codec (§9.2). RFC 3264 lets a peer switch among the formats an answer lists; this revision carries one a call, and says so. - The sequence, as essence-contribution §5 states it for a sound stream: the sequence
number extended by its distance from the one expected; a number seen before, or behind
and not remembered as missing, is
duplicate; numbers skipped are counted lost and remembered (the newest 4096); one that arrives behind and was missing is no longer lost: it is counted behind and kept. A packet’s extended number, as the reader states it, is the first packet’s number plus 2^16 for the first; for each later one, its 16-bit distance from the number expected (the highest so far plus one) is taken, ahead where it is below 2^15 and behind (that distance less 2^16) otherwise. - The format’s own rules: a telephone-event payload’s reports (§7.4). A report refused there refuses its packet, which was counted in the sequence.
A packet refused before step 7 is not counted in the sequence.
7.2 G.711
A PCMU or PCMA payload is one character a sample at 8000 Hz (RFC 3551 §4.5.14). It MUST
hold exactly the samples of the answer’s packet time, 160 for 20 ms: another length is
g711_ptime_length. This is this extension’s rule and stricter than RFC 3551 §4.2, which asks
a receiver to accept 0 to 200 ms. Its reason: the bridge’s answer asks for 20 ms (RFC 3264
§6.1), its jitter buffer and objects are of whole packets, and a peer that sends other lengths
has not done what the answer asked.
Decoding (G.711 §3.6, Tables 1 and 2). A character stands for the interval between two of
the law’s decision values (column 5), and decodes to that interval’s quantized value (column
7, Note 3): for µ-law, a character c of 128 or more is the positive interval n = 255 − c,
and one below 128 the negative n = 127 − c, decoding to y0 = 0 and yn = (xn + xn+1)/2;
for A-law, the character with its even bits inverted (exclusive-or 0x55) is 128 + n for a
positive interval and n for a negative one, decoding to (xn + xn+1)/2. The decision values
are µ-law’s x1 = 1, then fifteen steps of 2, and sixteen each of 4, 8, 16, 32, 64, 128 and
256 to x128 = 8159; and A-law’s x0 = 0, thirty-two steps of 2, then sixteen each of 4, 8,
16, 32, 64 and 128 to x128 = 4096. A value is G.711’s 13-bit (A-law) or 14-bit (µ-law)
uniform code, carried as a 16-bit sample: times 8 or times 4.
Reversible. Every character decodes to a sample of its own but one: µ-law’s 0x7F (a negative input between decision values 0 and 1) decodes to 0, as 0xFF does. So the samples a call is carried as (§9.2) give back every character that arrived, but that a µ-law negative zero comes back as the positive one. The bridge counts the negative zeros it received.
Encoding is the equipment’s to specify (§3.6). The bridge takes a 16-bit sample’s
magnitude (its absolute value, 32 768 for −32 768) and shifts it right by 2 (µ-law) or 3
(A-law) to the law’s code, finds the largest n from 0 to 127 whose decision value is at most
it, and writes that interval’s
character for the sample’s sign; a µ-law magnitude that shifts to 0 is the positive zero,
0xFF, whatever its sign. No all-zero suppression (§3.2) is applied. Every character, decoded
and encoded again, is itself, but µ-law’s 0x7F, which becomes 0xFF.
7.3 Opus
An Opus payload is one Opus packet (RFC 7587 §4.2), at a clock of 48 000 whatever the
bandwidth (§4.1). An empty one is opus_empty (an Opus packet is at least one octet). The
bridge does not decode it: it carries the packet as it came (§9.2), and makes nothing for a
packet that did not arrive or that discontinuous transmission (§3.1.3) left out. A
duplicate is refused by §7.1 before it reaches a track (§4.1: at most one copy goes on).
7.4 Telephone events
A telephone-event payload is one or more four-octet reports, each an event code, the end
bit, a reserved bit, a volume and a 16-bit duration (RFC 4733 §2.3); a report after the first
begins where the one before it ended (§2.5.1.5). A payload that is empty or not of whole
reports is dtmf_length. Each report, in order:
- An event above 15 (the answer’s
0-15, the DTMF events of §3.2) isdtmf_event_not_offered; a duration of 0 isdtmf_zero_duration(§2.3.5: DTMF events are not states). - An update of the event being reported, at its segment’s timestamp (§2.5.1.2): another
event code is
dtmf_event_changed; the marker bit on a packet of this one report isdtmf_marker(intermediate packets carry neither M nor E; in a packet of several reports the marker says that one of them begins an event, §2.5.1.5, and refuses none); a duration below the last isdtmf_duration_decreased; a report without the end bit after one with it isdtmf_end_withdrawn(§2.5.1.4: once set, the E bit stays set). Otherwise the duration is taken, and the end where the bit is set. - The next segment of a long event (§2.5.1.3): where the event being reported has not
ended, its last duration is 0xFFFF, and a report of the same event, in a packet that is not
a marked packet of one report, has another timestamp, that timestamp MUST be the last segment’s plus 0xFFFF (§2.5.2.3):
otherwise
dtmf_segment_gap. The event’s duration is its segments’ sum. - A late report of an event already over (one of the last sixteen, by its first
timestamp) is ignored and counted (§2.5.2.2); one at such an event’s timestamp naming
another event is
dtmf_event_changed. - A new event begins: a digit,
0–9,*,#,A–D, at its timestamp. One whose first report is in a packet without the marker carries the markdtmf_start_unmarked: its first packets were lost.
A digit is taken once, at its event’s start. Events share the call’s sequence numbers and SSRC (§2.1); the sound packets around them are read as §7.2 says.
7.5 Silence and comfort noise
A G.711 packet that does not begin where the last sound packet read in order (not behind,
§7.1) ended (its timestamp against that one’s plus its samples) carries the mark
timestamp_jump: silence its sender suppressed (RFC 3551 §4.1), or sound that was lost.
Nothing is made for the gap. An Opus packet’s length in time is in its own header (RFC 6716
§3), which the bridge does not read, so the packet after one is not marked, and a gap in an
Opus call shows in its timestamps alone. The bridge
neither offers nor answers comfort noise (RFC 3389), so a comfort-noise packet is
rtp_payload_type. The bridge suppresses no silence: it sends a packet every 20 ms of a call
it sends to, silence as the law’s zero.
8. The bridge as a user agent
8.1 Transports and the one peer
The bridge listens for SIP on UDP, TCP and TLS where its configuration says, each at a configured address (port 0 takes one the host gives, and the bridge logs it). It sends its own requests outside a dialog (a call it places, REGISTER) to its peer over UDP or TCP as configured, and its requests in a dialog over the connection the dialog came on where it is open. TLS is the gateway’s own TLS stack, with a certificate chain and key the operator names; the bridge accepts TLS and answers on it, and opens no TLS connection of its own in this revision. A request from any address but the peer’s is dropped and counted, unanswered: an answer would go to whoever the address names.
Responses go where RFC 3261 §18.2.2 sends them: over the connection the request came on
for a stream; over UDP to the top Via’s received and rport where present, else to the
datagram’s source address and the sent-by port (5060 where none).
8.2 Answering a request
The bridge answers a request from its peer by these rules, in this order; the first that decides is the answer. The status and the reason each gives are §12’s.
- A message the reader refuses (§4.1) is answered, where it is a request other than ACK
whose
Via,From,To,Call-IDandCSeqrows are present, with 400 (505 forsip_version, 513 forsip_too_large) and the reason; otherwise dropped. A response that does not read is dropped. - A retransmission: a request other than ACK whose branch and method are those of a request already answered with a final response is answered with that response again, and nothing else changes (RFC 3261 §17.2.3); its reason, if the response was a refusal, is not counted again. The newest 1024 are kept.
- ACK: of a dialog’s INVITE (its Call-ID, tags and CSeq), the call is confirmed, and
where the bridge’s 2xx carried an offer, the ACK’s body is its answer (§6.3): one refused
there ends the call,
ack_without_answer, with its reason (an empty body issdp_malformed), and the bridge sends BYE (RFC 3261 §13.3.1.4). Any other ACK is absorbed. ACK is never answered. - Credentials (where the bridge asks for them): a request outside a dialog, but CANCEL, is checked by §5.3.
- The method (RFC 3261 §8.2.1): INVITE, ACK, BYE, CANCEL and OPTIONS are supported.
REGISTER, PRACK, UPDATE, INFO, REFER, SUBSCRIBE, NOTIFY, MESSAGE and PUBLISH are
recognised and not supported: 405,
method_not_allowed; any other method is 501,method_unknown; both withAllow: INVITE, ACK, BYE, CANCEL, OPTIONS. - The Request-URI of a scheme other than
siporsips: 416,uri_scheme(§8.2.2.1). - CANCEL (§9.2): matching the INVITE of a call the bridge took, ringing, answered or
ended (the CANCEL’s branch, Call-ID, From tag and CSeq number): 200; where that INVITE is
still ringing, it is answered 487 and the call ends. Matching an INVITE the bridge refused,
or none: 481,
transaction_unknown. - Inside a dialog (a
Totag): one the bridge holds (Call-ID, its tag as theTotag, the peer’s as theFromtag), not ended: otherwise 481,dialog_unknown(§12.2.2). A CSeq number not above the dialog’s last is 500,cseq_out_of_order. - Merged (no
Totag): the Call-ID, From tag and CSeq number of an INVITE still ringing, by another branch: 482,loop_detected(§8.2.2.2). - Require: any option tag (the bridge supports none): 420,
extension_unsupported, withUnsupportedlisting them (§8.2.2.3). - The body (§8.2.3), where there is one: a
Content-Encodingother thanidentityis 415,media_type_unsupported, withAccept-Encoding: identity; aContent-Typeother thanapplication/sdp(or none) is 415 withAccept: application/sdp. - The method’s own:
- OPTIONS: 200 with
AllowandAccept: application/sdp(§11). - BYE: outside a dialog, 481,
dialog_unknown; inside one, 200, and the call ends (§15). - INVITE outside a dialog (§13.3): the Request-URI’s user part MUST be a number the
bridge answers (404,
number_unknown); the number’s open calls, ringing ones among them, below its limit (486,busy); an offer that reads (488,sdp_malformed) and is answered (§6.2; 488 with its reason); a media port free (486,busy). Then the dialog is made (§8.5) and the INVITE answered 200 with the answer, or with the bridge’s offer where the INVITE carried none; or, where the operator sets a ring, 180 first and 200 when it is over. - INVITE inside a dialog: §8.5.
- OPTIONS: 200 with
A request refused by rules 7 to 11 inside a dialog changes nothing of it. One accepted sets the dialog’s last CSeq number to its own.
8.3 The form of a response
Every response the bridge writes is, in this order of rows:
Via: everyViarow of the request, as it came;From: the request’s;To: the request’s, with;tag=<the bridge's tag>added where it had no tag (not on 100);Call-IDandCSeq: the request’s;- on a 180 or a 2xx to an INVITE that makes a dialog: each
Record-Routerow of the request, thenContact: <the bridge's URI>; on a 2xx to an INVITE inside a dialog,Contactalone; - the rows its rule names (
Allow,Accept,Accept-Encoding,Unsupported, theWWW-Authenticaterows); - on a refusal,
Warning: <code> <the bridge's host> "<reason>"(RFC 3261 §20.43): code 304 forsdp_no_audio, 302 forsdp_proto_not_carried, 305 forsdp_no_common_codecandreinvite_codec_change, 330 forsdp_multicast, and 399 for every other reason; - with a description,
Content-Type: application/sdp; Content-Length.
The reason phrase is RFC 3261 §21’s for the status.
The reference’s tags. The bridge’s tag for a request is the first eight octets, in
hexadecimal, of SHA-256 over wai-telephony/tag, the bridge’s secret, the Call-ID, one zero
octet and the From tag: the same for every response to a request and to its
retransmissions, and 64 bits from a random secret, which RFC 3261 §19.3 asks for.
8.4 Transactions and timers
Over UDP the bridge keeps RFC 3261 §17’s timers with T1 = 500 ms, T2 = 4 s and T4 = 5 s: its 2xx to an INVITE is sent again at T1, doubling to T2, until its ACK, and the call ends with a BYE where none comes in 64·T1 (§13.3.1.4); a non-2xx final response to an INVITE is sent again by Timer G until its ACK, for at most 64·T1 (§17.2.1); a request it sends is sent again by Timer A (INVITE) or Timer E (any other) and given up at 64·T1 (Timers B and F). Over a stream nothing is sent again. A retransmitted request is answered by §8.2 rule 1.
8.5 Dialogs and later offers
A dialog (RFC 3261 §12.1.1) holds the Call-ID, the bridge’s tag and the peer’s, the peer’s last
CSeq number, the peer’s Contact as its remote target, the request’s Record-Route as its
route set, and the call’s description. The bridge’s own requests in it (BYE) go to the peer’s
address with the remote target as Request-URI and the route set as Route rows, and carry
the dialog’s tags turned around and a CSeq above any the bridge sent in it.
A later INVITE in a dialog (§14.2):
- with no offer, is answered 200 with the bridge’s offer (§6.3), whose answer the ACK brings;
- with the same offer as the last (its
o=session id and version unchanged, RFC 3264 §8), is answered with the same answer; - with an offer that no longer names the call’s law, is refused 488,
reinvite_codec_change, withWarning: 305, and the call goes on as it was (§14.1: a non-2xx leaves the session unchanged); - otherwise is answered as §6.2 answers, the call’s law kept first under the offer’s type for it, the version raised by one: a new address or port, and a new direction (hold, RFC 3264 §8.4), are taken.
Refusals of the offer itself are those of §6.2, each 488, and come before the check of the
call’s law: an offer that names no law at all is sdp_no_common_codec.
8.6 Calls the bridge places, and registration
The bridge places a call to a number by an INVITE to the peer: Request-URI sip:<number>@<peer address>, its own From with a fresh tag, To of the Request-URI, a fresh Call-ID, CSeq 1,
Max-Forwards: 70, its Contact, and its offer (§6.3). It answers a challenge by §5.4.
Provisional responses are passed over (100rel is not supported); a final non-2xx ends the
attempt (acknowledged in the INVITE’s transaction, §17.1.1.3); the first 2xx makes the dialog,
its answer read by §6.3, and is acknowledged; a 2xx of another To tag (a fork) is
acknowledged and its dialog ended with BYE at once. Where no final response comes within the
operator’s dial time, the bridge sends CANCEL (§9.1).
Where the operator asks, the bridge registers its Contact with the peer (RFC 3261 §10.2):
REGISTER to the peer, To and From the configured address-of-record, Expires the
configured time, answering a challenge by §5.4, and registers again when half the time the
peer’s 2xx granted has passed.
9. Into the room
9.1 Admission: numbers, PINs and grants
A call joins a room as a participant under a room grant (live-rooms §4): a WRG1 record
the operator’s configuration gives. The configuration maps:
- a number the bridge answers to a list of grants: a call to it takes the first grant of the list no open call holds; a number’s calls are limited to its grants’ count, or to a limit the operator states; or
- a number to PIN entry: the call is answered, the caller enters a PIN as DTMF (§7.4)
ended by
#or by the PIN’s length, and a PIN the configuration names gives a list of grants as a number does. A wrong PIN is counted; after the configured number of tries (three by default), or when no PIN is entered within the configured time (20 seconds by default), the bridge ends the call with BYE. No prompt is played: what a caller hears before a PIN is accepted is silence.
The bridge admits the grant as live-ingest §3.2 admits a token, steps 1 to 6, against the room
the grant names, when the call is answered (or its PIN accepted): a grant refused there ends
the call with its reason. Step 7, the proof of possession, does not apply: no request presents
the grant; the operator’s configuration holds it, and the bridge acts as the grant’s
principal by the operator’s mapping. The grant MUST let its principal publish audio
(live-rooms §3.3): otherwise the call ends, publish_not_permitted. A grant whose principal
already publishes in the room through the bridge is not taken.
PINs, like passwords, are read from files the operator names, and are never written to a log or a record. The digits a caller enters are never carried into the room.
The caller’s number (the From URI) is not carried into the room. It enters the call’s
record (§11) only where the operator’s configuration says so. Nothing in this text says how
an operator identifies a person.
9.2 The caller’s track
A call publishes, from its admission, one audio track of the grant’s principal as live-ingest
§4.1 names it (namespace (room id, owner, audio), name 0), its receipts track, and the
owner’s catalog, through the same packager, groups and catalog as every source of the gateway
(live-ingest §4 to §6):
- G.711 is carried as
wai.audio.flac: each object a FLAC stream (RFC 9639) of one frame whose one subframe is verbatim (§9.2.4 there), 16 bits a sample at 8000 Hz, every sample the decoded character that arrived (§7.2). An object holds the bridge’s object duration (20 ms by default, one packet), and never spans a gap: a packet markedtimestamp_jump, or one after packets the jitter buffer skipped, begins a new object. So the room carries the call exactly, and a sink reads PCM. - Opus is carried as
wai.audio.opus.packet, one RTP payload an object, as it came (RFC 7587 §4.2), its media time its RTP timestamp at 48 000.
A call’s track is of one codec, the codec of its first sound packet (§7.1 step 6). Its groups are of the bridge’s group duration (100 ms by default; live-ingest §5.2), shorter than the gateway’s default so that a group’s receipts, which follow its end (live-ingest §8.3), come soon after its sound. A group also ends when its caller has sent no sound for a group’s duration: otherwise a caller who falls silent would hold the last of its sound back from every verifier until it spoke again.
Nothing is made for sound that did not arrive: the samples of a lost or skipped packet are counted, and no object holds them. G.711 has no concealment in its own text and RFC 3551 gives none; Opus’s is its decoder’s (RFC 7587 §3.1.3), which the bridge does not run, and a sink’s decoder may conceal as RFC 6716 says.
9.3 Time
- Media time. An object’s
live.rtpis the RTP timestamp of its first sample, extended past its 32 bits;live.rateis 8000 for G.711 and 48 000 for Opus;live.tis live-ingest §7’s mapping of it to the room clock. - Capture instant. Each object carries the CaptureTimestamp of playout timing inside its
Immutable Properties, and the gateway’s clock as the first HopTime (moq-streaming-format
§4.4; co-watch §15). A caller’s RTP timestamps have no epoch, so the capture
instant is the call’s anchor (the arrival, on the gateway’s clock, of the first sound
packet) plus the RTP time since that packet: it follows the peer’s clock as the timestamps
do, and is re-anchored at a packet’s arrival where it lies more than the gateway’s
max_skew_ms(live-ingest §7.1) from it. It is the time the sound reached the bridge, not the time anyone spoke, and is stated so. - The catalog states
wai_tempoon the call’s track,max_delay_msandplayout_delay_msthe bridge’s playout delay (§10.2).
9.4 The jitter buffer
A call’s packets go into the room in sequence order. A packet that is the next expected leaves the buffer at once; one that is not waits for the packets before it until the oldest packet held has waited the bridge’s jitter bound (60 ms by default), and then leaves, the numbers still missing counted as skipped. A packet for a number already left behind is counted late and dropped. The buffer holds at most the packets of four times its bound, and never holds a packet longer than the bound: so a call’s sound reaches its track at most the bound after it arrived, plus the object it fills (§9.2). Nothing is made in the buffer.
9.5 Receipts and the origin claim
Receipts. Every object’s receipt, as live-ingest §8 states them: the first hop’s.
The origin claim. Where the operator states the digital source type of what its trunk
carries (synthesis §3.2), the bridge signs, for each call’s track, an origin claim (synthesis
§3.1) whose subject is the track from its first group, of step capture and class
captured, source the stated type, operation telephony:received, and record the
digest of the call’s statement (§11). It attests that the bridge received this sound from its
peer under the call’s grant, and carries the bridge’s authority and no more: not who spoke,
not where the sound began, not that it was not made by a machine before it reached the peer.
With no type stated, no claim is signed. The claims are in the call’s record; this revision
defines no carriage of them in the room.
10. Out of the room: the mix
10.1 What a call hears
A call hears, as the operator’s configuration names them: the audio tracks of the room’s other calls through the bridge, and the audio tracks of named owners. The bridge subscribes to each at the relay with its receipts track (an owner’s track by its catalog, live-ingest §9.1), verifies every object against its receipt under the first hops it trusts before it uses it, exactly as a WHEP session verifies (live-ingest §9.2), and drops and counts what fails. A call never hears its own track.
10.2 Decoding and playout
The bridge decodes a track’s objects that are wai.audio.flac at 8000 Hz, of one or two
channels, whose frames’ subframes are constant or verbatim (RFC 9639 §9.2.3, §9.2.4): every
call’s track through a bridge, and any other track of that form. Two channels are taken as
the floor of their mean; a sample of more than 16 bits is shifted right (floor) to 16, one of
fewer left. Every other object gives the mix nothing, and is counted by its track and reason:
codec_not_decoded (Opus first among them: the bridge holds no Opus decoder),
flac_form_not_read (a fixed or linear predictor, a stereo decorrelation), rate_not_8000
(nothing is resampled), channels_over_two. A track the bridge cannot decode is still verified
and counted; a caller does not hear it, and the call’s record says so.
Each track’s samples go into a playout buffer. A buffer plays once it holds the bridge’s playout delay (160 ms by default: more than a group, since a group’s sound arrives verified only after its receipts, at the group’s end); it gives one 20 ms frame each tick; it stops, counting an underrun, when it holds less than a frame, and plays again once it holds its delay. A buffer holds at most the configured maximum (1 s by default), and drops its oldest samples past it, counted. Nothing is made: a buffer that does not play gives nothing.
10.3 The mix
Every 20 ms the bridge takes one frame from each buffer that plays, sums them sample by sample in 32-bit integers, and gives each call that sum less its own track’s frame, saturated to 16 bits (−32 768 to 32 767), encoded in the call’s law (§7.2) as one RTP packet of 160 characters. With nothing playing, the frame is silence. A call on hold (its peer receives nothing, §6.2) is sent nothing. The bridge’s packets carry its own SSRC, consecutive sequence numbers, and timestamps that rise by 160 a packet; the first packet of a call carries the marker bit (RFC 3551 §4.1 asks it of a talkspurt’s first packet only where silence is suppressed, and the bridge suppresses none, so no other does).
11. Records
When a call ends the bridge signs its record (live-ingest §16), with protocol telephony.
Its detail holds: the number the call was to (as the configuration names it) or the
bridge’s own placed call; whether a PIN was entered, and how many tries were wrong (never a
digit); the grant’s digest; the call’s statement and its digest; why it ended: the SIP
status that ended it, or bye, cancel, ack_without_answer, ack_timeout,
media_timeout, pin_refused, grant_refused, publish_not_permitted, relay_lost or
gateway_stopped; for the call’s RTP, the packets, those refused by reason (§12), lost, behind, late
and skipped by the jitter buffer, the µ-law negative zeros, the timestamp jumps, the digits
counted (never their values), and the longest wait in the jitter buffer; for each track the
call heard, the objects verified, those refused by the verifier’s reasons, those not mixed by
reason (§10.2), and its playout buffer’s frames played, underruns and samples dropped; the
packets sent to the caller; and the origin claim, where one was signed. Where the operator’s
configuration says so, it holds the caller’s From URI; otherwise no record says who called.
A call’s statement is a JSON object with exactly these members: kind
("wai-telephony-statement"), peer (the peer’s address and port), transport, call_id
(SHA-256 of the Call-ID, hexadecimal: the Call-ID itself is the peer’s), direction
(inbound or outbound), codec (the rtpmap of the call’s track’s codec), law (the law
the bridge sends), ssrc, source (where its RTP came from), and declared_source (the
operator’s stated type, or null). Its digest is SHA-256 over its RFC 8785 form.
12. Reasons
Refusals of a SIP message (§4.1), each answered as §8.2 rule 0 says:
| reason | status | when |
|---|---|---|
sip_start_line | 400 | a start line that is neither a request line nor a status line |
sip_version | 505 | another version than SIP/2.0 |
sip_header_line | 400 | no empty line ends the header; a line that is not name: value; a continuation with nothing before it |
sip_header_missing | 400 | one of the header fields every request or response carries is absent |
sip_header_value | 400 | a CSeq, Max-Forwards, Call-ID, From, To, top Via or Content-Length that does not read; a single-valued field twice; a From with no tag |
sip_via_branch | 400 | a branch without RFC 3261’s magic cookie |
sip_content_length | 400 | a message over a stream without Content-Length |
sip_body_short | 400 | a body shorter than its Content-Length |
sip_too_large | 513 | a message of more than 65 535 octets |
Refusals of credentials (§5.3):
| reason | status | when |
|---|---|---|
digest_missing | 401 | no Authorization |
digest_malformed | 400 | parameters that do not read; one required absent |
digest_algorithm | 401 | an algorithm the bridge did not offer |
digest_realm | 401 | no row of the bridge’s realm |
digest_nonce_unknown | 401 | a nonce the bridge never issued, or no longer keeps |
digest_nonce_stale | 401 | a nonce past its lifetime (stale=true) |
digest_uri_mismatch | 400 | uri that is not the Request-URI |
digest_qop | 401 | no qop, or one other than auth |
digest_nc_replayed | 401 | a nonce count not above the last accepted under the nonce |
digest_response_mismatch | 401 | a response no account of that user gives |
Refusals of a description (§6), each 488 where it is an offer to the bridge:
| reason | when |
|---|---|
sdp_malformed | lines that do not read as §6.1 says |
sdp_no_audio | no m=audio |
sdp_proto_not_carried | no m=audio of RTP/AVP with a port: SRTP among them |
sdp_no_common_codec | no G.711 law in the call’s stream: nothing the bridge both receives and sends |
sdp_connection | no connection address for the call’s stream |
sdp_multicast | a multicast connection address |
reinvite_codec_change | a later offer that drops the call’s law |
Refusals of a request by the user agent (§8.2):
| reason | status | when |
|---|---|---|
method_not_allowed | 405 | a method recognised and not supported |
method_unknown | 501 | a method not recognised |
uri_scheme | 416 | a Request-URI that is not sip or sips |
number_unknown | 404 | a number the bridge does not answer |
dialog_unknown | 481 | a request for a dialog the bridge does not hold; a BYE outside a dialog |
cseq_out_of_order | 500 | a CSeq not above the dialog’s last |
loop_detected | 482 | a merged request |
extension_unsupported | 420 | an option tag in Require |
media_type_unsupported | 415 | a body not application/sdp, or encoded |
transaction_unknown | 481 | a CANCEL that matches no INVITE |
busy | 486 | a number at its limit; no media port free |
Refusals of an RTP packet (§7):
| reason | when |
|---|---|
rtp_short | fewer octets than its header says |
rtp_version | a version other than 2 |
rtp_padding | a padding count of 0 or past the payload |
rtp_payload_type | a type the call’s description does not have the bridge receive |
rtp_source_not_stated | from an address the call’s description does not state |
rtp_codec_switch | a sound packet of another codec than the call’s first |
ssrc_changed | an SSRC that is not the call’s |
duplicate | a sequence number seen before |
g711_ptime_length | a G.711 payload of another length than the answer’s packet time |
opus_empty | an empty Opus payload |
dtmf_length | a telephone-event payload that is empty or not of whole reports |
dtmf_event_not_offered | an event above 15 |
dtmf_zero_duration | a DTMF report of duration 0 |
dtmf_event_changed | a report at an event’s timestamp naming another event |
dtmf_duration_decreased | an update whose duration falls |
dtmf_end_withdrawn | an update without the end bit after one with it |
dtmf_marker | an update with the marker bit |
dtmf_segment_gap | a long event’s next segment that does not begin where the last ended |
Marks: the packet or the event is kept, and says so.
| reason | when |
|---|---|
timestamp_jump | a sound packet that does not begin where the last ended |
dtmf_start_unmarked | an event whose first report has no marker |
13. Status and conformance
| part | status | where |
|---|---|---|
| the readers and the user agent’s answers (§4 to §8.3, §8.5) | implemented, with a reference and a second implementation | joule-moq-ingest telephony (sip, digest, md5, sdp, rtp, g711, uas); live-conformance/telephony/ and telephony_verify.py |
| the jitter buffer and the integer mix (§9.4, §10.2, §10.3) | implemented, held by unit tests | telephony::jitter, telephony::mix |
| the bridge: transports, transactions, calls, tracks, receipts, the mix’s subscriptions, grants and PINs (§8.1, §8.4, §8.6, §9, §10.1, §11) | implemented | telephony::bridge, feature telephony; tests/telephony_bridge.rs, tests/telephony_rt.rs |
| SIP over TLS | implemented on the gateway’s TLS stack, inbound; tested as far as a handshake and an OPTIONS on loopback | §8.1 |
| a fork’s second 2xx, the CANCEL of a call the bridge places when its dial time runs out, a ringing call cancelled, a named owner’s track in the mix | implemented; not exercised by a test of the bridge (the user agent’s CANCEL and ringing are, in the corpus) | §8.2, §8.6, §10.1 |
| SRTP, 100rel, session timers, REFER | not implemented; refused | §3 |
| carriage of digits, and of origin claims, in a room | not defined | §7.4, §9.5 |
The corpus. live-conformance/telephony/vectors.json holds, built by the reference from
fixed values (no outside media, no host, no network): RFC 1321’s MD5 suite and messages about
its block boundary; RFC 7616’s and RFC 2617’s digest examples and the form without qop;
G.711’s decode tables and the SHA-256 of both encodings of every 16-bit sample; SIP messages
read and refused; offers with their answers and refusals; signalling exchanges with the
bridge’s user agent (calls, retransmissions, CANCEL, ringing, a delayed offer, re-INVITEs,
every refusal of §8.2, and a digest exchange answering the reference’s own challenges), each
step with the responses written byte for byte; and RTP captures of calls with what the reader
makes of each packet. Planted faults, each refused with its own reason, include an offer with
no common codec, a digest response for a nonce the bridge never issued, RTP from an address the
description does not state, telephone events split wrongly (a long event’s segment that does
not begin where the last ended, and an update naming another event), a BYE for another
dialog, a re-INVITE that changes the codec mid-call, and a G.711 payload of the wrong length
for its packet time. Every reason of §12 is shown by a vector that names it.
joule-moq-ingest-telephony-vectors --check regenerates it byte for byte; telephony_verify.py,
written from this text and the texts it names and sharing no code with the reference,
recomputes every expectation (its MD5 and SHA-256 are Python’s own), then leaves out each of
its own checks in turn and requires the corpus to catch the absence, and alters the corpus one
stated value at a time and requires every altered copy to fail.
The bridge’s tests run on loopback with the tests’ own SIP peer and RTP callers: no
outside softphone or PBX. tests/telephony_bridge.rs: two calls into one room, each through
its own number and grant, each sending three seconds of PCMU; each caller hears every packet of
the other’s from the first it hears to the last, in order, character for character (the mix of
the two, less its own, is the other’s sound exactly); the first call’s track, read at a relay
subscriber, verifies against every receipt and its samples are the characters sent, none made;
each object states a capture instant within the call, the catalog the playout delay; each
call’s record holds its statement, nothing lost, and an origin claim naming the statement. A
call to a PIN number is challenged, refused for a nonce never issued, admitted with SHA-256
credentials, enters a wrong PIN and then the right one, and publishes under the PIN’s grant;
its record counts the tries and holds no PIN. Over the wire, an offer with no common codec is
488 with its warning, a BYE for another dialog 481, a re-INVITE that changes the codec 488, a
G.711 packet of the wrong length and telephone events split wrongly are counted by their
reasons, and (on Linux, where 127.0.0.2 routes) a packet from an address not stated and a
request from an address other than the peer’s are refused and dropped. OPTIONS is answered
over TCP, CRLFs before it, and over TLS. The bridge registers with the tests’ registrar
through a digest challenge, places a call through a 407 challenge, sends the room’s mix in the
law the answer named, and ends the call on the peer’s BYE.
Held to real time. tests/telephony_rt.rs (ignored unless asked for; it reads the host’s
load and refuses a loaded host unless told to measure anyway) carries one call both ways for 30
seconds and requires every packet heard in order with no underrun and every delay within 400
ms (an object, a group and its receipts, the playout delay, two mix ticks, and 80 ms for the
relay’s path and the host); and carries 50 calls at once for 20 seconds, requiring every packet
read and published, none lost or late, every call sent the mix every 20 ms with no gap, and
five of the tracks verified at a relay subscriber sample for sample.
What was held, dated. Every figure here is one host’s on one day, with the load it carried, and none is a property of this extension. On 11 November 2026, on a 32-thread x86-64 build host shared with other work, no quiet hour came: the tests were run with their load check overridden, at a one-minute load of 22 (the call both ways) and 24 (the fifty calls), in a release build, on the code of this revision; a run of the same tests an hour earlier, at loads of 18 and 21, held the same.
| test | carried | what was held |
|---|---|---|
| one call both ways, 30 s | 1500 of 1500 packets each way, in order, character for character | no underrun or gap; end-to-end delay 202.0 ms mean, 202.1 median, 202.9 at the 99th percentile, 203.4 at most (bound 400); the mix’s 1600 ticks none more than 10 ms late |
| 50 calls at once, 20 s | 50 000 of 50 000 packets read and published, one object each; none lost or late | every call sent the mix every 20 ms, no gap over 60 ms (the fewest a call received in the window 999 of about 1000); no tick late; five of the tracks verified at a relay subscriber sample for sample |
The delay is the bridge’s own, as stated: about 160 ms of it is the playout buffer’s, which holds more than a group because a group’s sound reaches the mix only once its receipts verify (§10.2), and the rest is a group’s length and the relay’s path. A ceiling past 50 calls was not sought.
14. Security and privacy considerations
- SIP over UDP and TCP, and RTP, are unauthenticated and unencrypted: anyone on the path can read a call, and anyone who can send from the peer’s address can inject into it. The bridge takes requests from its one peer’s address (§8.1) and RTP from the address a call’s description states (§7.1), and asks its peer for credentials where configured (§5); a deployment keeps the path to its peer on a network it controls, or uses TLS for signalling. SRTP is not carried in this revision (§3): a call’s sound crosses that path in the clear.
- Digest authentication with MD5 resists a passive observer’s replay (nonces, counts) but not an observer with time to guess a weak password. The bridge offers SHA-256 first; a password is read from a file, held in memory and never logged.
- A PIN is a short secret entered as tones over a call. The bridge limits tries and the time to enter one, never carries the digits into the room or a record, and reads PINs from files.
- The first hop’s receipts attest what the bridge received, under a grant the operator mapped to a number or a PIN. They do not attest who spoke: a call’s origin claim says so in its own terms (§9.5).
- A caller’s number is personal data in many jurisdictions. It is carried into no record a sink reads, and into the bridge’s own record only where the operator’s configuration says so (§9.1, §11).
- A room’s mix sent to a caller leaves the room’s verification: what the bridge sends over RTP carries no receipt. The bridge mixes only what verified (§10.1).
- Every buffer and queue is bounded, and what a full one drops is counted: the jitter buffer, each playout buffer, the retransmission cache, the nonces kept. The number of calls is bounded by the configuration.