Eleven resolved decision tickets plus two research notes, produced with /wayfinder before any code was written. They live next to the code they explain rather than in the issue tracker: they are reasoning documents, not open work, and in a tracker they would drift from the code with no diff and no review. The value is in the rejected alternatives. Why a counter under a row lock and not BIGSERIAL, why an event log and not a column on messages, why resync is a 200 with a discriminated body and not a 4xx, why the fan-out trait was deleted again, why voice is peer-to-peer with a hard six-person cap and no speculative SFU seam, and why Linux needs a native media stack while the other three platforms do not. Two entries record where the spec was wrong and got corrected: created_at was never a safe sort key, and an unread count cannot be derived without fetching because seq counts events rather than messages. Open work is tracked as issues, not here.
152 lines
8.2 KiB
Markdown
152 lines
8.2 KiB
Markdown
# Voice-Topologie: Mesh oder SFU
|
||
|
||
Type: grilling
|
||
Status: resolved
|
||
Blocked by: —
|
||
Parent: ../map.md
|
||
|
||
## Question
|
||
|
||
Läuft Audio direkt zwischen den Clients (P2P-Mesh) oder über einen serverseitigen
|
||
Medien-Router (SFU) — und was davon legt das Signaling-Protokoll heute schon fest?
|
||
|
||
Diese Frage wurde beim Charting bewusst offengelassen (Q6). **Entblockt am 14.08.2026:**
|
||
[*Recherche: WebRTC im Tauri-WebView (Mobile)*](01-research-webrtc-tauri-mobile.md) hat
|
||
bestätigt, dass Android, iOS und Windows WebRTC im WebView tragen. Linux nicht — das ist
|
||
eine eigene Frage in [*Voice auf Linux*](11-voice-auf-linux.md), deren Antwort auf diese
|
||
hier zurückwirken kann.
|
||
|
||
### Spannungsfeld
|
||
|
||
- **Mesh** passt zum Bedrohungsmodell: der Server sieht nie Klartext-Audio, DTLS-SRTP
|
||
bleibt zwischen den Peers. Passt außerdem zur harten Deployment-Regel — nichts
|
||
Zusätzliches zu betreiben. **Skaliert aber nicht**: bei n Teilnehmern hält jeder n−1
|
||
Verbindungen und lädt sein Audio n−1 mal hoch. Praktische Obergrenze liegt bei etwa
|
||
vier bis sechs Teilnehmern, auf Mobile eher darunter.
|
||
- **SFU** skaliert auf Dutzende Teilnehmer, **terminiert aber DTLS** und sieht damit den
|
||
Audio-Klartext. Das widerspricht dem Bedrohungsmodell „Betreiber ist Angreifer"
|
||
frontal. E2EE über einen SFU braucht Insertable Streams / SFrame — das ist ein
|
||
eigenes Forschungsprojekt und im Rust-Ökosystem dünn besiedelt. Zusätzlich ein
|
||
weiterer zu betreibender Dienst, gegen die Simplizitätsregel.
|
||
|
||
### Zu entscheiden
|
||
|
||
- Welche **Teilnehmerzahl** muss ein Voice-Room realistisch tragen? Zwei Personen im
|
||
Gespräch, ein Team-Standup mit acht, oder ein Discord-artiger Kanal mit dreißig?
|
||
Diese Zahl entscheidet die Frage fast allein.
|
||
- Wenn Mesh: was passiert **oberhalb** der Grenze — harte Begrenzung mit klarer
|
||
Fehlermeldung, oder Degradation?
|
||
- Wenn SFU irgendwann: welche **Nähte** muss das Signaling-Protokoll heute schon haben,
|
||
damit der Wechsel kein Bruch ist? (Wer bietet an, wer antwortet, wie werden Streams
|
||
identifiziert.)
|
||
- **TURN**: P2P scheitert in restriktiven Firmennetzen ohne Relay — und Firmen sind
|
||
Zielgruppe. Bestätigt ist bereits: TURN bleibt optionale externe Abhängigkeit. Was
|
||
genau ist die Zusage an den Nutzer, wenn kein TURN konfiguriert ist? Und wie merkt
|
||
der Client das früh genug, um es ehrlich anzuzeigen statt in einen Timeout zu laufen?
|
||
- Wie werden **TURN-Credentials** übermittelt, ohne sie fest in den Client zu backen?
|
||
(Zeitbeschränkte HMAC-Credentials sind der übliche Weg — gehört das ins
|
||
Signaling-Protokoll?)
|
||
|
||
### Zwei SDP-Fragen aus der WebRTC-Recherche, die hierher gehören
|
||
|
||
Beide sind formal Codec-/Transport-Themen, aber es sind SDP- bzw. ICE-Entscheidungen und
|
||
damit Topologie-nah:
|
||
|
||
- **`iceTransportPolicy: "relay"` erzwingen?** Verhindert, dass der Peer die IP-Adresse
|
||
des Gegenübers lernt (W3C WebRTC 1.0 nennt das ausdrücklich als Zweck). Der Server
|
||
kennt die IP ohnehin aus der WebSocket-Verbindung, also kostet es nichts an Vertraulichkeit
|
||
gegenüber dem Betreiber — es reduziert strikt die Exposition gegenüber dem *Peer*. Preis:
|
||
ohne TURN gibt es dann gar keine Verbindung mehr, auch im LAN nicht.
|
||
- **Opus mit `cbr=1` (und ggf. `usedtx=0`) verhandeln?** Variable Bitrate leckt über
|
||
Paketgrößen ein Sprech-/Pausen-Muster — ein feindlicher TURN-Betreiber bekommt damit
|
||
eine Voice-Activity-Detection frei Haus, obwohl er den Klartext nicht sieht. Konstante
|
||
Bitrate schließt das gegen Bandbreite.
|
||
|
||
### Randbedingung
|
||
|
||
Diese Entscheidung darf die Grenze „dieser Effort endet beim Signaling" nicht
|
||
überschreiten. Codecs, Echo-Cancellation und Audio-Pipeline sind out of scope — die beiden
|
||
Punkte oben sind die Ausnahme, weil sie im SDP ausgehandelt werden und damit ins Protokoll
|
||
gehören.
|
||
|
||
---
|
||
|
||
## Answer
|
||
|
||
Entschieden am 14.08.2026.
|
||
|
||
### 1. Mesh mit harter Obergrenze von sechs Teilnehmern
|
||
|
||
P2P-Mesh, DTLS-SRTP bleibt zwischen den Peers, **der Server fasst nie Audio an**. Das
|
||
entspricht dem Bedrohungsmodell direkt und braucht keinen weiteren Dienst — die harte Regel
|
||
„eine Binary + Postgres" bleibt für Voice unangetastet.
|
||
|
||
Oberhalb von sechs wird der Beitritt **abgelehnt**, nicht degradiert: eine klare Meldung ist
|
||
ehrlicher als ein Gespräch, das für alle schlechter wird, ohne dass jemand versteht warum.
|
||
Bei sechs Teilnehmern lädt jeder Client fünf Ströme hoch, also grob 120 kbit/s aufwärts — die
|
||
Grenze, ab der Mesh in der Praxis kippt.
|
||
|
||
**Kein spekulativer Vorbau für einen späteren SFU.** Das ist der Kern der Entscheidung und
|
||
bewusst gegen den Reflex getroffen: ein SFU-Protokoll ist client-zu-Server adressiert, ein
|
||
Mesh-Protokoll peer-zu-peer. Eine „SFU-taugliche Naht" heute wäre Selbsttäuschung — sie würde
|
||
Komplexität kosten und im Ernstfall trotzdem nicht passen. **Ein SFU wäre ein bewusster
|
||
Protokollbruch in einem eigenen Effort**, und das steht so in der Map.
|
||
|
||
Verworfen: SFU von Anfang an. Er trüge Dutzende Teilnehmer, und SFrame/Insertable Streams
|
||
sind laut Recherche auf allen Zielplattformen verfügbar (WKWebView ab 15.4, Chromium ab 141).
|
||
Dagegen stehen drei Kosten: ein weiterer zu betreibender Dienst gegen die Deployment-Regel,
|
||
ein erheblich schwereres Krypto-Design zulasten des E2EE-Efforts, und selbst mit SFrame
|
||
behielte der SFU Zugriff auf RTP-Metadaten — die `audio-level`-Header-Erweiterung ist ein
|
||
direktes Sprechaktivitäts-Orakel und müsste eigens wegverhandelt werden.
|
||
|
||
Ebenfalls verworfen: den SFU dauerhaft auszuschließen. Das hätte das Profil geschärft, aber
|
||
die Tür für Firmenkunden mit größeren Runden zugezogen — eine Produktentscheidung, die es
|
||
ohne Not heute nicht braucht.
|
||
|
||
### 2. Eine Datenschutz-Politik je Instanz, vom Server gesetzt
|
||
|
||
Die Instanz konfiguriert **einen** Schalter, der zwei Dinge zugleich regelt und im Signaling
|
||
an den Client übermittelt wird; der Client zeigt sichtbar an, welcher Modus gilt.
|
||
|
||
| Schalterstellung | `iceTransportPolicy` | Opus |
|
||
|---|---|---|
|
||
| **streng** | `"relay"` erzwungen — der Peer lernt die IP-Adresse nie | `cbr=1`, `usedtx=0` |
|
||
| **offen** | Standard-ICE (host / srflx / relay) | Standard (VBR mit DTX) |
|
||
|
||
Die Kopplung ist nicht Bequemlichkeit, sondern folgt der Bedrohung: **beide Lecks entstehen
|
||
am selben Ort.** Läuft Audio über das Relay, sieht der Betreiber — der erklärte Angreifer —
|
||
die Paketgrößen und hat damit einen Sprechaktivitäts-Detektor, obwohl er den Inhalt nicht
|
||
lesen kann. Läuft es direkt, bekommt er die Pakete gar nicht zu sehen, dafür lernt der Peer
|
||
die IP. Ein Schalter, ein anzuzeigender Zustand.
|
||
|
||
**Warum je Instanz und nicht global:** auf einem öffentlichen Server telefonieren Fremde
|
||
miteinander, und dass zwei Unbekannte gegenseitig ihre Anschlussadresse lernen, ist für ein
|
||
Datenschutzprodukt untragbar — dort gehört „streng" hin, und TURN wird auf dieser Instanz zur
|
||
Voraussetzung. Auf einer Firmeninstanz sind die Peers Kollegen im selben Netz; dort wäre
|
||
erzwungenes Relay nur Bandbreitenverschwendung, und ein Gespräch zwischen zwei Rechnern im
|
||
selben Büro liefe ohne Not über den Server.
|
||
|
||
**Bewusst in Kauf genommen:** im offenen Modus bleibt das Sprechmuster für Netzbeobachter auf
|
||
dem Pfad sichtbar, und die Zusage an den Nutzer hängt am Betreiber — deshalb muss der Client
|
||
sie anzeigen und nicht nur intern kennen.
|
||
|
||
**Damit bleibt TURN die optionale Abhängigkeit**, die die Map festgelegt hat: ohne TURN läuft
|
||
Voice im offenen Modus eingeschränkt (Firmen-NATs), im strengen Modus gar nicht — und der
|
||
Betreiber weiß, worauf er sich einlässt, weil er den Schalter selbst umlegt.
|
||
|
||
### 3. Beide Medienpfade müssen die Politik umsetzen
|
||
|
||
Aus [*Voice auf Linux*](11-voice-auf-linux.md): `RTCPeerConnection` im WebView auf drei
|
||
Plattformen, `livekit/rust-sdks` auf Linux. Sowohl `iceTransportPolicy` als auch die
|
||
Opus-`fmtp`-Parameter müssen auf beiden Wegen gesetzt werden können — beides ist
|
||
Standard-WebRTC und in beiden Stacks vorhanden, gehört aber in die Spec, damit es nicht auf
|
||
einem der Pfade vergessen wird.
|
||
|
||
### Folgen
|
||
|
||
[*Voice-Signaling-Protokoll*](09-voice-signaling-protokoll.md) ist damit entblockt. Zwei
|
||
Dinge gehen von hier dorthin: die **Raumgröße** muss serverseitig durchgesetzt werden (der
|
||
Beitritt wird abgelehnt, nicht nur in der UI verhindert), und die **Politik-Übermittlung**
|
||
gehört ins Signaling — zusammen mit den TURN-Zugangsdaten, die im strengen Modus zwingend
|
||
sind.
|