Self-hosted VPN reseller panel — VLESS+Reality/gRPC/XHTTP/WS-TLS + Hysteria2, Telegram bot, admin panel, one-command node install
Find a file
savsis 6546d5bed1 feat: two-sided referral program
Each user gets a short ref_code (backfilled lazily for pre-existing
accounts too) and a shareable t.me/<bot>?start=ref_<code> link, new
"Пригласить друга" menu item shows it plus how many referrals actually
converted and any bonus days waiting to be applied.

Reward fires once, on the referred user's first subscription of any
kind (free, gift, or paid) — not on signup, so an unconverted click
never pays out. Both sides get REFERRAL_BONUS_DAYS (config.py/.env,
default 3): the referrer's day count comes from settings.py's live-read
pattern, same as prices/HWID, so it's tunable without a restart even
before a panel UI exists for it. Bonus extends an active subscription
directly if the recipient has one, otherwise accumulates in
bonus_days_pending and gets folded into whichever subscription they
create next (redeemed automatically inside create_subscription, one
choke point regardless of which of bot.py's several call sites created
it — free trial, gift code, paid, or admin grant).

Guards: no self-referral, referrer must exist, first-touch attribution
only (a second ?start=ref_ link never overwrites it), and only takes
for genuinely new accounts (no existing subscriptions) — attaching a
referrer to an already-active user was never the intent.

Tested two ways, matching this repo's usual db.py-can-be-imported-
standalone / bot.py-needs-a-workaround split: 16 checks against a real
isolated sqlite db for the db.py logic (attribution, both reward paths,
double-reward guard, pending-bonus fold-in), then 9 more through an
actual `import bot` — aiogram/fastapi now have Python 3.14 wheels so
this imported for real rather than needing AST-extraction, modulo one
old blocker (xray_manager still imports the Unix-only fcntl for its
file lock) worked around with a tiny fake fcntl module in sys.modules,
same spirit as the fcntl shim already used elsewhere in this project's
history. Real start_deeplink and cb_referral calls, get_me() mocked to
avoid a live Telegram API call.

Not done: admin-panel UI toggle for REFERRAL_ENABLED/REFERRAL_BONUS_DAYS
(currently .env-only, like several other business tunables were before
they got a settings-page treatment) and a docs-tab writeup — happy to
add both if wanted, scoped this pass to the mechanic itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 22:29:15 +05:00
.github/workflows feat: optional custom admin login path — matches a Remnawave-listed security measure Marzban doesn't have 2026-09-14 01:58:45 +05:00
landing site: catch up the comparison table with v1.2.0 (hold/pause, node webhooks, custom admin path, drag-n-drop) 2026-09-14 05:58:27 +05:00
site feat: custom brand name everywhere + a working client-facing site out of the box 2026-09-13 23:52:54 +05:00
systemd perf: multi-worker uvicorn + fix N+1 on the hottest path in the app 2026-09-12 16:13:44 +05:00
.env.example feat: optional custom admin login path — matches a Remnawave-listed security measure Marzban doesn't have 2026-09-14 01:58:45 +05:00
.gitignore core: env-driven config, sqlite schema, xray/node management, subscription links 2026-09-10 17:45:36 +05:00
admin.html fix: node-provisioning status poll could run forever, silently or stuck on "waiting" 2026-09-14 09:09:36 +05:00
api.py fix: admin panel showed raw JSON error envelopes instead of the actual message 2026-09-14 04:25:34 +05:00
backup.py fix: prune old backup safety-copies and expired admin sessions instead of letting them pile up forever 2026-09-12 15:08:34 +05:00
bot.py feat: two-sided referral program 2026-09-26 22:29:15 +05:00
config.py feat: two-sided referral program 2026-09-26 22:29:15 +05:00
db.py feat: two-sided referral program 2026-09-26 22:29:15 +05:00
install.sh feat: custom brand name everywhere + a working client-facing site out of the box 2026-09-13 23:52:54 +05:00
legal.py feat: custom brand name everywhere + a working client-facing site out of the box 2026-09-13 23:52:54 +05:00
LICENSE docs: readme, MIT license, CI workflow 2026-09-10 17:45:43 +05:00
links.py perf: multi-worker uvicorn + fix N+1 on the hottest path in the app 2026-09-12 16:13:44 +05:00
mbs perf: multi-worker uvicorn + fix N+1 on the hottest path in the app 2026-09-12 16:13:44 +05:00
nodeprov.py feat: validate xray config before every restart, local + managed nodes 2026-09-12 09:34:52 +05:00
payments.py feat: plan prices, payment toggles and HWID limit now editable live from the admin panel, no restart 2026-09-13 23:27:06 +05:00
README.md feat: outbound webhooks for node lifecycle — closes the "users + nodes" gap from the comparison 2026-09-14 02:56:25 +05:00
requirements.txt feat: backup & restore built into the admin panel 2026-09-12 10:23:38 +05:00
settings.py feat: two-sided referral program 2026-09-26 22:29:15 +05:00
totp.py feat: TOTP two-factor auth for admin login 2026-09-12 15:43:52 +05:00
webhooks.py feat: outbound webhooks for payment/subscription events 2026-09-13 21:15:13 +05:00
xray_manager.py feat: validate xray config before every restart, local + managed nodes 2026-09-12 09:34:52 +05:00

MBS Panel

CI License: MIT Release Python Xray-core

Самостоятельная VPN-панель на VLESS+Reality (+ gRPC/XHTTP/WS-TLS транспорты) и Hysteria2. Телеграм-бот для выдачи подписок, веб-сайт с личным кабинетом, и админ-панель для управления нодами, юзерами и трафиком — всё в одном репозитории, без сторонних панелей типа x-ui или Marzban под капотом.

Сделано by savsis. Изначально писалось под конкретный проект (шеринг VPN среди своих), но получилось достаточно универсально, чтобы выложить как есть.

Лендинг с фичами и честным сравнением с Remnawave/Marzban: mbs.savsis.xyz

Что внутри

  • Бот (aiogram 3) — выдача подписок по кнопкам, гифт-коды, привязка тарифов (7 дней / месяц / 3 месяца / полгода / год), автоматическое отключение по истечении подписки (не раз в полчаса, а раз в 90 секунд — важно, чтобы просрочка реально обрывала доступ, а не продолжала работать).
  • API + сайт (FastAPI) — страница подписки со ссылкой happ:// и QR-кодом, готовый клиентский лендинг + личный кабинет (живут прямо в панели, подставляют название и реальные тарифы сами, ничего отдельно хостить не надо).
  • Своё название бренда — панель, бот, сайт, страница подписки, оферта/политика показывают одно и то же настраиваемое название вместо дефолтного «MBS Panel», меняется в один клик из Настроек, применяется сразу.
  • Админ-панель (чистый 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 — управление панелью прямо с сервера: пароль, статус, рестарт, логи, обновление.
  • Backup & Restore прямо в админке — скачал архив (база + .env) одной кнопкой, восстановил загрузкой файла. Ни у Remnawave, ни у Marzban такого нет из коробки, только community-скрипты.
  • Мультиадминство + 2FA — отдельные логины вместо одного пароля на всех, опциональная TOTP-двухфакторка (любой Google Authenticator/Authy) поверх пароля.
  • Проверка конфига Xray перед рестартом — xray run -test плюс проверка что серты реально читаемы юзером, под которым крутится Xray, до того как что-то применится и уронит сервис.
  • Drag-n-drop ноды — порядок нод в списке настраивается мышкой, как у Remnawave.
  • Rate-limit на вход — по IP, отдельно на пароль и на 2FA-код.
  • Свой путь входа — страницу логина можно увести с дефолтного /admin на любой другой (ADMIN_PATH в .env), доп. слой поверх rate-limit и 2FA — у Remnawave это в списке заявленных мер безопасности, у Marzban нет вообще.
  • Пауза подписки — временно отключить доступ без потери оплаченных дней (Marzban это умеет, Remnawave — нет): при возобновлении срок сдвигается ровно на длительность паузы.
  • Исходящие вебхуки — на оплату, выдачу/отзыв/паузу/возобновление подписки и на добавление/удаление/вкл-выкл ноды, с HMAC-подписью тела. У Remnawave это события по юзерам и нодам, у Marzban — только по юзерам; мы покрываем оба класса.
  • Поиск и фильтр по подпискам — по юзернейму/tg id/ноде/тарифу и по статусу, прямо в таблице.

Архитектура

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-записи (см. таблицу ниже).

bash <(curl -Ls https://mbs.savsis.xyz/install.sh)

(или напрямую с GitHub, если так удобнее: git clone https://github.com/devsavsis/mbs-panel.git && cd mbs-panel && sudo bash install.sh — скрипт один и тот же, mbs.savsis.xyz просто зеркало с автосинком)

Скрипт спросит домен панели, домен подписки, токен бота от @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) появится админ-меню.
  • На https://sub.example.com уже живёт готовый клиентский сайт (лендинг + личный кабинет) с подставленным названием и реальными тарифами — ничего отдельно разворачивать не нужно. Название меняется в Настройки → «Название» в панели, применяется сразу везде (сайт, бот, страница подписки, оферта/политика).

В конце установки 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). Чтобы продавать доступ:

  1. Заведи аккаунт в ЮKassa и/или Platega — оба требуют реального ИП/самозанятости и проходят собственную проверку (реквизиты, сайт с офертой). Панель это не автоматизирует.
  2. В .env: PAYMENTS_ENABLED=true, цены на тарифы (PRICE_7D, PRICE_1M и т.д. в рублях), и креды провайдера(ов) — YOOKASSA_SHOP_ID/YOOKASSA_SECRET_KEY и/или PLATEGA_MERCHANT_ID/PLATEGA_SECRET, включив соответствующий *_ENABLED.
  3. В личном кабинете провайдера пропиши webhook на https://<PANEL_DOMAIN>/payments/webhook/yookassa и/или .../payments/webhook/platega.
  4. 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.