fix(livekit): анонсировать клиентам внешний TURN — relay не работал совсем

coturn поднимался, был healthy и слушал 3478 — но клиенты о нём никогда не
узнавали: встроенный TURN выключен (`turn.enabled: false`), внешний в
конфигурации не объявлен, фронтенд `iceServers` не задаёт. За всё время
работы сервера в логах coturn нет ни одной аллокации.

Следствие: у участников из сетей, где прямое UDP-соединение не проходит,
не было relay-фолбэка вообще — только прямой UDP и TCP 7881. На
нагрузочном тесте 28.07 все разрывы `PEER_CONNECTION_DISCONNECTED`
пришлись на внешних участников и ни одного — на офисных.

Добавлена секция `rtc.turn_servers` (UDP и TCP на 3478). Эти серверы
только анонсируются клиенту в списке ICE — сам SFU через них не ходит
(см. iceServersForParticipant в LiveKit). Credentials генерируются по
механизму TURN REST API из общего `TURN_STATIC_AUTH_SECRET`, поэтому
`render-templates.sh` теперь подставляет его и `TURN_EXTERNAL_IP` также в
конфигурацию LiveKit.

TLS (5349/443) намеренно не анонсируется: сертификаты в coturn не
смонтированы, а неработающий `turns:` заставил бы клиента ждать таймаута
перед переходом к рабочему кандидату. Что нужно для его включения —
описано в разделе 8 руководства.

Там же исправлено умолчание в правилах ufw: помимо 3478 нужен диапазон
relay-аллокаций `49160:49200/udp`. Без него TURN отвечает на запросы, но
релей не работает, причём в логах coturn при этом тишина.
This commit is contained in:
2026-07-29 00:17:19 +03:00
parent 705f160912
commit e018837a1d
3 changed files with 84 additions and 23 deletions

View File

@@ -32,6 +32,40 @@ rtc:
use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP} use_external_ip: ${LIVEKIT_USE_EXTERNAL_IP}
node_ip: ${LIVEKIT_NODE_IP} node_ip: ${LIVEKIT_NODE_IP}
# Внешний TURN (сервис coturn, профиль `media`) — АНОНС КЛИЕНТАМ.
# Сам SFU через эти серверы не ходит: LiveKit лишь отдаёт их браузеру в
# списке ICE-серверов при подключении (см. iceServersForParticipant в
# pkg/service/roommanager.go), а клиент уже решает, нужен ли ему relay.
#
# Зачем. До 28.07.2026 coturn работал, но КЛИЕНТЫ О НЁМ НЕ ЗНАЛИ: секция
# `turn` ниже выключена (встроенный TURN не поднимаем), внешний в конфиге
# объявлен не был, а фронтенд `iceServers` не задаёт. За всё время работы
# в логах coturn — ноль ALLOCATE. Итог: у клиентов из сетей с жёстким NAT
# не было relay-фолбэка вообще, только прямой UDP и TCP 7881. Именно так
# объясняются `PEER_CONNECTION_DISCONNECTED` на нагрузочном тесте — все
# у внешних участников, ни одного у офисных (.forcc/LOAD-FINDINGS.md,
# причина C).
#
# `secret` обязан совпадать с `static-auth-secret` в turnserver.conf —
# оба рендерятся из одного TURN_STATIC_AUTH_SECRET (deploy/render-templates.sh).
# Логин/пароль LiveKit генерирует сам по механизму TURN REST API.
#
# UDP и TCP на 3478 — оба порта уже открыты в ufw. TLS (5349) намеренно не
# объявляем: в turnserver.conf сертификаты не смонтированы, и анонс
# неработающего `turns:` заставил бы клиента впустую ждать таймаута,
# прежде чем перейти к рабочему кандидату.
turn_servers:
- host: ${TURN_EXTERNAL_IP}
port: 3478
protocol: udp
secret: ${TURN_STATIC_AUTH_SECRET}
ttl: 14400
- host: ${TURN_EXTERNAL_IP}
port: 3478
protocol: tcp
secret: ${TURN_STATIC_AUTH_SECRET}
ttl: 14400
# Redis обязателен для сервиса egress (см. deploy/egress/) — он использует # Redis обязателен для сервиса egress (см. deploy/egress/) — он использует
# его как pub/sub и key-value хранилище состояния запущенных записей; # его как pub/sub и key-value хранилище состояния запущенных записей;
# без него egress не может получать room/track-события от LiveKit # без него egress не может получать room/track-события от LiveKit

View File

@@ -49,7 +49,10 @@ envsubst '${TURN_STATIC_AUTH_SECRET} ${TURN_REALM} ${TURN_EXTERNAL_IP}' \
< "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf" < "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf"
echo "[render] deploy/coturn/turnserver.conf готов" echo "[render] deploy/coturn/turnserver.conf готов"
envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY} ${REDIS_PASSWORD}' \ # TURN_EXTERNAL_IP и TURN_STATIC_AUTH_SECRET нужны и здесь: с 0.0.14 LiveKit
# анонсирует клиентам внешний coturn (секция `rtc.turn_servers`), и секрет
# обязан совпадать с `static-auth-secret` в turnserver.conf выше.
envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY} ${REDIS_PASSWORD} ${TURN_EXTERNAL_IP} ${TURN_STATIC_AUTH_SECRET}' \
< "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml" < "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml"
echo "[render] deploy/livekit/livekit.yaml готов" echo "[render] deploy/livekit/livekit.yaml готов"

View File

@@ -82,9 +82,14 @@ ufw allow 80/tcp # HTTP (редирект на HTTPS + ACME-challenge)
ufw allow 443/tcp # HTTPS ufw allow 443/tcp # HTTPS
ufw allow 7881/tcp # LiveKit RTC TCP fallback (профиль media) ufw allow 7881/tcp # LiveKit RTC TCP fallback (профиль media)
ufw allow 54000:54100/udp # LiveKit WebRTC media (ICE), см. docker-compose.yml ufw allow 54000:54100/udp # LiveKit WebRTC media (ICE), см. docker-compose.yml
# TURN (coturn) — только если включаете раздел 8: # TURN (coturn) — только если включаете раздел 8. Нужны ОБА пункта:
# сигнальные порты И диапазон relay-аллокаций (min-port/max-port из
# deploy/coturn/turnserver.conf). Без второго TURN отвечает на запросы, но
# сам релей не работает — клиент получает кандидата и не может им
# воспользоваться, а в логах coturn при этом тишина.
# ufw allow 3478/tcp # ufw allow 3478/tcp
# ufw allow 3478/udp # ufw allow 3478/udp
# ufw allow 49160:49200/udp
# Мониторинг (профиль `monitoring`): node-exporter работает в host-сети — # Мониторинг (профиль `monitoring`): node-exporter работает в host-сети —
# иначе он отдаёт сетевые метрики собственного контейнера вместо метрик # иначе он отдаёт сетевые метрики собственного контейнера вместо метрик
@@ -320,29 +325,48 @@ firewall) хватает для подавляющего большинства
(характерный симптом — конференция подключается по signaling, `connection (характерный симптом — конференция подключается по signaling, `connection
state: connected`, но собеседник не видит видео/не слышит звук). state: connected`, но собеседник не видит видео/не слышит звук).
По умолчанию `deploy/livekit/livekit.yaml.template` содержит **С версии 0.0.14 LiveKit анонсирует coturn клиентам** — секция
`turn.enabled: false`, и `rtc.turn_servers` не задан — standalone coturn `rtc.turn_servers` в `deploy/livekit/livekit.yaml.template` (UDP и TCP на
поднимается (профиль `media`), но LiveKit не раздаёт его клиентам как 3478, credentials по механизму TURN REST API из общего
ICE-фолбэк. `TURN_STATIC_AUTH_SECRET`). Встроенный TURN LiveKit при этом остаётся
выключенным (`turn.enabled: false`), чтобы не поднимать два TURN-сервера.
Включение (правки шаблона `deploy/livekit/livekit.yaml.template` + ⚠️ **Чем это было до 0.0.14, если вы обновляетесь со старой версии.** coturn
редеплой; **код-фикс не входит в это руководство без запроса** — обсудите поднимался и был healthy, но клиенты о нём не знали: в конфиге LiveKit
с командой перед изменением): внешний TURN объявлен не был, а фронтенд `iceServers` не задаёт. За всё
время работы в логах coturn не было ни одного ALLOCATE — то есть relay не
использовался никогда, и участники из сетей с жёстким NAT просто теряли
соединение (`PEER_CONNECTION_DISCONNECTED`).
1. Открыть 443 для TURN/TLS (наиболее надёжный фолбэк — TURN через тот же Что нужно проверить на своей инсталляции:
порт, что и остальной HTTPS-трафик, редко блокируется firewall'ами):
потребует отдельного TLS-сертификата для coturn (`cert-file`/`pkey-file` 1. **Порты в ufw — оба пункта** (см. шаг 1): `3478/tcp` + `3478/udp` для
в `deploy/coturn/turnserver.conf.template`) — можно переиспользовать тот сигнализации и `49160:49200/udp` для relay-аллокаций. Диапазон должен
же Let's Encrypt сертификат, что и nginx (тот же `/etc/letsencrypt`, уже совпадать с `min-port`/`max-port` в
смонтированный в nginx — coturn сейчас его не монтирует, потребуется `deploy/coturn/turnserver.conf.template`. Без него TURN отвечает на
доп. volume). запросы, но релей не работает — самый неприятный вариант, потому что в
2. В `livekit.yaml.template` включить `turn.enabled: true` и/или явно логах coturn при этом тишина.
прописать `rtc.turn_servers` со статическими credentials 2. **`TURN_EXTERNAL_IP` в `.env`** — реальный внешний IP или домен сервера.
(`TURN_STATIC_AUTH_SECRET` уже есть в `.env`). Именно это значение уезжает клиентам как адрес TURN-сервера, поэтому
3. `ufw allow 3478/tcp` + `ufw allow 3478/udp` (шаг 1, закомментированные `127.0.0.1` из dev-дефолта сделает анонс бесполезным.
строки) — сейчас coturn поднят, но порт не проверялся как обязательный. 3. После правок — `./deploy/render-templates.sh` (перерендерит конфиги из
4. Передеплой (`docker compose ... up -d --force-recreate livekit coturn`) шаблонов), затем `docker compose ... up -d --force-recreate livekit`.
и повторный кросс-сетевой тест именно с проблемной сетью. ⚠️ Перезапуск LiveKit **разрывает все активные конференции** — выбирайте
окно.
4. Проверка, что релей заработал: провести звонок из проблемной сети и
убедиться, что в логах появились аллокации:
`docker logs vidconf-coturn-1 --since 10m 2>&1 | grep -ci allocate`.
Ноль при живом звонке из-за NAT означает, что до coturn не дошли —
смотрите ufw и `TURN_EXTERNAL_IP`.
**TURN over TLS (порт 5349 или 443) — не настроен.** Это самый надёжный
фолбэк (проходит там, где режут UDP и нестандартные порты), но требует
смонтировать в coturn TLS-сертификат: раскомментировать `cert`/`pkey` в
`deploy/coturn/turnserver.conf.template`, добавить volume с
`/etc/letsencrypt` (nginx его уже монтирует, coturn — нет), открыть порт и
не забыть про перезапуск coturn при обновлении сертификата. Пока этого нет,
`turns:` намеренно не анонсируется: анонс неработающего адреса заставил бы
клиента ждать таймаута перед переходом к рабочему кандидату.
--- ---