Direct voice and encrypted relay fan-out
Document: SPEC-017. Revision: 1.2. Date: 2026-11-02. Lifecycle: approved design under D-086; engineering completion of P-029/P-030. Supersedes the relay-only route in SPEC-002/004, per-recipient media packet in SPEC-009/016, and independent relay-enforcement requirement in SPEC-003 for private encrypted direct paths only. SPEC-016 ephemeral offers, deadlines and trust remain authoritative. Implemented in alpha.10; release evidence records validation and deployment.
Approved video-delivery successor — D-167
D-167/SPEC-024 permits managed presentation routing independently of microphone routing. SFU paths are relayed and receive no P2P bitrate exemption. Native P2P voice consent, address disclosure, all-recipient microphone-key barrier, replay and authority rules remain active. Presentation-key acknowledgment gates only the relevant recipient in the updated client. The managed presentation profile supersedes native screen packet/route details only when explicitly negotiated. See SPEC-024.
Scope and authority
Screen-sharing extension: D-128–D-130 request separate video/presentation-audio capability under SPEC-023. D-134 completes its separately bounded transport and bitrate exception. The voice profile keeps its packet limits, privacy choices and leases; voice-clock/1 adds an authenticated optional microphone timestamp for compatible synchronized-media clients. Screen publication uses separate grants and its own data channel. Do not treat a voice grant as screen-publication permission.
D-086 explicitly approves the reviewed sender-key/signature design, private encrypted P2P, peer-address exposure, conforming-receiver moderation and relay-only preference, and authorizes building and deploying it. Public rooms of either encryption mode and private server-readable rooms remain application-relayed. Thread calls inherit parent access/encryption while keeping their actual call identity. This does not add the DM/direct-call product UI.
Conforming clients enforce host moderation on direct paths. The server cannot terminate two modified endpoints communicating privately. Relayed paths retain independent native-relay enforcement. An enforcement result means relay enforcement plus mandatory conforming-client rejection by the hard deadline; it is not evidence that all private peer connections were terminated. R-043/D-009 are amended only to this scope.
Compatibility and media
Require voice-fanout/1 in x-sloppy-client for preparation, joining and lease, in addition to the existing Opus and applicable voice-key capabilities. Room state advertises voiceRouting: {profile:"voice-fanout/1", stunServers:string[]}. Updated clients reject missing profiles. The new data-channel label is voice-fanout/1, unordered, zero retransmissions. A relay may retain the old Opus label/token profile during rollout; new control grants always select the new profile and cannot reopen legacy admission. Refresh/rejoin is required; text remains compatible.
A common signed media frame has exactly profile, serverId, room, domain, account, source, session, revision, sequence, encryption, and signature, plus either key, nonce, ciphertext for E2EE or audio for server-readable. Profile is voice-fanout/1; server UUID is always explicit, including local-only services. Source is the approved device UUID; session is attendance UUID; sequence is a nonnegative safe integer monotonically increasing through the call. Signature is Ed25519 over canonical JSON without signature. The existing bounded Opus envelope is base64url audio or encrypted plaintext. No recipient field belongs in the common object.
For E2EE, use a random 32-byte sender secret per own SPEC-016 offer, random 24-byte nonces and XChaCha20-Poly1305. Additional data is canonical JSON of the common context including key ID, excluding nonce/ciphertext/signature. Sign context, nonce and ciphertext together. A receiver verifies identity/signature, current authority, context, key deadline and replay before decoding. One replay window per source attendance spans direct and relay paths. Secret buffers are memory-only and cleared on retirement. Another key-holding listener cannot forge the sender signature.
The relay upload is exactly {targets:deviceUUID[], frame:CommonFrame}, with 1–9 unique targets, never self. The complete upload stays within the existing 4096-byte ceiling. The relay validates the common signature, grant ownership, server/call/mode/revision and quotas once, then independently checks each recipient's current lease and backpressure. Invalid target lists reject the upload; unavailable/unauthorized recipients are skipped individually. Each recipient gets the same common frame bytes. Direct channels carry the common frame itself. One congested target does not block others.
New media grants contain profile:"voice-fanout/1", explicit server UUID, encryption (e2ee or server_readable), approved signing key and account domain, beside existing media grant fields. The native relay receives no sender secret. Existing ingress quotas, 65536-byte per-channel backpressure threshold, ten-peer cap, lease/revision checks and generation fences remain.
Peer exchange and sender keys
Each voice lease may carry peerUpdate:{revision,direct,envelopes}. direct is boolean and can be true only in private E2EE calls. Envelopes are a replacement snapshot with at most one per other current attendance, at most nine. Each is signed by its source device and has exactly profile:"voice-peer/1", serverId, room, revision, source, session, target, targetSession, positive safe-integer serial, keys, acks, optional signal, and signature. No recipient can be outside current trusted live attendance. Serial increases per source attendance; identical retries are allowed but substituted/reordered older snapshots are rejected. Context changes invalidate stored exchanges.
keys contains at most four boxes for the current/staged source and current/staged target offers: {sourceKey,targetKey,nonce,ciphertext}. Encrypt the 32-byte sender secret using the existing directional ephemeral pair and a distinct voice-sender-key/1 AAD containing both complete unsigned offers. A source generates a new sender secret with its offer, not from static/recovery keys. Key boxes and their containing signature bind source and recipient. Receivers reject substituted secrets for an already pinned sender-key ID.
acks contains at most two source offer IDs from the recipient's current/staged offers, acknowledging successfully installed keys. A speaker waits for all trusted live recipients to acknowledge its active sender key before publication. A routing-only transition may temporarily omit an exchange containing SDP. A client may retain its previously signature-verified key-installation acknowledgement only while revalidation against the current account/device, server/room/revision and both attendance sessions succeeds and the corresponding advertised offers remain live. Cached evidence cannot extend key, approval or forwarding deadlines, validate a supplied invalid/conflicting exchange, or survive context/trust loss. This preserves the route-only PTT continuity requirement below without changing the wire format. A staged key can be distributed/acknowledged before its SPEC-016 activation. A missing acknowledgment cannot extend the old key. After expiry, audio pauses and recovery requires fresh PTT. Membership changes clear keys/exchanges and held PTT; periodic renewal preserves a valid talkspurt.
Source key lifetime, activation and receive-only overlap are inherited from SPEC-016. Replacement key distribution does not extend any offer. Leave, invalid trust, authority or context immediately clears key state and prohibits overlap. Only the current/staged key envelopes are stored with live attendance. Public polling must not extend client key or media deadlines.
The host authenticates each lease, validates signatures, box bounds, exact offer IDs and target attendance, and stores only bounded opaque boxes/SDP under attendance. State returns each attendee's direct preference and only the signed envelope addressed to the requesting active device. Non-attendees receive no envelopes. Stale revision publishes use voice_key rejection and the existing authenticated refresh/retry, including initial join. Envelopes disappear on revision, expiry or departure; secret bytes never reach the host.
Direct connection and authority
Use a separate participant RTCPeerConnection with only configured STUN servers. Offerer is the lexicographically smaller device UUID. signal is {attempt,type,sdp} with positive safe-integer attempt, offer or answer, and at most 16384 UTF-8 SDP bytes. Whole descriptions after bounded ICE gathering are signed inside the addressed envelope, including DTLS fingerprints. Answers match the current offer attempt; stale attempts and wrong target sessions fail. Keep one negotiation per peer, no trickle mailbox and no TURN candidate route. Every peer revalidates signatures and trusted identities before applying SDP.
Neither endpoint may gather or exchange participant candidates unless both advertise direct permission. Switching relay-only closes local participant connections immediately and publishes the restricted preference on the next lease. The existing application relay stays prepared/connected for fallback. Zero relay media does not imply zero Container cost.
Direct ingress applies the relay’s existing 600-message/524288-byte per-second budget before parsing or verifying signatures. Direct channels send bounded heartbeat JSON {ping:number} / {pong:number} once per second. Numbers identify outstanding local probes; unsolicited or replayed pongs do not establish health. Both selected ICE candidates must be confirmed non-relay through stats before a route counts as direct. Two successful consecutive probes establish/reestablish readiness; three seconds without a valid pong disables direct sending. A single delayed/lost probe does not force a switch. Connection failure or backpressure selects relay for that recipient.
Allow 3000 ms for ICE gathering and 15000 ms for connection establishment. Failed negotiations retry after 10000 ms, doubling to a maximum 60000 ms; successful connections reset backoff. Network-change events may bring forward one retry without starting duplicate attempts. Stable direct routes remain preferred; route changes alone do not clear PTT.
Every lease response exposes each live source's validForMS, at most 3000 and capped by its last issued media authority, attendance and generation/home authority deadlines. Clients anchor it to the start of their own lease request and reject expired senders on every received frame. The receiving client's own local authority also expires within three seconds. Ordinary polling cannot extend these deadlines. This authenticated HTTPS snapshot is the client-verifiable authority; no MEDIA_SECRET or peer HMAC verifier is distributed. Time rollback, sleep/elapsed deadlines and delayed responses must fail closed.
On authority expiry, remove queued/decoded/concealed audio as well as rejecting newly arriving packets. The worklet must independently expire speaker authorization using its audio clock, so stalled main-thread renewal cannot sustain buffered playback. Combined enforcement remains within the existing 4000 ms hard bound. Preserve original generation closure, one-active-call fences, foreign approval and fresh-press rules.
Configuration
Fixed profile constants and limits above are not operator/user settings. SPEC-016 owns key deadlines, SPEC-003 leases and SPEC-004 Opus/PTT/playout. Component ownership is SPEC-005 client and SPEC-008 server.
| Property | Classification, type/default | Scope and validation |
|---|---|---|
voiceRelayOnly | User preference, boolean; false when omitted | SPEC-005 owns encrypted per-device/home-account vault persistence and UI. Explicit true prevents P2P signaling with that device. Invalid stored types fail conservatively to relay-only; no account-wide sync or inherited override. Applies immediately to active call and future calls. |
PEER_STUN_URLS | Deployment setting, JSON string containing 0–4 distinct STUN URL strings; omitted defaults to [] | SPEC-002 owns canonical configuration. Each URL is at most 300 characters, stun: plus DNS hostname/IPv4/bracketed IPv6 and optional port 1–65535; no userinfo, query, fragment, path, credentials, TURN or implicit public provider. Null, malformed/duplicate/empty entries fail startup. Read at startup; response changes apply to newly created peer connections. The reference Cloudflare control configuration explicitly supplies [\"stun:stun.cloudflare.com:3478\"]. |
Testing
Unit tests cover the access/encryption matrix, preference validation, STUN validation/defaults, signed exchange bounds and recipient authorization, stale revisions/serials/sessions, per-speaker authority deadlines, key wrapping/acknowledgment/rotation, removed/new recipient exclusion, signature forgery by a key-holding listener, nonce/context/replay, direct readiness/backoff/fallback, and no IP/SDP/secret diagnostics. Native tests cover actual one-to-many forwarding, permissions, quotas, malformed lists, per-target expiry, concurrency and backpressure.
Browser tests render actual Opus over all-direct, all-relayed and mixed paths, verify packet counts, force path/control/key failure, and exercise five-minute rotation, thread/host movement, public E2EE and server-readable calls. Hosted tests use disposable identities and the existing bounded fixture. Same-machine tests establish code behavior, not universal NAT reachability or physical listening acceptance. Real geographically separated listeners remain a reported validation limit when unavailable.
Acceptance criteria
- Private E2EE participants establish authenticated direct paths when reachable; public channels in both modes and private readable channels send no participant signaling.
- A relay-only endpoint prevents candidate exchange for its pairs and an active preference change stops direct sending/gathering.
- Every frame has one common encryption/signature operation and at most one relay upload, independent of relayed recipient count.
- Mixed routing delivers to direct and relay listeners without duplicate playback; all-direct steady-state audio sends no application-relay media.
- Another listener with a sender secret cannot forge its speaker identity, and revoked/new attendance cannot use an inappropriate key generation.
- Five-minute expiry, staged distribution, receive-only overlap, fresh PTT after failed renewal and immediate membership clearing match SPEC-016.
- Direct senders stop being audible to conforming listeners within four seconds of accepted mute/revocation or loss of authority, including stalled client renewal and queued playout.
- Changed control/client/relay capabilities and deployment images match; live rollout and 100% allocation/observed-traffic reporting follow the deployment procedure.
- Release evidence distinguishes automated/local/hosted results from outstanding physical and cross-network acceptance.