7.4 KiB
Обфускация трафика OSTP
Философия
Классические туннельные протоколы (TLS, OpenVPN, WireGuard) имеют узнаваемые сигнатуры в хэндшейке или статичные заголовки пакетов. Механизм обфускации OSTP спроектирован так, чтобы начиная с первого байта трафик был максимально похож на случайный шум — и для DPI-систем был неотличим от него.
Деривация секретов
Все секреты протокола — ключ обфускации, PSK Noise-хэндшейка, диапазон паддинга хэндшейка и маркер junk-пакетов (см. ниже) — выводятся из общего access_key одним проходом HKDF-SHA256, с разделением по доменам через последний байт info:
PRK = HKDF-Extract(salt = SHA-256(access_key)[0..16], IKM = access_key || PROTOCOL_VERSION)
obfuscation_key = HKDF-Expand(PRK, info = SHA-256(access_key)[16..] || 0x01, 8 байт)
psk = HKDF-Expand(PRK, info = SHA-256(access_key)[16..] || 0x02, 32 байта)
handshake_pad = HKDF-Expand(PRK, info = SHA-256(access_key)[16..] || 0x03, 2 байта)
junk_marker = HKDF-Expand(PRK, info = SHA-256(access_key)[16..] || 0x04, 4 байта)
Версия протокола подмешивается в IKM, а не передаётся открытым байтом на проводе: пиры с разной версией протокола выведут разный obfuscation_key и просто не смогут деобфусцировать пакеты друг друга — жёсткий version gate без единого узнаваемого маркера на проводе. Ни один секрет никогда не передаётся — обе стороны независимо выводят одинаковые значения из общего access_key.
Алгоритм динамического маскирования
Датаграммы OSTP маскируются "на месте" прямо перед отправкой и сразу после получения. Сама маска выводится из шифротекста самого пакета, а не из статичного потока ключа или счётчика — поэтому она меняется от пакета к пакету автоматически:
mask = HMAC-SHA256(key = obfuscation_key, message = ciphertext[0..min(32, len)])
1. Фаза хэндшейка (is_handshake = true)
Пакет на проводе — [4 байта session_id][2 байта noise_len][Noise-полезная нагрузка]. Маска считается по Noise-полезной нагрузке (raw[6..]), и её первые 6 байт накладываются XOR'ом на session_id || noise_len.
2. Фаза передачи данных (is_handshake = false)
Пакет на проводе — [4 байта session_id][8 байт nonce][AEAD-шифротекст]. Маска считается по шифротексту, и её первые 12 байт накладываются XOR'ом на session_id || nonce.
Эффект схемы
Поскольку маска зависит одновременно от общего секрета и от содержимого шифротекста конкретного пакета, никакие два пакета — даже два подряд идущих в одной сессии — не используют одинаковый ключевой поток, и для этого не нужна явная схема на основе счётчика. Это полностью убирает корреляции между заголовками пакетов и повторяющиеся байтовые паттерны, делая статистический фингерпринтинг бесполезным.
Статистический паддинг
Помимо маскирования заголовков, OSTP защищается от анализа длин пакетов (Traffic Length Analysis). AdaptivePadder вычисляет случайный размер мусорных байт, добавляемых к полезной нагрузке ещё до шифрования:
- Динамическое распределение: длины паддинга подобраны так, чтобы напоминать профили длин обычного HTTPS-трафика или видеопотоков.
- Внутри шифротекста: добавленный паддинг находится внутри области AEAD-шифрования — пассивный наблюдатель не может отличить паддинг от полезной нагрузки и не видит настоящую границу сообщения.
Junk-пакеты и TCP-фрагментация
OSTP не пытается притворяться известным протоколом (TLS, HTTP и т.п.) — фильтр по сигнатуре всегда можно обновить под конкретную имитацию. Вместо этого используется подход в духе zapret: никакого узнаваемого заголовка вообще, плюс активная манипуляция границами пакетов — фингерпринтить попросту нечего.
- Junk-пакеты: перед хэндшейком клиент отправляет настраиваемое количество (
junk_pc) мусорных датаграмм случайного размера (junk_ps). Каждая несёт 4-байтовый маркер, выведенный из access_key (тот самыйjunk_markerвыше), а не фиксированную константу — константный маркер сам по себе стал бы универсальной сигнатурой для любого наблюдателя сразу по всем серверам OSTP. Сервер, перебирая кандидатов-ключей, выводит тот же маркер и тихо отбрасывает junk, не доходя до логирования «unauthorized probe». - TCP-фрагментация (только для транспорта UoT/TCP): первый пакет (хэндшейк) режется на мелкие куски (
frag_chunkбайт) с небольшими задержками (frag_sleepмс) между записями — DPI, анализирующий только первый TCP-сегмент, никогда не видит цельный хэндшейк для фингерпринтинга.
Обе фичи настраиваются per-профиль; ни одна не применяется поверх обычного UDP-транспорта, где отдельная junk-датаграмма выглядела бы для сервера точь-в-точь как случайный одиночный проб.