Дефолтный PeerIpKeyExtractor брал IP из TCP-peer'а, а не заголовков — а nginx проксирует локально, так что peer всегда 127.0.0.1. В итоге лимит "3 запроса/5с" на /auth и "10/100мс" на остальной API/v1 был не per-visitor, а глобальным на весь прод: несколько попыток логина от кого угодно — и все остальные посетители ловили 429 на /auth (это и мешало зайти в свежесозданную админку — 401 на пробное неверное дело быстро съедал бюджет, дальше только 429). Добавлен SmartIpKeyExtractor (x-forwarded-for/x-real-ip/forwarded, именно то, что реально шлёт nginx) в оба GovernorConfigBuilder.
Netrunner Control Plane
Бэкенд VPN-сервиса «Netrunner»: авторизация (Telegram / анонимный «Ghost Protocol»), биллинг подписок через TON, реферальная программа, оркестрация VPN-нод по SSH, Telegram-бот как второй канал входа и встроенная раздача SPA-фронтенда.
Стек: Rust (axum, sqlx/Postgres, teloxide, tokio) + React/TypeScript (Vite) фронтенд, собранные в единый Docker-образ.
Структура репозитория
| Путь | Что там | Подробнее |
|---|---|---|
src/ |
Rust-бэкенд (control plane) | см. ниже |
src/modules/ |
Бизнес-модули (auth, billing, nodes, referrals, users) | README |
src/db/ |
Модели и репозиторий (доступ к Postgres) | README |
frontend/ |
React/Vite SPA (личный кабинет, игра, админка нод) | README |
migrations/ |
SQL-миграции (sqlx) | — |
templates/init_node.sh |
Bash-скрипт инициализации новой VPN-ноды, исполняется по SSH | — |
tests/ |
Интеграционные тесты репозитория (sqlx::test, требуют Postgres) |
— |
Инфраструктура и процесс деплоя подробно описаны в DEPLOYMENT.md;
список закрытых и остаточных угроз безопасности — в SECURITY_AUDIT.md.
Архитектура бэкенда
Слоями, сверху вниз:
main.rs— точка входа: поднимает пул Postgres, собираетApiState(репозиторий + сервисы всех модулей) и параллельно запускает HTTP API, Telegram-бота и фоновые задачи биллинга до общего сигнала остановки (Ctrl+C / SIGTERM).api.rs— сборка axum-роутера: монтирует контроллеры модулей, rate-limiting (tower_governor), CORS, Prometheus-метрики (/metrics), health-пробы (/health,/health/ready) и раздачу статики фронтенда как SPA-фолбэк.modules/*/controller.rs— HTTP-контракты (axum-роуты, JSON DTO), тонкий слой без бизнес-логики.modules/*/service.rs— бизнес-логика модуля.db::repository::AppRepository— трейт доступа к БД; единственная реализация —PgRepository(Postgres/sqlx). Сервисы знают только о трейте, поэтому теоретически подменяемы в тестах без реальной БД.bot.rs— Telegram-бот (teloxide), использует те же сервисы, что и HTTP API; кнопки «Личный Кабинет»/«Тарифы»/«Синдикат» ведут в ЛК через общий WebApp-мост (короткоживущий bootstrap-токен в URL, фронт меняет его на cookie-сессию черезPOST /auth/exchange).tasks.rs— два фоновых цикла: сброс просроченных подписок + сверка блокчейна на предмет потерянных платежей, и health-check VPN-нод (TCP-пинг раз в 60с, переоценкаonline/offline).error.rs— единыйAppErrorсIntoResponse: маппинг в HTTP-статус, безопасное сообщение наружу и подробности только в логи/метрики.
Авторизация: единая cookie-сессия для лендинга и ЛК
Один способ входа (Telegram Login Widget / анонимный Ghost Protocol / bootstrap-мост
из бота) выдаёт одну и ту же пару cookie: короткий access-JWT (nrxp_token, 15 мин) и
ротируемый непрозрачный refresh-токен (nrxp_refresh, 30 дней, хеш хранится в
refresh_sessions, путь строго /api/v1/auth/refresh). Обе — HttpOnly; Secure; SameSite=Lax; в проде дополнительно Domain=.netrunner-vpn.com, поэтому одна и та же
сессия работает и на лендинге (netrunner-vpn.com), и на ЛК (account.netrunner-vpn.com)
без единого байта в localStorage или JS-читаемых cookie. Подробности — в
src/modules/auth/README.md.
Быстрый старт (локальная разработка)
cp .env.example .env # заполнить секреты (JWT_SECRET, ARGON_SALT, TELOXIDE_TOKEN, ...)
make dev # docker compose: db + redis + app с hot-reload (cargo watch)
Приложение поднимется на http://localhost:8080 (/health, /health/ready).
Остальные команды — make help.
Создание первого Owner-аккаунта админки
Единственный способ попасть в /admin/login (React-админка) — учётка с ролью
owner, логин+пароль (не Telegram). Создаётся one-shot CLI-флагом
--create-owner (нужен только DATABASE_URL, ни ключей, ни токенов;
пароль вводится скрыто в консоли, не через аргумент/env — не оседает в
истории шелла):
# Локально/дев (бинарник собран, .env заполнен):
./netrunner-control-plane --create-owner --username admin
# На проде (задеплоено через docker-compose.prod.yml) — выполнить на самом
# VPS из /root/netrunner-core:
docker compose -f docker-compose.prod.yml run --rm app \
./netrunner-control-plane --create-owner --username admin
docker compose run (не up) — контейнер разовый, не публикует порт 8080,
не конфликтует с уже работающим app. Логин уже занят — упадёт с понятной
ошибкой, а не тихо перезапишет существующего владельца.
Nodes, Cyberhack, биллинг — текущее состояние
GET /nodes/routingотдаёт полныйTunnelConfig:tunnel_*-поля,public_key/sni_domainноды и одноразовый (60с) session-токен, который выдаётся только при активной подписке юзера.- Health-check VPN-нод — фоновая задача (
src/tasks.rs, TCP-пинг раз в 60с) переоцениваетonline/offlineсама; ручнойPOST /admin/nodes/{id}/restartперезапускает прокси-контейнер на ноде по SSH-ключу. - Провижининг ноды асинхронный:
POST /admin/nodesсразу отвечает202 Acceptedсо статусомprovisioning, SSH-деплой идёт в фоне. - Курс TON кешируется в Redis (
ton_rate:<currency>, TTL 60с) —BillingService::initiate_paymentбьёт TonAPI только на промах кеша. - Cyberhack — server-authoritative. Доску генерирует и хранит сервер
(
src/modules/users/cyberhack.rs, порт алгоритма изCyberhack/src/lib.rs); клиент присылает не итоговый счёт, а сырой путь кликов, который сервер сам реплеит и валидирует (POST /users/hack/start→POST /users/hack/result). - Telegram-бот: «Тарифы» и «Синдикат» ведут в тот же ЛК, что и «Личный Кабинет» — единый WebApp-мост (см. выше).
AdminDashboard.tsx: кнопки «Force Restart» и удаления ноды подключены к соответствующим ручкам бэкенда.