/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ package configgen import ( "encoding/json" "fmt" "path/filepath" "strings" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/secrets" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/state" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/util" ) func GenerateLiveKitYAML(st *state.BootstrapState, sec *secrets.Secrets, redisAddr string) string { var webhookBlock string if len(st.Webhooks) > 0 { var urls []string for _, u := range st.Webhooks { urls = append(urls, fmt.Sprintf(" - '%s'", u)) } webhookBlock = fmt.Sprintf(`webhook: api_key: '%s' urls: %s `, sec.LiveKitAPIKey, strings.Join(urls, "\n")) } return fmt.Sprintf(`port: %d bind_addresses: - "127.0.0.1" log_level: info rtc: tcp_port: %d port_range_start: %d port_range_end: %d use_external_ip: true turn_servers: - host: "%s" port: 443 protocol: tls username: "%s" credential: "%s" - host: "%s" port: %d protocol: udp username: "%s" credential: "%s" redis: address: "%s" username: "" password: "%s" db: 0 use_tls: false keys: "%s": "%s" %s`, st.Ports.LiveKitHTTPLocal, st.Ports.LiveKitRTCTCP, st.Ports.LiveKitRTCUDPStart, st.Ports.LiveKitRTCUDPEnd, st.Domains.TURN, sec.TURNUsername, sec.TURNPassword, st.Domains.TURN, st.Ports.TURNListenPort, sec.TURNUsername, sec.TURNPassword, redisAddr, sec.KVPassword, sec.LiveKitAPIKey, sec.LiveKitAPISecret, strings.TrimSpace(webhookBlock), ) } func GenerateKVConf(sec *secrets.Secrets, bindHost string, port int, dataDir string) string { return fmt.Sprintf(`bind %s protected-mode yes port %d tcp-backlog 511 timeout 0 tcp-keepalive 300 daemonize no supervised no dir %s dbfilename dump.rdb appendonly yes appendfilename "appendonly.aof" appendfsync everysec requirepass %s `, bindHost, port, dataDir, sec.KVPassword) } func GenerateCoTURNConf(st *state.BootstrapState, sec *secrets.Secrets, publicIPv4, privateIPv4 string) string { external := publicIPv4 if privateIPv4 != "" && privateIPv4 != publicIPv4 { external = fmt.Sprintf("%s/%s", publicIPv4, privateIPv4) } return fmt.Sprintf(`listening-port=%d fingerprint lt-cred-mech user=%s:%s realm=%s server-name=%s no-multicast-peers no-loopback-peers stale-nonce no-tls no-dtls min-port=%d max-port=%d external-ip=%s `, st.Ports.TURNListenPort, sec.TURNUsername, sec.TURNPassword, st.Domains.TURN, st.Domains.TURN, st.Ports.TURNRelayUDPStart, st.Ports.TURNRelayUDPEnd, external) } func GenerateLiveKitUnit(st *state.BootstrapState) string { return fmt.Sprintf(`[Unit] Description=LiveKit Server After=network-online.target Wants=network-online.target [Service] User=livekit Group=livekit ExecStart=%s/livekit-server --config %s/livekit.yaml Restart=on-failure RestartSec=2 LimitNOFILE=1048576 WorkingDirectory=%s NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=%s %s %s /var/lib/livekit LockPersonality=true MemoryDenyWriteExecute=true RestrictSUIDSGID=true RestrictRealtime=true [Install] WantedBy=multi-user.target `, st.Paths.LiveKitBinDir, st.Paths.ConfigDir, st.Paths.LiveKitInstallDir, st.Paths.LiveKitLogDir, st.Paths.LiveKitInstallDir, st.Paths.ConfigDir) } func GenerateCaddyJSON(st *state.BootstrapState) string { caddyConfig := map[string]interface{}{ "storage": map[string]interface{}{ "module": "file_system", "root": st.Paths.CaddyStorageDir, }, "logging": map[string]interface{}{ "logs": map[string]interface{}{ "default": map[string]interface{}{ "level": "INFO", }, }, }, "apps": map[string]interface{}{ "tls": map[string]interface{}{ "automation": map[string]interface{}{ "policies": []interface{}{ map[string]interface{}{ "subjects": []string{st.Domains.LiveKit, st.Domains.TURN}, "issuers": []interface{}{ map[string]interface{}{ "module": "acme", "email": st.ACMEEmail, }, }, }, }, }, "certificates": map[string]interface{}{ "automate": []string{st.Domains.LiveKit, st.Domains.TURN}, }, }, "layer4": map[string]interface{}{ "servers": map[string]interface{}{ "main443": map[string]interface{}{ "listen": []string{":443"}, "routes": []interface{}{ map[string]interface{}{ "match": []interface{}{ map[string]interface{}{ "tls": map[string]interface{}{ "sni": []string{st.Domains.TURN}, }, }, }, "handle": []interface{}{ map[string]interface{}{ "handler": "tls", "connection_policies": []interface{}{ map[string]interface{}{ "alpn": []string{"acme-tls/1", "h2", "http/1.1"}, }, }, }, map[string]interface{}{ "handler": "proxy", "upstreams": []interface{}{ map[string]interface{}{ "dial": []string{fmt.Sprintf("127.0.0.1:%d", st.Ports.TURNListenPort)}, }, }, }, }, }, map[string]interface{}{ "match": []interface{}{ map[string]interface{}{ "tls": map[string]interface{}{ "sni": []string{st.Domains.LiveKit}, }, }, }, "handle": []interface{}{ map[string]interface{}{ "handler": "tls", "connection_policies": []interface{}{ map[string]interface{}{ "alpn": []string{"acme-tls/1", "http/1.1"}, }, }, }, map[string]interface{}{ "handler": "proxy", "upstreams": []interface{}{ map[string]interface{}{ "dial": []string{fmt.Sprintf("127.0.0.1:%d", st.Ports.LiveKitHTTPLocal)}, }, }, }, }, }, }, }, }, }, }, } data, err := json.MarshalIndent(caddyConfig, "", " ") if err != nil { panic("failed to marshal caddy config: " + err.Error()) } return string(data) + "\n" } func GenerateCaddyUnit(st *state.BootstrapState) string { return fmt.Sprintf(`[Unit] Description=Caddy (custom build with caddy-l4) for LiveKit + TURN/TLS After=network-online.target Wants=network-online.target [Service] User=caddy Group=caddy ExecStart=%s run --config %s/caddy.json ExecReload=%s reload --config %s/caddy.json Restart=on-failure LimitNOFILE=1048576 AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=true WorkingDirectory=%s [Install] WantedBy=multi-user.target `, st.Paths.CaddyBin, st.Paths.ConfigDir, st.Paths.CaddyBin, st.Paths.ConfigDir, st.Paths.CaddyStorageDir) } func GenerateCoTURNUnit(st *state.BootstrapState) string { return fmt.Sprintf(`[Unit] Description=CoTURN for LiveKit After=network-online.target Wants=network-online.target [Service] ExecStart=/usr/bin/turnserver -c %s/coturn.conf -n Restart=on-failure RestartSec=2 LimitNOFILE=1048576 NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target `, st.Paths.ConfigDir) } func GenerateKVUnit(st *state.BootstrapState, kvBin string) string { return fmt.Sprintf(`[Unit] Description=Redis-compatible KV store for LiveKit (managed by livekitctl) After=network-online.target Wants=network-online.target [Service] ExecStart=%s %s/kv.conf Restart=on-failure RestartSec=2 LimitNOFILE=1048576 NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target `, kvBin, st.Paths.ConfigDir) } type WriteAllConfigsParams struct { State *state.BootstrapState Secrets *secrets.Secrets PublicIPv4 string PrivateIPv4 string KVBin string } func WriteAllConfigs(params WriteAllConfigsParams) error { st := params.State sec := params.Secrets cfgDir := st.Paths.ConfigDir if err := util.EnsureDir(cfgDir, 0755, -1, -1); err != nil { return err } ugLiveKit := util.LookupUserGroup("livekit") ugCaddy := util.LookupUserGroup("caddy") lkUID, lkGID := -1, -1 if ugLiveKit != nil { lkUID, lkGID = ugLiveKit.UID, ugLiveKit.GID } caddyUID, caddyGID := -1, -1 if ugCaddy != nil { caddyUID, caddyGID = ugCaddy.UID, ugCaddy.GID } if err := util.EnsureDir(st.Paths.LiveKitLogDir, 0755, lkUID, lkGID); err != nil { return err } if err := util.EnsureDir(st.Paths.CaddyStorageDir, 0700, caddyUID, caddyGID); err != nil { return err } if err := util.EnsureDir(st.Paths.CaddyLogDir, 0755, caddyUID, caddyGID); err != nil { return err } if err := util.EnsureDir(st.Paths.KVDataDir, 0700, -1, -1); err != nil { return err } redisAddr := fmt.Sprintf("%s:%d", st.KV.BindHost, st.KV.Port) livekitYAML := GenerateLiveKitYAML(st, sec, redisAddr) if err := util.AtomicWriteText(filepath.Join(cfgDir, "livekit.yaml"), livekitYAML, 0640, lkUID, lkGID); err != nil { return err } kvConf := GenerateKVConf(sec, st.KV.BindHost, st.KV.Port, st.Paths.KVDataDir) if err := util.AtomicWriteText(filepath.Join(cfgDir, "kv.conf"), kvConf, 0600, -1, -1); err != nil { return err } coturnConf := GenerateCoTURNConf(st, sec, params.PublicIPv4, params.PrivateIPv4) if err := util.AtomicWriteText(filepath.Join(cfgDir, "coturn.conf"), coturnConf, 0600, -1, -1); err != nil { return err } caddyJSON := GenerateCaddyJSON(st) if err := util.AtomicWriteText(filepath.Join(cfgDir, "caddy.json"), caddyJSON, 0644, -1, -1); err != nil { return err } if util.FileExists(st.Paths.UnitDir) { if err := util.AtomicWriteText(filepath.Join(st.Paths.UnitDir, "livekit.service"), GenerateLiveKitUnit(st), 0644, -1, -1); err != nil { return err } if err := util.AtomicWriteText(filepath.Join(st.Paths.UnitDir, "caddy.service"), GenerateCaddyUnit(st), 0644, -1, -1); err != nil { return err } if err := util.AtomicWriteText(filepath.Join(st.Paths.UnitDir, "livekit-coturn.service"), GenerateCoTURNUnit(st), 0644, -1, -1); err != nil { return err } if err := util.AtomicWriteText(filepath.Join(st.Paths.UnitDir, "livekit-kv.service"), GenerateKVUnit(st, params.KVBin), 0644, -1, -1); err != nil { return err } } return nil }