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.
117 lines
7.3 KiB
Markdown
117 lines
7.3 KiB
Markdown
# Recherche: WS-Auth, Heartbeat und Reconnect
|
|
|
|
Type: research
|
|
Status: resolved
|
|
Blocked by: —
|
|
Parent: ../map.md
|
|
|
|
## Question
|
|
|
|
Welche etablierten Muster gibt es für Authentifizierung, Verbindungserhalt und Reconnect
|
|
bei langlebigen WebSocket-Verbindungen — und welche davon passen zu Axum 0.8 und einem
|
|
JWT-basierten Auth-Modell?
|
|
|
|
### Zu klären
|
|
|
|
- **Token-Übergabe beim Handshake.** Die Browser-`WebSocket`-API erlaubt keine
|
|
benutzerdefinierten Header. Übliche Auswege: Token im Query-String (landet in
|
|
Server-Logs — bei diesem Bedrohungsmodell heikel), `Sec-WebSocket-Protocol`-Missbrauch,
|
|
Cookie, oder ein erstes Auth-Frame nach dem Handshake mit kurzem Timeout. Was ist
|
|
gängige Praxis, was sind die dokumentierten Nachteile? **Achtung:** falls die
|
|
Verbindung im Tauri-Rust-Layer lebt (siehe *Wo lebt die WS-Verbindung im Client*),
|
|
entfällt die Header-Beschränkung komplett — die Recherche soll beide Fälle abdecken.
|
|
- **Token-Ablauf bei laufender Verbindung.** Das JWT ist beim Handshake gültig und läuft
|
|
nach Minuten ab, während die Verbindung Stunden steht. Muster: Verbindungs-TTL mit
|
|
erzwungenem Reconnect, In-Band-Re-Auth-Frame, serverseitige Revocation-Liste. Was
|
|
machen andere?
|
|
- **Heartbeat.** WebSocket hat Ping/Pong auf Protokollebene. Wann reicht das, wann braucht
|
|
es Application-Level-Heartbeats? Übliche Intervalle. Verhalten hinter Proxies und
|
|
Corporate-Firewalls (relevant: Firmenkunden sind Zielgruppe).
|
|
- **Reconnect-Strategie.** Exponentielles Backoff mit Jitter — konkrete Parameter aus
|
|
der Praxis. Thundering-Herd-Verhalten, wenn ein Server neu startet und alle Clients
|
|
gleichzeitig zurückkommen.
|
|
- **Axum 0.8 konkret.** Wie sieht ein WebSocket-Upgrade mit State-Zugriff und Middleware
|
|
aus? Greift `middleware::from_fn_with_state` beim Upgrade-Request? Wie werden viele
|
|
gleichzeitige Verbindungen sauber verwaltet und beim Shutdown beendet?
|
|
- **`tokio::sync::broadcast` und langsame Empfänger.** Der Kanal verwirft Nachrichten für
|
|
hinterherhinkende Receiver (`RecvError::Lagged`). Wie behandelt man das korrekt, wenn
|
|
„Lagged" eigentlich „Resync jetzt" bedeuten soll? Gibt es dafür ein etabliertes Muster
|
|
oder bessere Kanaltypen für diesen Zweck?
|
|
|
|
### Quellenqualität
|
|
|
|
Axum-/Tokio-Dokumentation und Quellcode, RFC 6455, MDN. Praxisberichte nur mit Datum.
|
|
|
|
### Erwartetes Ergebnis
|
|
|
|
Eine Markdown-Datei im Repo: pro Teilfrage die Optionen mit ihren dokumentierten Vor-
|
|
und Nachteilen, jeweils belegt. **Keine Empfehlung** — die Entscheidung fällt im Ticket
|
|
*Verbindungsaufbau & Auth über WS*, dieses Ticket liefert nur die Faktenlage.
|
|
|
|
## Answer
|
|
|
|
Vollständiges Ergebnis: [`../research/03-ws-auth-reconnect.md`](../research/03-ws-auth-reconnect.md)
|
|
— Faktenlage zu allen sechs Teilfragen, Quellenverzeichnis und ein Anhang „Was nicht belegt
|
|
werden konnte" mit 17 Punkten. Keine Empfehlung, wie gefordert.
|
|
|
|
### Die vier Befunde, die andere Tickets verändern
|
|
|
|
**1. Der `Authorization`-Header ist der leiseste Kanal — und das ist belegbar, nicht Folklore.**
|
|
In den Default-Konfigurationen von nginx, Apache und Caddy landet der Query-String im
|
|
Access-Log; Caddy redigiert `Authorization` aktiv zu `REDACTED`, `request.uri` dagegen nicht.
|
|
Kein Transportweg schützt vor dem Betreiber selbst — der Server *muss* das Token sehen. Der
|
|
Unterschied zwischen den Optionen ist ausschließlich, was den Handshake in Logfiles, Backups
|
|
und APM-Systemen **überlebt**. Nebenbei entkräftet: die verbreiteten Referrer- und
|
|
Browser-History-Argumente gegen den Query-String ließen sich für `new WebSocket(url)` **nicht**
|
|
belegen und gehören nicht ins Entscheidungsticket.
|
|
|
|
**2. `Sec-WebSocket-Protocol` ist für dieses Bedrohungsmodell schlechter als gedacht** — Caddy
|
|
loggt per Default *alle* Header außer der Redaktionsliste, und dieser gehört nicht dazu. Dazu
|
|
kommt: ein JWT ist zwar ein legaler Subprotocol-Token, aber `=` ist als Separator verboten
|
|
(Base64-Padding), und der Browser erzwingt ein Echo des Subprotokolls durch den Server.
|
|
|
|
**3. Die Browser-API verschluckt alle Fehlerdetails — der Rust-Layer nicht.** WHATWG schreibt
|
|
vor, dass DNS-Fehler, Connection-Refused, TLS-Fehler, Subprotocol-Mismatch und Abbruch nach
|
|
dem Handshake für das Skript **ununterscheidbar** sein müssen: immer `CloseEvent.code === 1006`.
|
|
Im WebView ist „Token abgelaufen" also nicht von „Server down" unterscheidbar. `tungstenite::Error::Http`
|
|
trägt dagegen die vollständige HTTP-Response inklusive Statuscode und Body. Diese Asymmetrie
|
|
zieht sich durch fast jede Teilfrage → starkes Argument in *Wo lebt die WS-Verbindung im Client*.
|
|
|
|
**4. `axum::serve(...).with_graceful_shutdown(...)` beendet WebSocket-Verbindungen nicht und
|
|
wartet nicht auf sie.** Belegt vierfach: der `close_rx`-Guard hängt nur an der hyper-Task, das
|
|
`on_upgrade`-`tokio::spawn` nicht; `UpgradeableConnection::graceful_shutdown` ist nach dem Hijack
|
|
laut hyper-util-Doku „does nothing"; axum-Issue #3003 (offen seit 21.10.2024) und PR #3831
|
|
(offen seit 11.07.2026) beschreiben genau das. **Das Projekt muss WS-Verbindungen selbst
|
|
tracken und beenden** — `CancellationToken` + `TaskTracker` sind die belegten Bausteine.
|
|
|
|
### Weitere Faktenlage, kompakt
|
|
|
|
- **`middleware::from_fn_with_state` greift beim Upgrade-Request** — das Upgrade ist ein normaler
|
|
GET, 401/403 sind regulär sendbar.
|
|
- **Token-Ablauf:** SignalR dokumentiert ausdrücklich, dass Auth **nicht** revalidiert wird.
|
|
Optionen sind TTL/Reconnect (AWS API GW: 2 h / 10 min idle, hart), In-Band-Refresh (Centrifugo,
|
|
Ably — Token landet dann in keinem Log) oder eine Revocation-Liste (im Betreiber-Angreifer-Modell
|
|
per Definition wirkungslos).
|
|
- **Heartbeat:** Protokoll-Ping ist aus dem WebView **nicht auslösbar** (WHATWG), im Rust-Layer
|
|
antwortet tungstenite automatisch. Proxy-Korridor der dokumentierten Idle-Timeouts: Azure
|
|
AppGW 20 s, GCP 30 s, nginx 60 s. Die verbreiteten Cloudflare-„100 s" und die HAProxy-Defaults
|
|
sind Mythen und ließen sich nicht belegen. Etablierte Intervalle: Socket.IO 25 s, Phoenix 30 s,
|
|
Centrifugo 25 s.
|
|
- **Reconnect:** Praxiswerte reichen von 10 ms (Phoenix, ohne Jitter) bis 15 s (Ably). Gegen
|
|
Thundering Herd sind vier Ansätze belegt: Client-Jitter, server-gesteuerter Hinweis
|
|
(`Retry-After`, Discord Op 7), Draining und Accept-Rate-Limiting — mit Postmortems von Discord
|
|
(29.04.2026) und Slack (15.03.2021).
|
|
- **`RecvError::Lagged(n)`:** `n` = übersprungene Nachrichten, der Receiver bleibt brauchbar, der
|
|
Cursor springt auf die älteste noch vorhandene Nachricht. ⚠️ **„Lagged = Resync" ist nirgends
|
|
als offizielles Muster dokumentiert** — gängige Community-Praxis, aber ohne Primärquelle.
|
|
Belegbar sind nur die Bausteine (`resubscribe()`, `Receiver::len()` als Frühwarnung,
|
|
`mpsc::try_send` pro Client).
|
|
- **`tokio::sync::watch` passt auffällig gut zum Dumb Pipe** — „retains only the last sent value"
|
|
ist exakt die Semantik von „der Cursor der Conversation X ist jetzt N". Kein Lag-Fehler möglich.
|
|
→ *Fan-out-Architektur*.
|
|
- **`PgListener` belegt eine dedizierte Postgres-Connection außerhalb des Pools** (aktuell
|
|
`max_connections = 10`), reconnected aber selbst. `NOTIFY` ist transaktional. Neue Gefahr:
|
|
eine volle Notify-Queue lässt Transaktionen **beim Commit fehlschlagen**.
|
|
- **Bestandsaufnahme:** `axum = "0.8.9"` ist **ohne `ws`-Feature** eingebunden, es gibt keinen
|
|
Graceful Shutdown, und `Extension<Claims>` wird nirgends gelesen.
|