Маскировка:
- 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>
Downloads were dying and stalling under real network conditions. Root causes
fixed in the muxer's stream-delivery path:
- dispatch_to_local held a DashMap Ref across a nested remove_stream call,
self-deadlocking the leg's reader task whenever a stream's backlog needed
eviction — permanently losing a tokio worker thread per occurrence.
- Eviction now runs off the hot path entirely, in a periodic background
reaper, and only fires when a stream is both over its byte budget AND has
made no delivery progress for an RTT-adaptive grace window (was a flat
300ms then 5s, which killed merely-slow-but-alive consumers).
- The server never told the client when a bridge ended normally (only on
failed connect), leaving the client's virtual TCP socket stuck in
CloseWait for a full idle timeout and slowing the whole engine loop.
- The client's upload path silently dropped in-flight chunks when all tunnel
legs were briefly down instead of pausing and retrying like the server
bridge already did.
- Tried end-to-end credit-based flow control for the download producer;
reverted the enforcement (kept the dormant frame/API) after it proved
worse than the pre-existing local channel backpressure — RTT-coupled
pauses produced burst-then-stall downloads and jitter/ping spikes on the
shared physical leg.
- Server-side diagnostics snapshot was a stub (always-empty tunnel state,
misleading placeholder trigger); now aggregates real per-session Muxer
metrics from the SessionManager.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>