mbs-panel/README.md
savsis 670579fccd feat: optional custom admin login path — matches a Remnawave-listed security measure Marzban doesn't have
Pulled a fresh copy of docs.rw's own Remnawave-vs-Marzban comparison
table (not working from memory of an earlier read) to check what's
still genuinely different after tonight's run of fixes — most rows
already match or beat both panels (multi-admin, 2FA, HWID limits,
backup/restore, host sorting, config validation, node autonomy, on-hold
status as of a few commits ago). One concrete, bounded, unclaimed row:
"Security measures in documentation" lists CF zero trust / custom path
/ Telegram OAuth / 2FA for Remnawave, nothing for Marzban. We already
had 2FA and rate-limiting; custom path was the missing, actually
implementable piece — everything else in that row is deployment
guidance, not panel code.

New ADMIN_PATH env var (config.py, defaults to "admin" — every existing
install keeps working exactly as before with zero action needed). The
page-serving route moves to whatever path is configured; root() on
PANEL_DOMAIN only falls through to serving admin.html when ADMIN_PATH
is still the default, otherwise it shows the same branded landing page
every other domain gets — so a scanner or a human guessing "/admin"
finds nothing once this is set, not even a redirect that confirms
something lives there.

Deliberately scoped to ONLY the page route. /admin/api/* stays fixed —
it's already behind real cookie+session auth (verified this while
auditing: every mutating admin route either calls require_admin() or
the equivalent _require_current_admin(), checked programmatically via
ast rather than trusting my memory of having added the check everywhere
— found nothing actually missing, which is itself worth knowing, not
just assumed). Moving the API namespace too would be a much bigger,
riskier rewrite of every @app decorator in the file for no real security
gain over what auth already provides.

Deliberately NOT exposed in the Settings UI, unlike almost everything
else made live-editable tonight. This one genuinely needs a process
restart to take effect (FastAPI resolves routes at import time, not
per-request), and a typo saved through the UI followed by a restart
is a real self-lockout risk with no web-based way back — same tier as
PANEL_DOMAIN/SUB_DOMAIN, which are also .env-only for the same reason.
.env + SSH is the correct blast radius for a setting that can lock you
out.

Verification: config.py's normalization (strip slashes, empty/lone-
slash/repeated-slash input all falling back to "admin" rather than
accidentally producing a route at bare "/") tested directly — 8 cases.
AST-extracted the updated root() out of api.py (still can't import the
module locally) and exercised its actual branching with a mocked
FileResponse/legal/request — confirmed the default case is byte-for-
byte the old behavior and the custom-path case stops serving admin.html
on PANEL_DOMAIN's root. Added a dedicated CI step that does what only a
real FastAPI import can prove: with ADMIN_PATH set, /xyz123secret is a
registered route, plain /admin is NOT (not just supplemented — actually
gone), and /admin/api/login is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 01:58:45 +05:00

220 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MBS Panel
[![CI](https://github.com/devsavsis/mbs-panel/actions/workflows/ci.yml/badge.svg)](https://github.com/devsavsis/mbs-panel/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Release](https://img.shields.io/github/v/release/devsavsis/mbs-panel?include_prereleases)](https://github.com/devsavsis/mbs-panel/releases)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
[![Xray-core](https://img.shields.io/badge/xray--core-latest-red)](https://github.com/XTLS/Xray-core)
Самостоятельная VPN-панель на VLESS+Reality (+ gRPC/XHTTP/WS-TLS транспорты) и Hysteria2. Телеграм-бот для выдачи подписок, веб-сайт с личным кабинетом, и админ-панель для управления нодами, юзерами и трафиком — всё в одном репозитории, без сторонних панелей типа x-ui или Marzban под капотом.
Сделано by savsis. Изначально писалось под конкретный проект (шеринг VPN среди своих), но получилось достаточно универсально, чтобы выложить как есть.
Лендинг с фичами и честным сравнением с Remnawave/Marzban: **[mbs.savsis.xyz](https://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 нет вообще.
## Архитектура
```mermaid
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` в БД):
```mermaid
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`)
```mermaid
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
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](https://t.me/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
bash <(curl -Ls https://panel.example.com/install/ТОКЕН.sh)
```
Вставляешь на чистый сервер (тот же Ubuntu 22/24 или Debian 11/12) — нода сама всё ставит и регистрируется. Если включал WS+TLS — скрипт выпустит для неё отдельный Let's Encrypt сертификат (нужна ещё одна A-запись, см. таблицу выше). Если включал Hysteria2 — поднимет отдельный процесс на UDP.
```mermaid
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](https://yookassa.ru) и/или [Platega](https://platega.io) — оба требуют реального ИП/самозанятости и проходят собственную проверку (реквизиты, сайт с офертой). Панель это не автоматизирует.
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](LICENSE).