From 81ab3b22805507a35f5e5c87d434e2ddb61d25b4 Mon Sep 17 00:00:00 2001 From: Sovran_SystemsOS Date: Tue, 1 Sep 2026 17:02:03 +0000 Subject: [PATCH] element-calling: fix Wi-Fi calls and tighten media/TURN ports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of 'calls fail on Wi-Fi but work on mobile data': LiveKit only advertised the public/WAN IP (rtc.node_ip), so LAN clients had to hairpin through the router for media. Fixes and cleanup: - rtc.advertise_internal_ip: true — also advertise the primary interface's LAN host candidate, so Wi-Fi callers connect directly (no hairpin). - Drop rtc.port_range_start/end (30000-40000) and keep the single UDP mux (udp_port: 7882). In LiveKit 1.13.x the range takes precedence over udp_port, so media was actually spread over 10000 ports. - Drop turn.tls_port: 5349 — LiveKit advertises turns::443 to clients regardless of tls_port, so a 5349 TURN/TLS listener was unreachable dead config (and needless attack surface). - Pin TURN relay allocation to 40000-40099 (disjoint from the media mux) and open/forward that range; the old default overlapped RTC media. - turn.allow_restricted_peer_cidrs with the LAN subnet derived from the primary interface: without it the relay refuses to deliver to the private LAN host candidate and its final hop would fall back to WAN hairpin. - Update Hub port guidance (server.py) to the new list. --- app/sovran_systemsos_web/server.py | 18 ++++++------ modules/element-calling.nix | 45 +++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index d5b7b32..9931f53 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -291,11 +291,10 @@ FEATURE_REGISTRY = [ "port_requirements": [ {"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"}, {"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"}, - {"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"}, - {"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"}, - {"port": "5349", "protocol": "TCP", "description": "TURN over TLS"}, - {"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"}, - {"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"}, + {"port": "7881", "protocol": "TCP", "description": "WebRTC media (TCP fallback)"}, + {"port": "7882", "protocol": "UDP", "description": "WebRTC media (UDP)"}, + {"port": "3478", "protocol": "UDP", "description": "TURN relay + STUN"}, + {"port": "40000-40099", "protocol": "UDP", "description": "TURN relay (WebRTC media)"}, ], }, { @@ -395,11 +394,10 @@ FEATURE_SERVICE_MAP = { # Port requirements for service tiles (keyed by unit name or icon) _PORTS_ELEMENT_CALLING = [ - {"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"}, - {"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"}, - {"port": "5349", "protocol": "TCP", "description": "TURN over TLS"}, - {"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"}, - {"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"}, + {"port": "7881", "protocol": "TCP", "description": "WebRTC media (TCP fallback)"}, + {"port": "7882", "protocol": "UDP", "description": "WebRTC media (UDP)"}, + {"port": "3478", "protocol": "UDP", "description": "TURN relay + STUN"}, + {"port": "40000-40099", "protocol": "UDP", "description": "TURN relay (WebRTC media)"}, ] # Units whose port requirements exist purely so the user can forward them in diff --git a/modules/element-calling.nix b/modules/element-calling.nix index 8b3efff..bd01f57 100755 --- a/modules/element-calling.nix +++ b/modules/element-calling.nix @@ -185,6 +185,14 @@ EOF fi echo "Detected primary network interface: $IFACE" + # Derive the LAN subnet this box sits on so the embedded TURN relay + # is allowed to hand media to LiveKit's LAN host candidate (see the + # allow_restricted_peer_cidrs block below). Computed from the primary + # interface's own address, so it always matches the subnet the LAN + # clients (phones on Wi-Fi) actually live on. + LAN_CIDR=$(ip -4 -o addr show dev "$IFACE" | awk '{print $4}' | grep -vE '^(127\.|169\.254\.)' | head -n1 | python3 -c 'import sys, ipaddress; s = sys.stdin.read().strip(); print(str(ipaddress.ip_network(s, strict=False)) if s else "")' 2>/dev/null) + echo "Derived LAN CIDR for TURN relay: ${LAN_CIDR:-}" + # Generate the full LiveKit config the daemon will load. turn.domain and # rtc.interfaces.includes are only known at runtime, so they are # substituted here. The cert/key paths point at the LoadCredential-staged @@ -229,10 +237,9 @@ port: 7880 rtc: use_external_ip: false node_ip: $PUBLIC_IP + advertise_internal_ip: true tcp_port: 7881 udp_port: 7882 - port_range_start: 30000 - port_range_end: 40000 interfaces: includes: - $IFACE @@ -244,10 +251,9 @@ port: 7880 rtc: use_external_ip: true skip_external_ip_validation: true + advertise_internal_ip: true tcp_port: 7881 udp_port: 7882 - port_range_start: 30000 - port_range_end: 40000 interfaces: includes: - $IFACE @@ -264,16 +270,37 @@ EOF # vhost (/livekit/jwt/sfu_webhook → 8073). LK_KEY=$(cut -d: -f1 < ${livekitKeyFile} | tr -d '[:space:]') + # TURN/TLS is intentionally not configured (no tls_port): LiveKit + # advertises turns::443 to clients regardless of tls_port, so + # a 5349 TURN/TLS listener would be unreachable and only adds attack + # surface. The staged cert/key stay for a future TURN/TLS-on-443 + # (Caddy layer4 SNI) setup. cat >> /run/livekit/livekit.yaml <> /run/livekit/livekit.yaml <> /run/livekit/livekit.yaml <