From c5392349cddb22089539c455eccbe96f0058eee0 Mon Sep 17 00:00:00 2001 From: grendervill Date: Sat, 19 Sep 2026 20:22:57 +0300 Subject: [PATCH] =?UTF-8?q?feat(turn):=20TURN/TLS=20=D1=81=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D0=BC,?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20=D1=80=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D1=8F=20=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 сертификата --- deploy/compose.test.yaml.tpl | 1 + deploy/compose.yaml.tpl | 1 + deploy/install.sh | 116 +++++++++++++++++++++++++++++++- deploy/livekit/livekit.yaml.tpl | 6 ++ deploy/smoke.sh | 15 ++++- 5 files changed, 135 insertions(+), 4 deletions(-) diff --git a/deploy/compose.test.yaml.tpl b/deploy/compose.test.yaml.tpl index 8444dc6..39ba396 100644 --- a/deploy/compose.test.yaml.tpl +++ b/deploy/compose.test.yaml.tpl @@ -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: diff --git a/deploy/compose.yaml.tpl b/deploy/compose.yaml.tpl index 9fa0ed6..aafc618 100644 --- a/deploy/compose.yaml.tpl +++ b/deploy/compose.yaml.tpl @@ -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: diff --git a/deploy/install.sh b/deploy/install.sh index 4d2fe7e..3b1dd71 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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 LiveKit TCP (по умолчанию 7881) --livekit-udp LiveKit UDP (по умолчанию 7882) --turn-udp TURN UDP (по умолчанию 3478) + --turn-domain домен TURN/TLS (по умолчанию turn.) + --turn-cert <файл> сертификат для TURN/TLS (нужен при включённом TURN) + --turn-key <файл> приватный ключ сертификата TURN/TLS --turn-tls TURN TLS (по умолчанию 5349) --external-proxy не занимать 80/443: отдать маршруты внешнему прокси (AGENT.md 10.13) --no-files-subdomain не использовать files. (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 <&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 diff --git a/deploy/livekit/livekit.yaml.tpl b/deploy/livekit/livekit.yaml.tpl index 60c1a06..7b5a07f 100644 --- a/deploy/livekit/livekit.yaml.tpl +++ b/deploy/livekit/livekit.yaml.tpl @@ -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} diff --git a/deploy/smoke.sh b/deploy/smoke.sh index 2286851..d92d75f 100755 --- a/deploy/smoke.sh +++ b/deploy/smoke.sh @@ -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