From c54dbfe2a517c51158d43d489f45a67778e7fcf4 Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Thu, 20 Aug 2026 14:07:06 -0500 Subject: [PATCH] feat(element-calling): fix Element X discovery and harden federated calling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The element-calling feature only advertised the LiveKit focus via the well-known org.matrix.msc4143.rtc_foci file, and relied on STUN auto-detection for the public IP. Element X queries the MatrixRTC transports registry endpoint and fails with MISSING_MATRIX_RTC_TRANSPORT when it is absent, and blocked STUN egress silently left LiveKit advertising a private IP (call connects but no video across servers). - synapse: enable msc4143_enabled and advertise matrix_rtc.transports (MSC4519) with the site's element-calling URL, so Element X can discover the LiveKit focus instead of erroring out - livekit: determine the public IP to advertise at runtime — explicit pin, then HTTPS egress detection (api.ipify.org / checkip.amazonaws.com / ifconfig.me), then STUN fallback with a warning; reject non-routable results (private/loopback/CGNAT) - lk-jwt-service: append optional extra homeservers to LIVEKIT_FULL_ACCESS_HOMESERVERS via the new sovran_systemsOS.elementCalling.fullAccessHomeservers option - add sovran_systemsOS.elementCalling.externalIP option to pin the advertised public IP for multi-WAN/VPN setups - add element-calling-public-check.service: boot-time diagnostics for public DNS (via 1.1.1.1, bypassing local loopback overrides), JWT healthz through Caddy and via the public IP, and the transports endpoint — turns the silent -no media- failure into a visible error - add restartTriggers so livekit/lk-jwt-service pick up regenerated runtime configs on rebuild --- modules/core/roles.nix | 35 +++++++ modules/element-calling.nix | 180 +++++++++++++++++++++++++++++++++++- 2 files changed, 212 insertions(+), 3 deletions(-) diff --git a/modules/core/roles.nix b/modules/core/roles.nix index bd180e5..d89407d 100755 --- a/modules/core/roles.nix +++ b/modules/core/roles.nix @@ -79,6 +79,41 @@ }; }; + # ── Element Calling (video/audio) tuning ────────────────── + elementCalling = { + fullAccessHomeservers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "matrix.peer.example.com" ]; + description = '' + Additional Matrix server_names (beyond this server itself) that may + trigger LiveKit room creation on this server's SFU via lk-jwt-service. + + Not needed for the common federated setup: each participant's client + always obtains its token from its own homeserver's JWT service and + publishes to its own SFU, and the participant who starts a call + creates the room on their own SFU — the remote user merely joins + (joining does not require full access). + + Only set this for asymmetric cases: e.g. a peer homeserver that has + no focus of its own, or calls whose first participant lands on this + server's SFU but belongs to the peer. + ''; + }; + externalIP = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "203.0.113.10"; + description = '' + Optional pin: force LiveKit to advertise this public IPv4 in its + host/TURN ICE candidates. Not required in normal operation — the + module auto-detects the public IP at runtime (HTTPS egress + detection, falling back to STUN). Set it only to override a + mis-detected address (e.g. multi-WAN/VPN setups). + ''; + }; + }; + # ── Domain setup registry ───────────────────────────────── domainRequirements = lib.mkOption { type = lib.types.listOf (lib.types.submodule { diff --git a/modules/element-calling.nix b/modules/element-calling.nix index f078c90..fff4a8e 100755 --- a/modules/element-calling.nix +++ b/modules/element-calling.nix @@ -136,7 +136,7 @@ EOF unitConfig = { ConditionPathExists = "/var/lib/domains/element-calling"; }; - path = [ pkgs.coreutils pkgs.findutils pkgs.iproute2 pkgs.gawk ]; + path = [ pkgs.coreutils pkgs.findutils pkgs.iproute2 pkgs.gawk pkgs.curl ]; script = '' MATRIX=$(cat /var/lib/domains/matrix) @@ -188,7 +188,59 @@ EOF # rtc.interfaces.includes are only known at runtime, so they are # substituted here. The cert/key paths point at the LoadCredential-staged # copies under /run/credentials. - cat > /run/livekit/livekit.yaml </dev/null | tr -d '[:space:]') + [ -z "$CANDIDATE" ] && continue + # Keep only plausible IPv4 literals (rejects hostnames, IPv6, junk). + case "$CANDIDATE" in + *[!0-9.]*) continue ;; + *) PUBLIC_IP="$CANDIDATE" ;; + esac + break + done + fi + + # Reject non-routable addresses (loopback, private, link-local, CGNAT). + # A detected/pinned address like this must never be advertised. + if [ -n "$PUBLIC_IP" ] && printf '%s' "$PUBLIC_IP" | grep -qE \ + '^(0\.|127\.|10\.|100\.64\.|169\.254\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)'; then + echo "WARNING: public IP candidate '$PUBLIC_IP' is not routable; falling back to STUN auto-detection." >&2 + PUBLIC_IP="" + fi + + if [ -n "$PUBLIC_IP" ]; then + cat > /run/livekit/livekit.yaml < /run/livekit/livekit.yaml <&2 + fi + + cat >> /run/livekit/livekit.yaml < /run/lk-jwt-service/env </dev/null; dig +short AAAA "$ELEMENT_CALLING" @1.1.1.1 2>/dev/null; } | tr '\n' ' ' ) + if [ -z "$IPS" ]; then + echo "ERROR: $ELEMENT_CALLING has no public A/AAAA records (via 1.1.1.1). Remote peers cannot reach this LiveKit; calls will connect without media." >&2 + FAIL=1 + else + echo "Public DNS for $ELEMENT_CALLING: $IPS" + for IP in $IPS; do + case "$IP" in + 0.*|127.*|169.254.*|100.64.*|::1|fe80:*|fc*:*|fd*:*) + echo "ERROR: $ELEMENT_CALLING publicly resolves to $IP (loopback/link-local/CGNAT). Remote peers cannot reach it." >&2 + FAIL=1 ;; + esac + done + fi + + # 2) lk-jwt-service healthz through Caddy (validates the proxy chain). + if curl -fsS --max-time 10 "https://$ELEMENT_CALLING/livekit/jwt/healthz" >/dev/null 2>&1; then + echo "OK: https://$ELEMENT_CALLING/livekit/jwt/healthz responds" + else + echo "ERROR: https://$ELEMENT_CALLING/livekit/jwt/healthz not reachable through Caddy." >&2 + FAIL=1 + fi + + # 3) Same healthz via the first public IP (tests the full NAT path). + # NOTE: if this box is behind the same NAT you are testing through, + # routers without hairpin NAT will fail this step — the warning is + # then expected and harmless; verify from an external device instead. + if [ -n "$IPS" ]; then + PUBIP=$(echo "$IPS" | awk '{print $1}') + if curl -fsS --max-time 15 --resolve "$ELEMENT_CALLING:443:$PUBIP" "https://$ELEMENT_CALLING/livekit/jwt/healthz" >/dev/null 2>&1; then + echo "OK: healthz reachable via public IP $PUBIP (NAT path works)" + else + echo "WARNING: healthz NOT reachable via public IP $PUBIP — check router port-forwarding (443/TCP) and NAT hairpin. Expected if the router lacks hairpin NAT; verify from an external device." >&2 + fi + fi + + # 4) MatrixRTC transports registry (MSC4519) — required by Element X. + CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "https://$MATRIX/_matrix/client/unstable/org.matrix.msc4143/rtc/transports") + case "$CODE" in + 401|200) + echo "OK: MatrixRTC transports endpoint present (HTTP $CODE; auth required is expected)" ;; + 404) + echo "ERROR: /_matrix/client/unstable/org.matrix.msc4143/rtc/transports missing (HTTP 404) — Element X cannot discover calling. Enable msc4143_enabled and matrix_rtc.transports in Synapse." >&2 + FAIL=1 ;; + *) + echo "WARNING: transports endpoint returned HTTP $CODE" >&2 ;; + esac + + if [ "$FAIL" -eq 1 ]; then + echo "── Element Calling self-check FAILED — see errors above ──" >&2 + exit 1 + fi + echo "── Element Calling self-check passed ──" + ''; + }; + ####### SYNAPSE RUNTIME CONFIG (element-calling additions) ####### systemd.services.element-calling-synapse-config = { description = "Generate Synapse runtime config for Element Calling"; @@ -317,6 +480,7 @@ EOF path = [ pkgs.coreutils ]; script = '' MATRIX=$(cat /var/lib/domains/matrix) + ELEMENT_CALLING=$(cat /var/lib/domains/element-calling) mkdir -p /run/matrix-synapse @@ -326,7 +490,17 @@ public_baseurl: "https://$MATRIX" serve_server_wellknown: true experimental_features: msc3266_enabled: true + # MSC4143: enables the MatrixRTC transports registry endpoint + # (/_matrix/client/unstable/org.matrix.msc4143/rtc/transports, MSC4519). + # Element X requires this endpoint to discover the LiveKit focus; without it + # mobile clients fail with MISSING_MATRIX_RTC_TRANSPORT / cannot start calls. + msc4143_enabled: true msc4222_enabled: true +# MSC4519: advertise this site's LiveKit focus via the transports registry. +matrix_rtc: + transports: + - type: livekit + livekit_service_url: "https://$ELEMENT_CALLING/livekit/jwt" max_event_delay_duration: "24h" rc_message: per_second: 0.5