Files
Sovran_SystemsOS/modules/element-calling.nix
T
Sovran Systems 8baefe1bfd fix(element-calling): enable LiveKit embedded TURN with runtime matrix domain + cert
LiveKit was exiting cleanly with "TURN domain required" because turn.enabled
was set in the build-time config but turn.domain was never provided to the
process (the old livekit-runtime-config.service wrote a YAML that nothing
read). A clean exit (status 0) meant Restart=on-failure never restarted it,
so the Hub reported the service as Inactive.

This replaces the dead runtime-config oneshot with livekit-turn-setup.service,
which at runtime:
  - reads the matrix domain from /var/lib/domains/matrix (no hardcoding)
  - copies Caddy's already-issued matrix cert/key into /var/lib/livekit
  - generates a complete LiveKit config (incl. turn.domain + TLS cert/key)
    at /run/livekit/livekit.yaml

The livekit.service ExecStart is overridden to load that runtime config
(mirroring the existing Caddy ExecStart override pattern in
modules/core/caddy.nix), since turn.domain is only known at runtime. The cert
is delivered via LoadCredential so it is readable under DynamicUser=true
without weakening the sandbox.

Also aligns the RTC media port range (rtc.port_range_start/end = 30000-40000)
so it matches the forwarded ports, and drops the now-redundant manual
30000-40000 firewall ranges (covered by services.livekit settings/openFirewall).
2026-06-23 20:03:26 -05:00

278 lines
9.1 KiB
Nix
Executable File

{ config, pkgs, lib, ... }:
let
livekitKeyFile = "/var/lib/livekit/livekit_keyFile";
in
lib.mkIf config.sovran_systemsOS.features.element-calling {
####### LIVEKIT KEY GENERATION #######
systemd.tmpfiles.rules = [
"d /var/lib/livekit 0750 root root -"
];
systemd.services.livekit-key-setup = {
description = "Generate LiveKit key file if missing";
wantedBy = [ "multi-user.target" ];
before = [ "livekit.service" "lk-jwt-service.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [ pkgs.openssl ];
script = ''
if [ ! -f ${livekitKeyFile} ]; then
API_KEY="devkey_$(openssl rand -hex 16)"
API_SECRET="$(openssl rand -base64 36 | tr -d '\n')"
echo "$API_KEY: $API_SECRET" > ${livekitKeyFile}
chmod 600 ${livekitKeyFile}
echo "LiveKit key file generated at ${livekitKeyFile}"
else
echo "LiveKit key file already exists, skipping generation"
fi
'';
};
####### ENSURE SERVICES START AFTER KEY EXISTS #######
systemd.services.livekit.after = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
systemd.services.livekit.wants = [ "livekit-key-setup.service" "livekit-turn-setup.service" ];
systemd.services.lk-jwt-service.after = [ "livekit-key-setup.service" ];
systemd.services.lk-jwt-service.wants = [ "livekit-key-setup.service" ];
####### CADDY SNIPPET #######
systemd.services.element-calling-caddy-config = {
description = "Generate Element Calling Caddy config snippet";
before = [ "caddy-generate-config.service" ];
requiredBy = [ "caddy-generate-config.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "/var/lib/domains/element-calling";
};
path = [ pkgs.coreutils ];
script = ''
MATRIX=$(cat /var/lib/domains/matrix)
ELEMENT_CALLING=$(cat /var/lib/domains/element-calling)
mkdir -p /run/caddy
cat > /run/caddy/element-calling.snippet <<EOF
$MATRIX {
reverse_proxy /_matrix/* http://localhost:8008
reverse_proxy /_synapse/client/* http://localhost:8008
header /.well-known/matrix/* Content-Type "application/json"
header /.well-known/matrix/* Access-Control-Allow-Origin "*"
header /.well-known/matrix/* Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
header /.well-known/matrix/* Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization"
respond /.well-known/matrix/client \`{ "m.homeserver": {"base_url": "https://$MATRIX" }, "org.matrix.msc4143.rtc_foci": [{ "type":"livekit", "livekit_service_url":"https://$ELEMENT_CALLING/livekit/jwt" }] }\`
respond /.well-known/matrix/server \`{"m.server":"$MATRIX:443"}\`
}
$ELEMENT_CALLING {
handle /livekit/jwt/sfu/get {
uri strip_prefix /livekit/jwt
reverse_proxy [::1]:8073 {
header_up Host {host}
header_up X-Forwarded-Server {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
}
}
handle {
reverse_proxy localhost:7880
}
}
EOF
'';
};
####### LIVEKIT TURN SETUP (runtime cert + config) #######
# Replaces the old dead livekit-runtime-config.service. At runtime this:
# * reads the matrix domain from /var/lib/domains/matrix (never hardcoded)
# * copies Caddy's already-issued matrix cert/key into /var/lib/livekit
# so LoadCredential can stage them for the (DynamicUser) livekit unit
# * writes a complete LiveKit config (with turn.domain substituted) that the
# overridden ExecStart loads.
systemd.services.livekit-turn-setup = {
description = "Stage TURN cert and generate LiveKit runtime config from domain files";
after = [ "caddy.service" "livekit-key-setup.service" ];
before = [ "livekit.service" ];
requiredBy = [ "livekit.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "/var/lib/domains/element-calling";
};
path = [ pkgs.coreutils pkgs.findutils ];
script = ''
MATRIX=$(cat /var/lib/domains/matrix)
mkdir -p /run/livekit
# Copy Caddy's already-issued matrix cert/key into LiveKit's state dir.
# The ACME CA hostname directory can vary, so glob for the domain dir.
CRT=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.crt" | head -n1)
KEY=$(find /var/lib/caddy -path "*/$MATRIX/$MATRIX.key" | head -n1)
cp "$CRT" /var/lib/livekit/turn.crt
cp "$KEY" /var/lib/livekit/turn.key
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
# Generate the full LiveKit config the daemon will load. turn.domain is
# only known at runtime, so it is substituted here. The cert/key paths
# point at the LoadCredential-staged copies under /run/credentials.
cat > /run/livekit/livekit.yaml <<EOF
port: 7880
rtc:
use_external_ip: true
udp_port: 7882
port_range_start: 30000
port_range_end: 40000
room:
auto_create: false
turn:
enabled: true
domain: $MATRIX
tls_port: 5349
udp_port: 3478
cert_file: /run/credentials/livekit.service/turn-cert
key_file: /run/credentials/livekit.service/turn-key
EOF
chmod 640 /run/livekit/livekit.yaml
'';
};
####### LIVEKIT SERVICE #######
services.livekit = {
enable = true;
openFirewall = true;
keyFile = livekitKeyFile;
settings = {
rtc.use_external_ip = true;
rtc.udp_port = 7882;
rtc.port_range_start = 30000;
rtc.port_range_end = 40000;
room.auto_create = false;
turn = {
enabled = true;
tls_port = 5349;
udp_port = 3478;
};
};
};
# Override ExecStart to load the runtime-generated config (which carries the
# runtime-only turn.domain), mirroring the Caddy ExecStart override pattern in
# modules/core/caddy.nix. Deliver the TURN cert/key via LoadCredential so they
# are readable under the upstream unit's DynamicUser=true sandbox without
# weakening it. Everything else about the standard unit is left intact.
systemd.services.livekit.serviceConfig.ExecStart = lib.mkForce [
""
"${pkgs.livekit}/bin/livekit-server --config /run/livekit/livekit.yaml --key-file /run/credentials/livekit.service/livekit-secrets"
];
systemd.services.livekit.serviceConfig.LoadCredential = [
"livekit-secrets:${livekitKeyFile}"
"turn-cert:/var/lib/livekit/turn.crt"
"turn-key:/var/lib/livekit/turn.key"
];
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
####### JWT SERVICE RUNTIME CONFIG #######
systemd.services.lk-jwt-service-runtime-config = {
description = "Generate lk-jwt-service runtime config from domain files";
before = [ "lk-jwt-service.service" ];
after = [ "livekit-key-setup.service" ];
requiredBy = [ "lk-jwt-service.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "/var/lib/domains/element-calling";
};
path = [ pkgs.coreutils ];
script = ''
ELEMENT_CALLING=$(cat /var/lib/domains/element-calling)
mkdir -p /run/lk-jwt-service
cat > /run/lk-jwt-service/env <<EOF
LIVEKIT_URL=wss://$ELEMENT_CALLING
EOF
chmod 640 /run/lk-jwt-service/env
'';
};
####### JWT SERVICE #######
services.lk-jwt-service = {
enable = true;
port = 8073;
keyFile = livekitKeyFile;
livekitUrl = "wss://placeholder.local";
};
systemd.services.lk-jwt-service.serviceConfig.EnvironmentFile = [
"/run/lk-jwt-service/env"
];
####### SYNAPSE RUNTIME CONFIG (element-calling additions) #######
systemd.services.element-calling-synapse-config = {
description = "Generate Synapse runtime config for Element Calling";
before = [ "matrix-synapse.service" ];
requiredBy = [ "matrix-synapse.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "/var/lib/domains/element-calling";
};
path = [ pkgs.coreutils ];
script = ''
MATRIX=$(cat /var/lib/domains/matrix)
mkdir -p /run/matrix-synapse
cat > /run/matrix-synapse/element-calling-config.yaml <<EOF
server_name: "$MATRIX"
public_baseurl: "https://$MATRIX"
serve_server_wellknown: true
experimental_features:
msc3266_enabled: true
msc4222_enabled: true
max_event_delay_duration: "24h"
rc_message:
per_second: 0.5
burst_count: 30
rc_delayed_event_mgmt:
per_second: 1
burst_count: 20
EOF
chown matrix-synapse:matrix-synapse /run/matrix-synapse/element-calling-config.yaml
chmod 640 /run/matrix-synapse/element-calling-config.yaml
'';
};
####### SYNAPSE OVERRIDES (element-calling needs) #######
services.matrix-synapse.extraConfigFiles = [
"/run/matrix-synapse/element-calling-config.yaml"
];
sovran_systemsOS.domainRequirements = [
{ name = "element-calling"; label = "Element Calling (LiveKit)"; example = "call.yourdomain.com"; }
];
}