99a9eee908
Маскировка: - ChangeCipherSpec middlebox-compat запись после ClientHello/ServerHello — без неё последовательность типов TLS-записей отличала нас от настоящего браузера даже при идеальном JA3/JA4-отпечатке. - SNI-aware stealth fallback: невалидный клиент проксируется на запрошенный им хост, а не всегда на один и тот же decoy — иначе активное зондирование с разными SNI на одном IP получало одинаковый ответ. Резолв только в публичные IP (защита от SSRF на внутреннюю сеть ноды через подставной SNI). - Профиль браузера привязан к session_id, а не к номеру попытки реконнекта: раньше при нескольких быстрых ретраях с одного IP летели ClientHello Chrome→Edge→Firefox подряд — сама по себе аномалия для DPI. - Бакетный паддинг Data/UdpData кадров вместо полного отсутствия паддинга; джиттер перед ServerHello вместо мгновенного детерминированного ответа. Версионирование: - PROTOCOL_VERSION в session_id ClientHello — сервер узнаёт версию клиента до отправки чего-либо и может включать версионно-зависимое поведение (сейчас — CCS) только для тех, кто его понимает. Позволяет катить будущие несовместимые изменения протокола без синхронного flag-day релиза. Тесты (core, было 0): - Round-trip тесты TLS-записей/hello-сообщений/кадров/кодека. - Полный цикл хендшейка с реальным выводом ключей на обеих сторонах. - Вся SSRF-защита (hostname/IP валидация, v4/v6). - Интеграционные тесты по настоящему TCP: легитимный хендшейк и stealth-fallback на мусорный ClientHello. tools/loadtest — нагрузочный тест поверх настоящего скомпилированного netrunner-server (реальный процесс, метрики RSS/CPU из /proc) и настоящего публичного клиентского API. Попутно найден и обойдён гоночный баг: Connect-кадр в ClientHandler::connect() уходит через try_send ещё до того, как поднимется хоть одна нога тунеля, и молча теряется при синтетической нагрузке. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Блок nrxp — протокол Netrunner eXchange Protocol
Прикладной протокол, который ездит внутри замаскированного TLS-канала. Снаружи
трафик выглядит как обычные TLS-записи ApplicationData (0x17), а внутри каждой
записи — один зашифрованный кадр NRXP с мультиплексированием потоков.
Блок отвечает только за «упаковку/распаковку» и ничего не знает о сети — он
превращает (stream_id, тип, payload) в байты и обратно.
Детали — в rustdoc, модуль
nrxp.
Файлы
| Файл | Что внутри |
|---|---|
frame.rs |
Структура кадра, (де)сериализация, паддинг. |
codec.rs |
Шифрующий слой: TxCodec/RxCodec (кадр ⇄ зашифрованный TLS). |
bridge.rs |
TLS-обёртка TlsBridge: хендшейк и ApplicationData. |
errors.rs |
TlsError + стратегия реакции ErrorAction (Wait/Redirect/Drop). |
Формат кадра (25-байтовый заголовок)
┌──────────┬───────────┬──────┬─────────────┬─────────────┬─────────┬─────────┐
│ Auth Tag │ Stream ID │ Type │ Payload Len │ Padding Len │ Payload │ Padding │
│ 16 байт │ 4 байта │ 1 б. │ 2 байта │ 2 байта │ N байт │ 0..255 │
└──────────┴───────────┴──────┴─────────────┴─────────────┴─────────┴─────────┘
└────────────────── FRAME_HEADER_SIZE = 25 ──────────────────┘
- Auth Tag — TOTP-подобный HMAC-тег (см.
crypto), анти-replay. - Stream ID — id логического потока (нечётные у клиента, чётные у сервера).
- Type —
Connect/Data/Close/Heartbeat/UdpConnect/UdpData. - Padding — против анализа длин. Управляющие кадры получают 0..255
случайных байт;
Data/UdpDataвыравниваются до ближайшего бакета (256/512/1024/2048/4096/8192) — кроме кадров, уже близких к максимальному размеру (крупные закачки), где паддинг только бил бы по throughput без выигрыша в приватности.
Конвейер
TX: Frame::new → into_bytes(tag) → AEAD encrypt in-place → TlsBridge::pack_app_data
RX: TlsBridge::unpack_app_data → AEAD decrypt in-place (staging) → Frame::parse
Инвариант: одна TLS-запись ApplicationData = ровно один кадр NRXP. На нём
держится потоковая расшифровка в codec.rs (буфер staging).