LUFFY Control Tower — это управляющая панель для VPN/Proxy-кластера с доступом через Telegram Mini App и Telegram-бота.
Система решает задачи:
- учет и хранение инвентаря нод (Ingress/Egress/Master);
- удаленное выполнение команд по SSH;
- безопасное обновление
haproxy.cfgна удаленных нодах; - мониторинг состояния инфраструктуры и уведомления в Telegram;
- интеграция с Marzban API для статистики и алертов.
FastAPI— HTTP API (/api/*) и health-check (/health).Static Mini App(static/index.html) — web-интерфейс Telegram Mini App.Aiogram— Telegram-бот (long polling).SQLite(aiosqlite, WAL) — хранилище нод и настроек.APScheduler— фоновые SRE/security/backup задачи.asyncssh— управление удаленными нодами.aiohttp— интеграция с Marzban API.
main.pyинициализирует логирование и БД.- Стартует планировщик фоновых задач.
- Параллельно запускаются два сервиса:
uvicorn(FastAPI) иaiogrampolling. - Статический Mini App отдается из
static/, API живет под/api.
main.py— точка входа приложения.config.py— загрузка конфигурации из.env.web/api.py— API Mini App + авторизация Telegram initData.bot/handlers.py— команды Telegram-бота.bot/middlewares.py— AntiFlood middleware.services/ssh_manager.py— универсальный SSH-исполнитель.services/deployer.py— автоматический bootstrap нод.services/haproxy_manager.py— безопасное применениеhaproxy.cfg.services/monitor.py— планировщик мониторинга.services/marzban.py— клиент Marzban API + детекция аномалий.services/security.py— auto-ban + SSH audit.services/backup.py— weekly backup БД с отправкой в Telegram.db/database.py— схема и доступ к SQLite (WAL).docker-compose.yml,Dockerfile— контейнеризация..github/workflows/deploy.yml— CD-пайплайн.
| Переменная | Назначение | Пример |
|---|---|---|
BOT_TOKEN |
токен Telegram-бота | 123456:ABC... |
ADMIN_ID |
Telegram user ID администратора | 123456789 |
WEB_HOST |
хост FastAPI внутри процесса | 0.0.0.0 |
WEB_PORT |
порт FastAPI и Docker mapping | 8085 |
WEB_APP_URL |
публичный HTTPS URL Mini App | https://tower.volart.pro |
SSH_DEFAULT_USER |
SSH пользователь по умолчанию | root |
SSH_KEY_PATH |
fallback-ключ на хосте | /root/.ssh/id_rsa |
SSH_PORT |
SSH порт нод | 2222 |
MARZBAN_URL |
URL Marzban API | http://127.0.0.1:8000 |
MARZBAN_USERNAME |
логин Marzban | admin |
MARZBAN_PASSWORD |
пароль Marzban | *** |
DB_PATH |
путь к SQLite (переопределяется compose) | /app/data/luffy_cluster.db |
Значение WEB_PORT должно быть согласовано в трех местах:
.env:WEB_PORT;docker-compose.yml: проброс${WEB_PORT}:${WEB_PORT};nginx:proxy_pass http://127.0.0.1:<WEB_PORT>;.
Если порты не совпадают, получите 502 Bad Gateway.
Поля:
id(PK, autoincrement);name(display name);ip(unique, not null);role(master,ingress,egress);billing_date(YYYY-MM-DD);status(active/offline, defaultactive);ssh_key(private key ноды, может бытьNULL);ssh_port(SSH порт);inbound_tag(одна из групп:IN-RU-DIRECT,IN-EU-DIRECT,IN-TRANSIT-GB,IN-TRANSIT-NO,IN-EU-TRANSIT-RECV,IN-EU-DIRECT-WARP);inbound_port(порт подключения узла в группе);group_sni(SNI группы);fingerprint(клиентский fingerprint);marzban_node_id(id ноды в Marzban, если зарегистрирована);marzban_node_status(unknown/connected/error);marzban_last_error(последняя ошибка Marzban);provision_status(pending/provisioning/ready/error).
Простое key-value хранилище:
key(PK);value(text).
Базовый префикс: /api
Все /api/* endpoint защищены require_telegram_admin:
- требуется заголовок
X-Telegram-Init-Data; - подпись
initDataпроверяется по HMAC-схеме Telegram; - проверяется свежесть
auth_date(окно 24 часа); user.idизinitDataдолжен совпадать сADMIN_ID.
GET /api/nodes- возвращает список нод;
ssh_keyне отдается наружу (заменяется флагомhas_ssh_key).
POST /api/nodes- добавляет ноду в инвентарь;
- если
ssh_keyпуст, генерируется новый RSA private key; - запускает pipeline: (опционально) bootstrap новой ноды -> add node в Marzban -> attach в выбранную inbound-группу;
- при частичном успехе возвращает
status=partialс текстом причины.
PUT /api/nodes/{node_id}- редактирует параметры подключения и бизнес-поля;
- при
reconnect_marzban=trueвыполняет перепривязку к Marzban/inbound после обновления.
DELETE /api/nodes/{node_id}- удаляет ноду из панели;
- удаляет host/node из Marzban;
- выполняет cleanup на удаленной ноде по SSH (опционально флагом).
GET /api/status/overview- агрегирует состояние Marzban и SSH доступность всех нод;
- возвращает
marzban_error_code(auth_401,auth_403,network,http_error).
GET /api/marzban/connection- явная проверка соединения с Marzban.
POST /api/marzban/reconnect- принудительный сброс токена и повторная авторизация.
POST /api/haproxy/apply- применяет новый конфиг HAProxy на целевой ноде;
- выполняет валидацию
haproxy -c, backup, restart и rollback при сбое.
Доступ к командам ограничен по ADMIN_ID.
Команды:
/start— отправляет кнопку открытия Mini App (WEB_APP_URL, толькоhttps://)./nodes— список нод из БД./sysinfo <ip>—uptime,free -m,df -h /по SSH./deploy <ip> <role> <password> <billing_date>— bootstrap новой ноды./top_users— топ активных пользователей по трафику из Marzban.
Планировщик запускает:
check_disk_space— каждые 6 часов;external_port_knocker— каждые 15 минут;check_billing— ежедневно в 10:00;marzban_manager.check_traffic_anomalies— каждый час;check_decoy_watchdog— каждые 5 минут;check_ssl_expiry— ежедневно в 09:00;check_link_latency— каждые 30 минут;auto_ban_scanners— каждые 15 минут;ssh_audit— каждые 15 минут;backup_database— воскресенье 03:00.
cp .env.example .env
# заполнить .env
docker compose up -d --build
curl -f http://127.0.0.1:${WEB_PORT:-8080}/healthМинимально:
server {
listen 80;
server_name tower.volart.pro;
location / {
proxy_pass http://127.0.0.1:8085;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}После TLS (certbot) доступ к Mini App должен быть только через https://.
GitHub Actions workflow Deploy Control Tower:
- SSH на сервер.
git pull origin main.docker compose down && docker compose up --build -d.- health-check
http://127.0.0.1:${WEB_PORT}/health. docker image prune -f.
- API не обслуживает незаверенные запросы без Telegram
initData. - Доступ по user-level ограничен одним
ADMIN_ID. ssh_keyне показывается через API.
- Статическая страница Mini App может открыться у любого по URL, но API-вызовы без валидного Telegram контекста будут
401/403. ssh_keyхранится в БД в открытом виде. Это удобно для автоматизации, но требует защиты хоста и файла БД.- В SSH-клиенте
known_hosts=None, то есть защита от MITM снижена (осознанный компромисс).
- ограничить доступ к домену на уровне Nginx (
allow/deny, basic auth, Cloudflare Access); - шифровать
ssh_keyв БД (хотя бы symmetric encryption через master key); - добавить аудит действий в API (кто/когда/что менял);
- добавить rate-limit на API на уровне Nginx.
- Открыть Mini App только кнопкой из
/start. - В форме «Добавить сервер» указать:
name,IP,role,ssh_port,billing_date;inbound_tag,inbound_port,group_sni,fingerprint;- флаг
Сервер новыйпри необходимости bootstrap.
- Поле SSH ключа оставить пустым для автогенерации или вставить свой.
- Нажать «Добавить сервер» и дождаться результата pipeline.
- В инвентаре нажать
Удалить. - Подтвердить удаление.
- Сервис удалит ноду из панели, Marzban и выполнит cleanup на удаленной ноде.
- Вставить IP целевой ноды.
- Вставить новый конфиг.
- Нажать «Проверить и Задеплоить».
- При синтаксической ошибке конфиг не применяется, возвращается текст ошибки.
docker compose ps
docker compose logs --tail=200 control-tower
curl -vk https://tower.volart.pro/health
curl -H "Host: tower.volart.pro" http://127.0.0.1/health -vПричины:
- неверный
proxy_passпорт; - backend не слушает нужный
WEB_PORT; - container рестартует/падает.
Проверки:
docker compose ps;curl http://127.0.0.1:<WEB_PORT>/health;nginx -t && systemctl reload nginx.
Типовой кейс: порт 443 занят другим сервисом (например, haproxy), и клиент получает чужой сертификат.
Проверки:
ss -lntp | grep :443;- оставить владельцем
:443толькоnginxдля Mini App-домена; - затем
certbot --nginx -d <domain>.
Проверьте:
- открытие строго из Telegram-бота;
- корректный
WEB_APP_URL=https://...; ADMIN_IDсовпадает с вашим Telegram user id;- в запросах есть
X-Telegram-Init-Data.
Если сервер не ходит в github.com:443, deploy через Action упадет на шаге git pull.
Варианты:
- открыть исходящий
443до GitHub; - использовать зеркала/прокси;
- временно выкатывать
scp/rsync+docker compose up -d --build.
- заполнен
.envбез заглушек; - корректны
BOT_TOKEN,ADMIN_ID,WEB_APP_URL; - health-check
200 OKпо HTTPS домену; - порт
443обслуживается нужным веб-сервером; - backup job реально отправляет файл в Telegram;
- команды
/start,/nodes,/sysinfoработают у администратора.