- .gitea/workflows/build.yml — cargo build --release + docker build/push в
ghcr.io/nineap/netrunner-proxy:latest (Dockerfile сам не компилирует Rust,
ждёт уже готовый target/release/netrunner-server).
- .gitea/workflows/deploy.yml — только вручную (workflow_dispatch), параметр
target: имя ноды из nodes.json или "all". Job "resolve" фильтрует список,
job "deploy" — matrix по результату, каждая нода видна в прогоне отдельной
строкой. Деплой пересоздаёт контейнер (pull+rm+run), не просто restart —
иначе новый образ не подхватится.
- nodes.json — список нод (name/host/proxy_port/decoy_host), шаблон с
CHANGE_ME — заполнить реальными IP.
Нужные секреты: GHCR_TOKEN (тот же, что и в остальных репо), PROXY_NODES_SSH_KEY
(общий master-ключ на все ноды), PROXY_INTERNAL_SECRET, BACKEND_URL (те же
значения, что уже настроены у бэкенда).
server/netrunner-server.dev.service копировался на удалённую машину без
переименования — оседал как netrunner-server.dev.service рядом со старым
netrunner-server.service, а systemctl start netrunner-server продолжал
использовать именно старый (неизменный) юнит. Из-за этого только что
задеплоенные --port=8443 не подхватывались вообще, пока не переименовал файл
руками на сервере.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Локальный debug-server и задеплоенный на DEV_IP systemd-сервис раньше
использовали 4443/443 соответственно — развели их на общий 8443, чтобы не
пересекаться с портами, занятыми другими дев-сервисами (например, Caddy на 443).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Сервер получает флаги --require-auth/--backend-url (по умолчанию выключено,
поведение уже развёрнутых нод не меняется). Auth-кадр хендшейка расширен с
"session_id:leg_id" до "session_id:leg_id:token"; при включённом
--require-auth сервер валидирует токен через новый AuthValidator
(BackendClient к netrunner-backend, с кешем на 60с).
Muxer теперь ведёт монотонный учёт трафика сессии (переживает реконнект
ног — раньше total_bytes мог занижаться после переподключения) и раз в ~30с
отчитывается бэкенду; при превышении лимита сессия разрывается. Токен
клиента прокидывается через EngineConfig/TunnelConfig до Tauri-плагина
(desktop.rs + Android Kotlin-плагин).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Маскировка:
- 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>