# 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):

```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://<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 `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.<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`)

```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`)

```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: "<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:**
```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="<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 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.<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.