Почему «create node» не работает
Панели управления парком Xray-нод разделяют конфигурацию на несколько сущностей, и большинство ошибок вида «нода добавлена, а пользователи её не видят» или «нода висит disconnected» возникают из-за нарушения порядка их создания. Ниже — воспроизводимый канон добавления VLESS+Reality-ноды через API. Кликами в UI путь тот же.
Иерархия сущностей, снизу вверх:
Config Profile — полная xray-конфигурация (шаблон для ноды), содержит Inbounds
└─ Inbound — протокол (VLESS/Trojan/SS) + транспорт (tcp/xhttp) + security (reality)
Node — сервер с xray → активирует ОДИН профиль + выбранные инбаунды
Host — endpoint в подписке → смотрит на инбаунд по UUID, отдаёт юзеру TLS-параметры
Internal Squad — группа → какие инбаунды реально доступны пользователям
Ключевое правило: пользователь получает конфигурацию только из хоста и только если инбаунд включён в его squad. Голый инбаунд без хоста юзеру не отдаётся. Инбаунд без сквада — недоступен.
Порядок добавления (строго, не переставлять)
- Создать Config Profile с Reality-инбаундом. Ключевую пару x25519 генерируем локально — в ряде версий API нет эндпоинта: приватный ключ — base64url от X25519PrivateKey, shortId —
openssl rand -hex 8. Инбаунд:protocol: vless,port: 443,security: reality, вrealitySettings—dest,serverNames,privateKey,shortIds. Обязательно outboundsfreedom(DIRECT) +blackhole(BLOCK) и routing, блокирующий private-подсети и bittorrent. - Привязать профиль к ноде и включить инбаунды. В теле ноды —
activeConfigProfileUuidиactiveInbounds: [...]. БезactiveInboundsнода пустая. - Создать хост на инбаунд. Хост ссылается на
configProfileUuid+configProfileInboundUuid.sni— адрес маскировочного домена,fingerprint: chrome. Reality pbk/sid хост берёт из инбаунда сам. - Включить инбаунд в Internal Squad — только merge: сначала GET существующих inbounds, добавить новый, убрать дубли, потом PATCH. Перезапись массива сотрёт остальные инбаунды группы.
- Проверить: нода
isConnectedиxrayUptime > 0, хост появился в выдаче подписки, коннект живой.
Три слоя обязаны совпасть
Самая коварная грабля — squad. Полноценное добавление инбаунда — это совпадение трёх независимых слоёв: инбаунд в config.inbounds[] профиля (потенциал), тот же инбаунд в activeInbounds ноды плюс открытый порт в firewall (разрешение на сервере) и инбаунд в массиве группы (доступ пользователям). Если пропустить третий слой, у панели ноль пользователей на инбаунде — она не генерирует его в xray-config, порт не слушается, а лог при этом честно сообщает «config up-to-date». Панель права относительно того, что видит. Пользователю — пусто.
Поэтому верификация только по фактическому эффекту, а не по статусу «применено»:
ss -tlnp | grep <порт> # порт реально слушается?
openssl s_client -connect <адрес>:<порт> -servername <SNI>
# субъект сертификата должен совпасть с ожидаемым serverName
Сам процесс ноды должен быть запущен с ключом, который выдала панель, иначе нода не подключится при живом контейнере. Служебный порт node-агента закройте фаерволом от посторонних. И учтите: порт начинает слушаться после полного рестарта контейнера ноды, а не после мягкого reload.
Подводные камни
- Хост держит инбаунд по UUID, а не по тегу. Перед сносом инбаунда сверяйте
hosts[].inbound.configProfileInboundUuid— иначе оборвёте живой хост. - Один видимый хост на ноду. Два user-facing хоста на один адрес заставляют клиента метаться, коннект «через раз». Несколько транспортов одной локации — отдельными инбаундами, не вторым видимым хостом.
flow: xtls-rprx-visionработает только наnetwork: tcp. На xhttp он не поддерживается и ломает коннект. Включить flow «всем инбаундам сразу» — убить xhttp-хосты.- Не перезаписывайте config профиля целиком. Добавляя инбаунд, берите свежий config через GET и делайте append, сохраняя существующие
outbounds,routing,dns. Сборка config с нуля затирает outbounds — получите «online, но трафик не идёт». - Снапшот перед каждой записью (профиль, сквад, нода, хост) — откат становится тривиальным.
Вывод. Добавление Reality-ноды — не один вызов «create node», а цепочка Config Profile → Node (+activeInbounds) → Host → Squad (merge). Пропуск любого звена даёт молчаливый отказ конкретного класса. Держите порядок, делайте merge вместо overwrite и снимайте снапшот перед каждой записью.