Skip to main content

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 (module telephony; the bridge behind its telephony feature) in joule-code/joule-code-rs/crates/; the corpus is live-conformance/telephony/, and its second implementation is live-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

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:

whatwhere 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 received405 with Allow (§8.2 rule 4)
forkingthe 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 sound488, 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 UPDATEnot 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 bridgenot offered or answered; the bridge sends every 20 ms (§7.5)
a G.711 packet of another length than the answer’s packet timerefused, g711_ptime_length (§7.2), this extension’s rule
decoding Opus, and any loss concealmentnot 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 eventsrefused, dtmf_event_not_offered; the bridge sends none
a DTMF digit carried into the roomnot done: digits are read for a PIN (§9.1) and counted, never published
SIP over WebSocket, SCTP, and SIP URIs resolved by DNSnot read: the bridge talks to its configured peer’s address
-sess digest algorithms and qop=auth-intnot offered; refused, digest_algorithm and digest_qop (§5.3)
RTCPnot 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:

  1. A message of more than 65 535 octets is sip_too_large. This bound is this extension’s.
  2. The header ends at the first empty line (CRLF CRLF), and is UTF-8 text of lines ending in CRLF: otherwise sip_header_line.
  3. 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). Otherwise sip_start_line. A version other than SIP/2.0 (compared ignoring case) is sip_version.
  4. 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 is name ":" value, white space allowed before the colon, the name a token: otherwise sip_header_line. Values are trimmed of white space at both ends.
  5. A compact name stands for its long name (§7.3.3, §20): i Call-ID, m Contact, e Content-Encoding, l Content-Length, c Content-Type, f From, s Subject, k Supported, t To, v Via. 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.
  6. Call-ID, Content-Length, CSeq, From, Max-Forwards and To given in two rows are sip_header_value (§7.3.1: only a comma-separated list may be repeated).
  7. A request without To, From, CSeq, Call-ID, Max-Forwards or Via, and a response without To, From, CSeq, Call-ID or Via, is sip_header_missing (§8.1.1).
  8. sip_header_value: a CSeq that 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; a Max-Forwards that is not one to three digits of at most 255; a Call-ID that is empty or holds white space; a From or a To that is not a name-addr ([display] <URI> and parameters) or an addr-spec (a URI and parameters); a request whose From has no tag parameter (§8.1.1.3).
  9. The top Via (the first element of the first Via row) reads as SIP/2.0/<transport> <sent-by> and parameters: otherwise sip_header_value. Its branch MUST begin with z9hG4bK and hold more after it (§8.1.1.7): otherwise sip_via_branch. A request of an RFC 2543 element, whose branch has no such prefix, is not read: this extension’s rule.
  10. The body (§18.3): with a Content-Length (one to nine digits, or sip_header_value), the body is that many octets after the empty line; fewer is sip_body_short, and octets past them are discarded. With none, over UDP the body runs to the datagram’s end; over a stream it is sip_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="&lt;realm&gt;", nonce="&lt;nonce&gt;", algorithm=&lt;algorithm&gt;, 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:

  1. No row: digest_missing.
  2. The first row of scheme Digest whose realm is the bridge’s. A Digest row whose parameters do not read (RFC 7616 §3.4’s name=value pairs, values quoted or tokens, no name twice) is digest_malformed (400); rows of other schemes are passed over; no row of the bridge’s realm is digest_realm.
  3. username, nonce, uri and response present: otherwise digest_malformed (400).
  4. algorithm (MD5 where absent) one the bridge offered: otherwise digest_algorithm.
  5. qop present and auth, with an nc of eight hexadecimal digits and a cnonce: qop absent, or of another value, is digest_qop; qop=auth without them is digest_malformed (400).
  6. nonce one the bridge issued and keeps: otherwise digest_nonce_unknown. A response computed for a nonce of anyone else’s is refused here.
  7. The nonce issued no more than the bridge’s nonce lifetime (300 seconds by default) ago: otherwise digest_nonce_stale, and the new challenge says stale=true.
  8. uri equal, octet for octet, to the request’s Request-URI (RFC 7616 §3.4.6): otherwise digest_uri_mismatch (400).
  9. response equal (ignoring case) to §5.1’s for an account the bridge holds, of that username: otherwise digest_response_mismatch.
  10. nc above every nonce count accepted under that nonce: otherwise digest_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:

  1. 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.
  2. 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 is rtp_version; with the padding bit set, a last octet of 0 or one past the payload is rtp_padding. A header extension and CSRCs are passed over.
  3. The payload type MUST be one the call’s description has the bridge receive: otherwise rtp_payload_type.
  4. The first packet read fixes the call’s SSRC; a packet of another is ssrc_changed.
  5. 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).
  6. 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.
  7. 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.
  8. 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:

  1. An event above 15 (the answer’s 0-15, the DTMF events of §3.2) is dtmf_event_not_offered; a duration of 0 is dtmf_zero_duration (§2.3.5: DTMF events are not states).
  2. 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 is dtmf_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 is dtmf_duration_decreased; a report without the end bit after one with it is dtmf_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.
  3. 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.
  4. 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.
  5. 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 mark dtmf_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.

  1. A message the reader refuses (§4.1) is answered, where it is a request other than ACK whose Via, From, To, Call-ID and CSeq rows are present, with 400 (505 for sip_version, 513 for sip_too_large) and the reason; otherwise dropped. A response that does not read is dropped.
  2. 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.
  3. 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 is sdp_malformed), and the bridge sends BYE (RFC 3261 §13.3.1.4). Any other ACK is absorbed. ACK is never answered.
  4. Credentials (where the bridge asks for them): a request outside a dialog, but CANCEL, is checked by §5.3.
  5. 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 with Allow: INVITE, ACK, BYE, CANCEL, OPTIONS.
  6. The Request-URI of a scheme other than sip or sips: 416, uri_scheme (§8.2.2.1).
  7. 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.
  8. Inside a dialog (a To tag): one the bridge holds (Call-ID, its tag as the To tag, the peer’s as the From tag), not ended: otherwise 481, dialog_unknown (§12.2.2). A CSeq number not above the dialog’s last is 500, cseq_out_of_order.
  9. Merged (no To tag): the Call-ID, From tag and CSeq number of an INVITE still ringing, by another branch: 482, loop_detected (§8.2.2.2).
  10. Require: any option tag (the bridge supports none): 420, extension_unsupported, with Unsupported listing them (§8.2.2.3).
  11. The body (§8.2.3), where there is one: a Content-Encoding other than identity is 415, media_type_unsupported, with Accept-Encoding: identity; a Content-Type other than application/sdp (or none) is 415 with Accept: application/sdp.
  12. The method’s own:
    • OPTIONS: 200 with Allow and Accept: 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.

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:

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):

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:

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):

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

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:

reasonstatuswhen
sip_start_line400a start line that is neither a request line nor a status line
sip_version505another version than SIP/2.0
sip_header_line400no empty line ends the header; a line that is not name: value; a continuation with nothing before it
sip_header_missing400one of the header fields every request or response carries is absent
sip_header_value400a 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_branch400a branch without RFC 3261’s magic cookie
sip_content_length400a message over a stream without Content-Length
sip_body_short400a body shorter than its Content-Length
sip_too_large513a message of more than 65 535 octets

Refusals of credentials (§5.3):

reasonstatuswhen
digest_missing401no Authorization
digest_malformed400parameters that do not read; one required absent
digest_algorithm401an algorithm the bridge did not offer
digest_realm401no row of the bridge’s realm
digest_nonce_unknown401a nonce the bridge never issued, or no longer keeps
digest_nonce_stale401a nonce past its lifetime (stale=true)
digest_uri_mismatch400uri that is not the Request-URI
digest_qop401no qop, or one other than auth
digest_nc_replayed401a nonce count not above the last accepted under the nonce
digest_response_mismatch401a response no account of that user gives

Refusals of a description (§6), each 488 where it is an offer to the bridge:

reasonwhen
sdp_malformedlines that do not read as §6.1 says
sdp_no_audiono m=audio
sdp_proto_not_carriedno m=audio of RTP/AVP with a port: SRTP among them
sdp_no_common_codecno G.711 law in the call’s stream: nothing the bridge both receives and sends
sdp_connectionno connection address for the call’s stream
sdp_multicasta multicast connection address
reinvite_codec_changea later offer that drops the call’s law

Refusals of a request by the user agent (§8.2):

reasonstatuswhen
method_not_allowed405a method recognised and not supported
method_unknown501a method not recognised
uri_scheme416a Request-URI that is not sip or sips
number_unknown404a number the bridge does not answer
dialog_unknown481a request for a dialog the bridge does not hold; a BYE outside a dialog
cseq_out_of_order500a CSeq not above the dialog’s last
loop_detected482a merged request
extension_unsupported420an option tag in Require
media_type_unsupported415a body not application/sdp, or encoded
transaction_unknown481a CANCEL that matches no INVITE
busy486a number at its limit; no media port free

Refusals of an RTP packet (§7):

reasonwhen
rtp_shortfewer octets than its header says
rtp_versiona version other than 2
rtp_paddinga padding count of 0 or past the payload
rtp_payload_typea type the call’s description does not have the bridge receive
rtp_source_not_statedfrom an address the call’s description does not state
rtp_codec_switcha sound packet of another codec than the call’s first
ssrc_changedan SSRC that is not the call’s
duplicatea sequence number seen before
g711_ptime_lengtha G.711 payload of another length than the answer’s packet time
opus_emptyan empty Opus payload
dtmf_lengtha telephone-event payload that is empty or not of whole reports
dtmf_event_not_offeredan event above 15
dtmf_zero_durationa DTMF report of duration 0
dtmf_event_changeda report at an event’s timestamp naming another event
dtmf_duration_decreasedan update whose duration falls
dtmf_end_withdrawnan update without the end bit after one with it
dtmf_markeran update with the marker bit
dtmf_segment_gapa long event’s next segment that does not begin where the last ended

Marks: the packet or the event is kept, and says so.

reasonwhen
timestamp_jumpa sound packet that does not begin where the last ended
dtmf_start_unmarkedan event whose first report has no marker

13. Status and conformance

partstatuswhere
the readers and the user agent’s answers (§4 to §8.3, §8.5)implemented, with a reference and a second implementationjoule-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 teststelephony::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)implementedtelephony::bridge, feature telephony; tests/telephony_bridge.rs, tests/telephony_rt.rs
SIP over TLSimplemented 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 miximplemented; 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, REFERnot implemented; refused§3
carriage of digits, and of origin claims, in a roomnot 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.

testcarriedwhat was held
one call both ways, 30 s1500 of 1500 packets each way, in order, character for characterno 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 s50 000 of 50 000 packets read and published, one object each; none lost or lateevery 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