Блог Clearway

Marzban node: настройка ноды, мультинода и защита от блокировок

Коротко

Marzban node — отдельный сервер с Xray-core, который принимает трафик абонентов; сама панель трафик не гоняет, она раздаёт нодам конфиг и собирает статистику. Минимальный путь: Docker + контейнер gozargah/marzban-node в network_mode: host, клиентский сертификат панели в /var/lib/marzban-node/ssl_client_cert.pem, порты 62050 (управление) и 62051 (Xray API) открыты только с IP панели, затем Nodes → Add Node. Дальше важно понимать архитектуру: все узлы исполняют один и тот же xray_config.json из панели, поэтому marzban мультинода — это не разные конфиги на нодах, а разные Hosts (по хосту на узел) плюс usage_coefficient для разной цены трафика. Версия Xray должна совпадать на всех нодах: конфиг пишет панель, а исполняет ядро на ноде, и новые поля (xhttp, mode, extra) на старом ядре просто не поднимут инбаунд. Если нода в панели connected, а абоненты не подключаются — почти всегда заблокирован её IP, и лечится это не сменой подсети, а входом через фронт на «белом» адресе.

Как 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_PROTOCOLrpycПротокол управления. rest стабильнее держит длинные сессии, но поддержан только свежими сборками панели и ноды — на старой панели нода зависнет в connecting, тогда возвращайте rpyc
SERVICE_PORT62050Порт, на который стучится панель
XRAY_API_PORT62051gRPC 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/xraygeoip.dat / geosite.dat
DEBUGfalseПодробные логи; в проде выключен

Запуск и проверка:

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 — записей, из которых генерируется подписка. Один инбаунд → сколько угодно хостов:

Поле хостаПримерКомментарий
RemarkDE-1 {USERNAME}Что абонент видит в приложении; поддерживает переменные
Addressde1.example.comАдрес конкретной ноды или CDN-фронта перед ней
Port443Может отличаться от порта инбаунда, если впереди прокси
SNI / Hostcdn.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 и экономики трафика — в отдельных материалах блога. Здесь важно зафиксировать одно: нода в дата-центре блокируется предсказуемо, и устойчивость даёт не смена ноды, а вход через «белый» адрес перед ней.

Частые вопросы

Чем Marzban node отличается от панели Marzban?

Панель хранит пользователей, лимиты, подписки и статистику, генерирует конфиг Xray и раздаёт его узлам; трафик абонентов через неё не идёт. Marzban node — отдельный сервер с Xray-core, который принимает VLESS/Reality/XHTTP-соединения и выпускает трафик в интернет. Соединение между ними инициирует панель: она стучится на порт 62050 ноды и предъявляет клиентский сертификат, который сама же выпустила.

Какие порты нужно открыть для marzban ноды?

62050 — сервисный порт, куда подключается панель, и 62051 — gRPC API Xray, откуда панель забирает статистику и список онлайн. Оба открываются только для IP панели: ufw allow from <IP_ПАНЕЛИ> to any port 62050 proto tcp и аналогично для 62051. Отдельно открываются порты рабочих инбаундов, обычно 443. Если 62051 закрыт, нода будет connected, но статистика останется нулевой.

Где взять сертификат для ноды и что делать при ошибке подключения?

Сертификат выпускает панель, он одинаков для всех нод: диалог Nodes → Add Node или GET /api/node/settings (поле certificate). Файл кладётся в /var/lib/marzban-node/ssl_client_cert.pem целиком, вместе со строками BEGIN/END CERTIFICATE, после замены обязателен docker compose up -d --force-recreate. Если панель не подключается — по порядку: доступность 62050 со стороны панели (включая security group хостера), целостность файла (openssl x509 -in... -noout -subject), синхронность времени (timedatectl), затем docker logs marzban-node.

Можно ли в Marzban назначить конкретного пользователя на конкретную ноду?

Штатно — нет. Все ноды получают один и тот же xray_config.json, а распределение идёт через Hosts: у каждого узла свой хост со своим адресом, и абонент получает в подписке все хосты своих инбаундов, переключаясь между ними сам. Рабочий обходной приём — завести отдельные теги инбаундов под группы нод на разных портах, привязать к каждому тегу хосты только нужных узлов и выдавать доступ по тегам через поле inbounds. В форках и в Marzneshin назначение инбаундов на ноду реализовано штатно.

Почему инбаунд работает на одной ноде и не поднимается на другой?

Почти всегда — разные версии Xray-ядра. Панель пишет конфиг, а исполняет его ядро на ноде: новых полей вроде xhttp, mode или extra старое ядро не понимает, и инбаунд не стартует. Проверьте docker exec marzban-node xray version на всех узлах и приведите к одной версии (marzban-node core-update), а образ зафиксируйте тегом вместо :latest. Отдельно помните, что extra должно совпадать на ноде и в клиенте, а редкие obfuscation-поля ломают совместимость между версиями Xray и между приложениями.

Как учитывать разную стоимость трафика на разных нодах?

Через usage_coefficient в настройках ноды — множитель списания. При коэффициенте 2.0 абонент, прокачавший 1 ГБ через дорогой узел, потеряет 2 ГБ квоты. Фактический расход смотрите в Nodes Usage и регулярно сверяйте с биллингом хостера: систематическое расхождение обычно означает трафик мимо учёта — например, старый хост, забытый в чьих-то подписках.

Нода connected, но абоненты не подключаются — что это?

Проверьте доступность порта инбаунда из проблемной сети: nc -vz IP 443, mtr -T -P 443 IP. Если из-за рубежа порт открыт, а из мобильной сети RU соединение не устанавливается вовсе, это блокировка по IP, и протокол уже не важен: Reality маскирует содержимое, но не меняет адрес назначения. Смена IP или подсети даёт передышку до следующей волны; устойчивое решение — вход через whitelisted-адрес CDN перед нодой, при этом в Marzban правятся только Address/SNI/Port в хостах.

Whitelisted-вход для вашего VPN-сервиса

Clearway даёт «белый» CDN-вход перед вашей нодой — устойчивый к троттлингу операторов. Первые 10 ГБ бесплатно, без карты.

Попробовать →