feat(M2): UDP voice/media plane -- SFU relay, Opus, AEAD, jitter buffer
Adds the full voice pipeline: 14-byte binary frame header, ChaCha20-Poly1305 AEAD keyed from the TLS exporter, libopus encode/decode with FEC/PLC/DTX, an adaptive per-ssrc jitter buffer, a miniaudio capture/playback engine, an APM passthrough stub, and the UdpBinding/StreamAnnounce signaling chain wired through ConnSession/SessionRegistry into a new server-side SFU (MediaRelay) that decrypts and re-encrypts frames per channel member. Exit criterion verified: test_m2_voice — two headless clients relay 50 encrypted Opus frames through the server; ctest --preset m1-dev is 9/9 green. Also corrects protocol.md's UdpBinding diagram, which described the UDP-side binding packet as AEAD-sealed when it is in fact a plaintext bootstrap frame (separate from the TCP/TLS UdpBinding ack). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -128,11 +128,14 @@ Client Server
|
||||
│ │
|
||||
│ ◀── ServerStateSnapshot (channel tree, users) ───│ initial sync
|
||||
│ │
|
||||
│ UdpBinding(udp_token) [TCP/TLS] ─────────────────▶│ confirms token, no-ops if mismatched
|
||||
│ ◀── UdpBinding(ack=true) [TCP/TLS] ───────────────│
|
||||
│ │
|
||||
│ ===== UDP side (parallel) ===================== │
|
||||
│ (media keys derived from TLS exporter — no 2nd │
|
||||
│ handshake; see security.md §2) │
|
||||
│ UdpBinding(udp_token) [AEAD, exported keys] ────▶│ binds 5-tuple → session_id
|
||||
│ ◀── UdpBinding ack [AEAD] ───────────────────────│
|
||||
│ UDP_BINDING frame(udp_token) [plaintext] ───────▶│ binds 5-tuple → session_id
|
||||
│ ── voice frames (AEAD, exported keys) ──────────▶│
|
||||
│ │
|
||||
│ JoinChannelRequest(id, password?) ──────────────▶│
|
||||
│ ◀── JoinChannelResult(ok, members, audio_cfg) ───│
|
||||
@@ -151,11 +154,12 @@ Notes:
|
||||
- **Auth over TLS.** Passwords cross the wire only inside TLS 1.3 and are verified against
|
||||
an Argon2id hash at rest (see security.md). `auth_methods` in `ServerHello` advertises
|
||||
whether `guest` is enabled.
|
||||
- **UDP token.** `AuthResult.udp_token` is a short-lived opaque token. The client sends it
|
||||
in the first UDP message (`UdpBinding`) so the server can bind the UDP 5-tuple to the
|
||||
authenticated session without trusting the source address. This is the only UDP message
|
||||
that carries identity material; everything after is implicit via the bound tuple +
|
||||
media-AEAD session.
|
||||
- **UDP token.** `AuthResult.udp_token` is a short-lived opaque token. The client confirms it
|
||||
over TCP/TLS (`UdpBinding` request/ack) and also sends it as the payload of a plaintext
|
||||
`UDP_BINDING`-type media frame so the server can bind the UDP 5-tuple to the authenticated
|
||||
session without trusting the source address. This bootstrap frame is the only UDP message
|
||||
that carries identity material in the clear; everything after (voice frames) is AEAD-sealed
|
||||
and routed purely by the bound tuple + media-AEAD session.
|
||||
- **Snapshot then deltas.** After auth the server pushes a `ServerStateSnapshot` (full
|
||||
channel tree + visible users), then streams incremental `ChannelEvent`/`UserEvent`
|
||||
deltas. Clients reconcile by id.
|
||||
|
||||
Reference in New Issue
Block a user