18 KiB
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://<serverName>/.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):
{
"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://<matrixRTC.ingress.host>".
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 secretNames → 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.<apex> bzw. app.<apex>, 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)
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: "<public-IP-of-node>"
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)
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:
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.
# 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:
matrixRTC:
sfu:
useStunToDiscoverPublicIP: false
manualIP: "<deine-öffentliche-IP>"
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:
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:
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:
TOKEN="<gültiges-access-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 referenzierenspec.tls[].secretNameauf 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.<name>.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.