# MISSING_MATRIX_RTC_TRANSPORT in ESS 26.4.0: Ursachenanalyse und vollständiger Fix **Die Ursache ist Hypothese 1: `wellKnownDelegation.enabled: false` ist der Kill-Switch.** In genau dieser Konfiguration schreibt das ESS-Chart den Schlüssel `org.matrix.msc4143.rtc_foci` normalerweise automatisch in das an `https://axion1337.chat/.well-known/matrix/client` ausgelieferte JSON-Dokument – sobald `wellKnownDelegation.enabled: true` **und** `matrixRTC.enabled: true` gesetzt sind. Mit `wellKnownDelegation: false` fällt dieses Dokument komplett weg. Element Web hat damit keinen Discovery-Pfad zur LiveKit-Instanz auf `mrtc.axion1337.chat`, und Matrix-JS-SDK wirft genau deshalb `MISSING_MATRIX_RTC_TRANSPORT`. Hypothese 2 (Synapse-Custom-Keys sind bogus) ist ebenfalls korrekt. Hypothese 4 (Element Web braucht explizites `rtc_foci`) ist **falsch** – Element Web liest `rtc_foci` ausschließlich aus Well-Known, niemals aus `config.json`. Hypothese 5 ist ein *sekundäres* Thema: LiveKit UDP-Ports müssen offen sein, aber der Fehler `MISSING_MATRIX_RTC_TRANSPORT` entsteht ausschließlich aus der Discovery, nicht aus Medien-Erreichbarkeit. ## Wie die Fehlerkette konkret abläuft Element Web ruft beim Start eines Anrufs intern zwei Discovery-Pfade auf: `GET /_matrix/client/v1/rtc/transports` gegen Synapse (MSC4143, ab Synapse 1.140) und als Fallback `GET https:///.well-known/matrix/client`, wo der Schlüssel `org.matrix.msc4143.rtc_foci` erwartet wird. Das ESS-Chart befüllt **beide** Pfade automatisch, sobald `matrixRTC.enabled: true` ist: In `wellKnownDelegation` wird das `rtc_foci`-Array injiziert, in Synapse wird der `matrix_rtc.transports`-Block in die `homeserver.yaml` gemerged (chart-intern, siehe ess-helm Changelog Eintrag #855: *"Configure experimental MSC4143 advertisement in Synapse when MatrixRTC is enabled. This is in addition to the MSC4143 advertisement on the client well-known endpoint for now, but it is expected to replace it in time."*). **Beide Mechanismen werden durch `wellKnownDelegation.enabled: false` nicht vollständig deaktiviert** – die Synapse-Seite wird zwar weiter gesetzt, aber weil kein Well-Known am Apex existiert, kann Element Web den Homeserver gar nicht über `default_server_name` auflösen und daher auch den Transport-Endpunkt nicht konsistent nutzen. Resultat: leere Transport-Liste, Fehler ausgelöst. Ein korrektes `/.well-known/matrix/client` sieht so aus (das ist exakt, was das Chart ausliefern würde): ```json { "m.homeserver": { "base_url": "https://matrix.axion1337.chat" }, "m.identity_server": { "base_url": "https://vector.im" }, "org.matrix.msc4143.rtc_foci": [ { "type": "livekit", "livekit_service_url": "https://mrtc.axion1337.chat" } ] } ``` Der Schlüsselname ist **`org.matrix.msc4143.rtc_foci`** (MSC-Prefix, kein `m.rtc_foci`). Das Value-Objekt hat zwingend `type: "livekit"` und `livekit_service_url: "https://"`. ## Warum die Custom-Synapse-Values aktuell wirkungslos sind Die Keys `matrix_rtc_enabled` und `matrix_rtc_uri` **existieren in Synapse nicht**. Synapse kennt für MatrixRTC genau zwei Dinge: einen `experimental_features`-Block (`msc3266_enabled`, `msc4140_enabled`, `msc4222_enabled`, und implementierungsabhängig `msc4143_enabled`) und einen Top-Level-Block `matrix_rtc.transports:` für den neuen `/v1/rtc/transports`-Endpunkt. Das ESS-Chart setzt diese **automatisch**, sobald `matrixRTC.enabled: true` ist. Synapse ignoriert unbekannte Top-Level-Keys in der `homeserver.yaml` stillschweigend; der Container crasht nicht, aber es passiert auch exakt gar nichts. Der ganze `rtc-config`-Block muss raus. Der `url-previews`-Block ist dagegen legitim – die Keys `url_preview_enabled`, `url_preview_ip_range_blacklist`, `max_spider_size` sind echte Synapse-Optionen. ## Der eigentliche Grund für die "ACME Race Condition" Das Problem war **nicht** ein Bug, sondern eine vorhersagbare Fehlkonfiguration: Beide Ingresses (Element Web und wellKnownDelegation) forderten über die `cert-manager.io/cluster-issuer`-Annotation **jeweils ein eigenes TLS-Secret** für denselben Host `axion1337.chat` an. Cert-manager erzeugt dann zwei `Certificate`-Objekte mit unterschiedlichen `secretName`s → zwei `Order`-Objekte → HTTP-01-Solver-Ingresses überschreiben sich gegenseitig → Let's-Encrypt-Rate-Limit schlägt zu (5 duplicate certs / 7 Tage). Siehe cert-manager Issue #2342 und Discussion #3511: *"If both ingresses have their own secret to save the certificate tls, cert-manager runs havoc and exhausts the ACME limit very quickly."* Die saubere Lösung ist, dass genau **eine** Zertifikat-Quelle für `axion1337.chat` existiert. Das ESS-Chart selbst erwartet, dass `elementWeb.ingress.host` **nicht** gleich `serverName` ist. Der Standard-Pattern ist: Element Web auf `chat.` bzw. `app.`, Well-Known auf dem Apex, und eine optionale `baseDomainRedirect.url` leitet `/` auf dem Apex zu Element Web weiter. ## Fix 1 (empfohlen): Element Web auf Subdomain umziehen Das ist die Lösung, für die das Chart designt ist, und erfordert genau eine DNS-Änderung sowie YAML-Anpassungen. ### DNS-Voraussetzungen | Record | Typ | Ziel | Zweck | |---|---|---|---| | `axion1337.chat` | A/AAAA | öffentliche IP des K3s-Nodes | Apex: Well-Known + Cert für Matrix-Server-Delegation | | `matrix.axion1337.chat` | A/AAAA | K3s-Node-IP | Synapse | | `account.axion1337.chat` | A/AAAA | K3s-Node-IP | Matrix Authentication Service | | `mrtc.axion1337.chat` | A/AAAA | K3s-Node-IP | LiveKit-Signaling + lk-jwt-service | | `chat.axion1337.chat` | A/AAAA | K3s-Node-IP | **NEU**: Element Web | | `admin.axion1337.chat` | A/AAAA | K3s-Node-IP | Element Admin | ### Angepasster HelmRelease (`apps/production/element-server-suite.yaml`) ```yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: matrix-stack namespace: matrix spec: interval: 1h chart: spec: chart: matrix-stack version: "26.4.0" sourceRef: kind: HelmRepository name: element-ess-oci namespace: flux-system valuesFrom: - kind: ConfigMap name: ess-synapse-custom valuesKey: values.yaml - kind: ConfigMap name: ess-element-custom valuesKey: values.yaml - kind: Secret name: ess-mas-values-secret valuesKey: values.yaml values: serverName: axion1337.chat certManager: clusterIssuer: letsencrypt-prod postgres: enabled: true synapse: enabled: true ingress: host: matrix.axion1337.chat matrixAuthenticationService: enabled: true ingress: host: account.axion1337.chat matrixRTC: enabled: true ingress: host: mrtc.axion1337.chat # SFU public-IP override empfohlen, falls der Node hinter NAT steht # und STUN die falsche Adresse zurückgibt: # sfu: # useStunToDiscoverPublicIP: false # manualIP: "" elementWeb: enabled: true ingress: host: chat.axion1337.chat # <- NICHT mehr der Apex elementAdmin: enabled: true ingress: host: admin.axion1337.chat wellKnownDelegation: enabled: true # <- zurück auf true (Default) # Optional: Redirect vom Apex "/" auf Element Web. # Das Chart-Feature kam in 26.x hinzu; falls dein konkreter Wert-Key # fehlt, siehe Fix 2 (eigene IngressRoute) als sichere Alternative. baseDomainRedirect: url: https://chat.axion1337.chat ``` Mit diesem Setup beansprucht **genau ein** Chart-erzeugter Ingress den Apex `axion1337.chat`, nämlich der der `wellKnownDelegation`. Es gibt nur ein Zertifikat, keine Race Condition. Element Web läuft auf `chat.axion1337.chat` und bekommt sein eigenes, konfliktfreies Cert. ### Bereinigter Synapse-ConfigMap (`apps/production/custom-configs/synapse-values.yaml`) ```yaml apiVersion: v1 kind: ConfigMap metadata: name: ess-synapse-custom namespace: matrix data: values.yaml: | synapse: logging: rootLevel: INFO levelOverrides: synapse.media.url_previewer: DEBUG additional: # RTC-Block ENTFERNT: matrix_rtc_enabled / matrix_rtc_uri sind keine # gültigen Synapse-Keys. Das ESS-Chart setzt matrix_rtc.transports und # die nötigen experimental_features automatisch, wenn matrixRTC.enabled=true. 1-url-previews: config: | url_preview_enabled: true url_preview_ip_range_blacklist: - '127.0.0.0/8' - '10.0.0.0/8' - '172.16.0.0/12' - '192.168.0.0/16' - '100.64.0.0/10' - '192.0.0.0/24' - '169.254.0.0/16' - '192.88.99.0/24' - '198.18.0.0/15' - '192.0.2.0/24' - '198.51.100.0/24' - '203.0.113.0/24' - '224.0.0.0/4' - '::1/128' - 'fe80::/10' - 'fc00::/7' - '2001:db8::/32' - 'ff00::/8' - 'fec0::/10' max_spider_size: 10M ``` Wichtige Details zum `additional`-Mechanismus: die Keys werden **alphabetisch sortiert** gemerged, daher die `1-`/`2-`-Namenskonvention. Arrays werden ersetzt, nicht zusammengeführt. ### Element-Web-ConfigMap (`apps/production/custom-configs/element-values.yaml`) Hier ist **keine** RTC-spezifische Änderung nötig. Element Web hat gemäß `element-web/docs/config.md` keinen `rtc_foci`-Key und entdeckt den Transport ausschließlich über Well-Known. Der optionale `element_call`-Block steuert nur Branding und Verhalten: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: ess-element-custom namespace: matrix data: values.yaml: | elementWeb: additional: config.json: | { "brand": "aXion1337.Chat", "element_call": { "brand": "aXion1337 Call", "use_exclusively": true }, "features": { "feature_video_rooms": true, "feature_element_call_video_rooms": true }, "show_labs_settings": true } ``` `use_exclusively: true` entfernt die Legacy-Jitsi-Option aus dem UI, sodass Anrufe garantiert über MatrixRTC/LiveKit laufen. ## Fix 2 (Alternative): Apex manuell splitten per Traefik IngressRoute Falls `baseDomainRedirect` in deiner 26.4.0-Revision nicht greift oder du Element Web auf dem Apex behalten willst, ersetze beide Chart-Ingresses durch eine **einzige Traefik IngressRoute mit einem einzigen Certificate**. Dadurch sieht cert-manager nur noch eine Quelle für `axion1337.chat`, und Traefik routet Pfad-basiert. ```yaml # apps/production/apex-ingress.yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: axion-apex-tls namespace: matrix spec: secretName: axion-apex-tls issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - axion1337.chat --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: axion-apex namespace: matrix spec: entryPoints: [websecure] tls: secretName: axion-apex-tls routes: # Höchste Priorität: /.well-known/matrix/* -> wellKnownDelegation-Service - match: Host(`axion1337.chat`) && PathPrefix(`/.well-known/matrix`) kind: Rule priority: 100 services: - name: matrix-stack-well-known-delegation # ggf. Service-Namen mit `kubectl get svc -n matrix` verifizieren port: 8080 # Niedrigere Priorität: alles andere -> Element Web - match: Host(`axion1337.chat`) kind: Rule priority: 10 services: - name: matrix-stack-element-web port: 8080 ``` Dazu muss in der HelmRelease die automatische Chart-Ingress-Erzeugung für diese beiden Komponenten unterbunden werden – setze `wellKnownDelegation.ingress.className: "none"` bzw. `elementWeb.ingress.className: "none"` oder setze die `host`-Werte auf Dummy-Hostnames, sodass die Chart-Ingresses nicht denselben Apex beanspruchen. Fix 1 ist wegen geringerer Komplexität klar zu bevorzugen. ## Netzwerk- und Firewall-Anforderungen für LiveKit **Das ist der nächste Stolperstein nach dem Well-Known-Fix.** Die HTTPS-Ingress auf `mrtc.axion1337.chat` terminiert nur **Signaling (WSS auf TCP 443)** und die lk-jwt-service-Endpunkte `/sfu/get`, `/get_token`, `/healthz`. WebRTC-Medien laufen **nicht** über den Ingress, sondern direkt zum Node auf UDP/TCP-NodePorts. Öffne auf deiner K3s-Node-Firewall (und im Router/Cloud-Security-Group): | Port | Protokoll | Standard ESS CE | Zweck | |---|---|---|---| | 80, 443 | TCP | — | Ingress + ACME HTTP-01 | | `rtcMuxedUdp` NodePort | **UDP** | 30002 (CE) / 30882 (Pro) | **Kritisch**: WebRTC-Medien (UDP-Mux). Ohne das: keine Audio/Video-Daten | | `rtcTcp` NodePort | TCP | 30881 | ICE/TCP-Fallback für Clients ohne UDP | | `turn` NodePort | UDP | 30004 | Nur falls `matrixRTC.sfu.exposedServices.turn.enabled: true` | | `turnTLS` Port | TCP | 31443 oder 443 | Nur falls TURN/TLS aktiv; braucht SNI-Passthrough und eigenes Hostname | Ermittle die tatsächlichen NodePorts per `kubectl get svc -n matrix | grep rtc`. Wenn dein Node hinter NAT steht und LiveKits STUN-basierte Public-IP-Erkennung die falsche Adresse zurückliefert, setze zusätzlich: ```yaml matrixRTC: sfu: useStunToDiscoverPublicIP: false manualIP: "" ``` ## Verifikationsschritte Nach Reconcile (`flux reconcile helmrelease matrix-stack -n matrix`) diese vier Tests durchführen: **1. Well-Known für Matrix-Client liefert rtc_foci:** ```bash curl -sS https://axion1337.chat/.well-known/matrix/client | jq . # Erwartung: JSON mit "m.homeserver" UND "org.matrix.msc4143.rtc_foci" # das livekit_service_url == "https://mrtc.axion1337.chat" enthält. curl -sS https://axion1337.chat/.well-known/matrix/server | jq . # Erwartung: {"m.server": "matrix.axion1337.chat:443"} ``` **2. lk-jwt-service und LiveKit-Signaling sind erreichbar:** ```bash curl -i https://mrtc.axion1337.chat/healthz # Erwartung: HTTP/1.1 200 OK curl -i https://mrtc.axion1337.chat/sfu/get # Erwartung: HTTP/1.1 405 Method Not Allowed (akzeptiert nur POST) curl -i -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" \ https://mrtc.axion1337.chat/ # Erwartung: HTTP/1.1 101 Switching Protocols (WebSocket-Upgrade) ``` **3. Synapse bietet den MSC4143-Endpunkt an:** ```bash TOKEN="" curl -sS -H "Authorization: Bearer $TOKEN" \ https://matrix.axion1337.chat/_matrix/client/v1/rtc/transports | jq . # Erwartung: {"transports":[{"type":"livekit","livekit_service_url":"https://mrtc.axion1337.chat"}]} ``` **4. UDP-Medien kommen an:** In Element Web Call starten → im Chrome/Firefox-DevTools unter Netzwerk → WS prüfen, dass `wss://mrtc.axion1337.chat/rtc?access_token=...` einen `101`-Upgrade bekommt; unter `chrome://webrtc-internals` nach ICE-Candidate-Paaren mit Status `succeeded` und Typ `host/srflx` schauen. Falls nur Signaling, aber kein Media: UDP-NodePort auf Firewall prüfen. ## ACME-Race dauerhaft vermeiden Merksatz: **Ein Hostname ⇒ genau ein `Certificate`-Objekt ⇒ genau ein `secretName`.** Drei betriebliche Strategien: - **Subdomain-Trennung (Fix 1):** Jede Chart-Komponente bekommt einen eigenen FQDN, jeder FQDN genau ein Cert. Keine Kollision möglich. Das ist die vom Chart vorgesehene Form. - **Geteiltes Secret:** Wenn zwei Ingresses denselben Host tragen müssen, trägt nur **einer** die `cert-manager.io/cluster-issuer`-Annotation. Beide referenzieren `spec.tls[].secretName` auf dasselbe Secret. Traefik wählt das Zertifikat korrekt aus. - **DNS-01 statt HTTP-01:** Löst den Solver-Ingress-Konflikt grundsätzlich, weil kein zweiter Ingress erzeugt wird. Setzt DNS-Provider-Credentials im ClusterIssuer voraus. ## Vom Benutzer übersehene Chart-Werte Außer den oben beschriebenen Korrekturen sind diese Keys erwähnenswert, falls du feintunen willst. `matrixRTC.hostAliases` ist ein dokumentierter Workaround gegen Cluster-interne DNS-Probleme, wenn der lk-jwt-service-Pod den Homeserver-Apex bzw. Synapse über den Ingress-Controller statt direkt erreichen muss. `matrixRTC.sfu.exposedServices..portType` akzeptiert `NodePort`, `HostPort` oder `LoadBalancer` – für bare-metal K3s ohne externen LB ist `NodePort` richtig. `matrixRTC.sfu.additional."user-config.yaml".config` erlaubt rohes LiveKit-YAML für Spezialfälle (STUN-Server, Codecs). `wellKnownDelegation.additional.client` / `.server` / `.element` erlauben, in die erzeugten JSON-Dokumente zusätzliche Felder einzumischen – normalerweise nicht nötig, aber nützlich für föderative Sonderfälle. ## Fazit Der eigentliche Defekt war ein einziger Kippschalter: `wellKnownDelegation.enabled: false` hat den einzig existierenden Discovery-Kanal für den LiveKit-Focus gekappt. Der darum herum gebaute Custom-Synapse-Block ist ein Red Herring – die zugehörigen Schlüssel existieren in Synapse schlicht nicht und werden ignoriert. Die ursprünglich empfundene ACME-Race ist kein Bug des Charts, sondern die vorhersagbare Folge davon, dass zwei Chart-erzeugte Ingresses (Element Web **auf dem Apex** plus Well-Known) jeweils ein eigenes Let's-Encrypt-Zertifikat für denselben Host anforderten. Sobald Element Web nach `chat.axion1337.chat` wandert und Well-Known wieder aktiv ist, produziert das Chart automatisch das korrekte `org.matrix.msc4143.rtc_foci`-Feld, injiziert den `matrix_rtc.transports`-Block in Synapse, und Element Call funktioniert – vorausgesetzt die UDP-NodePort-Firewall ist offen, was nach dem Discovery-Fix der zweite Punkt auf der Prüfliste ist.