Содержание статьи +
- TL;DR
- Зачем это знать
- Где эта статья в общей картине
- Что такое ICE – за пределами учебного определения
- Четыре типа кандидатов – что каждый из них на самом деле
- Как сбор кандидатов разворачивается во времени
- Проверка подключения – снова STUN, теперь peer-to-peer
- TURN в WebRTC – учётки, lifetime и каналы, о которых вы, возможно, не хотели знать
- ICE-серверы в `RTCPeerConnection` – конфигурация, имеющая большое значение
- ICE restart – что происходит при смене сети посреди звонка
- Consent freshness – как держать путь живым
- Сигнальный канал – что должно передавать ваше приложение
- Рабочий пример – сайзинг TURN для 5 000 мест на B2B-конференции
- Выбор TURN-сервера в 2026 – четыре паттерна для продакшена
- Восемь продакшен-ловушек и как их избежать
- Где здесь Фора Софт
- Ключевые выводы
- Что почитать дальше
Последняя проверка: 25 мая 2026 года по документам IETF RFC 8445 (ICE, июль 2018), RFC 8489 (STUN, февраль 2020), RFC 8656 (TURN, февраль 2020), RFC 8838 (Trickle ICE, январь 2021), RFC 8839 (атрибуты SDP для ICE, январь 2021), RFC 7675 (Consent Freshness, октябрь 2015), RFC 5780 (NAT Behaviour Discovery using STUN, май 2010), RFC 9429 (JSEP, апрель 2024), W3C WebRTC 1.0 Recommendation (март 2023), исходному коду Chromium libwebrtc 122+, Pion v3.x, mediasoup v3.14, LiveKit v1.7, документации Cloudflare Realtime TURN (апрель 2026), документации Twilio STUN/TURN (Q1 2026) и релизным заметкам coturn v4.8.0 (январь 2026).
TL;DR
ICE, STUN и TURN – три протокола, обеспечивающие соединение WebRTC-звонков через публичный интернет. STUN определяет, какой IP-адрес видит внешний мир; TURN пересылает медиапоток, когда прямой путь недоступен; ICE – это алгоритм, который собирает все возможные маршруты, тестирует их параллельно и выбирает рабочий. Глубинный взгляд, актуальный в 2026 году: ICE – это не столько про NAT, сколько про конечный автомат установления соединения внутри RTCPeerConnection, который генерирует события icecandidate в сигнальный канал, переключает iceconnectionstate между состояниями checking → connected → disconnected → failed → closed и сохраняет работоспособность при смене сети с помощью ICE restart и проверок актуальности согласия (consent freshness).
В продакшене продукты чаще всего падают на ICE тремя предсказуемыми способами: недостаточные инвестиции в TURN (из-за чего 15–20% сеансов пользователей и 100% сессий ИИ-голосовых агентов просто не подключаются), неправильная настройка политики сбора кандидатов (из-за чего корпоративные пользователи за фаерволом не могут присоединиться к звонку) и игнорирование событий iceconnectionstate (из-за чего приложение не восстанавливается при переключении Wi-Fi на 5G).
Эта статья охватывает весь жизненный цикл: математику приоритета кандидатов из RFC 8445, тайминги trickle из RFC 8838, учётные записи TURN из RFC 8656, четыре паттерна для продакшена и восемь ловушек, на которых продукты падают именно в тех сетях, что имеют наибольшее значение.
Зачем это знать
Любой современный продукт – видеоконференции, телемедицинские консультации, браузерные просмотрщики видеонаблюдения, ИИ-голосовые агенты и интерактивный live shopping – зависит от корректной работы ICE. Пользователь нажимает «присоединиться», браузер открывает камеру, сигнализация обменивается offer и answer – и дальше ничего не происходит. Экран бесконечно показывает «соединяемся…». Почти всегда причина – неправильно настроенный список ICE-серверов, отсутствующий TURN-кандидат или переход iceconnectionstate, который код приложения не обработал.
Последствия ошибки проявляются двумя способами: пользователи в корпоративных Wi-Fi, университетских сетях и мобильных сетях с carrier-grade NAT просто уходят из продукта, потому что он не может установиться, а те, кто всё же подключился, сталкиваются с ростом затрат – счёт за TURN-трафик тихо достигает шестизначных сумм в год по egress-метру облачного провайдера.
Эта статья адресована продакт-менеджеру, прорабатывающему WebRTC-проект, основателю, выбирающему между Twilio и self-hosted coturn, инженеру, впервые читающему RFC 8445, и архитектору, которому нужно точно понять, сколько TURN-регионов потребуется глобальному продукту в 2026 году.
Транспортный фон – что такое NAT, как работает STUN на уровне байтов, четыре классические разновидности NAT – описан в нашей сопровождающей статье про NAT, STUN, TURN, ICE для стриминга. Эта же статья углубляется дальше: WebRTC-специфика сигнализации, JavaScript API и паттерны развёртывания в продакшене.
Где эта статья в общей картине
Полная картина состоит из трёх слоёв, и без каждого из них не обойтись. Первый – сетевая теория: что такое NAT, четыре его классических типа, STUN binding request, TURN allocate request. Подробнее об этом – в сопровождающей статье про NAT, STUN, TURN, ICE для стриминга. Второй – сигналинг: как SDP offer и answer передают ICE-кандидатов и учётные данные. Подробный разбор этого механизма – в статье про SDP offer/answer. Третий – то, чему посвящена данная статья: поведение, специфичное для WebRTC. Здесь рассматриваются, как RTCPeerConnection управляет ICE, как JavaScript API предоставляет доступ к конечному автомату, как на практике реализован trickle ICE, как работает ICE restart при смене сети и какие конфигурации реально используются в продакшене в 2026 году.
Если вы не читали ни одной сопутствующей статьи, эта всё равно остаётся самодостаточной – каждый термин объясняется при первом упоминании. Сопутствующие материалы помогут углубиться в детали транспортного слоя или SDP-слоя, если потребуется.
Что такое ICE – за пределами учебного определения
Учебное определение прямо из спецификации: «Interactive Connectivity Establishment – это техника обхода NAT для UDP-основанных медиапотоков, устанавливаемых по модели offer/answer» – RFC 8445, июль 2018. Это верно, но упускает половину истории, важную на практике.
Полезнее так: ICE – это конечный автомат, конвейер сбора кандидатов и протокол проверки связности, работающие одновременно внутри WebRTC-стека. Алгоритм решает вопрос: «Могут ли эти два пира установить связь друг с другом?» – и отвечает на него за несколько сотен миллисекунд, хотя нижележащая сеть изначально не предназначена для этой задачи. Каждый компонент отвечает за свою часть работы, а магия заключается в том, как они перекрываются по времени.
Конечный автомат – в браузере он называется iceconnectionstate у объекта RTCPeerConnection – проходит состояния new, checking, connected, completed, disconnected, failed и closed. Конвейер сбора кандидатов работает параллельно: для каждого сетевого интерфейса стек создаёт host-кандидата, отправляет STUN binding request, чтобы определить server-рефлексивный кандидат, а затем – TURN allocate request, чтобы получить relay-кандидата. Каждый собранный кандидат вызывает событие icecandidate в вашем JavaScript, которое передаётся через сигнальный канал другому peer. Протокол проверки связности – STUN binding requests, на этот раз между peer-to-peer, а не клиент-сервер – запускается, как только с обеих сторон становится доступна хотя бы одна пара кандидатов, и параллельно проверяет все возможные маршруты, пока один из них не сработает.
Ключевая мысль: ICE спроектирован под три ограничения. Первое – ни одна стратегия не работает на каждой сети: корпоративные фаерволы блокируют UDP, мобильные операторы используют симметричный CGNAT, домашние роутеры ведут себя непредсказуемо, а IoT-устройства вообще не имеют публичного IP-адреса – поэтому алгоритм обязан попробовать все правдоподобные пути. Второе – задержка установления соединения важнее любой отдельной оптимизации: звонок, соединившийся за 1,5 секунды, лучше, чем тот, что подключился за 3 секунды, даже если второй нашёл более оптимальный маршрут – поэтому алгоритм должен совмещать сбор кандидатов, сигнализацию и проверку одновременно. Третье – сетевые условия могут меняться прямо во время звонка: телефон переключается с Wi-Fi на 5G, ноутбук меняет VPN, а сеть отеля блокирует UDP – поэтому алгоритм обязан поддерживать прозрачное повторное согласование.
Как только вы внутренне примете эти три ограничения, каждая деталь RFC 8445 перестанет казаться произвольным решением и станет очевидным ответом на сложную задачу.
Четыре типа кандидатов – что каждый из них на самом деле
ICE собирает четыре типа кандидатов. Эта таксономия старше WebRTC – она была определена ещё в RFC 5245 в 2010 году – но любая реализация WebRTC в 2026 году использует её без изменений.
Host-кандидат – это реальный сетевой адрес устройства на одном из его интерфейсов. Ноутбук с включённым Wi-Fi и подключённым Ethernet генерирует два host-кандидата. Телефон с Wi-Fi и 5G – тоже два. Адрес может выглядеть как 192.168.1.42 (приватный IPv4) или fe80::1a:b3 (link-local IPv6). Host-кандидаты собираются мгновенно – без необходимости в сетевых round-trip – и проверяются первыми, потому что если соединение host-to-host удастся, вы получите самый дешёвый возможный путь: ноль хопов через локальную сеть.
Server-reflexive кандидат, сокращённо srflx, – это публичный адрес, который STUN-сервер зафиксировал при получении binding request от устройства. STUN-обмен занимает один UDP-раундтрип: устройство отправляет 20-байтовый binding request на STUN-сервер, тот копирует в атрибут XOR-MAPPED-ADDRESS адрес и порт источника, которые увидел, и отправляет ответ. Устройство получает адрес, видимый внешнему миру после того, как все промежуточные NAT’ы изменили пакет. Server-reflexive кандидаты собираются за 20–200 мс в зависимости от RTT до STUN-сервера.
Peer-reflexive кандидат, сокращённо prflx, – это публичный адрес, обнаруженный прямо во время ICE-проверки соединения, а не на этапе сбора кандидатов. Механика такова: peer A отправляет STUN-запрос на привязку peer'у B через пару кандидатов. NAT peer'а B изменяет исходный адрес запроса, но созданное им отображение (mapping) не фиксируется ни одним STUN-сервером, поскольку запрос направляется напрямую peer'у, а не на сервер. Когда RTCPeerConnection peer'а B получает этот запрос, он анализирует исходный адрес и добавляет новый peer-рефлексивный кандидат в свой чек-лист. Peer-рефлексивные кандидаты – это способ, с помощью которого ICE обнаруживает отображения, возникающие в ходе звонка за симметричными NAT с привязкой к целевому порту. Настраивать их не нужно – они появляются автоматически как побочный эффект проверки соединения.
Relay-кандидат, сокращённо relay, – это транспортный адрес, выделенный сервером TURN. Устройство отправляет запрос на выделение (allocate request), сервер резервирует публичный адрес и порт для эксклюзивного использования устройством, после чего устройство добавляет этот адрес в качестве relay-кандидата. Relay-кандидаты – это вариант «последнего шанса», поскольку весь медиапоток проходит через сервер TURN в обоих направлениях: задержка увеличивается вдвое (два round-trip до сервера), а трафик оплачивает пользователь.
Четыре типа кандидатов имеют разные приоритеты в алгоритме ICE. RFC 8445 §5.1.2.2 определяет type preference – целое число от 0 до 126 – и даёт рекомендацию по реализации: host – 126, peer-reflexive – 110, server-reflexive – 100, relay – 0. Ноль для relay – сознательный выбор: он гарантирует, что пары relay–relay окажутся в самом низу приоритетного списка и будут использоваться только в крайнем случае. Внутри каждого типа кандидаты дополнительно упорядочиваются по local preference (обычно проводной Ethernet предпочтительнее Wi-Fi, а тот – сотовой сети) и по component ID (компонент 1 RTP предпочтительнее компонента 2 RTCP, если они не мультиплексированы).
Полная формула приоритета из RFC 8445, раздел 5.1.2.1:
priority = (2^24) × type_preference
+ (2^8) × local_preference
+ (2^0) × (256 - component_id)Host-кандидат на проводном Ethernet с local preference 65535 и компонентом 1 получает приоритет (2^24 × 126) + (2^8 × 65535) + 255 = 2_130_706_431. Relay-кандидат на том же интерфейсе с теми же параметрами получает (2^24 × 0) + (2^8 × 65535) + 255 = 16_776_960. Отношение примерно 127:1 – ровно то, чего требует алгоритм: host-кандидаты доминируют с большим запасом, а relay-кандидаты конкурируют только между собой.
Как сбор кандидатов разворачивается во времени
В учебнике сбор кандидатов выглядит последовательным – сначала host, потом STUN, потом TURN. На практике браузер совмещает всё, что можно совместить, и тайминг важен, потому что пользователь смотрит на спиннер «соединяемся…».
Когда JavaScript вызывает pc.createOffer(), а затем pc.setLocalDescription(offer), браузер начинает сбор данных. Для каждого сетевого интерфейса, о котором сообщила операционная система, браузер:
- Немедленно создаёт host-кандидата (обратный путь не требуется) и генерирует событие icecandidate.
- Отправляет STUN-запрос на привязку каждому STUN-серверу из конфигурации iceServers. Каждый полученный ответ становится server-reflexive-кандидатом и вызывает ещё одно событие icecandidate.
- Отправляет TURN-запрос на выделение ресурса каждому TURN-серверу из iceServers. Успешное выделение ресурса превращает его в relay-кандидата.
Код приложения отслеживает эти события:
pc.addEventListener('icecandidate', (event) => {
if (event.candidate) {
// Отправляем этого одного кандидата другому peer через сигнальный канал.
signaling.send({ type: 'ice-candidate', candidate: event.candidate });
} else {
// event.candidate === null означает конец сбора.
// Большинство современного кода на это не опирается — Trickle ICE обрабатывает инкрементальные кандидаты.
}
});На принимающей стороне peer добавляет каждого кандидата по мере поступления:
signaling.on('ice-candidate', async ({ candidate }) => {
try {
await pc.addIceCandidate(candidate);
} catch (err) {
// Частая причина: кандидат пришёл раньше setRemoteDescription.
// Поставьте кандидата в очередь и проиграйте после setRemoteDescription.
}
});Перекрытие во времени выглядит следующим образом. В момент 0 оба пира создают офферы и начинают сбор кандидатов. К ≈ 5 мс генерируются и отправляются через сигналинг хост-кандидаты. К ≈ 50–200 мс приходят серверные рефлексивные кандидаты от STUN. К ≈ 100–500 мс поступают релейные кандидаты от TURN. Проверки связности запускаются сразу, как только становится известна хотя бы одна пара – обычно это первый хост-кандидат с каждой стороны – и первая валидная пара (чаще всего host-host или host-srflx) устанавливается примерно за 300 мс. Индикатор «соединяемся…» исчезает задолго до завершения выделения ресурсов на TURN.
Это перекрытие – главная инженерная идея Trickle ICE, описанная в спецификации RFC 8838 (январь 2021). Без Trickle браузер ждал бы каждого кандидата, прежде чем отправить хоть одного, добавляя 200–500 мс лишней задержки к каждому соединению. С Trickle первая валидная пара выигрывает сразу после появления, а остальные кандидаты проверяются в фоне – на случай, если найдётся более удачный путь.
Текущее состояние Trickle ICE: все современные браузеры (Chrome 122+, Firefox 125+, Safari 17+) используют трикл по умолчанию. Все современные SFU-стэки (mediasoup, LiveKit, Janus, Pion, Jitsi) также используют трикл по умолчанию. Ветки без трикла остались в основном для совместимости с SIP-шлюзами легаси-корпоративных PBX, которые появились до этого RFC. Если вы разрабатываете WebRTC-продукт в 2026 году, можно считать, что Trickle ICE работает, и писать простой код: «отправляем каждого кандидата по мере его появления»; если интегрируетесь с SIP-шлюзом, проверьте документацию шлюза на поддержку a=ice-options:trickle и подготовьте fallback.
Проверка подключения – снова STUN, теперь peer-to-peer
Пара кандидатов становится «валидной», когда одна сторона отправляет STUN binding request другой стороне и получает в ответ соответствующий binding response с ожидаемого адреса. Формат STUN-сообщения такой же, как в клиент-серверной модели: 20-байтовый заголовок с magic cookie 0x2112A442 и transaction ID, – однако запрос передаётся по transport-адресу пары кандидатов, а не на STUN-сервер.
Запрос содержит три важных атрибута. USERNAME – это конкатенация ufrag удалённого peer’а и локального ufrag, разделённых двоеточием. Ufrag и пароль передаются в SDP offer/answer (a=ice-ufrag: и a=ice-pwd:) – полный контекст SDP см. в подробном разборе SDP offer/answer. MESSAGE-INTEGRITY – HMAC-SHA1 от всего сообщения, подписанный паролем удалённого peer’а. Таким образом каждая сторона доказывает, что знает секрет из SDP, предотвращая атаки типа off-path injection. PRIORITY – приоритет локального кандидата, отправляющего проверку.
Темп определяется таймером Ta, по умолчанию – 50 мс, как указано в RFC 8445 §14.2. Каждые 50 мс управляющий агент выбирает из своего списка проверку (checklist) пару с наивысшим приоритетом, находящуюся в состоянии «frozen» или «waiting», и отправляет запрос на установление связи (binding request). Первый полученный ответ помечает эту пару как «succeeded». Если у управляющего агента появляется пара с более высоким приоритетом, чем текущая выбранная (selected pair), ICE заменяет текущую пару, и трафик начинает передаваться по более качественному пути. Роль controlling/controlled определяется STUN-атрибутами ICE-CONTROLLING и ICE-CONTROLLED; в WebRTC инициатор (offerer) выступает в роли controlling, а отвечающий (answerer) – в роли controlled, согласно RFC 9429 §3.5.5.
Проверка двусторонняя. Пара становится выбранной, только если обе стороны успешно прошли проверку. Этот четырёхэтапный обмен данными важен, потому что асимметричный NAT, нарушающий обратное соединение, может находиться у любой из сторон.
Когда первая выбранная пара начинает передавать медиа, iceconnectionstate переходит из checking в connected. Когда все пары в списке завершены и больше проверок не ожидается, состояние переходит в completed. Большинство приложений обрабатывают connected и completed одинаково; разница имеет значение только для управляющего агента, который может продолжать поиск лучших пар.
TURN в WebRTC – учётки, lifetime и каналы, о которых вы, возможно, не хотели знать
В WebRTC у протокола TURN есть три эксплуатационные особенности, с которыми каждая команда сталкивается впервые.
Учётки короткоживущие и подписаны HMAC. Механизм «long-term credential» из RFC 8489 – статические логин и пароль – теоретически допустим, но на практике неприемлем: утечка учётных данных позволяет любому использовать ваш TURN-кластер как бесплатный egress. Индустриальный стандарт WebRTC, принятый всеми крупными TURN-серверами, – паттерн time-bounded credential: сервер приложения выдаёт клиенту логин вида <unix-timestamp>:<user-id> и пароль base64(HMAC-SHA1(static-secret, username)). TURN-сервер хранит тот же HMAC-ключ static-secret, пересчитывает HMAC по переданному логину и принимает клиента, если хеш совпадает. Логин содержит временную метку истечения срока действия; TURN-сервер отклоняет запросы с устаревшей временной меткой. Утечка учётных данных становится бессмысленной уже через несколько минут.
Клиентский код, обращающийся к вашему эндпоинту для получения учётных данных:
async function fetchIceServers() {
const response = await fetch('/api/turn-credentials');
const { iceServers } = await response.json();
return iceServers;
}
const pc = new RTCPeerConnection({
iceServers: await fetchIceServers(),
iceTransportPolicy: 'all', // или 'relay' — см. ниже
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});Серверная функция выдачи учётной записи (Node.js, десять строк):
import crypto from 'crypto';
function mintTurnCredentials(userId, ttlSeconds = 600) {
const username = `${Math.floor(Date.now() / 1000) + ttlSeconds}:${userId}`;
const hmac = crypto.createHmac('sha1', process.env.TURN_STATIC_SECRET);
hmac.update(username);
const password = hmac.digest('base64');
return { username, password, ttl: ttlSeconds };
}TTL в 600 секунд – наиболее распространённое значение по умолчанию; некоторые команды устанавливают 24 часа для длительных встреч, но чем больше TTL, тем шире окно для злоупотреблений при утечке данных. Учётная запись авторизует allocate; после этого TURN-сервер продлевает аллокацию по запросам клиента на обновление, и истечение срока действия учётной записи не прерывает уже установленный звонок.
Аллокации по умолчанию действуют 600 секунд – клиент должен их продлевать. Когда TURN-сервер отвечает на Allocate, в ответе содержится атрибут LIFETIME, по умолчанию равный 600 секундам (RFC 8656 §7.2). Клиент отправляет Refresh каждые LIFETIME / 2 секунд, чтобы аллокация оставалась активной. Браузеры выполняют это автоматически – писать код не требуется. Однако важно понимать: если TURN-сервер перегружен и теряет запросы на обновление, аллокация истекает, и звонок обрывается без предупреждения. Мониторинг количества аллокаций и частоты ошибок при обновлении – первая метрика, которую стоит добавить на дашборд.
Channels и permissions – оптимизация, о которой обычно не нужно думать. RFC 8656 описывает два способа отправки медиа через TURN. Первый – Send-indication, оборачивающая каждый медиа-пакет в STUN-сообщение со встроенным адресом peer’а, – добавляет 36 байт оверхеда на пакет. Второй – channel: клиент один раз биндит канал на peer’а (ChannelBind), после чего использует компактный 4-байтовый заголовок ChannelData для каждого последующего пакета. Channels экономят 32 байта на пакет – при 50 пакетах в секунду на стрим это 1,6 КБ/с на peer’а в одном направлении, умноженные на тысячи параллельных relay-звонков. Все современные WebRTC TURN-клиенты по умолчанию используют channels. Браузер сам биндит канал. Знать стоит: когда вы смотрите пакетный дамп и видите ChannelData вперемешку со STUN – это нормально.
ICE-серверы в `RTCPeerConnection` – конфигурация, имеющая большое значение
Объект конфигурации iceServers – важнейшая настройка в любом WebRTC-продукте. Ошибка в ней приводит к тому, что соединения без предупреждения обрываются на тех сетях, которые имеют наибольшее значение.
Минимально корректная конфигурация в 2026 году:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: [
'turn:turn.example.com:3478?transport=udp',
'turn:turn.example.com:3478?transport=tcp',
'turns:turn.example.com:443?transport=tcp'
],
username: shortLivedUsername,
credential: shortLivedPassword
}
],
iceTransportPolicy: 'all',
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require'
});У каждой из этих настроек своя история. Один STUN никогда не достаточен – STUN-only конфигурация это причина номер один, почему демо работает локально и падает у первого пользователя с мобильного оператора. Всегда включайте TURN. Всегда выставляйте TURN на UDP/3478, TCP/3478 и TLS/443. Путь UDP – самый дешёвый. Путь TCP покрывает сети, блокирующие UDP. Путь TLS/443 покрывает сети, блокирующие всё, кроме HTTPS – большинство корпоративных firewall'ов, университетских сетей и агрессивных captive portals подпадают сюда. Браузер пробует все три в порядке приоритета и выбирает самый дешёвый, который работает.
iceTransportPolicy: 'all' указывает браузеру собирать все типы кандидатов. Альтернатива 'relay' ограничивает сбор только relay-кандидатами – браузер даже не пытается использовать host- или server-reflexive пути. Используйте 'relay' в приватных продуктах, где важно скрыть реальные IP-адреса пользователей друг от друга (например, деанонимизирующий чат, телемедицинский сервис с требованиями HIPAA, где необходимо логировать каждое соединение через одну точку). Используйте 'all' во всех остальных случаях, поскольку стоимость принудительного 100%-го релея эквивалентна стоимости 100%-го relay-развёртывания – она в пять раз выше базового уровня в 15–20%.
bundlePolicy: 'max-bundle' указывает браузеру объединять каждый трек (аудио, видео, экран и данные) в один транспорт на уровне ICE. Без этого каждый трек открывает отдельную ICE-сессию, браузер собирает кандидатов пять раз, и установка соединения занимает в пять раз больше времени. С этим – один транспорт передаёт всё через расширение BUNDLE из RFC 9143. Всегда max-bundle.
rtcpMuxPolicy: 'require' мультиплексирует RTP и RTCP на один порт. Без него ICE создаёт два транспорта на трек – один для медиа, другой для контроля. С ним – один. Всегда require.
В массиве iceServers может быть любое количество записей. Несколько STUN-серверов обеспечивают резервирование. Несколько TURN-регионов снижают задержку для географически распределённой аудитории: TURN-сервер во Франкфурте не должен релеить звонок между двумя телефонами из Нью-Йорка, если в Нью-Йорке есть свой TURN. Браузер собирает кандидатов со всех серверов параллельно; формула приоритета помещает relay-кандидатов из региона с наименьшим RTT в начало списка relay-тов.
ICE restart – что происходит при смене сети посреди звонка
Пользователь подключается к видеозвонку по домашнему Wi-Fi. Посредине выходит из зоны, и телефон передаёт соединение на 5G. IP-адрес телефона меняется. NAT-mapping, что был на Wi-Fi, мёртв. Все существующие пары ICE-кандидатов невалидны. Без ICE restart звонок падает; с ним пере-согласование незаметно, и пользователь не замечает.
ICE restart инициируется controlling-агентом – в WebRTC это offerer – который генерирует новый ufrag и пароль, а также формирует свежий SDP offer с a=ice-ufrag: и a=ice-pwd:, отличающимися от предыдущего. Другой peer обнаруживает новые учётные данные в setRemoteDescription, собирает обновлённый набор кандидатов и запускает новый connectivity check. Текущая selected pair продолжает передавать медиа, пока новая пара не станет валидной, после чего происходит переключение на неё.
В коде приложения ICE restart запускается через { iceRestart: true } в createOffer:
async function restartIce() {
const offer = await pc.createOffer({ iceRestart: true });
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: pc.localDescription });
// Другой peer отвечает answer'ом; запускается новый сбор; выбирается новая пара.
}Когда код приложения запускает ICE restart? Три типичных случая. Событие смены сети. Браузер генерирует событие networkchange или connectionchange, когда операционная система сообщает об изменении состояния сетевого интерфейса. Код приложения отслеживает это и проактивно вызывает restartIce(). Состояние ICE перешло в disconnected. iceconnectionstate переходит в disconnected, когда пробы проверки актуальности согласия (consent freshness) начинают отказываться; если состояние остаётся в disconnected более 5 секунд, приложение инициирует перезапуск. Состояние ICE перешло в failed. Это происходит, когда все пары в списке проверок (checklist) завершаются неудачей. На этом этапе ICE считается неработоспособным, и перезапуск обязателен – без него соединение будет потеряно.
Современные браузеры (Chrome 122+, Firefox 125+, Safari 17+) реализуют более агрессивный подход: «ICE restart при каждом re-неготиации» – любой offer/answer с новыми треками также запускает рестарт ICE. Такой подход консервативнее, чем требует спецификация, но упрощает ментальную модель приложения: каждое пере-согласование даёт свежее состояние ICE.
Удобный метод RTCPeerConnection.restartIce() добавлен в рекомендацию W3C WebRTC (март 2023) и поддерживается всеми современными браузерами. Явного createOffer({ iceRestart: true }) не требуется: вызов restartIce() сразу генерирует negotiationneeded, а существующий механизм перенеготиации в приложении выполняет остальную работу.
Consent freshness – как держать путь живым
Звонок подключился, пользователь начал говорить, медиа течёт. На двенадцатой минуте корпоративный firewall, за которым сидит пользователь, решает почистить NAT-mapping, по которому 30 секунд не было активности, и тихо его удаляет. Следующий медиа-пакет от удалённого peer'а тихо дропается. Пользователь не слышит ничего. Экран удалённого peer'а замирает. Без механизма обнаружения этого приложение не знает, что что-то пошло не так.
Этот механизм – consent freshness, описанный в спецификации RFC 7675 (октябрь 2015). Каждые 15 секунд каждая сторона отправляет STUN-запрос на привязку по текущей выбранной паре и ожидает ответ в течение 30 секунд. Если ответ приходит – согласие считается актуальным, пара остаётся активной. Если три последовательных запроса завершаются таймаутом, ICE помечает пару как неработоспособную, iceconnectionstate переходит в disconnected, и в приложении срабатывает логика перезапуска.
Механизм consent freshness позволяет обнаружить, что партнёр (peer) отключился без вызова close(). Если у peer A выдернули сетевой кабель, проба consent freshness от peer B к A не проходит, B понимает, что A отключился, и очищает свою сессию.
Код приложения должен отслеживать iceconnectionstatechange и анализировать переходы:
pc.addEventListener('iceconnectionstatechange', () => {
switch (pc.iceConnectionState) {
case 'disconnected':
// Consent freshness падает. Может быть транзитный сбой — подождём несколько секунд.
scheduleIceRestartAfter(5000);
break;
case 'failed':
// Все пары провалились. Restart обязателен.
restartIce();
break;
case 'connected':
case 'completed':
// Восстановились. Отменяем pending restart.
cancelScheduledIceRestart();
break;
}
});Частая ловушка: приложения трактуют disconnected как фатальное состояние и сразу разрывают соединение. На самом деле в примерно 80% случаев состояние восстановимо – транзитная потеря пакетов, кратковременный беспроводной handover, двухсекундный сотовый dropout. Правильная реакция: подождать 5 секунд перед перезапуском и 10 секунд перед тем, как объявить звонок потерянным.
Сигнальный канал – что должно передавать ваше приложение
ICE не определяет протокол сигнализации. RFC 8445 полностью оставляет вопрос «как кандидаты доберутся до другого пира» на усмотрение приложения. WebRTC-продукты используют то, что у них уже есть: WebSocket к Node.js, gRPC-стрим, HTTP long-poll, кастомный протокол поверх MQTT. Транспорт не важен – важно, чтобы каждый ICE-кандидат и каждый offer/answer надёжно и в правильном порядке дошли до другой стороны.
Минимальный протокол включает пять типов сообщений:
// 1. Offer — начальный SDP от controlling peer
{ type: 'offer', from: 'alice', to: 'bob', sdp: '...' }
// 2. Answer — ответный SDP от controlled peer
{ type: 'answer', from: 'bob', to: 'alice', sdp: '...' }
// 3. ICE-кандидат — один из многих, trickled по мере сбора
{ type: 'ice-candidate', from: 'alice', to: 'bob', candidate: '...' }
// 4. End-of-candidates — сигнализирует завершение сбора
{ type: 'end-of-candidates', from: 'alice', to: 'bob' }
// 5. Hangup — peer закрывает сессию
{ type: 'hangup', from: 'alice', to: 'bob' }Единственная задача сигнального сервера – пересылать сообщения между пиринговыми узлами; он не интерпретирует SDP и не обрабатывает ICE-кандидатов. На практике каждый сигнальный сервер также выполняет аутентификацию, управление членством в комнатах, отслеживание присутствия и передачу собственных сообщений приложения – например, чат, реакции, поднятые руки – но это задачи приложения, а не WebRTC.
Для стандартизированного случая WHIP/WHEP – когда одна из сторон является медиасервером (например, live-энкодер, отправляющий данные на ingest-сервер, или зритель, подключающийся к SFU) – протокол сигнализации на основе HTTP стандартизирован в RFC 9725 (WHIP, март 2025) и драфте WHEP. Подробнее о паттерне сигнализации, специфичном для WHIP, включая обмен учётными данными TURN через один HTTP POST, читайте в нашем подробном разборе WHIP.
Рабочий пример – сайзинг TURN для 5 000 мест на B2B-конференции
Команда прорабатывает инфраструктуру B2B-конференцпродукта с расчётным пиком в 5 000 одновременных встреч, в среднем по 4 участника на встречу. На каждого участника требуется 1,5 Мбит/с видео и 50 кбит/с аудио в каждом направлении. Показатель relay-rate зависит от аудитории: для потребительских продуктов он составляет 15–20%, для B2B с тяжёлым корпоративным трафиком – 30–40%. Примем значение 35%.
Шаг 1: общее число участников и трафик на одного. 5 000 × 4 = 20 000 участников. Uplink на участника: 1,55 Мбит/с. Downlink на участника: 3 × 1,55 = 4,65 Мбит/с (в 4-человеческом SFU mesh каждый получает три потока).
Шаг 2: участники, чей трафик проходит через TURN. 20 000 × 35% = 7 000 – через ретрансляцию.
Шаг 3: полоса TURN на relay-уровне. Для каждого relay-участника TURN видит полный uplink и downlink. Uplink на TURN: 7 000 × 1,55 Мбит/с = 10,85 Гбит/с. Downlink на TURN: 7 000 × 4,65 Мбит/с = 32,55 Гбит/с. Итого на TURN: 43,4 Гбит/с – устойчивая нагрузка на пике.
Шаг 4: месячный egress. 43,4 Гбит/с × 86 400 с/день × 30 дней × 12,5% (коэффициент busy hour к среднему значению) ≈ 14 000 ТБ/мес = 14 ПБ/мес. (Множитель 12,5% – это отношение пропускной способности в часы пик к средней за сутки для B2B-трафика в рабочее время. У потребительских сервисов с вечерними пиками этот коэффициент составляет 20%. У круглосуточных промышленных систем – 100%.)
Шаг 5: стоимость у hyperscaler. Тариф на исходящий трафик AWS EC2 в 2026 году – около $0,04 за ГБ после применения объёмных скидок. При объёме 14 000 ТБ ежемесячная стоимость составит примерно $560 000.
Шаг 6: стоимость на bare-metal колокейшен с peering. Транзит на bare-metal по 95-му перцентилю – около $0,50–$1,00 за Мбит/мес. Пиковая нагрузка 43,4 Гбит/с ≈ $20 000–$40 000 в месяц на транзит, плюс аренда стойки и амортизация оборудования.
Шаг 7: стоимость у managed TURN-провайдера. Cloudflare Realtime TURN по $0,05/ГБ (апрель 2026): 14 000 ТБ × $0,05/ГБ = $700 000/мес – цены сопоставимы с hyperscaler, но с anycast-покрытием в 330+ городов. Twilio при объёмном контракте находится в той же ценовой зоне.
Разрыв в 15–20 раз между стоимостью egress у hyperscaler и bare-metal колокейшеном – главная причина, по которой каждый значимый WebRTC-продукт в итоге переносит TURN-серверы с hyperscaler. Большинство команд проходят один и тот же путь: прототип на одном coturn в AWS EC2 → рост до многорегионального кластера coturn → анализ egress-расчётов → переход на managed-решение (Cloudflare, Twilio, Daily.co, Subspace) до тех пор, пока внутренняя инфраструктурная зрелость не достигнет нужного уровня → и, наконец, развертывание bare-metal кластера в двух-трёх точках колокейшена с приватными interconnect к региональным операторам.
Для ИИ-голосового агента, где 100% трафика проходит через TURN (одна сторона – сервер без публичного IP), та же арифметика даёт примерно в 3 раза больше полосы на участника (только одно направление, но каждый байт учитывается) и примерно в 3 раза выше стоимость. Именно такая схема развёртывания превратила TURN-пропускную способность в экзистенциальную статью расходов для продуктов AI-voice в 2025–2026 годах.
Выбор TURN-сервера в 2026 – четыре паттерна для продакшена
Каждая команда, работающая с WebRTC, рано или поздно сталкивается с выбором решения для TURN: managed, self-hosted, hybrid или cloud-native. У каждого из этих подходов – своя кривая затрат, свой операционный профиль и степень соответствия уровню зрелости команды.
Паттерн 1: managed TURN как услуга (TURN-as-a-service). Twilio Network Traversal Service, Cloudflare Realtime TURN, Xirsys, Subspace, ExpressTURN. Оплата производится за гигабайт переданных данных; провайдер берёт на себя управление кластером. Минимальная цена в 2026 году – $0,05/ГБ (Cloudflare). Лучше всего подходит для: продуктов с аудиторией до 10 K MAU, команд без выделенных инженеров по инфраструктуре, решений, которым с самого старта требуется глобальное anycast-покрытие. Хуже всего подходит для: продуктов, приближающихся к объёму в 1 ПБ/мес передаваемого трафика, где маржинальная стоимость релея начинает доминировать в валовой марже.
Паттерн 2: self-hosted coturn или eturnal. Традиционный вариант по умолчанию. coturn – самый функциональный open-source TURN-сервер в 2026 году, он входит в состав каждого крупного SFU-стека: LiveKit, mediasoup, Janus и используется в большинстве корпоративных видеоконференций. Версия coturn v4.8.0 (январь 2026) включает улучшенную защиту от DDoS, настраиваемые размеры буферов сокетов, исправление уязвимости CVE-2025-69217 и ускоренную валидацию пакетов. eturnal от ProcessOne – активно поддерживаемая альтернатива с более чистым конфигурационным интерфейсом и более отзывчивой командой разработчиков. Оба решения работают на стандартной Linux-инфраструктуре.
Лучше всего подходит для: команд, в которых есть хотя бы один инженер по инфраструктуре, владеющий iptables, настройкой сетевого стека Linux и ротацией дежурств в PagerDuty. Хуже всего подходит для: команд, не готовых брать на себя нагрузку по on-call поддержке.
Паттерн 3: cloud-native Pion TURN на Kubernetes. Pion – это Go-библиотека для реализации TURN-клиентов и серверов; STUNner – это нативное для Kubernetes развёртывание TURN-кластера за балансировщиком нагрузки на основе Pion. Подходит лучше всего для команд, которые уже используют Kubernetes повсеместно и хотят, чтобы TURN интегрировался в ту же операционную модель. Go-рантайм обеспечивает меньший объём потребления памяти по сравнению с coturn при высоких объёмах выделений; нативное развёртывание в Kubernetes позволяет использовать автоматическое масштабирование, которого у coturn нет из коробки.
Паттерн 4: hybrid – managed для fallback, self-hosted для primary. Растущий тренд 2026 года: развертываем self-hosted coturn или Pion в качестве основного (primary) и настраиваем iceServers так, чтобы managed-провайдер был третьей или четвёртой записью. Если основной кластер недоступен или регион выходит из строя, браузер автоматически переключается на managed-решение. Стоимость: вы платите провайдеру только за трафик, переданный через relay – обычно это менее 1% от общего объёма. Выгода: вы не теряете связь, если TURN-сервер в одном из регионов падает.
Рамка решения для выбора между четырьмя вариантами:
| Критерий | Managed | Self-hosted coturn | Pion/STUNner на K8s | Hybrid |
|---|---|---|---|---|
| Время до первого звонка | 1 день | 1 неделя | 1–2 недели | 1–2 недели |
| Стоимость при 100 ТБ/мес | $$$$ | $ | $ | $ + малое $$ |
| Стоимость при 10 ПБ/мес | $$$$$ | $$ | $$ | $$ + малое $$ |
| Дежурная нагрузка | нет | высокая | средняя | низкая |
| Глобальный anycast | встроен | DIY | DIY | встроен |
| Лучше всего для | < 10K MAU | зрелая инфра-команда | Kubernetes-native | belt-and-braces |
Восемь продакшен-ловушек и как их избежать
Ловушка 1: STUN-only ICE-серверы. Самая распространённая ошибка настройки WebRTC. Продукт работает в локальной Wi-Fi-сети, но беззвучно падает на всех мобильных операторах с симметричным CGNAT. Всегда используйте хотя бы один TURN в iceServers. STUN-only – это демонстрация, а не готовое решение.
Ловушка 2: TURN только по UDP. Корпоративные фаерволы, сети отелей и агрессивные captive-порталы полностью блокируют UDP. URL turn:host:3478?transport=udp недоступен ни в одной из этих сетей. Всегда делай доступными turn:host:3478?transport=tcp и turns:host:443?transport=tcp (TLS поверх TCP на порту 443) вместе с UDP-записью. Браузер сам выберет наиболее подходящий и работающий вариант.
Ловушка 3: долгоживущие TURN-учётки в клиентском JavaScript. Некоторые туториалы до сих пор хардкодят username и credential прямо в коде браузера. Любой, у кого есть доступ к DevTools, сможет их извлечь и использовать ваш TURN-кластер как бесплатный egress. Генерируйте на сервере короткоживущие учётные записи с HMAC-SHA1, возвращайте их через аутентифицированный API-эндпоинт и обновляйте по мере истечения их TTL в ходе звонка.
Ловушка 4: игнорирование iceconnectionstatechange. Код приложения, который никогда не слушает disconnected или failed, не может восстановиться после сетевого сбоя. Пользователь вышел из зоны Wi-Fi, звонок умер, UI бесконечно показывает «connected», хотя медиа не течёт. Всегда обрабатывайте переходы.
Ловушка 5: рестарт ICE на каждый транзитный disconnect. Зеркало ловушки 4. Код, вызывающий restartIce() при переходе в disconnected, провоцирует бурю переподключений при каждом кратковременном сбое в беспроводной сети. Подождите 5 секунд перед рестартом – около 80% разрывов соединения проходят сами.
Ловушка 6: один регион TURN. Один TURN-кластер в us-east-1 добавляет 150 мс односторонней задержки каждому европейскому звонку. К моменту, когда выбирается relay-путь (поскольку прямые соединения не работают для корпоративного пользователя за файрволом), суммарная задержка достигает 300+ мс односторонней – и качество связи ухудшается на всём протяжении звонка. Размещайте TURN в каждом регионе, где находятся пользователи. Три региона (US-East, EU-West, AP-Southeast) охватывают 95% глобального потребительского трафика; пять регионов (плюс US-West и AP-South) – 99%.
Ловушка 7: непланирование 100%-relay-сценария. Продукт, добавляющий ИИ-голосового агента или SIP-шлюз, внезапно начинает релеить 100% звонков – ведь одна из сторон – это сервер без публичного IP-адреса. Счёт за TURN утраивается за одну ночь. Этот сценарий нужно просчитать до внедрения фичи: архитектурная диаграмма и модель затрат должны чётко отображать долю релеев.
Ловушка 8: доверие iceTransportPolicy: 'relay' как гарантии анонимности. Установка iceTransportPolicy в 'relay' направляет каждый байт трафика через TURN, скрывая ваш реальный IP-адрес от пира – но сам TURN-сервер видит оба IP, а сервер приложения, выдающий учётные записи для TURN, как правило, ведёт логи сопоставлений. Если требуется настоящая сетевая анонимность (например, для анонимного чата), iceTransportPolicy: 'relay' необходим, но недостаточен; важно также контролировать, что именно логирует ваш TURN.
Где здесь Фора Софт
ICE, STUN и TURN лежат в основе каждого WebRTC-проекта, который мы реализуем – видеоконференции, телемедицина, e-learning, браузерные просмотрщики видеонаблюдения, ИИ-голосовые агенты и интерактивный live shopping – всё это зависит от стабильной работы этого сетевого слоя в условиях реальных, зачастую враждебных сетей. С 2014 года мы эксплуатируем coturn-кластеры в продакшене, развёртываем STUNner на базе Pion в Kubernetes для cloud-native клиентов, подключаем управляемых TURN-провайдеров (Twilio, Cloudflare Realtime, Xirsys) как резервные каналы для команд, которым с самого начала требуется глобальное покрытие, и строим сервисы выдачи учётных данных, размещаемые перед каждым продакшен-кластером TURN. Жёсткие уроки – когда перезапускать ICE, как масштабировать TURN-кластер под B2B и потребительские продукты, как поддерживать 100%-relay-режим для ИИ-голосовых агентов – приходят из практики, а не из RFC.
Ключевые выводы
- ICE – это конечный автомат, конвейер сбора кандидатов и протокол проверки связности, работающие параллельно внутри RTCPeerConnection.
- Четыре типа кандидатов – host, server-reflexive, peer-reflexive, relay – имеют приоритеты согласно RFC 8445: 126, 100, 110 и 0; кандидат типа host доминирует с соотношением 127:1.
- Trickle ICE (RFC 8838) сокращает время установления соединения с 2–3 секунд до менее чем 500 мс, объединяя сбор, сигнализацию и проверку.
- Учётные записи TURN должны быть короткоживущими HMAC-SHA1-подписями, выдаваемыми на сервере; долгоживущие учётки, хранящиеся в клиентском JavaScript, являются серьёзной уязвимостью.
- Всегда включайте URL'ы turn:, turn:transport=tcp и turns:port=443 – браузер сам выберет наиболее эффективный, который работает.
- Следите за iceconnectionstatechange; перезапускайте ICE при возникновении failed, а перед перезапуском при disconnected дождитесь 5 секунд.
- Полоса пропускания TURN доминирует в структуре расходов на масштабе: managed-услуги стоят около $0,05 за гигабайт; размещение на bare-metal снижает маржинальную стоимость в 15–20 раз.
Что почитать дальше
- SDP offer/answer подробно – протокол сигнализации, передающий ICE-кандидатов и учётные данные.
- SFU vs MCU vs Mesh: три топологии WebRTC – как ICE работает по-разному в трёх топологиях WebRTC.
- Безопасность WebRTC: DTLS, SRTP, fingerprints, identity – криптографический слой, расположенный поверх ICE.