feat(turn): TURN/TLS с сертификатом, порты релея и автообновление сертификата

- install.sh: --turn-domain/--turn-cert/--turn-key, проверка наличия сертификата,
  копирование в /opt/glchat/certs, UFW открывает диапазон релея
- livekit.yaml.tpl: relay_range_start/end, cert_file/key_file
- compose: сертификаты монтируются в LiveKit read-only
- systemd timer glchat-cert-refresh: ежедневно копирует обновлённый сертификат
  из внешнего ACME-хранилища и перезапускает LiveKit при изменении
- smoke.sh: TURN-порты обязательны при включённом TURN + проверка CN сертификата
This commit is contained in:
2026-09-19 20:22:57 +03:00
parent 5353c5d5a2
commit c5392349cd
5 changed files with 135 additions and 4 deletions
+1
View File
@@ -67,6 +67,7 @@ services:
ports: []
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
- ${GLCHAT_TURN_CERT_DIR}:/etc/certs:ro
logging: *default-logging
deploy:
resources:
+1
View File
@@ -70,6 +70,7 @@ services:
- "${LIVEKIT_UDP_PORT}:7882/udp"
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
- ${GLCHAT_TURN_CERT_DIR}:/etc/certs:ro
logging: *default-logging
deploy:
resources:
+113 -3
View File
@@ -52,6 +52,11 @@ LIVEKIT_WS_PORT="${LIVEKIT_WS_PORT:-7880}"
LIVEKIT_TCP_PORT="${LIVEKIT_TCP_PORT:-7881}"
LIVEKIT_UDP_PORT="${LIVEKIT_UDP_PORT:-7882}"
TURN_UDP_PORT="${TURN_UDP_PORT:-3478}"
TURN_DOMAIN_OVERRIDE="${TURN_DOMAIN_OVERRIDE:-}"
TURN_RELAY_START="${TURN_RELAY_START:-30000}"
TURN_RELAY_END="${TURN_RELAY_END:-40000}"
TURN_CERT_SRC="${TURN_CERT_SRC:-}"
TURN_CERT_SRC_KEY="${TURN_CERT_SRC_KEY:-}"
TURN_TLS_PORT="${TURN_TLS_PORT:-5349}"
PROXY_MODE="${PROXY_MODE:-embedded}"
PROFILE="${PROFILE:-auto}"
@@ -108,6 +113,9 @@ install.sh — установка glchat (AGENT.md 10).
--livekit-tcp <port> LiveKit TCP (по умолчанию 7881)
--livekit-udp <port> LiveKit UDP (по умолчанию 7882)
--turn-udp <port> TURN UDP (по умолчанию 3478)
--turn-domain <domain> домен TURN/TLS (по умолчанию turn.<domain>)
--turn-cert <файл> сертификат для TURN/TLS (нужен при включённом TURN)
--turn-key <файл> приватный ключ сертификата TURN/TLS
--turn-tls <port> TURN TLS (по умолчанию 5349)
--external-proxy не занимать 80/443: отдать маршруты внешнему прокси (AGENT.md 10.13)
--no-files-subdomain не использовать files.<domain> (fallback /files/*)
@@ -152,6 +160,9 @@ parse_args() {
--livekit-tcp) LIVEKIT_TCP_PORT="${2:?}"; shift 2 ;;
--livekit-udp) LIVEKIT_UDP_PORT="${2:?}"; shift 2 ;;
--turn-udp) TURN_UDP_PORT="${2:?}"; shift 2 ;;
--turn-domain) TURN_DOMAIN_OVERRIDE="${2:?}"; shift 2 ;;
--turn-cert) TURN_CERT_SRC="${2:?}"; shift 2 ;;
--turn-key) TURN_CERT_SRC_KEY="${2:?}"; shift 2 ;;
--turn-tls) TURN_TLS_PORT="${2:?}"; shift 2 ;;
--profile) PROFILE="${2:?}"; shift 2 ;;
--max-upload) MAX_UPLOAD_MB="${2:?}"; shift 2 ;;
@@ -242,6 +253,9 @@ resolve_derived_values() {
[ "$PROXY_MODE" = "external" ] && turn_external_tls="true"
TURN_EXTERNAL_TLS="$turn_external_tls"
TURN_DOMAIN_RESOLVED="${TURN_DOMAIN_OVERRIDE:-turn.${DOMAIN}}"
TURN_CERT_DIR="${TARGET_DIR}/certs"
disable_simulcast="true"
if [ "${PROFILE_LIVEKIT_SIMULCAST:-0}" -gt 0 ] 2>/dev/null; then
disable_simulcast="false"
@@ -262,10 +276,12 @@ resolve_derived_values() {
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR}"
GLCHAT_CADDY_PROFILE="${CADDY_PROFILE}"
GLCHAT_TURN_CERT_DIR="${TURN_CERT_DIR}"
export FILES_DOMAIN_RESOLVED CADDY_PROFILE LIVEKIT_PUBLISH TURN_EXTERNAL_TLS \
livekit_extra_codecs \
PROFILE_LIVEKIT_DISABLE_SIMULCAST PROFILE_LIVEKIT_USE_EXTERNAL_IP \
GLCHAT_DATA_DIR GLCHAT_CADDY_PROFILE
GLCHAT_DATA_DIR GLCHAT_CADDY_PROFILE GLCHAT_TURN_CERT_DIR \
TURN_DOMAIN_RESOLVED TURN_CERT_DIR
}
validate_inputs() {
@@ -283,6 +299,11 @@ validate_inputs() {
if [ -n "$ADMIN_PASSWORD" ] && [ "${#ADMIN_PASSWORD}" -lt 8 ]; then
die "пароль администратора короче 8 символов"
fi
# LiveKit требует сертификат для TURN/TLS (проверено на v1.9.7: без него
# сервер не стартует). Либо передаём файлы, либо отключаем TURN.
if [ "$TURN_ENABLED" = "true" ] && [ "$DRY_RUN" != "1" ] && [ -z "$TURN_CERT_SRC" ]; then
die "TURN включён, но не задан сертификат: передайте --turn-cert/--turn-key или запустите с --no-turn"
fi
}
check_ports_free() {
@@ -503,6 +524,7 @@ SKIP_TLS=${SKIP_TLS}
# --- интерполяция compose (пути и профиль) ---
GLCHAT_DATA_DIR=${GLCHAT_DATA_DIR}
GLCHAT_CADDY_PROFILE=${CADDY_PROFILE}
GLCHAT_TURN_CERT_DIR=${GLCHAT_TURN_CERT_DIR}
# --- порты ---
APP_PORT=${APP_PORT}
@@ -627,6 +649,27 @@ ensure_swap() {
# install_turn_certificate копирует сертификат TURN/TLS туда, где его читает
# контейнер LiveKit. Продление выполняет glchat-cert-refresh.timer.
install_turn_certificate() {
[ "$TURN_ENABLED" = "true" ] || return 0
if [ "$DRY_RUN" = "1" ]; then
log_file_only "DRY-RUN: копирование сертификата TURN/TLS в ${TURN_CERT_DIR}"
return 0
fi
mkdir -p "$TURN_CERT_DIR"
chmod 750 "$TURN_CERT_DIR"
if [ -n "$TURN_CERT_SRC" ]; then
install -m 640 "$TURN_CERT_SRC" "${TURN_CERT_DIR}/turn.crt"
install -m 640 "${TURN_CERT_SRC_KEY:?нужен --turn-key}" "${TURN_CERT_DIR}/turn.key"
ok "сертификат TURN/TLS установлен: ${TURN_CERT_DIR}/turn.crt"
elif [ -f "${TURN_CERT_DIR}/turn.crt" ]; then
ok "используем ранее установленный сертификат TURN/TLS"
else
warn "сертификат TURN/TLS не найден: TURN не запустится (см. --turn-cert)"
fi
}
render_all_templates() {
info "генерируем конфиги в ${TARGET_DIR}"
mkdir -p "${TARGET_DIR}"/{caddy/logs,caddy/data,caddy/config,livekit,fail2ban,nftables}
@@ -661,6 +704,7 @@ render_all_templates() {
"CADDY_CPUS=1.0" "CADDY_MEMORY_LIMIT=256m" \
"CADDY_IMAGE_TAG=${CADDY_IMAGE_TAG}" "LIVEKIT_IMAGE_TAG=${LIVEKIT_IMAGE_TAG}" \
"GLCHAT_CADDY_PROFILE=${CADDY_PROFILE}" \
"GLCHAT_TURN_CERT_DIR=${TURN_CERT_DIR}" \
"LIVEKIT_API_KEY=${LIVEKIT_API_KEY}" "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}" \
"HTTP_PORT=${HTTP_PORT}" "HTTPS_PORT=${HTTPS_PORT}"
@@ -680,7 +724,11 @@ render_all_templates() {
"LIVEKIT_UDP_PORT=${LIVEKIT_UDP_PORT}" \
"LIVEKIT_USE_EXTERNAL_IP=${use_external_ip}" \
"TURN_ENABLED=${TURN_ENABLED}" \
"TURN_DOMAIN=turn.${DOMAIN}" \
"TURN_DOMAIN=${TURN_DOMAIN_RESOLVED}" \
"TURN_RELAY_START=${TURN_RELAY_START}" \
"TURN_RELAY_END=${TURN_RELAY_END}" \
"TURN_CERT_FILE=$([ "$TURN_ENABLED" = "true" ] && echo /etc/certs/turn.crt || echo '')" \
"TURN_KEY_FILE=$([ "$TURN_ENABLED" = "true" ] && echo /etc/certs/turn.key || echo '')" \
"TURN_UDP_PORT=${TURN_UDP_PORT}" \
"TURN_TLS_PORT=${TURN_TLS_PORT}" \
"TURN_EXTERNAL_TLS=${turn_external_tls}" \
@@ -710,6 +758,64 @@ render_all_templates() {
fi
}
# install_cert_refresh копирует обновлённый сертификат TURN и перезапускает LiveKit.
install_cert_refresh() {
[ "$TURN_ENABLED" = "true" ] || return 0
[ -n "$TURN_CERT_SRC" ] || return 0
[ "$DRY_RUN" = "1" ] && return 0
cat >/usr/local/sbin/glchat-cert-refresh <<EOF
#!/usr/bin/env bash
# Сгенерировано deploy/install.sh: копирует свежий сертификат TURN/TLS
# из внешнего ACME-хранилища и перезапускает LiveKit при изменении.
set -eu
SRC_CRT="${TURN_CERT_SRC}"
SRC_KEY="${TURN_CERT_SRC_KEY}"
DST_DIR="${TURN_CERT_DIR}"
if [ ! -r "\$SRC_CRT" ] || [ ! -r "\$SRC_KEY" ]; then
echo "glchat-cert-refresh: исходный сертификат недоступен" >&2
exit 1
fi
before="\$(sha256sum "\$DST_DIR/turn.crt" 2>/dev/null | cut -d' ' -f1 || true)"
install -m 640 "\$SRC_CRT" "\$DST_DIR/turn.crt"
install -m 640 "\$SRC_KEY" "\$DST_DIR/turn.key"
after="\$(sha256sum "\$DST_DIR/turn.crt" | cut -d' ' -f1)"
if [ "\$before" != "\$after" ]; then
docker restart glchat-livekit >/dev/null
echo "glchat-cert-refresh: сертификат обновлён, LiveKit перезапущен"
fi
EOF
chmod 0755 /usr/local/sbin/glchat-cert-refresh
cat >/etc/systemd/system/glchat-cert-refresh.service <<'EOF'
[Unit]
Description=glchat: обновление сертификата TURN/TLS
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/glchat-cert-refresh
EOF
cat >/etc/systemd/system/glchat-cert-refresh.timer <<'EOF'
[Unit]
Description=glchat: ежедневная проверка сертификата TURN/TLS
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now glchat-cert-refresh.timer >/dev/null 2>&1 ||
warn "не удалось включить glchat-cert-refresh.timer"
ok "автообновление сертификата TURN настроено (systemd timer, ежедневно)"
}
install_cli_and_units() {
info "устанавливаем glchat CLI и systemd-юнит"
if [ "$DRY_RUN" = "1" ]; then
@@ -732,7 +838,7 @@ configure_ufw() {
}
info "настраиваем UFW"
if [ "$DRY_RUN" = "1" ]; then
log_file_only "DRY-RUN: ufw default deny incoming; allow ${SSH_PORT}, ${LIVEKIT_TCP_PORT}/tcp, ${LIVEKIT_UDP_PORT}/udp, TURN"
log_file_only "DRY-RUN: ufw default deny incoming; allow ${SSH_PORT}, ${LIVEKIT_TCP_PORT}/tcp, ${LIVEKIT_UDP_PORT}/udp, TURN ${TURN_UDP_PORT}/udp, ${TURN_TLS_PORT}/tcp, relay ${TURN_RELAY_START}-${TURN_RELAY_END}/udp"
return 0
fi
ufw --force reset >/dev/null 2>&1 || true
@@ -748,6 +854,8 @@ configure_ufw() {
if [ "$TURN_ENABLED" = "true" ]; then
ufw allow "${TURN_UDP_PORT}/udp" comment 'turn' >/dev/null
ufw allow "${TURN_TLS_PORT}/tcp" comment 'turn-tls' >/dev/null
# Relay-диапазон: через эти порты TURN отдаёт медиа (AGENT.md 4.2).
ufw allow "${TURN_RELAY_START}:${TURN_RELAY_END}/udp" comment 'turn-relay' >/dev/null
fi
ufw --force enable >/dev/null
ok "UFW активен: $(ufw status | head -1)"
@@ -1075,6 +1183,7 @@ main() {
install_packages
install_docker
generate_secrets
install_turn_certificate
write_env_file
render_all_templates
apply_profile
@@ -1084,6 +1193,7 @@ main() {
configure_fail2ban
configure_ssh_hardening
install_cli_and_units
install_cert_refresh
build_or_pull_image
start_stack
bootstrap_admin
+6
View File
@@ -41,3 +41,9 @@ turn:
udp_port: ${TURN_UDP_PORT}
tls_port: ${TURN_TLS_PORT}
external_tls: ${TURN_EXTERNAL_TLS}
# Диапазон портов релея должен быть открыт в firewall (§4.2).
relay_range_start: ${TURN_RELAY_START}
relay_range_end: ${TURN_RELAY_END}
# LiveKit требует сертификат для TURN/TLS; файлы готовит установщик.
cert_file: ${TURN_CERT_FILE}
key_file: ${TURN_KEY_FILE}
+14 -1
View File
@@ -121,9 +121,22 @@ else
if ss -tulnH 2>/dev/null | grep -q ":${port} "; then
pass "слушает ${spec}"
else
skip "не слушает ${spec} (проверьте конфиг LiveKit/TURN)"
fail "не слушает ${spec} (TURN не запустился: проверьте cert_file в livekit.yaml)"
fi
done
# TURN/TLS должен отдавать валидный сертификат на своём домене.
turn_domain="$(grep -A3 '^turn:' "${GLCHAT_ETC_DIR}/livekit/livekit.yaml" 2>/dev/null |
awk '/domain:/ {print $2; exit}')"
if [ -n "$turn_domain" ] && command -v openssl >/dev/null 2>&1; then
cert_cn="$(echo | timeout 8 openssl s_client -connect "127.0.0.1:${TURN_TLS_PORT}" \
-servername "$turn_domain" 2>/dev/null |
openssl x509 -noout -subject 2>/dev/null | sed 's/.*CN *= *//')"
if [ "$cert_cn" = "$turn_domain" ]; then
pass "TURN/TLS отдаёт сертификат для ${turn_domain}"
else
fail "TURN/TLS: сертификат не для ${turn_domain} (CN=${cert_cn:-нет})"
fi
fi
fi
fi