Secure Call Protocol (SCP)
📋 Changelog v2.1.1 (сентябрь 2026)
- NEW QUIC SFU медиа-сервер (:4001/UDP) — RFC 9000 + RFC 9221 Datagrams, встроенный TLS 1.3 (ALPN
chepuha-media-v1,quic-media), мгновенный stream handshake с проверкой токена без блокировки очереди. - NEW Внутренний RPC аутентификации — эндпоинт
/api/v1/internal/verify-token-onlyна FastAPI для сверхбыстрой неблокирующей валидации сессий медиа-сервером. - NEW Zero-Copy Native Pipeline (Windows & Android) — прямой аппаратный конвейер без промежуточного копирования кадров в память Flutter. На Windows: D3D11 Shared Texture + WinMFT аппаратный H.264 энкодер; на Android: Camera2 Dual-Surface + MediaCodec HW декодер.
- NEW Intra Refresh (GDR) — плавное покадровое обновление H.264 (период 30 кадров) вместо тяжелых ключевых IDR-кадров, устраняющее всплески битрейта и замирания видео.
- NEW Forward Error Correction (XOR FEC) — паритетные пакеты для восстановления одиночных потерь видеопакетов в группе без задержки ожидания RTT.
- CHANGED Буферизация и фрагментация — размер пакета увеличен до
MAX_PACKET_SIZE = 1400байт, полезная нагрузка чанка зафиксирована на1152байта (вписывается в 1200 MTU с учётом заголовков и GCM Tag). - NEW DSCP QoS (0xB8 Expedited Forwarding) — аппаратная маркировка TOS/DSCP 46 для приоритезации медиатрафика в Wi-Fi и 4G/5G сетях.
📋 Changelog v2.0.0 (август 2026)
- BREAKING Заголовок расширен с 16 до 20 байт: версия и тип пакета теперь занимают по полному байту каждый
- BREAKING Поле
Timestampизменено с uint32 (секунды) на uint64 (миллисекунды) - CHANGED Nonce AES-GCM реструктурирован: устранена уязвимость повторного использования при высокой частоте пакетов
- NEW Пакет
0x07 NACK— запрос ретрансмиссии потерянного пакета по seq-номеру - NEW STUN-интеграция для обнаружения внешнего IP:порт (XOR-MAPPED-ADDRESS) — поддержка CGNAT
- NEW Множественные ICE-кандидаты: локальный (LAN) + рефлексивный (STUN/srflx)
- REMOVED Наивное дублирование всех медиапакетов (заменено NACK-ретрансмиссией)
1. Общее описание
Secure Call Protocol (SCP v2.1.1) — прикладной бинарный протокол реального времени поверх QUIC (RFC 9000 + RFC 9221 Datagrams) и UDP для передачи зашифрованных медиапотоков со сверхнизкой задержкой (Ultra-Low Latency). Протокол обеспечивает аудио- и видеосвязь как в парных P2P-сессиях, так и в масштабных групповых конференциях через SFU.
• Двухканальный транспорт: Первичный QUIC SFU (:4001, RFC 9221 Datagrams, TLS 1.3) + Резервный UDP Relay (:4000) с автоматическим фоллбэком.
• Zero-Copy Native Pipeline: Прямой доступ к аппаратным кодекам без копирования кадров в память Dart/Flutter (D3D11 Shared Texture + WinMFT на Windows, Camera2 Dual-Surface + MediaCodec на Android).
• Intra Refresh (GDR): Плавное обновление кадров H.264 за 30 фреймов без спайков IDR-кадров и перегрузки полосы.
• QoS Маркировка: DSCP EF (0xB8 / класс 46) для приоритетной доставки пакетов в сетевых очередях маршрутизаторов и вышек LTE/5G.
• Forward Error Correction (FEC): XOR Parity защита для мгновенного восстановления потерянных видеопакетов без ожидания RTT.
• Заголовок 20 байт: Выровненные поля, uint64 timestamp в мс, 96-битный криптографически безопасный nonce AES-256-GCM.
• E2EE: Медиа-сервер выступает чистым ретранслятором (SFU) и не имеет ключей дешифрования.
2. Архитектура сети
Протокол SCP строго разделяет логику управления (Signaling Plane через WebSocket) и передачи медиа (Media Plane через QUIC/UDP):
Первичный транспорт: QUIC SFU (Порт 4001/UDP)
Клиенты по умолчанию подключаются к высокопроизводительному Go SFU серверу по протоколу QUIC (RFC 9000). Ненадежные датаграммы (RFC 9221 Unreliable Datagrams) используются для аудио- и видеопакетов, гарантируя отсутствие задержек Head-of-Line Blocking при кратковременных сетевых потерях. Надежный QUIC Stream используется для начальной аутентификации сессии и управляющих команд (PLI, мут, выбор слоёв симулкаста).
Резервный транспорт: UDP Relay (Порт 4000/UDP)
В случае корпоративных файрволов или провайдеров, блокирующих QUIC-трафик, клиент автоматически переключается на классический UDP Relay с DSCP EF (0xB8) QoS-маркировкой.
Парные вызовы (Peer-to-Peer STUN Punching)
При вызове 1-на-1 клиенты обмениваются через WebSocket собранными ICE-кандидатами (локальный host LAN-адрес и внешний srflx-адрес, полученный от STUN). Клиенты производят UDP Hole Punching. При успехе трафик направляется напрямую между устройствами, минуя сервер.
Групповые вызовы (SFU Fan-out)
При групповых конференциях каждый участник отправляет один зашифрованный медиа-поток на сервер. Go SFU ретранслирует датаграммы остальным участникам комнаты по ID без выполнения ресурсоемкого транскодирования или дешифрации.
3. Структура пакетов v2.0
3.1 Заголовок пакета (20 байт)
В версии 2.0 заголовок расширен с 16 до 20 байт. Поля Version и PacketType выделены в отдельные байты для устранения битовой упаковки и упрощения парсинга. Поле Timestamp расширено до uint64 в миллисекундах.
| Смещение | Размер | Поле | Тип | Описание |
|---|---|---|---|---|
| 0 | 1 байт | Version CHANGED |
uint8 | Версия протокола. Текущее значение: 2 |
| 1 | 1 байт | PacketType CHANGED |
uint8 | Тип пакета (полный байт, см. §3.2) |
| 2 – 3 | 2 байта | SeqNumber |
uint16, BE | Порядковый номер пакета для NACK-детектирования потерь. Циклически переполняется через 65535. |
| 4 – 7 | 4 байта | RoomID |
uint32, BE | Детерминированный идентификатор комнаты звонка (производный от callId) |
| 8 – 11 | 4 байта | UserID |
uint32, BE | Идентификатор отправителя |
| 12 – 19 | 8 байт | TimestampMs CHANGED |
uint64, BE | Время генерации пакета в миллисекундах с Unix epoch. Используется для RTT-измерений и построения уникального IV нonce. |
| 20+ | N байт | Payload |
bytes | Зашифрованная полезная нагрузка (медиаданные или служебный контент) |
_ = buf[19] гарантирует компилятору Go, что буфер имеет длину не менее 20 байт, устраняя проверки границ при последующем чтении полей.
3.2 Типы пакетов
| Код | Название | Направление | Описание |
|---|---|---|---|
0x01 | Auth | C → S | Авторизация сессии. Payload: JWT-токен (bytes) |
0x02 | Ping / Migration | C ↔ S | Keepalive и миграция IP. Payload: MigrationToken (4 байта) |
0x03 | Audio | C → S → C | Зашифрованный аудиофрейм (G.711μ / Opus) |
0x04 | Video | C → S → C | Зашифрованный H.264 видеочанк с субзаголовком фрагментации |
0x05 | Feedback | S → C | Адаптивный feedback: потери, джиттер, рекомендованный слой |
0x06 | P2P Punch | C ↔ C | Специальный пакет UDP Hole Punching для установки P2P |
0x07 | NACK NEW | C → S → C | Запрос ретрансмиссии потерянного пакета. Payload: seq (2 байта) |
4. Авторизация сессий
Защита медиасервера от DDoS и несанкционированного доступа реализована через одноразовые токены с привязкой к сетевому адресу (IP:Port).
1. Клиент получает JWT-токен от сигнального FastAPI бэкенда.
2. Клиент отправляет пакет авторизации (PacketType = 0x01) с JWT в Payload.
3. Go-сервер делает внутренний запрос к FastAPI (/api/v1/internal/verify-token). При успехе IP:Port заносится в whitelist (SessionManager) и клиенту отправляется обратно пакет 0x01 с MigrationToken в Payload.
[20:24].
5. Сквозное шифрование (E2EE)
Payload пакетов 0x03 и 0x04 шифруется алгоритмом AES-256-GCM на клиенте с использованием общего секрета, полученного через ECDH (X25519) — Diffie-Hellman на эллиптических кривых. Медиасервер не имеет доступа к ключам.
5.1 Спецификация IV/Nonce (12 байт = 96 бит)
В версии 2.0 нonce полностью переработан для устранения уязвимости повторного использования, существовавшей в v1.x при высокой частоте пакетов (50 аудиопакетов/сек с timestamp в секундах давали одинаковый IV у 50 подряд идущих пакетов).
Bytes 0–5: TimestampMs48 — младшие 48 бит uint64 timestamp в мс
Bytes 6–9: Seq32 — uint32: (seqEpoch << 16) | seqNumber
seqEpoch инкрементируется при каждом переполнении seqNumber
Bytes 10–11: Salt — фиксированное значение 0xAAAA
| Байты | Поле | Размер | Уникальность |
|---|---|---|---|
| 0–5 | TimestampMs48 | 48 бит | Не повторяется ~8919 лет |
| 6–9 | Seq32 = epoch<<16 | seq | 32 бита | Rolling counter, не повторяется при любом FPS |
| 10–11 | Salt = 0xAAAA | 16 бит | Фиксированный разделитель |
6. Передача Audio и Video
Аудиопоток (PacketType: 0x03)
Захват аудио: 48 кГц, монофонический, 20 мс фреймы. Цепочка обработки на стороне отправителя (Rust DSP pipeline):
PCM16 raw → HighPass 80Hz → RNNoise нейросетевое шумоподавление → G.711μ кодирование → AES-256-GCM шифрование → SCP packet 0x03
Цепочка обработки на стороне получателя:
SCP packet 0x03 → AES-256-GCM дешифрование → Адаптивный JitterBuffer → G.711μ декодирование (PLC при потере) → PCM16 → AudioTrack / SoLoud
Видеопоток (PacketType: 0x04)
Видеофреймы H.264 (Annex-B) кодируются аппаратно с использованием Intra Refresh (GDR) с периодом 30 кадров. Это устраняет всплески трафика от огромных IDR-кадров (Keyframe spikes) и обеспечивает ровный битрейт.
Видеокадр нарезается на чанки с максимальным размером полезной нагрузки MAX_CHUNK_PAYLOAD_SIZE = 1152 байт. Это строго гарантирует, что итоговый пакет (20B SCP заголовок + 4B субзаголовок слайса + 1152B payload + 16B AES-GCM Tag) не превысит 1200 MTU и полностью защищён от фрагментации на уровне IP:
[ 20 байт: SCP Header v2 ]
[ 2 байта: FrameID ] [ 1 байт: FragIndex ] [ 1 байт: TotalFrags ]
[ N байт (до 1152): H.264 NAL chunk ]
[ 16 байт: AES-256-GCM Auth Tag ]
Anti-DoS и тайм-аут сборки: Если в течение 120 мс с момента первого фрагмента не получены все остальные слайсы кадра — буфер кадра сбрасывается для предотвращения утечек памяти.
7. NACK-ретрансмиссия (PacketType: 0x07) NEW v2.0
Версия 1.x использовала наивное дублирование каждого медиапакета через 4–8 мс, что удваивало исходящий трафик. В v2.0 это заменено на NACK-based selective retransmission.
Механизм
Отправитель: сохраняет последние 128 отправленных медиапакетов в кольцевом буфере (ring buffer, индексируется как seq % 128).
Получатель: отслеживает порядковые номера входящих пакетов (lastReceivedSeq). При обнаружении пропуска (gap > 0 при учёте wraparound) и если пропущенный seq не старше 30 пакетов назад — отправляет NACK.
Сервер: пакеты 0x07 маршрутизируются fan-out как обычные медиапакеты — до всех участников комнаты. Отправитель, получив NACK со своим seq, достаёт пакет из ring buffer и повторно отправляет.
Формат Payload пакета 0x07
Bytes 20–21: NackSeq (uint16, BE) — порядковый номер запрашиваемого пакета
Ограничения
NACK не отправляется для пакетов, отставших более чем на 30 seq (они уже устарели). Для I-кадров (keyframes) при значительных потерях используется PLI (Picture Loss Indication) через WebSocket сигнальный канал.
8. Структура пакета Feedback (PacketType: 0x05)
Payload пакета 0x05 имеет фиксированный размер 7 байт, начиная со смещения 20 (после нового 20-байтного заголовка):
| Смещение | Размер | Поле | Тип | Описание |
|---|---|---|---|---|
| 20 | 1 байт | TargetMedia | uint8 | 0x01 — Аудио, 0x02 — Видео, 0x03 — Все |
| 21 | 1 байт | VideoLayerMask | uint8 | 0x01: 720p, 0x02: 360p, 0x00: Mute видео |
| 22–23 | 2 байта | SuggestedAudioBitrate | uint16, BE | Рекомендуемый битрейт Opus в Кбит/с |
| 24 | 1 байт | MeasuredLoss | uint8 | Измеренный packet loss в % (0–100) |
| 25–26 | 2 байта | MeasuredJitter | uint16, BE | Джиттер в миллисекундах |
9. P2P ICE / STUN Hole Punching NEW v2.0
Версия 2.0 добавляет поддержку STUN-рефлексивных кандидатов для работы P2P за CGNAT и симметричным NAT мобильных операторов.
9.1 Сбор ICE-кандидатов
При подключении клиент собирает двух типа кандидатов:
host (локальный): IP-адрес из NetworkInterface.list(), приоритет по типу интерфейса (Wi-Fi > Ethernet > cellular).
srflx (server reflexive): внешний IP:port, полученный через STUN Binding Request к stun.l.google.com:19302. Ответ содержит атрибут XOR-MAPPED-ADDRESS (0x0020), из которого извлекается реальный публичный адрес.
9.2 Формат STUN Binding Request
Bytes 0–1: Message Type = 0x0001 (Binding Request)
Bytes 2–3: Message Length = 0x0000
Bytes 4–7: Magic Cookie = 0x2112A442
Bytes 8–19: Transaction ID (12 случайных байт)
9.3 Парсинг XOR-MAPPED-ADDRESS
port = (port_hi ^ 0x21) << 8 | (port_lo ^ 0x12)
ip[0] ^= 0x21; ip[1] ^= 0x12; ip[2] ^= 0xA4; ip[3] ^= 0x42
9.4 Процесс Hole Punching
1. Оба клиента отправляют своих кандидатов (host + srflx) через WebSocket сигнальный сервер (p2p_candidate message).
2. При получении кандидата пира клиент начинает UDP Hole Punching: отправляет пакет 0x06 на каждый кандидат каждые 200 мс (до 20 попыток).
3. При получении 0x06 от пира — NAT пробит с обеих сторон, P2P соединение считается установленным.
4. Fallback на relay: если за 4 секунды P2P не установлен — медиа продолжает идти через SFU-сервер.
5. P2P watchdog: если от пира нет пакетов более 3 секунд — автоматический fallback на relay.
10. Алгоритм адаптивного Симулкаста
Сервер измеряет packet loss и джиттер, отправляя Feedback-пакеты каждые 2 секунды. Клиент адаптирует качество видео:
| VideoLayerMask | Разрешение | Качество H.264 | Throttle FPS |
|---|---|---|---|
0x01 | 720p (max) | 65 | без ограничений |
0x02 | 360p | 45 | +60мс при loss>8% |
0x00 | — (mute) | — | — |
10.1 Гистерезис апгрейда (Anti-flapping)
Для предотвращения постоянного переключения качества при нестабильной сети применяется гистерезис 8 секунд: апгрейд с 360p на 720p разрешён только если клиент находился на 360p не менее 8 секунд.
10.2 Simulcast Heartbeat Lease (§8.5)
Сервер дублирует Feedback-пакет каждые 2 секунды для удержания слоя 360p. Если клиент не получал Feedback более 5 секунд — автоматически деградирует до 360p (защита от потери управляющих пакетов).
11. Миграция соединения (Connection Migration)
При смене IP-адреса (Wi-Fi → LTE) у клиента меняется UDP-порт. Бесшовная миграция обеспечивается через MigrationToken:
1. При авторизации (0x01) Go-сервер генерирует случайный MigrationToken (uint32) и отправляет клиенту.
2. Клиент периодически посылает Ping (0x02) с MigrationToken в Payload.
3. При получении Ping с нового IP сервер сверяет токен с сессией UserID. При совпадении — атомарно обновляет RemoteAddr в SessionManager без разрыва звонка.
4. После rebind сокета клиент повторно собирает ICE-кандидатов (новый локальный порт) и отправляет их пиру для перезапуска Hole Punching при необходимости.
12. Высокоскоростной QUIC SFU Транспорт (:4001/UDP) NEW v2.1.1
Протокол QUIC (RFC 9000) реализован на базе библиотеки github.com/quic-go/quic-go на Go-медиасервере (порт :4001). QUIC объединяет надёжность TLS 1.3 с минимальными задержками UDP.
12.1 Архитектура QUIC Media Session
1. TLS 1.3 Handshake: Клиент и сервер согласуют ALPN chepuha-media-v1 или quic-media с шифрованием трафика (защита от прослушивания промежуточными узлами и глубокого анализа пакетов DPI).
2. Stream-аутентификация: Сразу после TLS-рукопожатия клиент открывает однонаправленный стрим и передаёт авторизационный пакет: [4B RoomID] [4B UserID] [4B TokenLen] [Token bytes].
3. Быстрая внутренняя валидация: Go-сервер верифицирует токен через локальный эндпоинт FastAPI POST /api/v1/internal/verify-token-only (задержка < 1 мс). В ответ клиент получает 4-байтный MigrationToken.
4. Unreliable Datagrams (RFC 9221): После аутентификации все медиа-пакеты (аудио, видео, FEC) пересылаются методом SendDatagram / ReceiveDatagram без подтверждений ACK и без задержки повторной доставки. Если датаграмма теряется в сети, она не задерживает последующие кадры.
13. Аппаратный Zero-Copy Media Pipeline NEW v2.1.1
В версии 2.1.1 полностью устранены промежуточные преобразования кадров через буферы памяти Dart. Захват, кодирование, декодирование и рендеринг видео выполняются на нативном GPU/VPU уровне платформы.
13.1 Архитектура Windows (DirectX 11 + WinMFT)
Webcam Capture (Media Foundation)
│
▼ (Direct3D 11 NV12/RGBA Surface)
WinMFT H.264 Encoder (Hardware MFT with Intra Refresh GDR)
│
▼ (Zero-Copy Bitstream)
Rust Native Pipeline (Slice <= 1152B → AES-256-GCM → QUIC Datagram)
│
Local Preview Bridge (D3D11 Shared Texture Handle → Flutter Native Texture ID)
• D3D11 Shared Texture: Кадры локальной камеры и удалённых участников проецируются напрямую в текстуры DirectX 11, зарегистрированные в движке Flutter через FlutterDesktopRegisterExternalTexture. Процессор (CPU) не участвует в копировании пикселей.
• WinMFT Intra Refresh: Аппаратный энкодер настраивается с CODECAPI_AVEncVideoIntraRefreshMode = 1 и периодом 30 кадров. Вместо тяжёлых IDR-кадров каждые 2 секунды кодек плавно обновляет строки макроблоков, исключая фризы декодера.
13.2 Архитектура Android (Camera2 + MediaCodec)
• Camera2 Dual-Surface: Сессия камеры отдаёт поток одновременно на SurfaceTexture (локальное превью Flutter) и аппаратный InputSurface энкодера MediaCodec (H.264 Baseline/High Profile).
• MediaCodec HW Decoder: Входящий бинарный поток H.264 демультиплексируется в Rust, расшифровывается и передается напрямую в инпут-буферы MediaCodec, связанного с целевым Surface на экране.
14. Защита от потерь: Forward Error Correction (FEC) и QoS NEW v2.1.1
14.1 XOR Parity Forward Error Correction (FEC)
Для критически важных видеопакетов реализован алгоритм блочного XOR-паритета. Пакеты объединяются в группы по $K$ пакетов ($K = 4..8$ в зависимости от измеренного packet loss). Сервер или клиент формирует дополнительный паритетный пакет (SCP_TYPE_FEC = 0x03):
FEC_Payload = Packet[0] ^ Packet[1] ^ ... ^ Packet[K-1]
При потере ровно одного пакета из группы получатель мгновенно восстанавливает его по формуле: Lost_Packet = FEC_Payload ^ (Все остальные полученные) без отправки NACK и ожидания RTT, предотвращая артефакты видеопотока.
14.2 QoS Маркировка (DSCP EF 0xB8)
При открытии UDP-сокетов на клиентах и на Go SFU сервере устанавливается байт Type of Service (TOS):
// Go (Linux / Windows)
conn.SyscallConn().Control(func(fd uintptr) {
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, 0xB8)
})
Значение 0xB8 соответствует классу Expedited Forwarding (EF / DSCP 46). Это указывает сетевому оборудованию (домашним роутерам с WMM, магистральным коммутаторам и базовым станциям операторов связи) обрабатывать медиа-пакеты SCP в очередях с наивысшим приоритетом, минимизируя джиттер и задержки.