mbs-api ran single-worker uvicorn with no --workers flag at all — one
event loop handling every request. Now install.sh (and mbs update, so
existing installs pick it up too) compute a worker count from nproc
(clamped 1-4, matching typical VPS core counts) and bake it into the
systemd unit via sed substitution of a __WORKERS__ placeholder. Safe
to parallelize: verified no api.py module-level mutable state, all
of it already goes through sqlite (payment idempotency and the
xray-config file lock are already correct across separate processes,
not just asyncio tasks within one — confirmed both are OS/db-level,
not in-process). Verified with a mock dry-run of the new systemd-unit
section (fake nproc, real sed substitution) producing the expected
ExecStart line for several core counts.
Also: build_subscription_text() — called on every single hit of
/sub/{token}, the single most frequently called endpoint in the whole
app, since every VPN client re-fetches on every reconnect — was doing
one db.get_node() call per distinct node in a user's subscriptions
instead of fetching once. Same N+1 shape as the admin-endpoint bugs
fixed yesterday, except this one is on the hot path, not just the
admin panel. Fixed to batch-fetch via db.list_nodes() once.
Verified: correct output for a 4-node subscription (each node's
address appears exactly once, de1's 4 transports all present,
nothing silently dropped) and measured ~1.3ms/call average.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|---|---|---|
| .github/workflows | ||
| landing | ||
| site | ||
| systemd | ||
| .env.example | ||
| .gitignore | ||
| admin.html | ||
| api.py | ||
| backup.py | ||
| bot.py | ||
| config.py | ||
| db.py | ||
| install.sh | ||
| LICENSE | ||
| links.py | ||
| mbs | ||
| nodeprov.py | ||
| payments.py | ||
| README.md | ||
| requirements.txt | ||
| totp.py | ||
| xray_manager.py | ||
MBS Panel
Самостоятельная VPN-панель на VLESS+Reality (+ gRPC/XHTTP/WS-TLS транспорты) и Hysteria2. Телеграм-бот для выдачи подписок, веб-сайт с личным кабинетом, и админ-панель для управления нодами, юзерами и трафиком — всё в одном репозитории, без сторонних панелей типа x-ui или Marzban под капотом.
Сделано by savsis. Изначально писалось под конкретный проект (шеринг VPN среди своих), но получилось достаточно универсально, чтобы выложить как есть.
Что внутри
- Бот (aiogram 3) — выдача подписок по кнопкам, гифт-коды, привязка тарифов (7 дней / месяц / 3 месяца / полгода / год), автоматическое отключение по истечении подписки (не раз в полчаса, а раз в 90 секунд — важно, чтобы просрочка реально обрывала доступ, а не продолжала работать).
- API + сайт (FastAPI) — страница подписки со ссылкой
happ://и QR-кодом, личный кабинет, JSON API для сайта. - Админ-панель (чистый HTML/CSS/JS, без фреймворков и сборки) — дашборд, полная карточка юзера (история подписок, ручная выдача, устройства), подписки, гифт-коды, ноды (полное редактирование, не только вкл/выкл), трафик по Stats API самого Xray со сбросом счётчика по клику.
- Мультинодовость — добавляешь новую ноду в панели, получаешь одну команду
bash <(curl ...), вставляешь на чистый сервер — нода сама ставит Xray, генерит ключи, регистрируется в панели. Как у Remnawave/3x-ui, только свой велосипед. - Протоколы на выбор при добавлении ноды: VLESS TCP+Reality, VLESS gRPC+Reality, VLESS XHTTP+Reality, VLESS WS+TLS (с реальным Let's Encrypt сертификатом), Hysteria2 (QUIC, отдельный процесс, obfs).
- Лимит устройств (HWID) — как у Remnawave, опционально: ограничение числа устройств на подписку через
x-hwidзаголовок. - Приём оплаты — ЮKassa и Platega из коробки, опционально; без них бот просто бесплатно выдаёт по кнопке.
- CLI
mbs— управление панелью прямо с сервера: пароль, статус, рестарт, логи.
Архитектура
flowchart LR
tg["Telegram юзер"] -->|кнопки| bot["bot.py (aiogram)"]
browser["Браузер / Happ-клиент"] -->|"/sub/{token}"| api["api.py (FastAPI)"]
admin["Админ в браузере"] -->|"/admin/api/*"| api
bot --> db[("SQLite (db.py)")]
api --> db
api -->|локально| xray["Xray на этой же ноде"]
api -->|SSH, paramiko| remote["Управляемые ноды"]
subgraph node1 [Управляемая нода]
remote --> xray2["Xray"]
end
Панель и первая VPN-нода живут на одном сервере. Дополнительные ноды подключаются по SSH (management-ключ, генерится сам при первом добавлении ноды) — панель не устанавливает на себя ничего от ноды, только управляет клиентами через SSH-команды и Xray Stats API.
Связь панели с нодой
Два разных пути в зависимости от типа ноды (node.kind в БД):
flowchart TB
api["api.py / bot.py"] --> disp{"node.kind?"}
disp -->|local| xm["xray_manager.py"]
xm -->|"правит /usr/local/etc/xray/config.json напрямую + xray api statsquery"| xrayLocal["Xray на этой же машине"]
disp -->|managed| np["nodeprov.py"]
np -->|"SSH, ключ /root/.ssh/mbs_nodes_ed25519"| xrayRemote["Xray на удалённой ноде"]
subgraph ssh ["По SSH (nodeprov.py)"]
direction TB
f1["remote_add_client / remote_remove_client — правят config.json на ноде"]
f2["remote_query_stats — xray api statsquery"]
f3["remote_reset_stats — statsquery -reset"]
f4["remote_node_status — /proc/loadavg, /proc/meminfo, systemctl"]
end
np -.-> ssh
Публичный (.pub) ключ отдаётся самим install-скриптом ноды при первом запуске (curl .../mgmt-pubkey.txt >> authorized_keys) — панель никогда не просит пароль от новой ноды, только добавляет туда свой ключ через сам же install-скрипт, который админ запускает руками на новом сервере.
Поток оплаты (когда PAYMENTS_ENABLED=true)
sequenceDiagram
participant U as Юзер (бот)
participant B as bot.py
participant P as ЮKassa/Platega
participant A as api.py
participant X as Xray
U->>B: выбрал тариф
B->>P: создать платёж
P-->>B: ссылка на оплату
B-->>U: кнопка "Оплатить"
U->>P: оплатил
P->>A: webhook (подпись проверяется)
A->>X: добавить клиента
A->>U: уведомление в Telegram
Панель и первая VPN-нода живут на одном сервере. На 443 порту нельзя одновременно держать и настоящий HTTPS (для сайта/подписки), и замаскированный под HTTPS VLESS+Reality трафик — поэтому перед Xray стоит nginx stream модуль с ssl_preread, который смотрит SNI входящего TLS-соединения и роутит: домены панели/подписки идут на nginx-бэкенд, всё остальное (в том числе Reality-трафик с левым SNI) — на Xray.
Установка — одна команда
Нужен чистый сервер на Ubuntu 22.04/24.04 или Debian 11/12, root-доступ и три поднятых DNS A-записи (см. таблицу ниже).
git clone https://github.com/devsavsis/mbs-panel.git && cd mbs-panel && sudo bash install.sh
Скрипт спросит домен панели, домен подписки, токен бота от @BotFather и список Telegram ID админов — и дальше всё сам: ставит зависимости, Xray, nginx, выпускает сертификаты Let's Encrypt, генерирует Reality-ключи, поднимает systemd-сервисы, настраивает firewall (ufw) и fail2ban. В конце покажет пароль от админки и ссылку на панель.
DNS-записи
Подними их до запуска install.sh — иначе Let's Encrypt не сможет выпустить сертификаты.
| Запись | Тип | Куда указывает | Зачем |
|---|---|---|---|
panel.example.com |
A | IP сервера панели | Админка (веб-интерфейс) |
sub.example.com |
A | IP сервера панели | Страница подписки + личный кабинет |
de1.example.com |
A | IP сервера панели | Адрес, на который подключаются VPN-клиенты (первая нода) |
Для каждой дополнительной ноды (добавляется позже через панель) — ещё одна A-запись на IP той ноды, например de2.example.com, nl1.example.com и т.д. Панель сама подскажет, какую запись нужно создать, и выдаст готовую команду установки, когда жмёшь «Добавить ноду».
Если Cloudflare — держи эти записи DNS only (серое облако), не проксируй: и Reality-трафику, и Let's Encrypt нужен прямой доступ до сервера.
После установки
- Зайди на
https://panel.example.com, залогинься паролем из вывода скрипта. - Смени пароль в любой момент:
mbs pass новый_пароль(без аргумента — сгенерит случайный). - В боте у себя (Telegram ID из ADMIN_IDS) появится админ-меню.
В конце установки install.sh шлёт один пинг на stats.api.savsis.xyz (только название ОС) — просто счётчик "сколько раз панель установили", никаких доменов/токенов/паролей туда не уходит, IP не сохраняется. Отключить: MBS_SKIP_STATS=1 sudo bash install.sh.
CLI mbs
Ставится автоматически в /usr/local/bin/mbs.
mbs pass [пароль] сменить пароль админ-панели (без аргумента — случайный)
mbs status статус bot / api / xray / nginx
mbs restart перезапустить bot + api
mbs logs [bot|api|xray] последние строки лога (по умолчанию api)
mbs domain текущий домен панели
mbs update обновить код с GitHub и перезапустить
mbs update тянет git pull (только fast-forward — если на сервере что-то правили руками, честно откажется и не полезет мержить), ставит зависимости, проверяет, что новый код вообще компилируется, и только потом перезапускает. Если после рестарта mbs-bot/mbs-api не поднялись — сам откатывает на предыдущий коммит и поднимает его. .env и база (mbs.db) не в гите — их не тронет ни при каком раскладе.
Добавление ноды
В панели: Ноды → Добавить ноду → выбираешь страну, протоколы (Reality-транспорты всегда включены, WS+TLS и Hysteria2 — опционально) → получаешь команду вида:
bash <(curl -Ls https://panel.example.com/install/ТОКЕН.sh)
Вставляешь на чистый сервер (тот же Ubuntu 22/24 или Debian 11/12) — нода сама всё ставит и регистрируется. Если включал WS+TLS — скрипт выпустит для неё отдельный Let's Encrypt сертификат (нужна ещё одна A-запись, см. таблицу выше). Если включал Hysteria2 — поднимет отдельный процесс на UDP.
sequenceDiagram
participant Admin as Админ
participant Panel as Панель
participant Node as Новая нода
Admin->>Panel: Ноды → Добавить ноду
Panel->>Panel: генерит Reality-ключи, токен
Panel-->>Admin: команда bash <(curl ...ТОКЕН.sh)
Admin->>Node: вставляет команду по SSH
Node->>Node: ставит Xray, настраивает transports
Node->>Panel: POST /nodes/register/ТОКЕН
Panel->>Panel: нода активна, доступна в боте
Приём оплаты
По умолчанию бот выдаёт подписки бесплатно по кнопке — платежи выключены (PAYMENTS_ENABLED=false). Чтобы продавать доступ:
- Заведи аккаунт в ЮKassa и/или Platega — оба требуют реального ИП/самозанятости и проходят собственную проверку (реквизиты, сайт с офертой). Панель это не автоматизирует.
- В
.env:PAYMENTS_ENABLED=true, цены на тарифы (PRICE_7D,PRICE_1Mи т.д. в рублях), и креды провайдера(ов) —YOOKASSA_SHOP_ID/YOOKASSA_SECRET_KEYи/илиPLATEGA_MERCHANT_ID/PLATEGA_SECRET, включив соответствующий*_ENABLED. - В личном кабинете провайдера пропиши webhook на
https://<PANEL_DOMAIN>/payments/webhook/yookassaи/или.../payments/webhook/platega. mbs restart— бот начнёт показывать способ оплаты вместо мгновенной выдачи, подписка активируется автоматически по вебхуку с уведомлением в Telegram.
Заполни реальными данными site/offer.html (публичная оферта) и site/privacy.html (политика конфиденциальности) перед подачей заявки в ЮKassa — они нужны для их проверки, шаблоны уже на сайте (/offer.html, /privacy.html), но с плейсхолдерами вместо твоих реквизитов.
Лимит устройств (HWID)
Как в Remnawave — ограничение, сколько разных устройств может использовать одну подписку. Работает не через сам VPN-протокол (Xray физически не видит "железо" клиента), а на уровне выдачи самой подписки: современные клиенты (Happ, v2rayTun и т.д.) при запросе /sub/{token} шлют заголовок x-hwid — уникальный ID устройства. Панель запоминает первые N увиденных hwid на юзера; при попытке добавить N+1-е устройство — отказ (404 + заголовок x-hwid-max-devices-reached).
Выключено по умолчанию (HWID_LIMIT_ENABLED=false) — клиенты, которые не шлют x-hwid, при включённом лимите вообще не получат подписку, так что включай только если знаешь, что твои пользователи сидят на приложениях с поддержкой этого заголовка. HWID_FALLBACK_LIMIT — лимит по умолчанию для всех, в админке (Подписки → кнопка «Устройства» у юзера) можно посмотреть/удалить привязанные устройства и задать индивидуальный лимит.
Стабильность и производительность
install.shретраит все сетевые шаги (apt, git, pip, certbot, установка Xray) — 3 попытки с паузой, типичная причина падения свежих VPS-установок это не баги, а моргнувшая сеть/зеркало пакетов.- SQLite в WAL-режиме +
synchronous=NORMAL(стандартная безопасная связка для этого режима) — меньше блокировок между ботом и апи, которые пишут в одну базу параллельно. - Индексы на всех горячих путях (
subscriptions(active, expires_at)— дергается ботом каждые 90 секунд на проверку истечения;subscriptions(tg_id),devices(tg_id),payments(status)) — на паре сотен юзеров разницы не увидишь, но на тысячах — увидишь. - CI гоняет не только синтаксис, а реальный импорт
api.py/bot.pyцеликом плюс smoke-тесты платёжной и HWID-логики на живом Ubuntu — ловит баги при импорте/вайринге модулей до того, как они попадут на прод.
Частые проблемы при установке
- Certbot не может выпустить сертификат — почти всегда значит DNS A-записи ещё не проехали (обнови и подожди 5-10 минут) или порт 80 занят чем-то ещё (
ss -ltnp | grep :80). - Xray не стартует после установки —
mbs logs xray, почти всегда конфликт порта 443 с чем-то другим, что уже там висит (другая панель, старый nginx на 443 напрямую). - Панель недоступна, но сервисы
active— проверьnginx -tиsystemctl status nginx, скорее всего SNI-роутер (/etc/nginx/stream.d/mbs.conf) не подхватился —nginx -s reloadпосле любых правок туда вручную.
Разработка / вклад
Код простой — без сборки фронта, без ORM, без лишних абстракций. admin.html — один файл, vanilla JS. Питон-часть — обычные функции, SQLite напрямую.
PR и issues welcome. CI на каждый пуш гоняет compile-check по питону, синтаксис-проверку шелл-скриптов и smoke-тест генерации install-скрипта ноды.
Авторы:
github.com/devsavsis github.com/welfizx
Лицензия
MIT — см. LICENSE.