2026-02-17 12:22:36 +00:00

434 lines
11 KiB
Go

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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
}