#!/usr/bin/env bash # Beschreibung: Aktualisiert livekit.yaml (rtc.ips.includes) bei IP-Wechsel (IPv4 ODER IPv6), # restarte den LiveKit-Dienst (Docker oder Docker Compose), wartet konfigurierbar und prüft Health. # Mit Backup + Rollback, optionalen ntfy-Notifications, YAML-Update via yq (Fallback eingebaut). # Synapse: https://git.techniverse.net/scriptos/livekit-ip-watch.git # Autor: Patrick Asmus # Web: https://www.cleveradmin.de # Version: 1.1 # Datum: 27.10.2025 # Modifikation: healthcheck auf HTTP mit Custom URL umgestellt. ##################################################### set -euo pipefail ############################ # Konfiguration (Variablen) ############################ # Pfad zur LiveKit-Konfiguration CONFIG_FILE="/home/docker-container/matrix-rtc/data/matrix-element-call-livekit/config.yaml" # Betriebsmodus: "compose" oder "docker" RUNTIME="compose" # Compose: Kompletter Pfad zur Compose-Datei + Service-Name COMPOSE_FILE_PATH="/home/docker-container/matrix-rtc/docker-compose.yaml" COMPOSE_SERVICE="matrix-element-call-livekit" # Plain-Docker: Container-Name (nur genutzt, wenn RUNTIME="docker") CONTAINER_NAME="matrix-element-call-livekit" # IP-Variante: IPv4 oder IPv6 ENABLE_IPV6=false # Wartezeit nach Neustart (Sekunden), dann Healthcheck WAIT_AFTER_RESTART=20 # Healthcheck-Konfiguration: HTTP HEALTHCHECK_DOMAIN="rtc.matrix.techniverse.net" HEALTHCHECK_TIMEOUT=3 # ntfy (optional) NTFY_URL="" NTFY_TOKEN="" ############################ # Hauptscript ############################ # Skriptverzeichnis, Lock- und State-Dateien SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOCKFILE="$SCRIPT_DIR/livekit-ip-watch.lock" STATE_FILE_V4="$SCRIPT_DIR/last_ip_v4" STATE_FILE_V6="$SCRIPT_DIR/last_ip_v6" BACKUP_DIR="$SCRIPT_DIR/backups" mkdir -p "$BACKUP_DIR" log() { printf '[%(%Y-%m-%d %H:%M:%S)T] %s\n' -1 "$*" >&2; } die() { log "ERROR: $*"; notify "error" "$*"; exit 1; } warn() { log "WARN: $*"; notify "warn" "$*"; } info() { log "INFO: $*"; } # Ntfy notify() { local level="${1:-info}" shift || true local msg="${*:-}" [[ -z "$NTFY_URL" ]] && return 0 local hdr=(-H "Title: livekit-ip-watch" -H "Priority: default" -H "Tags: $level") [[ -n "$NTFY_TOKEN" ]] && hdr+=(-H "Authorization: Bearer $NTFY_TOKEN") curl -fsS -X POST "${hdr[@]}" -d "$msg" "$NTFY_URL" >/dev/null 2>&1 || true } # Lock acquire_lock() { if command -v flock >/dev/null 2>&1; then exec 9>"$LOCKFILE" if ! flock -n 9; then info "Läuft bereits (Lock vorhanden): $LOCKFILE" exit 0 fi else set -o noclobber if ! : > "$LOCKFILE"; then info "Läuft bereits (Lock vorhanden): $LOCKFILE" exit 0 fi set +o noclobber fi trap 'release_lock' EXIT } release_lock() { rm -f "$LOCKFILE" 2>/dev/null || true } # IP-Ermittlung get_ipv4() { local ip ip="$(dig +short myip.opendns.com @resolver1.opendns.com 2>/dev/null || true)" [[ -z "$ip" ]] && ip="$(curl -fsS https://api.ipify.org 2>/dev/null || true)" [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ip="" echo "$ip" } get_ipv6() { local ip ip="$(curl -fsS -6 https://api6.ipify.org 2>/dev/null || true)" if [[ "$ip" =~ : ]]; then echo "$ip" else echo "" fi } have_yq() { command -v yq >/dev/null 2>&1; } backup_config() { [[ -r "$CONFIG_FILE" ]] || die "Config nicht lesbar: $CONFIG_FILE" local ts backup ts="$(date +%Y%m%d%H%M%S)" backup="$BACKUP_DIR/$(basename "$CONFIG_FILE").$ts.bak" cp -a "$CONFIG_FILE" "$backup" echo "$backup" } update_yaml_with_yq() { local cidr="$1" YQ_VAR_CIDR="$cidr" yq -i ' .rtc |= (. // {}) | .rtc.ips |= (. // {}) | .rtc.ips.includes = [env(YQ_VAR_CIDR)] ' "$CONFIG_FILE" } update_yaml_fallback() { local cidr="$1" local tmp tmp="$(mktemp)" awk -v newcidr="$cidr" ' BEGIN{ in_rtc=0; in_ips=0; in_includes=0; injected=0 } { line=$0 if ($0 ~ /^[[:space:]]*rtc:[[:space:]]*$/) { in_rtc=1; in_ips=0; in_includes=0 } else if (in_rtc && $0 ~ /^[[:space:]]*[[:alnum:]_]+:[[:space:]]*$/ && $0 !~ /^[[:space:]]*ips:/) { if (!in_ips && !in_includes && !injected) { print " ips:" print " includes:" print " - " newcidr injected=1 } } if (in_rtc && $0 ~ /^[[:space:]]*ips:[[:space:]]*$/) { in_ips=1; in_includes=0 } if (in_ips && $0 ~ /^[[:space:]]*includes:[[:space:]]*$/) { in_includes=1; next } if (in_includes) { if ($0 ~ /^[[:space:]]*-[[:space:]]*[^[:space:]]+/) { next } else { if (!injected) { print " - " newcidr injected=1 } in_includes=0 in_ips=($0 ~ /^[[:space:]]*ips:/)?1:0 } } print line } END{ if (in_rtc && !injected) { print " ips:" print " includes:" print " - " newcidr } } ' "$CONFIG_FILE" > "$tmp" mv "$tmp" "$CONFIG_FILE" } detect_compose_cmd() { if command -v docker-compose >/dev/null 2>&1; then echo "docker-compose" else echo "docker compose" fi } restart_service() { if [[ "$RUNTIME" == "compose" ]]; then local dir file dir="$(dirname "$COMPOSE_FILE_PATH")" file="$(basename "$COMPOSE_FILE_PATH")" local COMPOSE_CMD COMPOSE_CMD="$(detect_compose_cmd)" ( cd "$dir" && "$COMPOSE_CMD" -f "$file" restart "$COMPOSE_SERVICE" ) else docker restart "$CONTAINER_NAME" fi } healthcheck_http() { local url="https://${HEALTHCHECK_DOMAIN}/livekit/sfu/" curl -fsS --max-time "$HEALTHCHECK_TIMEOUT" "$url" >/dev/null } main() { acquire_lock [[ -f "$CONFIG_FILE" ]] || die "Config-Datei nicht gefunden: $CONFIG_FILE" if [[ "$RUNTIME" == "compose" ]]; then [[ -f "$COMPOSE_FILE_PATH" ]] || die "Compose-Datei nicht gefunden: $COMPOSE_FILE_PATH" [[ -n "$COMPOSE_SERVICE" ]] || die "COMPOSE_SERVICE ist leer." else [[ -n "$CONTAINER_NAME" ]] || die "CONTAINER_NAME ist leer." fi local current_ip cidr state_file if [[ "$ENABLE_IPV6" == true ]]; then current_ip="$(get_ipv6)" [[ -z "$current_ip" ]] && die "Konnte keine öffentliche IPv6 ermitteln." cidr="${current_ip}/128" state_file="$STATE_FILE_V6" else current_ip="$(get_ipv4)" [[ -z "$current_ip" ]] && die "Konnte keine öffentliche IPv4 ermitteln." cidr="${current_ip}/32" state_file="$STATE_FILE_V4" fi local last_ip="" [[ -f "$state_file" ]] && last_ip="$(cat "$state_file" 2>/dev/null || true)" if [[ "$current_ip" == "$last_ip" && -n "$last_ip" ]]; then info "IP unverändert: $current_ip" notify "info" "Keine Änderung. Öffentliche IP bleibt $current_ip." exit 0 fi info "Neue IP erkannt: $current_ip (alt: ${last_ip:-none})" local backup backup="$(backup_config)" info "Backup erstellt: $backup" if have_yq; then info "Aktualisiere YAML mit yq → .rtc.ips.includes = [ \"$cidr\" ]" update_yaml_with_yq "$cidr" else warn "yq nicht gefunden – Fallback-Editor wird verwendet." update_yaml_fallback "$cidr" fi info "Restart des Dienstes..." restart_service info "Warte $WAIT_AFTER_RESTART Sekunden..." sleep "$WAIT_AFTER_RESTART" if healthcheck_http; then info "Healthcheck OK." echo "$current_ip" > "$state_file" notify "info" "IP geändert auf $current_ip, Dienst gesund." exit 0 fi warn "Healthcheck fehlgeschlagen – führe Rollback durch." cp -a "$backup" "$CONFIG_FILE" info "Rollback-Konfiguration wiederhergestellt: $backup" info "Restart nach Rollback..." restart_service info "Warte $WAIT_AFTER_RESTART Sekunden (Rollback)..." sleep "$WAIT_AFTER_RESTART" if healthcheck_http; then warn "Rollback erfolgreich. System läuft wieder mit alter Konfiguration." notify "warn" "Rollback erfolgreich. Bitte prüfen. Alte IP bleibt ${last_ip:-unbekannt}." exit 10 else die "Rollback fehlgeschlagen. Manuelles Eingreifen erforderlich." fi } main "$@"