Что такое 3x-ui: панель поверх Xray, а не протокол
3x-ui (репозиторий MHSanaei/3x-ui) — форк заброшенной панели x-ui от Vaxilu. Сама она ничего не проксирует: это веб-морда и супервизор над Xray-core. Конфигурация лежит в SQLite, при старте панель собирает из неё config.json и запускает бинарник Xray. Отсюда простое правило: что умеет ваша версия Xray — умеет и панель, чего Xray не умеет — панель не добавит.
Что оператор получает из коробки:
- Инбаунды протоколов Xray: VLESS, VMess, Trojan, Shadowsocks, SOCKS, HTTP, Dokodemo-door, WireGuard.
- Транспорты: TCP, WebSocket, HTTPUpgrade, gRPC, mKCP, XHTTP (бывший SplitHTTP); безопасность — TLS и Reality с генерацией ключей прямо в UI.
- Клиенты внутри инбаунда с индивидуальным лимитом трафика, сроком, числом IP и своим
subIdдля подписки. - Статистика по инбаундам и клиентам, список онлайн-клиентов, системные метрики (CPU, RAM, диск, сеть, uptime).
- Управление ядром: переключение версии Xray из панели, просмотр логов, ручная правка шаблона конфига (routing, DNS, outbounds, sniffing).
- Сервисное: выпуск сертификатов через acme, включение BBR, Telegram-бот, бэкап БД, fail2ban.
Ключевой архитектурный факт, который определяет всё остальное: 3x-ui управляет только тем Xray, который стоит на той же машине. Понятия «нода» в панели нет. Три сервера — это три независимые панели, три базы, три набора клиентов и три разные подписки.
Файловая раскладка после установки:
| Путь | Что это |
|---|---|
/etc/x-ui/x-ui.db | SQLite: настройки панели, инбаунды, клиенты, счётчики трафика |
/usr/local/x-ui/bin/config.json | сгенерированный конфиг Xray — перезаписывается при каждом рестарте |
/usr/local/x-ui/bin/xray-linux-amd64 | бинарник ядра |
/usr/local/x-ui/access.log, error.log | логи Xray (путь задаётся в шаблоне конфига) |
/root/cert/ | сертификаты, если выпускали через меню панели |
/etc/systemd/system/x-ui.service | юнит systemd |
Править config.json руками бесполезно: при следующем x-ui restart он будет собран заново из базы. Кастомные routing, DNS и outbounds задаются в разделе Xray Configs — этот шаблон лежит в таблице settings и переживает рестарты.
По ресурсам 3x-ui vpn-стек нетребователен: панель и Xray спокойно живут на 1 vCPU / 512 МБ, в простое связка занимает, по наблюдениям, ~60–150 МБ RSS. Упирается не панель, а сеть и CPU на шифровании.
Установка и то, что нужно закрыть в первый же час
Штатная установка одной командой:
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
Есть и вариант в Docker (docker compose из репозитория) — он удобен, когда на сервере уже есть контейнерный стек, но тогда следите за прокидыванием портов инбаундов и монтированием db/ наружу.
Свежие сборки при чистой установке генерируют случайные логин, пароль, порт панели и webBasePath и печатают их в конце вывода — сохраните их сразу. При апгрейде со старого x-ui можно унаследовать admin/admin на порту 54321, это надо проверить явно.
После установки доступна утилита x-ui:
x-ui # интерактивное меню: обновление, сертификаты, fail2ban, BBR
x-ui status
x-ui restart
x-ui settings # текущие логин, порт, webBasePath
x-ui log # логи панели
x-ui banlog # кого забанил лимит IP
x-ui setting -username op -password 'длинный-пароль' -port 8443 -webBasePath /a7Kd93Ls8Q
Минимум, который делается до того, как на панель попадёт первый платящий клиент:
- Свой
webBasePath. Панель на/по дефолтному порту (2053 в новых сборках) находится сканерами за часы. Длинный случайный путь — самая дешёвая защита. - Не выставлять панель в интернет вообще. Чистый вариант: Listen IP
127.0.0.1и доступ через SSH-туннель.
``bash ssh -L 2053:127.0.0.1:2053 root@ВАШ_СЕРВЕР # затем http://127.0.0.1:2053/ваш_путь/ ` Если панель должна быть снаружи — только TLS на своём домене и ограничение по источнику: `bash ufw allow from 203.0.113.10 to any port 8443 proto tcp ufw deny 8443/tcp ` Учтите: если на сервере крутится Docker, правила ufw не действуют на проброшенные контейнерами порты — их закрывают в цепочке DOCKER-USER`.
- TLS отдельно на порт панели и отдельно на порт подписки. Сертификат выпускается из меню
x-ui(acme, DNS- или HTTP-валидация) и указывается в Panel Settings двумя разными полями. - 2FA и Secret Token. Для панели, торчащей наружу, — обязательно. Честная оговорка: после включения 2FA скриптовый логин в API ломается (нужен код в форме входа), поэтому ботам обычно оставляют отдельный вариант доступа или ходят к панели по localhost.
- fail2ban. Ставится пунктом меню; фильтр
3x-ipl, логи/var/log/3xipl.logи/var/log/3xipl-banned.log. Тот же механизм обслуживает лимит IP на клиента — см. следующий раздел.
Лимиты трафика, срока и IP на клиента: как устроено внутри
Ради этого панель обычно и берут. У каждого клиента внутри инбаунда свой набор ограничений:
| Поле в UI | Что делает | Как хранится |
|---|---|---|
| Total Flow (GB) | лимит суммарного трафика up+down | в БД — байты, 0 = без лимита |
| Expire Date | дата отключения | unix-время в миллисекундах |
| IP Limit | максимум одновременных IP | целое, 0 = без лимита |
| Reset (days) | автосброс счётчика трафика раз в N дней | целое, 0 = не сбрасывать |
| уникальный идентификатор клиента в статистике | строка, менять больно | |
| Subscription (subId) | связывает клиента со ссылкой-подпиской | строка |
| Telegram ID | адресат уведомлений бота | строка |
Логика отключения простая: как только up+down >= total либо now > expiry_time, клиент помечается исчерпанным и Xray перестаёт его пускать. Считаются трафик и срок независимо — сгорает то, что наступит раньше. Исчерпанные собираются в отдельный список, откуда их массово удаляют или продлевают. Автопродления срока нет: продление — руками, ботом или через API.
Полезная деталь: отсчёт срока с первого подключения. Если задать срок в днях, а не датой, панель кладёт в expiry_time отрицательное значение, и таймер стартует в момент первого коннекта. Удобно для триалов и заранее нарезанных ключей — ключ не сгорает, пока лежит непроданным. Побочный эффект: любые ваши SQL-отчёты должны отдельно обрабатывать expiry_time < 0, иначе такие клиенты выглядят «просроченными в 1970 году».
Главная ловушка — IP Limit. Он реализован не в ядре, а разбором access-лога Xray: панель считает уникальные IP на email и через fail2ban банит лишние. Следствия:
- если в Xray Configs → log параметр
accessвыставлен вnone, лимит IP не работает вообще, и список онлайн-клиентов будет пустым; - это лимит по IP, а не по устройствам: телефон, переключившийся с Wi-Fi на LTE, какое-то время выглядит как два подключения, поэтому
1почти всегда больно — рабочий минимум2–3; - за NAT несколько разных людей с одного адреса считаются как один клиент;
- бан отрабатывает с задержкой в десятки секунд и снимается по таймеру fail2ban, а не мгновенно.
Включение лога в шаблоне конфига:
"log": {
"access": "/usr/local/x-ui/access.log",
"error": "/usr/local/x-ui/error.log",
"loglevel": "warning"
}
Ротация обязательна — на нескольких сотнях активных клиентов access.log растёт быстро и способен забить диск за неделю. Xray не переоткрывает файл по сигналу, поэтому только copytruncate:
cat >/etc/logrotate.d/xray <<'EOF'
/usr/local/x-ui/*.log {
daily
rotate 3
missingok
notifempty
copytruncate
compress
}
EOF
Статистика, подписка и Telegram-бот: что реально закрывает панель
Статистика. Счётчики берутся из stats API Xray и пишутся в базу: трафик по инбаунду, по клиенту, признак онлайна. Сброс — точечно по клиенту, по инбаунду или целиком. Исторических графиков «по дням» нет: панель показывает накопленный итог с момента последнего сброса. Ответить «сколько клиент выкачал в мае» без внешнего сборщика невозможно — если нужны графики и алерты, данные снимают снаружи (из БД или API) и складывают в свой Prometheus/Grafana.
Подписка. Встроенный subscription-сервис поднимается отдельным слушателем (по умолчанию порт 2096) со своим путём, доменом и сертификатом. Клиент получает https://sub.example.com:2096/sub/<subId> — base64-список ссылок; по параллельному пути /json/<subId> отдаётся JSON-формат для клиентов, которые его понимают. Настраиваются Update Interval, заголовки профиля и имя.
Ограничение по сравнению с Marzban/Remnawave: шаблонизации подписки в 3x-ui практически нет. Профиль с кастомным routing, набором правил и отдельными remarks под каждое приложение вы на ней не соберёте — отдаётся то, что панель сгенерировала из инбаундов. Обходной путь один: свой прокси перед подпиской, который переписывает выдачу (заодно там же удобно ставить Cache-Control: no-store — часть мобильных клиентов агрессивно кэширует старую подписку).
Telegram-бот. Токен, admin chat ID и cron-расписание задаются в Panel Settings. Что реально полезно оператору:
- отчёт по трафику и клиентам в чат по расписанию;
- уведомления, когда у клиента заканчивается трафик или срок;
- поиск клиента по email/UUID и выдача ссылки прямо в чате;
- автоматическая отправка
x-ui.dbв чат по расписанию — самый дешёвый оффсайт-бэкап из существующих (и одновременно утечка всех ключей, если чат не приватный); - рестарт панели командой.
Биллинга и продажи подписок бот не делает — это админский пульт, а не магазин. Для продаж всё равно понадобится свой бот, который ходит в API.
База, бэкап, перенос и прямые запросы
Вся жизнь панели — в одном файле, и относиться к нему надо как к связке ключей: в нём и UUID клиентов, и приватные ключи Reality, и хеш пароля админа.
Бэкап из UI — кнопка, отдающая x-ui.db. На сервере правильнее делать консистентную копию средствами SQLite, а не cp на живой базе:
cat >/usr/local/bin/xui-backup.sh <<'EOF'
#!/bin/bash
set -e
DST=/root/xui-backup
mkdir -p "$DST"
TS=$(date +%F_%H%M)
sqlite3 /etc/x-ui/x-ui.db ".backup '$DST/x-ui-$TS.db'"
tar czf "$DST/cert-$TS.tgz" /root/cert 2>/dev/null || true
find "$DST" -type f -mtime +14 -delete
EOF
chmod +x /usr/local/bin/xui-backup.sh
( crontab -l 2>/dev/null; echo "17 4 * * * /usr/local/bin/xui-backup.sh" ) | crontab -
Восстановление и переезд на другой сервер — одна и та же процедура:
# на новом сервере: та же или более свежая версия панели
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
systemctl stop x-ui
cp /root/x-ui-2026-07-20_0417.db /etc/x-ui/x-ui.db
chown root:root /etc/x-ui/x-ui.db && chmod 600 /etc/x-ui/x-ui.db
systemctl start x-ui && x-ui status
Довниз (положить базу от новой версии в старую панель) не делайте — миграции схемы в одну сторону. После переезда меняются IP и, возможно, домен: инбаунды переедут как есть, но ссылки клиентов будут указывать на старый адрес. Если адрес входа зашит в подписку, а не в сами ключи, миграция для пользователей проходит незаметно — ещё один аргумент отдавать клиентам ссылку-подписку, а не голый vless://.
Прямые запросы к базе экономят много времени, счётчики лежат в client_traffics:
apt install -y sqlite3
# топ-20 по расходу
sqlite3 -header -column /etc/x-ui/x-ui.db \
"select email,
round((up+down)/1073741824.0,2) as gb,
round(total/1073741824.0,2) as limit_gb,
case when expiry_time>0 then datetime(expiry_time/1000,'unixepoch')
when expiry_time<0 then 'не активирован'
else 'бессрочно' end as expires,
enable
from client_traffics order by up+down desc limit 20;"
# у кого срок истекает в ближайшие 3 суток
sqlite3 /etc/x-ui/x-ui.db \
"select email, datetime(expiry_time/1000,'unixepoch')
from client_traffics
where expiry_time > strftime('%s','now')*1000
and expiry_time < (strftime('%s','now')+259200)*1000;"
Писать в базу на живой панели не стоит: сами клиенты хранятся ещё и в JSON-поле settings таблицы inbounds, и рассинхрон двух мест ловится потом долго. Для изменений — API:
BASE=https://panel.example.com:8443/ВАШ_ПУТЬ
curl -s -c /tmp/c.txt -X POST $BASE/login -d 'username=op&password=ПАРОЛЬ'
curl -s -b /tmp/c.txt $BASE/panel/api/inbounds/list | jq '.obj[].remark'
# добавить клиента: 100 ГБ, без срока, 3 IP
curl -s -b /tmp/c.txt -X POST $BASE/panel/api/inbounds/addClient \
--data-urlencode 'id=1' \
--data-urlencode "settings={\"clients\":[{\"id\":\"$(cat /proc/sys/kernel/random/uuid)\",\"email\":\"user42\",\"totalGB\":107374182400,\"expiryTime\":0,\"enable\":true,\"limitIp\":3,\"subId\":\"sub42\",\"flow\":\"\"}]}"
curl -s -b /tmp/c.txt -X POST $BASE/panel/api/inbounds/resetClientTraffic/1/user42
Обратите внимание на totalGB: имя поля обманывает, значение задаётся в байтах. Есть также updateClient, delClient, getClientTraffics/{email}, onlines. Но API здесь — надстройка над сессией панели: авторизация по cookie, часть параметров передаётся строкой с вложенным JSON, поля меняются между версиями. Для бота-продажника хватает, для серьёзной интеграции — заметно менее приятно, чем документированные REST API соседей.
3x-ui vs Marzban vs Remnawave: чем отличаются и кому что
Сравнение по осям, которые реально меняют жизнь оператора:
| 3x-ui | Marzban | Remnawave | |
|---|---|---|---|
| Стек | Go, один бинарник + SQLite | Python/FastAPI + SQLite/MySQL | TypeScript/NestJS + PostgreSQL, Docker |
| Модель | панель = один сервер | панель + marzban-node | панель + ноды как сущность первого класса |
| Один юзер на N серверов | нет: свой клиент на каждой панели | да, один юзер → все ноды | да, юзер → squad из нод |
| Подписка | встроенная, почти без шаблонов | шаблонизируемая (Jinja) | шаблоны + правила выдачи, HWID-привязка |
| API | HTTP поверх сессии панели | REST + JWT, документирован | REST + API-ключи, документирован |
| Админы и роли | один аккаунт | sudo + дополнительные админы | несколько администраторов |
| Лимит устройств | по IP из access.log, грубо | по IP, аналогично | лимит по HWID устройств |
| Ресурсы под панель | десятки МБ RAM | умеренно | ощутимо больше (Postgres + сервисы) |
| Порог входа | низкий: 15 минут до первого ключа | средний | средний, ставится по инструкции |
| Развитие | активное | по наблюдениям, замедлилось; часть сообщества ушла на форки | активное |
Кому 3x-ui. Один-два сервера, до нескольких сотен клиентов, продажи через своего бота или руками. Нужна панель, которая ставится за вечер, ест копейки ресурсов и не тащит Docker-стек. Отдельный сильный сценарий — «сервер у нового хостера на пробу»: подняли, проверили доступность подсети у операторов, снесли.
Кому Marzban. Уже есть работающий сервис и интеграции — мигрировать без причины смысла нет. Для нового проекта выбирать его стоит осознанно, зная про темп развития.
Кому Remnawave. Несколько локаций, один пользователь должен видеть все серверы в одной подписке, нужны роли, API-ключи под биллинг, HWID-лимиты и внятное добавление/вывод ноды.
Практическое правило: пока сервер один — разницы почти нет, и 3x-ui выигрывает простотой. На третьем сервере схема «три панели» начинает стоить дороже, чем переезд на панель с нодами. Третья панель — это ещё один набор клиентов, ещё одна база для бэкапа и ручная синхронизация продлений в трёх местах.
Где 3x-ui упирается: ограничения, о которых узнают поздно
Честный список того, что всплывает после нескольких месяцев эксплуатации:
- Один администратор. Ролей, аккаунтов для поддержки, разграничения прав и журнала действий нет. Дать доступ саппорту = отдать полный доступ к панели и ко всем ключам.
- Нет истории трафика. Только накопленный счётчик — «сколько было в мае» отвечает внешний сборщик, не панель.
- API привязан к сессии и меняется между версиями. Обновление панели вполне способно сломать самописного бота: тестируйте апдейт на копии базы, а не в проде.
- Ручные правки
config.jsonтеряются при первом же рестарте. Всё кастомное — только через шаблон в Xray Configs. - SQLite — не бесплатная магия. На тысячах клиентов и частой записи счётчиков появляются подтормаживания и
database is locked; лечится это не тюнингом, а переездом на панель с нормальной СУБД. - Мультисервер отсутствует как концепция: ни общей базы клиентов, ни единой подписки, ни автоматического исключения упавшей ноды.
- Переключение версии Xray из панели удобно, но опасно. Смена мажорной версии меняет поведение транспортов; на проде переключайте только после проверки на тестовой машине и держите наготове предыдущую.
И отдельная категория — то, что панель в принципе не решает. Когда IP ноды попадает под блокировку у оператора, ни 3x-ui, ни Marzban, ни Remnawave тут ни при чём: проблема на уровне адреса и маршрута, а не панели. Лечится это слоем перед нодой — вход через whitelist-CDN, где клиент идёт на «белый» домен и адрес из разрешённого пула, а сама нода закрыта origin-файрволом и перестаёт быть мишенью. Практика подключения разобрана отдельно, в материале про 3x-ui за CDN; общая логика белых списков — в обзоре обхода белых списков.
Два технических факта, на которых чаще всего спотыкаются при такой связке, стоит держать в голове заранее:
- У Yandex CDN нет POST, поэтому аплинк XHTTP уходит GET-ом, а GET допустим только при явно заданном
"mode": "packet-up". Убрать режим «чтобы было как в документации» — сломать вход. Подробности — в разборе packet-up и XHTTP. - Блок
extra(иpath) на ноде и в клиентской ссылке должен совпадать символ в символ. Экзотические поля обфускации ломают совместимость между версиями Xray: клиент на другом ядре просто не поднимет соединение, а в логах будет невнятная ошибка рукопожатия.
Вывод по панели: 3x-ui хороша ровно в своей нише «один сервер, минимум обвязки, максимум скорости запуска». Переоценивать её не надо, но и менять на тяжёлую панель до появления второй-третьей локации смысла нет.