Привет, друзья!


Представляю вашему вниманию первую часть перевода этой замечательной книги по WebRTC. Данная часть посвящена тому, что такое WebRTC, процессу сигнализации и установки соединения (первые 3 части оригинала).


Справедливости ради следует отметить, что на Хабре уже публиковался "вольный" перевод первых 2 частей оригинала (часть 1, часть 2), но автор по какой-то причине решил не продолжать. Я, свою очередь, решил начать с самого начала, без лишних вольностей и сокращений.


Если вам это интересно, прошу под кат.


Содержание этой части



Что такое WebRTC?


WebRTC (Web Real-Time Communication — коммуникация в режиме реального времени) — это API (Application Programming Interface — программный интерфейс приложения) и протокол. Протокол WebRTC — это набор правил, позволяющий двум агентам WebRTC (браузерам) вести двунаправленную (bi-directional) безопасную коммуникацию в реальном времени. WebRTC API позволяет разработчикам использовать протокол WebRTC. WebRTC API в настоящее время определен только для JavaScript.


Возможно, вам уже известна другая пара с похожим взаимодействием HTTP и Fetch API. В нашем случае протокол WebRTC — это HTTP, а WebRTC API — это Fetch API.


Протокол WebRTC поддерживается рабочей группой rtcweb IETF. WebRTC API задокументирован в W3C как webrtc.


Зачем изучать WebRTC?


Если попытаться кратко описать особенности WebRTC, получится вот такой список. Притом он не является исчерпывающим, это просто примеры интересных характеристик, с которыми вы встретитесь, изучая WebRTC. Не волнуйтесь, если не знакомы с какими-то терминами, все они будут раскрыты далее:


  • Открытый стандарт.
  • Разные реализации.
  • Доступность в браузерах.
  • Обязательное шифрование.
  • Отображение NAT (NAT Traversal).
  • Перепрофилирование существующих технологий.
  • Контроль перегрузки (congestion control).
  • Низкая задержка (на уровне долей секунды, sub-second latency).

Протокол WebRTC — это собрание других технологий


В процессе установки соединения с помощью WebRTC можно выделить 4 этапа:


  • Сигнализация (signalling).
  • Подключение (установка соединения) (connection).
  • Безопасность (securing).
  • Коммуникация (взаимодействие) (communication).

Переходы между этапами происходят последовательно. Обязательным условием для начала следующего этапа является успешное завершение предыдущего.


Интересный факт о WebRTC — каждый этап состоит из большого количества других протоколов. Для создания WebRTC было объединено множество существующих технологий. В этом смысле WebRTC — это комбинация и конфигурация хорошо известных технологий, появившихся в начале 2000-х годов.


Каждому из перечисленных 4 этапов посвящен отдельный раздел, но для начала давайте рассмотрим их на самом высоком уровне.
Поскольку этапы зависят друг от друга, в дальнейшем это облегчит объяснение и понимание назначения каждого из них.


Сигнализация: как пиры (peers) находят друг друга


При запуске WebRTC агент не знает, с кем и по поводу чего будет происходит коммуникация. И именно сигнализация дает нам прозрачность. Это первый этап и его назначение – подготовка вызова (звонка) (call) для того, чтобы два агента WebRTC могли начать коммуникацию.


Сигнализация осуществляется с помощью существующего протокола SDP (Session Description Protocol — протокол описания сессии). Напомним, что SDP – это текстовый протокол. Каждое сообщение SDP состоит из нескольких пар ключ/значение и содержит список "медиа разделов" (media sections). SDP, которыми обмениваются агенты WebRTC, содержит следующую информацию:
-IP и порты, по которым можно получить доступ к агенту (candidates — кандидаты);


  • какое количество аудио и видео треков хочет отправить агент;
  • какие аудио и видео кодеки поддерживаются каждым агентом;
  • значения, используемые в процессе подключения (uFrag/uPwd);
  • значения, используемые для обеспечения безопасности (certificate fingerprint — отпечаток сертификата).

Обратите внимание, что сигнализация, как правило, происходит вне WebRTC; WebRTC, обычно, не используется для передачи сигнальных сообщений (signalling messages). Для передачи SDP между подключенными пирами могут использоваться такие технологии, как конечные точки REST, веб-сокеты или прокси аутентификации (authentication proxies).


Подключение и отображение NAT с помощью STUN/TURN


В процессе сигнализации агенты WebRTC получают достаточно информации для того, чтобы попытаться выполнить подключение. Для этого используется другая технология под названием ICE.


ICE (Interactive Connectivity Establishment — установка интерактивного соединения) — это еще один протокол, предшествующий появлению WebRTC. ICE позволяет устанавливать соединение между двумя агентами. Агенты могут находиться в одной (локальной) сети или в разных концах света. ICE — это решение для установки прямого соединения без центрального сервера.


Настоящее волшебство — это "отображение NAT" и серверы STUN/TURN. И это все, что вам нужно для коммуникации с агентом ICE, находящимся в другой подсети (subnet).


После успешного подключения ICE WebRTC приступает к установке зашифрованного транспортного канала. Он используется для передачи аудио, видео и других данных.


Обеспечение безопасности канала передачи данных с помощью DTLS и SRTP


После того, как мы установили двунаправленный канал коммуникации (с помощью ICE), нам необходимо сделать этот канал безопасным. Это делается с помощью двух протоколов, также разработанных задолго до появления WebRTC. Первый протокол — это DTLS (Datagram Transport Layer Security — протокол датаграмм безопасности транспортного уровня), который является просто TLS поверх UDP. TLS — это криптографический протокол, который используется для безопасной коммуникации через HTTPS. Второй протокол — это SRTP (Secure Real-time Transport Protocol — используется для безопасной передачи данных в реальном времени).


Сначала WebRTC выполняет рукопожатие (handshake) DTLS с помощью соединения ICE. В отличие от HTTPS, WebRTC не использует центральный орган (central authority) для проверки сертификатов. Вместо этого WebRTC проверяет, что сертификат, переданный через DTLS, совпадает с отпечатком (fingerprint, мы подробно поговорим об этом в разделе, посвященном безопасности), переданным в процессе сигнализации. В дальнейшем DTLS-подключение используется для передачи сообщений по DataChannel (каналу передачи данных).


Для передачи аудио/видео используется другой протокол под названием RTP (Real-time Transport Protocol — протокол передачи данных в реальном времени). Пакеты, передаваемые по RTP, защищаются с помощью SRTP. Сессия SRTP начинается с извлечения ключей из установленной сессии DTLS.


Поздравляем! Если предыдущие этапы завершились успешно, у нас имеется двунаправленная и безопасная коммуникация. Если соединение между агентами WebRTC является стабильным, можно приступать к обмену данными. К сожалению, в реальном мире мы постоянно сталкиваемся с потерей пакетов и ограниченной пропускной способностью, о чем мы поговорим в следующем разделе.


Коммуникация между пирами через RTP и SCTP


Итак, мы установили безопасное двунаправленное соединение между двумя агентами WebRTC и наконец-то приступаем к коммуникации! Для этого используется два протокола: RTP и SCTP (Stream Control Transmission Protocol — протокол передачи с управлением потоком). RTP используется для передачи медиа, зашифрованного с помощью SRTP, а SCTP — нужен для отправки и приема сообщений по DataChannel, зашифрованных с помощью DTLS.


RTP является довольно минималистичным, но он предоставляет все необходимое для потоковой передачи данных в реальном времени. Гибкость RTP позволяет разработчикам решать проблемы, связанные с задержкой, потерей данных и перегрузкой, самыми разнообразными способами.


Последним протоколом в стеке является SCTP. Он предоставляет множество настроек, связанных с доставкой сообщений. Мы, например, можем пожертвовать надежностью и правильным порядком доставки пакетов данных в пользу низкой задержки доставки. Именно она является критически важной для коммуникации в реальном времени.


WebRTC — коллекция протоколов


На первый взгляд WebRTC может показаться перепроектированным (over-engineered). Но мы можем ему это простить, так как с его помощью мы можем решать большое количество проблем. Гениальность WebRTC заключается в его скромности: он не пытается решать все задачи самостоятельно. Вместо этого, он объединяет множество существующих специализированных технологий в единое целое.


Это позволяет исследовать и изучать каждую часть по-отдельности. Очень подходящее сравнение WebRTC – это оркестратор (orchestrator) большого количества других протоколов.





Как работает WebRTC (API)?


В этом разделе мы поговорим о том, как протокол WebRTC реализован в JavaScript API. Это не подробный обзор, а всего лишь попытка нарисовать общую картину того, что происходит в процессе коммуникации в реальном времени.


new RTCPeerConnection


RTCPeerConnection — "WebRTC-сессия" верхнего уровня. Она объединяет все упомянутые выше протоколы. Выполняется подготовка всех необходимых подсистем, но пока еще ничего не происходит.


addTrack


addTrack создает новый поток данных (stream) RTP. Для этого потока генерируется случайный источник синхронизации (Synchronization Source, SSRC). Поток находится внутри описания сессии (Session Description, SDP) (в медиаразделе), генерируемого createOffer. Каждый вызов addTrack создает новый SSRC и медиараздел.


После установки SRTP-сессии и шифрования, эти медиапакеты начинают передаваться через ICE.


createDataChannel


createDataChannel создает новый SCTP-поток при его отсутствии. По умолчанию SCTP отключен, он включается только при запросе любой стороной канала передачи данных.


После установки DTLS-сессии и шифрования, эти пакеты с данными начинают передаваться через ICE.


createOffer


createOffer генерирует описание локального состояния (local state) сессии, передаваемого удаленному (в значении "находящемуся далеко", remote) пиру.


Вызов createOffer ничего не меняет для локального пира.


setLocalDescription


setLocalDescription фиксирует (commits) запрошенные (произведенные) изменения. addTrack, createDataChannel и аналогичные вызовы являются временными до вызова setLocalDescription. setLocalDescription вызывается со значением, сгенерированным createOffer.


setRemoteDescription


setRemoteDescription – способ информирования локального агента о состоянии удаленных кандидатов. Это сигнализация, выполняемая JavaScript API.


После вызова setRemoteDescription обеими сторонами, агенты WebRTC имеют достаточно информации для начала коммуникации P2P (Peer-To-Peer — равный к равному).


addIceCandidate


addIceCandidate позволяет WebRTC-агенту добавлять дополнительных кандидатов ICE в любое время. Данный интерфейс отправляет кандидата ICE прямо в подсистему ICE и не оказывает никакого другого влияния на общее соединение.


ontrack


ontrack — это функция обратного вызова (callback), которая вызывается при получении RTP-пакета от удаленного пира. Входящие пакеты помещаются в описание сессии, которое передается в setRemoteDescription.


oniceconnectionstatechange


oniceconnectionstatechange — это колбэк, который вызывается при изменении состояния агента ICE. Так мы получаем уведомления об установке и завершении соединения.


onconnectionstatechange


onconnectionstatechange — это комбинация состояния ICE и DTLS. Мы можем использовать этот коллбэк для получения уведомления об успешной установке ICE и DTLS.


Сигнализация


В момент своего создания агент WebRTC ничего не знает о другом пире. Он не имеет ни малейшего представления о том, с кем будет установлено соединение и чем они будут обмениваться. Сигнализация — это подготовка к совершению звонка. После обмена необходимой информацией агенты могут общаться друг с другом напрямую.


Сообщения, передаваемые в процессе сигнализации — это просто текст. Агентам неважно, как они передаются (какой транспорт для этого используется). Как правило, они передаются через веб-сокеты, но это необязательно.


Как это работает?


WebRTC использует протокол SDP. Через него два агента обмениваются состоянием, необходимым для установки соединения. Сам протокол легко читать. Сложность возникает при изучении значений, генерируемых WebRTC.


Этот протокол не является специфичным для WebRTC. Сначала мы рассмотрим SDP в отрыве от WebRTC, а после – его применение в WebRTC.


Что такое протокол описания сессии (Session Description Protocol, SDP)?


Протокол описания сессии определен в RFC 8866. Он состоит из пар ключ/значение. Каждая пара находится на отдельной строке. Он похож на файл INI. Описание сессии состоит из 0 и более описаний медиа (media descriptions). Об описании сессии можно думать как о массиве описаний медиа.


Описание медиа, обычно, относится к определенному потоку медиаданных. Поэтому, если мы хотим описать звонок, содержащий 3 видеопотока и 2 аудиопотока, у нас будет 5 описаний медиа.


Изучение SDP


Каждая новая строка в описании сессии начинается с одного символа — ключа. Затем следует знак равенства. Все остальное (до новой строки) — это значение.


SDP определяет все ключи, которые являются валидными. Для ключей могут использоваться только буквы латинского алфавита. Каждый ключ имеет определенное значение.


Рассмотрим небольшой кусочек описания сессии:


a=first-value
a=second-value

У нас есть 2 строки и они обе начинаются с ключа a. Значением первой строки является first-value, а второй — second-value.


Ключи SDP, используемые в WebRTC


Не все ключи, определенные в SDP, используются в WebRTC. Используются только ключи, фигурирующие в протоколе установки сессии JavaScript (JavaScript Session Establishment Protocol, JSEP), определенном в RFC 8829. Прямо сейчас достаточно понимать следующие 7 ключей:


  • v — версия (version) (0);
  • o — источник (origin), уникальный идентификатор, полезный для повторной установки соединения;
  • s — название сессии (-);
  • t — расчет времени (timing) (0 0);
  • m — описание медиа (m=<media> <port> <proto> <fmt> ...);
  • a — атрибут, свободное текстовое поле. Наиболее часто встречающийся ключ;
  • c — данные о подключении (IN IP4 0.0.0.0).

Медиаописания в описании сессии


Описание сессии может состоять из неограниченного количества описаний медиа.


Определение медиаописания состоит из списка форматов (formats). Эти форматы соответствуют типам полезной нагрузки RTP (RTP Payload Types). Кодек определяется атрибутом со значением rtpmap в описании сессии. Каждое описание медиа может состоять из неограниченного количества атрибутов.


Рассмотрим еще один кусочек описания сессии:


v=0
m=audio 4000 RTP/AVP 111
a=rtpmap:111 OPUS/48000/2
m=video 4000 RTP/AVP 96
a=rtpmap:96 VP8/90000
a=my-sdp-value

У нас есть два описания медиа: одно описывает аудио с форматом 111, другое — видео с форматом 96. Первое описание содержит один атрибут. Этот атрибут определяет (привязывает, maps) тип полезной нагрузки 111 как Opus. Второе описание содержит два атрибута. Первый атрибут определяет тип полезной нагрузки 96 как VP8, второй — содержит кастомное значение my-sdp-value.


Полный пример


В следующем примере представлены все ключи SDP, используемые в WebRTC:


v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
c=IN IP4 127.0.0.1
t=0 0
m=audio 4000 RTP/AVP 111
a=rtpmap:111 OPUS/48000/2
m=video 4002 RTP/AVP 96
a=rtpmap:96 VP8/90000

  • v, o, s, c и t определены, но они не влияют на сессию WebRTC;
  • у нас имеется два описания медиа. Одно с типом audio, другое — video;
  • каждое описание содержит один атрибут. Он определяет детали конвейера (pipeline) RTP.

Как SDP и WebRTC работают вместе


Следующим кусочком пазла является понимание того, как WebRTC использует SDP.


Что такое предложения и ответы?


WebRTC использует модель предложение/ответ (offer/answer). Это означает, что если один агент "предлагает" начать коммуникацию, другой агент "отвечает", хочет он этого или нет.


Это позволяет отвечающему отклонить неподдерживаемые им кодеки, указанные в описаниях медиа. Таким способом пиры определяют, какими форматами данных они будут обмениваться.


Трансиверы


Трансиверы (приемопередатчики, transceivers) — это специфическая для WebRTC концепция, которую вы встретите в API. Их основной задачей является преобразование "описания медиа" в JavaScript API. Каждое описание медиа становится трансивером. При каждом создании трансивера в локальное описание сессии добавляется новое описание медиа.


Каждое описание сессии в WebRTC имеет атрибут, определяющий направление передачи данных (direction). Это позволяет агенту объявлять такие вещи, как, например, "Я собираюсь отправить тебе этот кодек, но не хочу ничего получать в ответ". Валидными значениями направления являются:


  • send (sendonly, sending — отправка);
  • recv (recvonly, receiving — получение);
  • sendrecv (отправка и получение);
  • inactive (неактивное состояние).

Значения SDP, используемые в WebRTC


Ниже представлен список некоторых часто встречающихся атрибутов описания сессии, используемых агентами. Многие из этих значений контролируют подсистемы, которые мы еще не обсуждали (но скоро обсудим).


group:BUNDLE


Сборка (bundling) — это передача нескольких типов трафика через одно соединение (часто это называют "батчингом", batching). В некоторых реализациях WebRTC для каждого медиапотока выделяется отдельное соединение. Сборка является предпочтительной.


fingerprint:sha-256


Это хеш сертификата, используемого пиром для DTLS. После завершения рукопожатия DTLS мы сравниваем хеш с сертификатом для подтверждения того, что мы общаемся с тем, кого ожидаем.


setup:


Контролирует поведение агента DTLS. Определяет, чем является агент (клиентом или сервером), после установки ICE. Возможные значения:


  • setup:active — запуск в качестве клиента DTLS;
  • setup:passive — запуск в качестве сервера DTLS;
  • setup:actpass — просим другого агента WebRTC сделать выбор.

ice-ufrag


Значение фрагмента пользователя (user fragment) для агента ICE. Используется для аутентификации трафика ICE.


ice-pwd


Пароль для агента ICE. Используется для аутентификации трафика ICE.


rtpmap


Используется для определения связи между конкретным кодеком и типом полезной нагрузки RTP. Типы полезной нагрузки являются динамическими, поэтому для каждого вызова его инициатор выбирает типы полезной нагрузки для каждого кодека.


fmtp


Определяет дополнительные значения типа полезной нагрузки. Может использоваться для настройки профиля видео или кодировки.


candidate


Кандидат ICE, полученный от агента ICE. Один из адресов, по которым доступен агент WebRTC.


ssrc


Определяет конкретный трек медиапотока (media stream track).


label — это идентификатор потока. mslabel — идентификатор контейнера, который может содержать несколько потоков.


Пример описания сессии


Полное описание сессии, генерируемое клиентом WebRTC:


v=0
o=- 3546004397921447048 1596742744 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 0F:74:31:25:CB:A2:13:EC:28:6F:6D:2C:61:FF:5D:C2:BC:B9:DB:3D:98:14:8D:1A:BB:EA:33:0C:A4:60:A8:8E
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111
c=IN IP4 0.0.0.0
a=setup:active
a=mid:0
a=ice-ufrag:CsxzEWmoKpJyscFj
a=ice-pwd:mktpbhgREmjEwUFSIJyPINPUhgDqJlSd
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1
a=ssrc:350842737 cname:yvKPspsHcYcwGFTw
a=ssrc:350842737 msid:yvKPspsHcYcwGFTw DfQnKjQQuwceLFdV
a=ssrc:350842737 mslabel:yvKPspsHcYcwGFTw
a=ssrc:350842737 label:DfQnKjQQuwceLFdV
a=msid:yvKPspsHcYcwGFTw DfQnKjQQuwceLFdV
a=sendrecv
a=candidate:foundation 1 udp 2130706431 192.168.1.1 53165 typ host generation 0
a=candidate:foundation 2 udp 2130706431 192.168.1.1 53165 typ host generation 0
a=candidate:foundation 1 udp 1694498815 1.2.3.4 57336 typ srflx raddr 0.0.0.0 rport 57336 generation 0
a=candidate:foundation 2 udp 1694498815 1.2.3.4 57336 typ srflx raddr 0.0.0.0 rport 57336 generation 0
a=end-of-candidates
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=setup:active
a=mid:1
a=ice-ufrag:CsxzEWmoKpJyscFj
a=ice-pwd:mktpbhgREmjEwUFSIJyPINPUhgDqJlSd
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=ssrc:2180035812 cname:XHbOTNRFnLtesHwJ
a=ssrc:2180035812 msid:XHbOTNRFnLtesHwJ JgtwEhBWNEiOnhuW
a=ssrc:2180035812 mslabel:XHbOTNRFnLtesHwJ
a=ssrc:2180035812 label:JgtwEhBWNEiOnhuW
a=msid:XHbOTNRFnLtesHwJ JgtwEhBWNEiOnhuW
a=sendrecv

Вот что мы должны понять из этого сообщения:


  • у нас имеется два медиараздела: один для аудио и один для видео;
  • оба являются трансиверами sendrecv. Мы получаем два потока и можем отправить два потока в ответ;
  • у нас имеются кандидаты ICE и детали аутентификации, что позволяет предпринять попытку установить соединение;
  • у нас имеется отпечаток сертификата, что позволяет сделать звонок безопасным.

Подключение


Большинство разрабатываемых сегодня приложений реализует клиент-серверную архитектуру подключения. Такая архитектура предполагает наличие сервера с известным и стабильным транспортным адресом (transport address) (IP и порт). Клиент отправляет запрос, а сервер на него отвечает.


В WebRTC используется другая архитектура — одноранговая сеть (Peer-to-Peer, P2P). В такой сети задача установки соединения распределяется между пирами. Это обусловлено тем, что транспортный адрес не может быть определен заранее, и может меняться в течение сессии (session). WebRTC собирает всю доступную информацию и делает многое для обеспечения возможности двунаправленной коммуникации (bi-directional communication) между агентами.


Установка такого соединения — задача не из простых. Агенты могут находиться в разных сетях, т.е. не иметь прямого соединения. И даже если оно есть, могут быть другие проблемы. Например, клиенты могут использовать разные протоколы (UPD <-> TCP) или разные версии IP (IPv4 <-> IPv6).


Несмотря на это, WebRTC предоставляет некоторые преимущества по сравнению с клиент-серверной архитектурой.


Уменьшение размера передаваемых данных


Поскольку обмен данными между агентами осуществляется напрямую, нам не нужно "платить" за сервер, предназначенный для ретрансляции (перенаправления) этих данных.


Снижение задержки


Прямая коммуникация является более быстрой. Когда пользователь вынужден отправлять данные через сервер, ретрансляция увеличивает задержку.


Повышение безопасности


Прямая коммуникация является более безопасной. Поскольку сервер не участвует в передаче данных, пользователи могут быть уверены, что данные не будут расшифрованы, пока не достигнут адресата.


Как это работает?


Описанный выше процесс называется установкой интерактивного соединения (Interactive Connectivity Establishment, ICE).


ICE — это протокол, который пытается определить наилучший способ для установки соединения между двумя агентами. Каждый агент публикует (publishes) путь, по которому он может быть достигнут (reachable). Такие пути называются кандидатами (candidates). По сути, кандидат — это транспортный адрес, который один агент считает достижимым для другого агента. Затем ICE определяет наиболее подходящую пару кандидатов.


Для того, чтобы убедиться в необходимости ICE, нужно понимать, какие трудности нам приходится преодолевать на пути установки интерактивного соединения.


Ограничения реального мира


Основное назначение ICE — преодоление ограничений, накладываемых на подключение реальным миром. Кратко обсудим эти ограничения.


Разные сети


В большинстве случаев второй агент будет находиться в другой сети. Как правило, соединение устанавливается между агентами, находящимися в разных сетях.


Ниже представлен граф двух независимых сетей, соединенных вместе через публичный Интернет. В каждой сети у нас имеется два хоста.





Для хостов, находящихся в одной сети, установка соединения — простая задача. Коммуникацию между 192.168.0.1 -> 192.168.0.2 легко организовать. Такие хосты могут общаться друг с другом без посторонней помощи.


Однако, хост, использующий Router B, не имеет возможности общаться с хостами, использующими Router A. Как определить разницу между 192.168.0.1 из Router A и таким же IP из Router B? Эти IP являются частными (закрытыми для внешнего мира, private)! Хост из Router B может отправлять данные в Router A, но запрос завершится ничем. Как Router A определить, какому хосту следует передавать сообщение?


Ограничения протоколов


В одних сетях запрещена передача трафика по UDP, в других — по TCP. Некоторые сети могут иметь очень низкую максимальную единицу передачи данных (Maximum Transmission Unit, MTU). Существует большое количество настроек сетей, которые могут сделать коммуникацию по меньшей мере затруднительной.


Правила межсетевых экранов


Еще одной проблемой является "глубокая проверка пакетов" ("Deep Packet Inspection") и другая фильтрация сетевого трафика. Некоторые сетевые администраторы применяют такое программное обеспечение в отношении каждого пакета. Во многих случаях это ПО не понимает WebRTC, считает пакеты WebRTC подозрительными пакетами UDP, передаваемыми по порту, не входящему в белый список (whitelist).


Отображение NAT


Отображение результата преобразования сетевых адресов (Network Address Translation, NAT) (NAT Mapping) — это магия, которая делает подключение WebRTC возможным. Это то, благодаря чему два пира из разных сетей могут общаться друг с другом. Рассмотрим, как работает отображение NAT.


NAT не использует ретранслятор, прокси или сервер. У нас есть Agent 1 и Agent 2, которые находятся в разных сетях. Несмотря на это, они могут обмениваться данными. Вот как это выглядит:





Для обеспечения возможности подобной коммуникации мы прибегаем к помощи отображения NAT. Agent 1 использует порт 7000 для установки соединения WebRTC с Agent 2. 192.168.0.1:7000 привязывается (bind) к 5.0.0.1:7000. Это позволяет Agent 2 "достигать" Agent 1, отправляя пакеты по адресу 5.0.0.1:7000. Создание отображения NAT похоже на автоматическую версию переадресации портов (port forwarding) в роутере.


Обратной стороной NAT является то, что не существует единой формы отображения (например, статической переадресации портов), в разных сетях поведение может быть различным. Интернет-провайдеры и производители аппаратного обеспечения могут делать это по-разному. В некоторых случаях сетевые администраторы вообще могут его отключить.


Хорошей новостью является то, что все эти особенности мониторятся и учитываются, что позволяет агенту ICE создавать отображение NAT и его атрибуты.


Документом, описывающим данный процесс, является RFC 4787.


Создание отображения NAT


Создание отображения — самая легкая часть. Отображение создается, когда мы отправляем пакет по адресу, находящемуся за пределами нашей сети. Отображение NAT — это просто временные публичные IP и порт, занимаемый нашим NAT. Исходящие сообщения переписываются таким образом, что адресом их источника (source address) становится созданное отображение. При отправке сообщения в отображение, оно автоматически перенаправляется в хост внутри создавшего его NAT. И этот процесс становится сложным, когда речь заходит о деталях создания отображения.


Варианты создания отображения NAT


Создание отображение делится на три категории.


Автономное (не зависящее от конечной точки, endpoint-independent) отображение


Для каждого отправителя внутри NAT создается отдельное отображение. Если мы отправляем два пакета по двум удаленным адресам, для обоих пакетов используется одно и тоже отображение. Оба удаленных хоста будут видеть один и тот же IP и порт источника. При получении ответов от хостов, они передаются в один и тот же локальный обработчик (local listener).


Это лучший сценарий. Для осуществления звонка необходимо, чтобы хотя бы одна сторона была такого типа.


Зависящее от адреса (address dependent) отображение


Новое отображение создается при отправке пакета по новому адресу. Если мы отправляем два пакета в два разных хоста, создается два отображения. Если мы отправляем два пакета в один удаленный хост, но разные порты, создается ОДНО отображение.


Зависящее от адреса и порта отображение


Новое отображение создается, если отличаются удаленный IP или порт. Если мы отправляем два пакета в один удаленный хост, но в разные порты, создается два отображения.


Варианты фильтрации отображения NAT


Фильтрация отображения — это правила, определяющие, кто может использовать отображение. Существует три возможных варианта:


Автономная фильтрация


Отображение может использовать кто угодно. Мы можем делиться им с другими пирами, и они смогут отправлять в него трафик.


Зависящая от адреса фильтрация


Отображение может использовать только создавший его хост. Если мы отправляем пакет в хост A, он может отправить в ответ любое количество пакетов. Если хост B отправит пакет в это отображение, данный пакет будет игнорироваться (will be ignored).


Зависящая от адреса и порта фильтрация


Отображение может использоваться только создавшим его хостом и портом. Если мы отправляем пакет в хост A:5000, он может ответить любым количеством пакетов. Пакет, отправленный хостом A:5001, будет игнорироваться.


Обновление отображения NAT


Рекомендуется уничтожать (destroy) отображение, которое не используется в течение пяти минут, но это зависит от Интернет-провайдеров и производителей "железа".


STUN


Утилиты прохождения сессий для NAT (Session Traversal Utilities for NAT, STUN) — это протокол, предназначенный для работы с NAT. Он определен в RFC 8489, в котором также определяется структура пакетов STUN. Протокол STUN также используется ICE/TURN.


STUN позволяет программно создавать отображения NAT. До STUN мы могли создавать отображения, но не могли получать информацию о созданных IP и портах. STUN не только позволяет создавать отображения, он также предоставляет информацию, которой можно поделиться с другими для того, чтобы они могли передавать данные в отображение.


Начнем с базового описания STUN. Позже мы рассмотрим, как STUN используется в ICE и TURN. Рассмотрим процесс запрос/ответ (request/response) для создания отображения, а также поговорим о том, как получить информацию об IP и портах. Данный процесс происходит, когда у нас есть сервер stun: в путях (urls) ICE для WebRTC PeerConnection (например, new RTCPeerConnection({ iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }] })). STUN помогает конечной точке, находящейся за NAT, получить детали о созданном отображении, путем отправки запроса к серверу STUN о предоставлении информации о том, что он видит извне (observes).


Структура протокола


Каждый пакет STUN имеет следующую структуру:


 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|     STUN Message Type     |         Message Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Magic Cookie                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transaction ID (96 bits)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             Data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN Message Type


Тип пакета. В данный момент нас интересует следующее:


  • запрос на привязку или связывание (Binding Request) — 0x0001;
  • ответ на привязку (Binding Response) — 0x0101.

Для создания отображения NAT мы выполняем Binding Request. Сервер отправляет нам Binding Response.


Message Length


Длина раздела Data (размер данных). Этот раздел содержит данные, определенные в Message Type.


Magic Cookie


Фиксированное значение 0x2112A442 в сетевом порядке байтов (network byte order). Это помогает отличать STUN от других протоколов.


Transaction ID


96-битный идентификатор, уникально определяющий запрос/ответ. Это помогает определять пары запросов и ответов.


Data


Данные содержат список атрибутов STUN. Атрибут STUN имеет следующую структуру:


0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Type                  |            Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Value (variable)                ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN Binding Request не использует атрибуты. Это означает, что в запросе содержится только заголовок (header).


STUN Binding Response использует XOR-MAPPED-ADDRESS (0x0020). Данный атрибут содержит IP и порт из отображения NAT.


Создание отображения NAT


"Цена" создания отображения NAT с помощью STUN — один запрос. Мы отправляем Binding Request серверу STUN. Он возвращает нам Binding Response. Binding Response содержит Mapped Address. Mapped Address — то, как сервер STUN видит отображение NAT. Mapped Address — то, что может использоваться другой стороной для отправки данных в наш адрес.


Mapped Address — это наш Public IP (публичный адрес) или Server Reflexive Candidate в терминологии WebRTC.


Определение типа NAT


К сожалению, Mapped Address может использоваться не во всех случаях. Если NAT является зависящим от адреса, мы можем получать данные только от сервера STUN. В этом случае сообщения, отправленные другой стороной в Mapped Address, будут потеряны (will be dropped). Это делает Mapped Address бесполезным. И проблема эта решается тем, что сервер STUN перенаправляет пакеты к пиру. Такое решение называется TURN.


RFC 5780 описывает метод для определения типа NAT. Это позволяет заранее определить возможность установки прямого соединения.


TURN


Отображение NAT с помощью ретрансляторов (Traversal Using Relays around NAT, TURN), определенный в RFC 8656, это решение проблемы отсутствия возможности прямого соединения. Такое может произойти при несовместимости типов NAT или использовании разных протоколов агентами. TURN также может использоваться для обеспечения конфиденциальности. Создавая коммуникацию с помощью TURN, мы обфусцируем (скрываем) настоящие адреса клиентов.


TURN использует выделенный сервер. Этот сервер является прокси для клиента. Клиент подключается к серверу TURN и создает Allocation (размещение). Ему выделяется временный IP/порт/протокол, который может использоваться для передачи данных. Данный обработчик называется Relayed Transport Address. Это своего рода адрес переадресации (forwarding address), который может использоваться другой стороной для передачи трафика через TURN. Для каждого пира создается отдельный Relayed Transport Address, что требует предоставления Permission (разрешения) на коммуникацию.


При передаче трафика через TURN, он передается через Relayed Transport Address. При получении трафика удаленным пиром, он видит, что трафик пришел от TURN.


Жизненный цикл TURN


Поговорим о том, что должен сделать клиент, который хочет создать размещение в TURN. Коммуникация с тем, кто использует TURN, не требует совершения дополнительных действий. Другой пир просто получает IP и порт, которые могут использоваться для передачи данных.


Размещения


Размещения — ядро TURN. Allocation — "сессия TURN". Для создания размещения в TURN мы обращаемся к Server Transport Address сервер TURN (портом по умолчанию является 3478).


При создании размещения серверу необходимо предоставить следующую информацию:


  • имя пользователя/пароль — создание размещения требует аутентификации;
  • транспорт, используемый размещением — транспортный протокол между сервером (Server Transport Address) и пирами: UDP или TCP;
  • Even-Port — мы можем запрашивать последовательность портов для нескольких размещений, не относящихся к WebRTC.

При успешном запросе мы получаем от сервера TURN ответ со следующими атрибутами в разделе Data:


  • XOR-MAPPED-ADDRESSMapped Address клиента TURN (TURN Client). При отправке данных в Relayed Transport Address, они перенаправляются в этот адрес;
  • RELAYED-ADDRESS — адрес, передаваемый другим клиентам. При отправке пакета по этому адресу, он передается клиенту TURN;
  • LIFETIME — сколько времени осталось до уничтожения размещения TURN. Время жизни размещения может быть увеличено через отправку запроса Refresh.

Разрешения


Удаленный хост не может отправлять данные в наш Relayed Transport Address до тех пор, пока мы не предоставим ему разрешение. При создании разрешения мы говорим серверу TURN, что указанным IP и порту разрешен входящий трафик.


Удаленный хост должен предоставить нам IP и порт в том виде, в каком они отображаются на сервере TURN. Это означает, что он должен отправить STUN Binding Request. Распространенной ошибкой является отправка такого запроса не тому серверу TURN. После отправки запроса удаленный хост обращается к нам с просьбой о предоставлении разрешения.


Предположим, что мы хотим создать разрешение для хоста, находящегося за Address Dependent Mapping. Если мы получим Mapped Address от другого сервера TURN, весь входящий трафик будет потерян. При каждом взаимодействии с другим хостом создается новое отображение. Время жизни разрешения составляет 5 минут.


SendIndication/ChannelData


Эти сообщения предназначены для клиента TURN для отправки сообщений удаленному пиру.


SendIndication — самодостаточное сообщение. Внутри него находятся данные для отправки, а также адресат. Отправка большого количества сообщений удаленному пиру является расточительной. При отправке 1000 сообщений IP адрес удаленного пира будет повторен 1000 раз!


ChannelData позволяет отправлять данные без дублирования IP-адреса. Мы создаем канал (channel) с IP и портом. При отправке сообщения в нем указывается ChannelId, а IP и порт заполняются сервером. Это позволяет уменьшить нагрузку на сервер при отправке большого количества сообщений.


Обновление


Размещения уничтожаются автоматически. Для сохранения размещения его LIFETIME (время жизни) должно периодически обновляться.


Использование TURN


TURN может использоваться в двух формах. Как правило, у нас имеется один пир, выступающий в роли "клиента TURN", и другой сторона, обращающаяся к "клиенту" напрямую. В некоторых случаях может потребоваться использовать TURN на обеих сторонах, например, по причине того, что обе стороны находятся в сетях, блокирующих UDP, поэтому соединение с соответствующими серверами TURN выполняется через TCP.


Одна аллокация TURN для коммуникации





Две аллокации TURN для коммуникации





ICE


ICE — то, как WebRTC подключает двух агентов. Этот протокол определен в RFC 8445. ICE — протокол для установки соединения. Он описывает все возможные маршруты (routes) между двумя пирами и обеспечивает стабильность подключения.


Эти роуты называются Candidate Pairs (парами кандидатов) и представляют собой пару локального и удаленного транспортных адресов. Вот где в игру вступают STUN и TURN. Эти адреса могут быть нашими локальными IP и портом, отображением NAT или Relayed Transport Address. Каждая сторона собирает адреса, которые она хочет (и может) использовать, передает их другой стороне и выполняет попытку подключения.


Два агента ICE взаимодействуют с помощью пинг-пакетов (ping packets) ICE (которые формально называются проверками подключения — connectivity checks) для установки соединения. После установки соединения стороны могут обмениваться данными. Это похоже на использование обычных веб-сокетов. Все необходимые проверки выполняются с помощью протокола STUN.


Создание агента ICE


Агент ICE может быть Controlling (управляющим) или Controlled (управляемым). Управляющий агент — это тот, который выбирает Candidate Pair. Как правило, пир, отправляющий предложение об установке соединения (offer), является управляющей стороной.


Каждая сторона должна иметь user fragment и password. Стороны должны обменяться этими значениями до начала проверок подключения. user fragment отправляется в виде обычного текста и может использоваться для демультиплексирования (demuxing) нескольких сеансов ICE. password используется для генерации атрибута MESSAGE-INTEGRITY. В конце каждого пакета STUN имеется атрибут с хешем содержимого пакета — password используется в качестве ключа. Это позволяет аутентифицировать пакет, т.е. убедиться в том, что он не был подменен или модифицирован в процессе передачи.


В случае с WebRTC эти значения передаются через Session Description (описание сессии), о котором рассказывалось в предыдущем разделе.


Сбор кандидатов (Candidate Gathering)


Теперь нам необходимо собрать все адреса, по которым достижимы агенты. Эти адреса называются кандидатами.


Хост


Кандидат хоста предоставляется локальным интерфейсом. Это может быть UDP или TCP.


mDNS


Кандидат mDNS похож на кандидата хоста, но его IP-адрес скрывается. Вместо IP-адреса другой стороне предоставляется UUID в качестве названия хоста. После этого мы настраиваем многоадресный обработчик (multicast listener), который отвечает на запросы к опубликованному UUID.


Если мы находимся в одной сети с агентом, мы можем найти друг друга через мультикаст (multicast). Если мы и агент находимся в разных сетях, установить соединение не получится. Как минимум, до тех пор, пока администратор сети явно не разрешит такую передачу пакетов.


Это позволяет обеспечить конфиденциальность. В случае с кандидатом хоста пользователь видит наш IP адрес через WebRTC (даже не пытаясь установить соединение), но в случае с кандидатом mDNS он получит только случайный UUID.


Server Reflexive


Кандидат от сервера (Server Reflexive Candidate) генерируется сервером STUN в ответ на STUN Binding Request.


При получении STUN Binding Response содержащийся в нем XOR-MAPPED-ADDRESS — это наш кандидат от сервера.


Peer Reflexive


Кандидат от пира (Peer Reflexive Candidate) — это когда мы получаем входящий запрос от неизвестного адреса. Поскольку ICE — это протокол с аутентификацией, мы знаем, что трафик является валидным. Это всего лишь означает, что удаленный пир общается с нами с неизвестного адреса.


Это обычно происходит при общении Host Candidate с Server Reflexive Candidate. Поскольку мы общаемся за пределами нашей подсети, создается новое отображение NAT. Помните, мы говорили о том, что проверки подключения — это на самом деле пакеты STUN? Формат ответа STUN позволяет пиру вернуть адрес кандидата от пира (peer-reflexive address).


Relay


Релейный кандидат генерируется сервером TURN.


После первоначального рукопожатия (handshake) мы получаем RELAYED-ADDRESS — наш релейный кандидат.


Проверки подключения


Теперь мы знаем user fragment, password и кандидатов удаленного агента. Выполняем попытку подключения! Кандидаты разбиваются попарно. Поэтому если у нас было три кандидата от каждой стороны, мы получаем девять пар кандидатов.


Вот как это выглядит:





Выбор кандидатов


Управляющий и управляемый агенты начинают передавать трафик через каждую пару. Это требуется, когда один агент находится за Address Dependent Mapping, что приводит к созданию Peer Reflexive Candidate.


Каждая Candidate Pair (пара кандидатов), которая "видит" трафик, становится парой Valid Candidate (валидных кандидатов). Управляющий агент берет одного Valid Candidate и выдвигает (nominate) его. Выдвинутые сторонами кандидаты становятся Nominated Pair (номинированной парой). Агенты снова пытаются установить двунаправленную коммуникацию. При достижении успеха, Nominated Pair становится Selected Candidate Pair (выбранной или избранной парой кандидатов). Эта пара используется на протяжении оставшейся части сессии.


Перезагрузка


Если Selected Candidate Pair прекращает работу по какой-либо причине (истек срок жизни отображения NAT, упал сервер TURN) агент ICE переходит в состояние Failed. Оба агента могут быть перезапущены, и процесс начнется сначала.


На этом первая часть перевода завершена.


Благодарю за внимание и happy coding!




Комментарии (5)


  1. Dmitry2019
    23.03.2022 14:34

    Очень познавательно. Как раз сейчас пытаюсь разобраться, как прикрутить WebRTC модуль для GStreamer. NodeJS на сервере должен запустить GStreamer и веб клиент должен увидеть стрим.


    1. robert_ayrapetyan
      23.03.2022 17:20

      К сожалению, основные проблемы, например, синхронизация звука и видео на стороне клиента, мало где описаны.


      1. Dmitry2019
        23.03.2022 17:36

        На сторону клиента идёт RTP стрим. Там всё зависит от SDP конфигурации, которая должна автоматически генерироваться. Если я её руками делаю, то звук и видео по RTP работают нормально. Я не нашёл нормального описания, как сконфигурировать WebRTC бин в GStreamer.


        1. robert_ayrapetyan
          23.03.2022 17:46

          1. Dmitry2019
            23.03.2022 18:19

            Спасибо, посмотрю. Выглядит многообещающе