Как marzban нода связана с панелью (и почему это важно для firewall)
Marzban умеет запускать Xray прямо на сервере панели. Это нормально ровно до момента, когда появляются платящие абоненты: панель с базой пользователей и точка выхода на одном IP — один блокируемый адрес на весь сервис.
Вынос трафика на отдельную ноду даёт три вещи: адрес панели не попадает в подписки, точку выхода можно менять без переезда БД (пользователи, лимиты, статистика остаются в панели), и узлы можно ставить у разных хостеров.
Ключевая механика, из-за которой чаще всего не сходится firewall: соединение инициирует панель. Она стучится на ноду, предъявляет клиентский сертификат, который сама же выпустила, а нода проверяет его по файлу ssl_client_cert.pem. Нода наружу сама не ходит и адрес панели знать не обязана — ей нужен только входящий доступ на 62050/62051 с IP панели. Исходящие правила на ноде под управляющий канал не нужны.
Marzban node — это «Xray плюс тонкая обвязка для управления»: ни БД, ни веб-морды. По наблюдениям, 1–2 vCPU и 1 ГБ RAM спокойно держат несколько сотен онлайн-сессий; упираетесь вы обычно не в CPU, а в полосу и в характер трафика — видеостриминг съедает канал в разы быстрее мессенджеров. CPU становится узким местом уже на гигабитах, и в первую очередь на TLS-хендшейках, а не на прокачке байт.
Установка marzban node: контейнер и переменные окружения
Быстрый путь — официальный скрипт из Marzban-scripts. Он создаёт /opt/marzban-node/docker-compose.yml, каталог данных /var/lib/marzban-node и CLI-обёртку:
apt update && apt -y upgrade
curl -fsSL https://get.docker.com | sh
sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban-node.sh)" @ install
После установки доступны marzban-node up|down|restart|logs|status|core-update (набор команд отличается между версиями скрипта — сверяйтесь с marzban-node --help).
Ручной compose проще править под себя:
services:
marzban-node:
image: gozargah/marzban-node:latest
container_name: marzban-node
restart: always
network_mode: host
environment:
SERVICE_PROTOCOL: "rest"
SERVICE_PORT: "62050"
XRAY_API_PORT: "62051"
SSL_CLIENT_CERT_FILE: "/var/lib/marzban-node/ssl_client_cert.pem"
volumes:
- /var/lib/marzban-node:/var/lib/marzban-node
network_mode: host — не «для удобства», а рабочее требование: Xray должен слушать реальные порты хоста без NAT-проброса, иначе в логах вместо клиентских адресов появляется адрес docker-моста, а Reality и XHTTP ведут себя непредсказуемо. Из-за host-режима секция ports: не пишется вообще.
| Переменная | По умолчанию | Что делает |
|---|---|---|
SERVICE_PROTOCOL | rpyc | Протокол управления. rest стабильнее держит длинные сессии, но поддержан только свежими сборками панели и ноды — на старой панели нода зависнет в connecting, тогда возвращайте rpyc |
SERVICE_PORT | 62050 | Порт, на который стучится панель |
XRAY_API_PORT | 62051 | gRPC API Xray: статистика и онлайн |
SSL_CLIENT_CERT_FILE | — | Клиентский сертификат панели; без него нода не примет подключение |
SSL_CERT_FILE / SSL_KEY_FILE | /var/lib/marzban-node/ssl_cert.pem / ssl_key.pem | Собственная пара ноды, генерируется при первом старте |
XRAY_EXECUTABLE_PATH | /usr/local/bin/xray | Путь к бинарю ядра — меняется при ручном обновлении Xray |
XRAY_ASSETS_PATH | /usr/local/share/xray | geoip.dat / geosite.dat |
DEBUG | false | Подробные логи; в проде выключен |
Запуск и проверка:
docker compose up -d
ss -tlnp | grep -E '62050|62051' # порты слушаются?
docker logs -f marzban-node
В логах свежей ноды инбаундов не будет — это нормально, конфиг Xray приедет из панели после подключения. Отдельно: :latest удобен на старте, но в мультиноде он же источник расхождения версий ядра — про это ниже.
Сертификат и подключение ноды к панели
Сертификат выпускает панель, и он один на все ноды. В UI его показывают в диалоге Nodes → Add Node, но в проде удобнее забрать через API:
TOKEN=$(curl -s -X POST https://panel.example.com/api/admin/token \
-d "username=admin&password=СЕКРЕТ" | jq -r.access_token)
curl -s https://panel.example.com/api/node/settings \
-H "Authorization: Bearer $TOKEN" | jq -r.certificate \
> /var/lib/marzban-node/ssl_client_cert.pem
chmod 600 /var/lib/marzban-node/ssl_client_cert.pem
docker compose -f /opt/marzban-node/docker-compose.yml up -d --force-recreate
Файл должен начинаться с -----BEGIN CERTIFICATE----- и заканчиваться -----END CERTIFICATE-----. Самая частая ошибка установки — копипаст через веб-терминал, когда теряются переносы строк: нода стартует, но панель к ней не подключается. Быстрая проверка: openssl x509 -in /var/lib/marzban-node/ssl_client_cert.pem -noout -subject.
Закрываем сервисные порты — наружу они торчать не должны:
ufw allow from <IP_ПАНЕЛИ> to any port 62050 proto tcp
ufw allow from <IP_ПАНЕЛИ> to any port 62051 proto tcp
ufw allow 443/tcp # рабочий инбаунд, открыт клиентам
ufw enable
Если на ноде крутится что-то ещё в Docker с публикацией портов, помните: ufw не фильтрует трафик, который docker-proxy пробрасывает через цепочку DOCKER-USER. Для ноды в network_mode: host это не проблема, для соседних контейнеров — проблема.
Добавление ноды: UI (Nodes → Add Node: имя, адрес, порт 62050, API-порт 62051) или API:
curl -X POST https://panel.example.com/api/node \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"de-1","address":"203.0.113.10","port":62050,
"api_port":62051,"usage_coefficient":1,"add_as_new_host":false}'
add_as_new_host: true добавит адрес ноды хостом ко всем инбаундам. Для первой ноды удобно, для мультиноды лучше false и ручные хосты — иначе подписки распухают и абонент получает адреса, которые вы ему не собирались показывать.
| Симптом в панели | Что проверять |
|---|---|
connecting бесконечно | 62050 закрыт со стороны панели. Security group хостера проверяется отдельно от ufw; с панели — nc -vz <IP_НОДЫ> 62050 |
certificate verify failed | Битый или чужой ssl_client_cert.pem; после замены обязателен --force-recreate |
| Ошибки TLS-рукопожатия | Разъехалось время: timedatectl, systemctl enable --now systemd-timesyncd |
Нода connected, статистика нулевая | Не открыт 62051 — панель не достаёт Xray API |
| Нода падает сразу после привязки | Конфиг из панели не запускается на этом ядре (следующий раздел) |
Полезная привычка: смотреть docker logs --tail 100 marzban-node на ноде и marzban logs на панели одновременно — сразу видно, с какой стороны обрывается.
Marzban мультинода: один конфиг, много выходов
Здесь ломается интуиция большинства операторов. Marzban мультинода не означает разные конфиги на разных нодах: панель хранит единственный /var/lib/marzban/xray_config.json и раздаёт его всем подключённым узлам. Все ноды поднимают одни и те же инбаунды на одних и тех же портах.
Распределение клиентов идёт на уровне Hosts — записей, из которых генерируется подписка. Один инбаунд → сколько угодно хостов:
| Поле хоста | Пример | Комментарий |
|---|---|---|
| Remark | DE-1 {USERNAME} | Что абонент видит в приложении; поддерживает переменные |
| Address | de1.example.com | Адрес конкретной ноды или CDN-фронта перед ней |
| Port | 443 | Может отличаться от порта инбаунда, если впереди прокси |
| SNI / Host | cdn.example.com | Согласуется с сертификатом и фронтом |
| Path | /xh | Для XHTTP/WS |
Что из этого следует на практике:
- Абонент получает в подписке все хосты своих инбаундов и переключается сам (в Happ, v2rayNG, Streisand это выбор сервера или автоподбор по задержке). Балансировщика на стороне панели нет — «раскидать нагрузку по нодам» из панели не получится.
- Жёсткой привязки «пользователь → нода» в ванильном Marzban нет. Рабочий приём: завести отдельные теги инбаундов под группы узлов (
VLESS_XHTTP_EUна 8443,VLESS_XHTTP_RUна 8444), привязать к каждому тегу хосты только нужных нод и выдавать доступ через полеinboundsпользователя. Ноды всё равно слушают оба порта, в подписку попадает только разрешённый адрес. Это приём, а не штатная функция; в форках и в Marzneshin назначение инбаундов на ноду сделано нормально — проверяйте свою ветку. usage_coefficient— множитель списания трафика на узел. Коэффициент2.0спишет абоненту 2 ГБ за реально прокачанный 1 ГБ. Это единственный штатный экономический рычаг, если узлы стоят по-разному.- Расход по узлам — в Nodes Usage. Сверяйте с биллингом хостера: систематическое расхождение обычно означает трафик мимо учёта — например, старый хост, забытый в чьих-то подписках, или прямые подключения к origin в обход фронта.
После правки xray_config.json нужен marzban restart на панели — она перегенерирует конфиг и разошлёт его по нодам. Править Xray руками на самой ноде бессмысленно: следующая синхронизация всё затрёт.
Версии Xray на нодах: где мультинода ломается тихо
Самый неприятный класс багов: конфиг один, а ядра разные. Панель формирует JSON под ту версию Xray, которую держит в голове оператор, а исполняет его ядро на ноде. Старое ядро либо игнорирует новые поля, либо не поднимает инбаунд вообще — и вы видите отвалившуюся ноду без внятного объяснения.
docker exec marzban-node xray version # на каждом узле
Обновление, если ставили официальным скриптом:
marzban-node core-update
docker compose -f /opt/marzban-node/docker-compose.yml restart
Скрипт кладёт бинарь в каталог данных ноды (путь печатается в выводе — обычно подкаталог xray-core внутри /var/lib/marzban-node). Если ставите ядро руками, обязательно пропишите на него XRAY_EXECUTABLE_PATH, иначе контейнер продолжит запускать встроенное ядро из образа, и xray version покажет не тот бинарь, который вы обновили.
Где это стреляет чаще всего:
- XHTTP. Транспорт появился в Xray-core в конце 2024 года как замена SplitHTTP, и поля вокруг него дозревали ещё несколько релизов. На ноде с ядром годичной давности инбаунд с
"network": "xhttp"просто не стартует. По наблюдениям, разумный минимум для XHTTP в проде — одна свежая ветка на всех нодах сразу; официальной таблицы совместимости полей нет, ориентир — changelog релизов Xray-core. - Поле
extra. Оно должно совпадать на ноде и в клиенте — это часть согласования транспорта, а не косметика. Расхождение даёт классический симптом «в одном приложении работает, в другом нет»: соединение может даже установиться, а потом молча встать. - Экзотические obfuscation-поля. Всё, что появилось недавно или живёт только в форках ядра, ломает совместимость между версиями Xray и между клиентами. В проде держите минимальный набор полей и вводите новые только после проверки на тестовой ноде и на всех приложениях, которыми реально пользуются абоненты.
Операционная гигиена: одинаковая версия ядра на всех узлах, обновление по одному с проверкой подключения после каждого, фиксированный тег образа вместо :latest. Отката тут нет — только руками.
Нода connected, а клиенты не подключаются: блокировка по IP
Типовая картина: в панели connected, Xray жив, логи чистые, а абоненты у конкретного оператора не подключаются. Разделите проблему на «панель ↔ нода» и «клиент ↔ нода» и проверьте вторую часть с хоста внутри проблемной сети:
nc -vz 203.0.113.10 443 # TCP вообще устанавливается?
curl -m 5 -vk https://203.0.113.10/
mtr -T -P 443 203.0.113.10 # где обрывается маршрут
Если из-за рубежа порт открыт, а из мобильной сети RU соединение не устанавливается вовсе, дело не в конфиге ноды. При режиме белых списков оператор пропускает трафик только к «разрешённым» подсетям, остальное режется по умолчанию, и подсеть обычного VPS-хостера туда не входит. Протокол в этот момент не решает ничего: Reality маскирует содержимое соединения, но не меняет адрес назначения, а до этого адреса соединение не доходит.
| Мера | Что реально происходит |
|---|---|
| Сменить IP у того же хостера | Соседний адрес часто в той же /24 и отсекается вместе с ней |
| Переехать к другому провайдеру | Работает до следующей волны |
| Выдать домен вместо IP | Не помогает: режется доступ к адресу, куда домен резолвится |
Рабочая схема — фронт на адресе, который уже в белом списке: клиент подключается к CDN-фронту, тот проксирует трафик на вашу origin-ноду. Для DPI это обращение к разрешённому адресу, реальный IP ноды абоненту не нужен. Marzban при этом не переустанавливается — правятся только Address, SNI/Host и Port в Hosts. Это и есть задача класса whitelist-CDN для VPN-операторов; у нас она и решается.
Одна деталь, на которой ломаются почти все, кто ставит XHTTP за CDN впервые: у Yandex CDN нет POST, поэтому аплинк вырождается в GET, а GET разрешён только при явном mode: packet-up. Без него часть клиентов подключается, часть — нет, и симптом выглядит как «плохой клиент».
"xhttpSettings": {
"host": "cdn.example.com",
"path": "/xh",
"mode": "packet-up"
}
Отдельно закройте origin: после переезда за фронт нода должна принимать 443 только с адресов CDN плюс секретный заголовок на входе — иначе прямой IP всё равно найдут сканеры и заблокируют. Подробный разбор XHTTP за CDN, защиты origin и экономики трафика — в отдельных материалах блога. Здесь важно зафиксировать одно: нода в дата-центре блокируется предсказуемо, и устойчивость даёт не смена ноды, а вход через «белый» адрес перед ней.