Secure Call Protocol (SCP)

Спецификация бинарного протокола реального времени для защищённых коммуникаций
v2.1.1 Обновлено (Сентябрь 2026)

📋 Changelog v2.1.1 (сентябрь 2026)

📋 Changelog v2.0.0 (август 2026)

1. Общее описание

Secure Call Protocol (SCP v2.1.1) — прикладной бинарный протокол реального времени поверх QUIC (RFC 9000 + RFC 9221 Datagrams) и UDP для передачи зашифрованных медиапотоков со сверхнизкой задержкой (Ultra-Low Latency). Протокол обеспечивает аудио- и видеосвязь как в парных P2P-сессиях, так и в масштабных групповых конференциях через SFU.

Ключевые свойства протокола v2.1.1:
• Двухканальный транспорт: Первичный 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 Зашифрованная полезная нагрузка (медиаданные или служебный контент)
BCE-оптимизация Go-сервера: явная фиктивная проверка _ = buf[19] гарантирует компилятору Go, что буфер имеет длину не менее 20 байт, устраняя проверки границ при последующем чтении полей.

3.2 Типы пакетов

КодНазваниеНаправлениеОписание
0x01AuthC → SАвторизация сессии. Payload: JWT-токен (bytes)
0x02Ping / MigrationC ↔ SKeepalive и миграция IP. Payload: MigrationToken (4 байта)
0x03AudioC → S → CЗашифрованный аудиофрейм (G.711μ / Opus)
0x04VideoC → S → CЗашифрованный H.264 видеочанк с субзаголовком фрагментации
0x05FeedbackS → CАдаптивный feedback: потери, джиттер, рекомендованный слой
0x06P2P PunchC ↔ CСпециальный пакет UDP Hole Punching для установки P2P
0x07NACK NEWC → 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.

Авторизационный ответ Go-сервера также использует 20-байтный заголовок v2. MigrationToken располагается в байтах [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–5TimestampMs4848 битНе повторяется ~8919 лет
6–9Seq32 = epoch<<16 | seq32 битаRolling counter, не повторяется при любом FPS
10–11Salt = 0xAAAA16 битФиксированный разделитель
Гарантия уникальности: комбинация 48-битного ms-timestamp и 32-битного rolling seq гарантирует уникальность нonce при любой частоте пакетов на протяжении нескольких тысяч лет. Nonce reuse невозможен при условии монотонно возрастающего системного времени.

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) — порядковый номер запрашиваемого пакета
Экономия трафика: при типичном packet loss 1–3% NACK-ретрансмиссия добавляет только 2–6% overhead вместо +100% от наивного дублирования.

Ограничения

NACK не отправляется для пакетов, отставших более чем на 30 seq (они уже устарели). Для I-кадров (keyframes) при значительных потерях используется PLI (Picture Loss Indication) через WebSocket сигнальный канал.

8. Структура пакета Feedback (PacketType: 0x05)

Payload пакета 0x05 имеет фиксированный размер 7 байт, начиная со смещения 20 (после нового 20-байтного заголовка):

СмещениеРазмерПолеТипОписание
201 байтTargetMediauint80x01 — Аудио, 0x02 — Видео, 0x03 — Все
211 байтVideoLayerMaskuint80x01: 720p, 0x02: 360p, 0x00: Mute видео
22–232 байтаSuggestedAudioBitrateuint16, BEРекомендуемый битрейт Opus в Кбит/с
241 байтMeasuredLossuint8Измеренный packet loss в % (0–100)
25–262 байтаMeasuredJitteruint16, 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.264Throttle FPS
0x01720p (max)65без ограничений
0x02360p45+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 и без задержки повторной доставки. Если датаграмма теряется в сети, она не задерживает последующие кадры.

Двухканальный Fallback: В случае, если провайдер блокирует QUIC по порту 4001, клиент мгновенно переключается на классический UDP сокет на порт 4000.

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 в очередях с наивысшим приоритетом, минимизируя джиттер и задержки.