Files
obsidian-vault/personal/tech/xray-outbound-subscription-3xui.md
T

23 KiB
Raw Blame History

Xray outbound из внешней подписки в 3x-ui (vless-space)

🔴 2026-09-15 (финал сессии) — EGRESS ВСЁ ЕЩЁ direct. НЕ ЗАКРЫТО.

Статус на конец сессии: архитектура собрана, но трафик vless-space уходит в direct, а не через подписку. Подтверждено Alex живым запросом — IP = 90.189.160.148 (TrueNAS), т.е. egress НЕ работает.

Что осталось (одна правка, не выполнена): observatory.subjectSelector = ["space-"], а должен быть ["sub1-"].

// ФАКТИЧЕСКОЕ состояние config.json на момент завершения сессии:
"balancers":   [ { "tag": "space-balancer", "selector": ["sub1-"], "strategy": {"type":"leastLoad"} } ]   // ✅ верно
"observatory": { "subjectSelector": ["space-"], ... "sampling": 3 }                                        // ❌ ПУСТО — space-* удалены
"routing.rules": [ { "user":["vless-space"], "balancerTag":"space-balancer" } ]                            // ✅ верно
"outbounds": 31 (3 базовых + 28 × sub1-*)                                                                  // ✅ верно

Механизм отказа: observatory.subjectSelector матчит space-* → после удаления статики ни один сервер не пингуетсяleastLoad не находит живых кандидатов → трафик падает в первый outbound списка (direct). Молча: ошибок в логе нет, non existing outTag не появляется.

Правка: в панели Xray → Observatory (или Settings → Xray Config) заменить subjectSelector space-sub1-. Либо через xrayTemplateConfig в БД — обязательно по безопасному порядку (см. §WAL ниже).

🔑 ПРАВИЛО: selector балансировщика и observatory.subjectSelectorДВА РАЗНЫХ ПОЛЯ, менять ВМЕСТЕ. Сменил префикс подписки — проверь ОБА. Проверка:

docker exec xray-admin cat /app/bin/config.json | jq -c '{bal:.routing.balancers[0].selector, obs:.observatory.subjectSelector}'
# оба должны быть одинаковы, напр. ["sub1-"]

🏁 Архитектура (собрана и подтверждена; egress — см. блок выше)

Компонент Значение
outbound_subscriptions remark=vless-space, tag_prefix=sub1-, update_interval=600, enabled
outbound'ы 28 × sub1-* (все серверы подписки, авто-обновление 600 с)
статические space-01…10 удалены из шаблона (были костылём)
балансировщик space-balancer, selector: ["sub1-"] ← матчит префикс подписки
стратегия leastLoad + observatory.sampling: 3
правило {"user":["vless-space"], "balancerTag":"space-balancer"}
клиенты 10095 user1, kraken-user, vless-space — все целы

Формула (все 4 пункта обязательны): balancerTag (не outboundTag) + leastLoad (не leastPing) + observatory.sampling: 3 + observatory.subjectSelector = тот же префикс, что у балансировщика. ⚠️ xray -test печатает Configuration OK при всех четырёх ошибках — проверять только рантайм-логом non existing outTag и живым запросом egress.

🔴 ВТОРОЙ КРИТИЧНЫЙ ПИТФОЛЛ: подписка попадает в config.json ТОЛЬКО после рестарта

Запись outbound_subscriptions в БД (в т.ч. видимая в -wal) сама по себе конфиг не меняет. Панель рендерит outbound'ы подписки при генерации config.json — на старте и по таймеру update_interval.

Симптом: sqlite3 … "SELECT COUNT(*) FROM outbound_subscriptions" → 1, strings x-ui.db-wal | grep sub1- → есть, а jq '[.outbounds[].tag|select(startswith("sub1-"))]|length'0.

Лечение: docker restart xray-admin. После рестарта — 28 из 28. ⚠️ docker stop+start тоже работает, но обрывает SSH (питфолл 2).

Порядок проверки после добавления подписки в UI: (1) запись в БД есть → (2) docker restart xray-admin → (3) sub1-* в config.json = 28 → (4) subjectSelector совпадает с selector → (5) живой запрос egress ≠ 90.189.160.148.


🔴 КРИТИЧНЫЙ ПИТФОЛЛ: подмена x-ui.db при живом контейнере ТЕРЯЕТ панельные данные

Что случилось 2026-09-15 (стоило потери записи подписки):

1. Панель 3x-ui работает, пишет в WAL  →  outbound_subscriptions содержит запись
2. Скачал x-ui.db при РАБОТАЮЩЕМ контейнере   ← ЗАПИСЬ ЖИВЁТ В WAL, НЕ В ФАЙЛЕ
3. docker stop → панель сбросила WAL в файл (уже поздно — я работаю с копией)
4. cp мой_файл → x-ui.db  →  ЗАПИСЬ ПОДПИСКИ УНИЧТОЖЕНА

Правильный порядок (единственный безопасный):

# 1. СНАЧАЛА остановить
docker stop xray-admin
# 2. СКАЧАТЬ с checkpoint — после stop WAL уже слит, данные на месте
sqlite3 x-ui.db "PRAGMA wal_checkpoint(TRUNCATE);"   # если контейнер НЕ останавливали
# 3. только потом править/подменять

Если контейнер нельзя останавливать — читать данные через strings из -wal, а не через sqlite3 на файле:

strings /mnt/RED_2TB/docker/xray-admin/x-ui.db-wal | grep -o 'sub1-'

Признак, что попался: sqlite3 x-ui.db "SELECT COUNT(*) FROM outbound_subscriptions;"0, хотя панель показывает запись. Значит запись в WAL, а не в файле. НЕ перезаписывать файл.

Восстановление: если запись потеряна — заново добавить в UI (Xray → Outbound Subscriptions). URL подписки в доке не хранится (REDACTED), бэкапы БД старше правки тоже пусты — достаётся из strings WAL либо у Alex.


Штатный механизм подписки — как работает (2 шага)

  1. outbound_subscriptions создаёт outbound'ы с префиксом (sub1-) и обновляет их каждые update_interval секунд. Настраивается в панели: Xray → Outbound Subscriptions → Add.
  2. Балансировщик создаётся ОТДЕЛЬНО (Xray → Balancers), его selector матчит префикс подписки (sub1-) → подхватывает все серверы, включая будущие.

Одного без другого не хватает: подписка без балансировщика = 28 outbound'ов, на которые никто не маршрутизирует. Балансировщик без подписки = селектор ни на что не матчится.

Таблица sub_balancers — НЕ про это. Это клиентские балансировщики для sub-ссылок (JSON в клиентскую подписку), к серверному роутингу отношения не имеют. Не путать.


Задача

Добавить в xray-admin (3x-ui на TrueNAS) третьего клиента vless-space, чей трафик идёт не напрямую (как user1) и не через reverse-Кра́кен (как kraken-user), а через серверы внешней Xray-подписки profilegrid.net, с автоматическим выбором живого сервера. user1 и kraken-user не трогаются.

[клиент vless-space] → vpn.mallexxx.duckdns.org:443 → xray-admin
                                                         └→ 28 × sub1-* (leastLoad) → интернет

Ключевые решения и почему

Решение Почему
Править settings.xrayTemplateConfig в x-ui.db, а не config.json Подтверждено фактом: 3x-ui генерирует /app/bin/config.json из БД. /app/bin/config.json править бесполезно (перезапишется).
Статика space-01…10 → штатная outbound_subscriptions Статика требовала ручной пересборки шаблона при смене серверов у провайдера. Штатная подписка даёт update_interval из коробки. Выбор Alex.
Балансировщик переиспользован, менялся только selector space-balancer уже был в UI и работал; править selector space-sub1- дешевле, чем создавать новый.
НЕ трогать user1 и kraken-user Их egress-цепочки не связаны с подпиской. Проверяется после каждого изменения.
Caddyfile не трогать vless-space идёт через существующий путь /vlessxray-admin:10095.
НЕ класть существующий инбаунд в xrayTemplateConfig 🔴 Шаблонная версия того же тега перебивает панельную → клиенты исчезают. В шаблоне — только outbound'ы/правила/балансировщик, инбаунд api.
leastLoad, не leastPing В Xray 26.x leastPing не работает — балансировщик не создаётся, рантайм non existing outTag.
Правило — balancerTag, не outboundTag 3x-ui понимает только balancerTag (issue #5638) и не теряет балансировщик при генерации конфига.

🔴 ОБЯЗАТЕЛЬНО: leastLoad, а НЕ leastPing

// ✅ РАБОТАЕТ в Xray 26.7.28
"balancers": [ { "tag": "space-balancer", "selector": ["sub1-"],
                 "strategy": { "type": "leastLoad" } } ]
"observatory": { "subjectSelector": ["sub1-"],
                 "probeUrl": "https://www.google.com/generate_204",
                 "probeInterval": "30s", "enableConcurrency": true, "sampling": 3 }
// ❌ НЕ РАБОТАЕТ — тег не регистрируется, трафик молча теряется
"strategy": { "type": "leastPing" }

A/B на минимальном конфиге (docker, TrueNAS): leastPingnon existing outTag; burstObservatory + leastPing → то же; leastLoad + observatorythe outbound sub1-xx is alive:0.249, taking detour [...] .

⚠️ xray -test печатает Configuration OK и для leastPing — валидация конфига ошибку НЕ ловит. Проверять только рантайм-логом + живым запросом. ⚠️ sampling: 3 в observatory обязателен — без него Xray 26.7.28 молча не создаёт балансировщик.

Питфоллы (все проверены фактом)

  1. 🔴 Подмена БД при живом контейнере теряет панельные данные — см. шапку. docker stop ДО скачивания БД.
  2. 🔴 SSH к TrueNAS обрывается на docker stop xray-adminclosed by remote host. Контейнер сам поднимается (restart: unless-stopped). Останавливать и применять одной короткой командой.
  3. sqlite3 внутри контейнера ОТСУТСТВУЕТ. Использовать docker cp БД на хост, хостовый sqlite3, либо strings для WAL.
  4. Подписка даёт дубли: 28 строк → 10 уникальных хостов. Одна подписка отдаёт несколько записей на хост с разными pbk/sid (перебор ключей). Балансировщик это учитывает сам — дедуп не нужен.
  5. REALITY в outbound: параметры в streamSettings.realitySettings (serverName, publicKey, shortId, spiderX, fingerprint), flow: xtls-rprx-vision — в users[0].flow.
  6. spx в ссылке URL-энкодирован (%2F…) — обязателен unquote.
  7. routing.balancers.selector — префиксный матч: ["sub1-"] подхватывает sub1-germany, sub1-latvia-yt, … и будущие.
  8. Правки в routing.rulesВ КОНЕЦ — существующие (api, geoip:private→blocked, bittorrent→blocked, kraken-user→via-kraken) сохраняют приоритет.
  9. Панель может создать дубль балансировщика — если при правке UI создаётся новый тег, старый остаётся. Проверять jq '.routing.balancers' — должно быть ровно одна запись, на которую смотрит правило. (Факт 2026-09-15: в конфиге оказались ДВА — space-balancer и vless-space-balancer, оба selector:["sub1-"]. Правило смотрит на space-balancer; второй — мусор.)
  10. 🔴 observatory.subjectSelector — отдельное поле, не связанное с balancers[].selector. Сменил префикс в балансировщике → обязательно смени и в observatory. Иначе: серверы в конфиге есть, ошибок в логе нет, а трафик уходит в direct (см. шапку). Самый коварный питфолл сессии — тихий отказ без диагностических признаков.
  11. Запись outbound_subscriptions не влияет на config.json до рестарта/тика таймера — см. второй критичный питфолл в шапке. Не делать вывод «подписка не работает» по пустому config.json сразу после добавления в UI.

Диагностика: «трафик уходит в direct»

Признак Причина Проверка
IP = 90.189.160.148 (TrueNAS), лог чистый observatory.subjectSelector не матчит outbound'ы jq -c '{bal:.routing.balancers[0].selector, obs:.observatory.subjectSelector}' → должны совпадать
sub1-* = 0 в config.json, но есть в БД подписка не отрендерена docker restart xray-admin
non existing outTag: <балансировщик> в логе outboundTag вместо balancerTag ИЛИ leastPing ИЛИ нет sampling jq -c '.routing.rules[]|select(.balancerTag!=null)'
Балансировщика нет в config.json нет sampling: 3 в observatory jq -c '.observatory'

Проверка после применения (3 команды)

docker exec xray-admin cat /app/bin/config.json | jq -r '[.outbounds[].tag]|length'
docker exec xray-admin cat /app/bin/config.json | jq -r '[.outbounds[].tag|select(startswith("sub1-"))]|length'   # ожидается 28
docker exec xray-admin cat /app/bin/config.json | jq -c '.routing.balancers'
docker logs --since 3m xray-admin 2>&1 | grep -c "non existing"   # ожидается 0

Артефакты (Mac)

Рабочая папка: ~/tmp-xray-space/

Файл Назначение
xui_v10_src.db снимок живой БД до чистки
xui_v10.db БД со вычищенной статикой (3 outbound'а)
strip_space_outbounds.sh удаление space-01…10 из шаблона
write_v10.sh запись шаблона в БД + integrity_check
live2.db, prev_v9.db снимки БД для диагностики WAL
b_*.db копии бэкапов (проверка на наличие подписки)

Параметры клиента:

email vless-space
UUID a792c483-07e2-4723-9c50-78054c0abc07
subId 24df9391356b48ff
sub-ссылка https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff

Бэкапы

Путь Что
/mnt/RED_2TB/docker/backups/xray-admin-before-vless-space-20260915-012057/ исходная точка отката (до всех работ)
/mnt/RED_2TB/docker/backups/xray-admin-before-cleanup-20260915-035749/ перед чисткой статики
/mnt/RED_2TB/docker/backups/xray-admin-v9-20260915-102126/ последний рабочий (со статикой)
/mnt/RED_2TB/docker/xray-admin/x-ui.db.prev-v9 БД до подмены

Ограничение (снято)

«auto» здесь НЕ автоматическоеСНЯТО 2026-09-15. Штатная outbound_subscriptions обновляет список серверов каждые 600 с. Ручная пересборка шаблона больше не нужна.

Открытый вопрос безопасности (не решён)

/sub/<subId> отдаётся без авторизации (/sub/abc тоже 200). Варианты: подписка по токену, ротация subId. Задано Alex 2026-09-15, решения нет.

Статус: ЧТО ОСТАЛОСЬ СДЕЛАТЬ (передать следующей сессии)

1. observatory.subjectSelector: ["space-"]["sub1-"] — единственная причина, почему egress падает в direct.

Путь через UI: Xray → Observatory (или Settings → Xray Config → observatory) → поле subjectSelector → заменить чип space- на sub1- → Save → docker restart xray-admin.

Путь через БД (xrayTemplateConfig) — строго по порядку:

# 1) СНАЧАЛА остановить контейнер (иначе WAL потеряется — см. §КРИТИЧНЫЙ ПИТФОЛЛ)
docker stop xray-admin
# 2) скачать БД на Mac, править ЛОКАЛЬНО, проверить
sqlite3 xui.db "PRAGMA journal_mode=DELETE;"
sqlite3 xui.db "UPDATE settings SET value = json_set(value, '$.observatory.subjectSelector', json('[\"sub1-\"]')) WHERE key='xrayTemplateConfig';"
sqlite3 xui.db "PRAGMA integrity_check;"   # → ok
rm -f xui.db-wal xui.db-shm
# 3) залить, chown 950:root, старт

2. Удалить дубль балансировщика vless-space-balancer в Xray → Balancers (мусор, ни на что не влияет).

3. Живой тест egress — после правки IP должен стать нероссийским, не 90.189.160.148.

Проверка одним запросом (все 4 условия сразу):

docker exec xray-admin cat /app/bin/config.json | jq -c '{bal:.routing.balancers[0].selector, obs:.observatory.subjectSelector, sub1:([.outbounds[].tag|select(startswith("sub1-"))]|length), rule:[.routing.rules[]|select(.balancerTag!=null)]|length}'
# ожидается: bal==obs==["sub1-"], sub1>=1, rule==1

Связанные заметки

🔴 Попутные находки (2026-09-15)

1. hermes-taiga ходит НЕ через xray-admin, а через vless-proxy (мёртвый)

Env hermes-taiga TELEGRAM_PROXY=socks5://vless-proxy:1080, DISCORD_PROXY=socks5://vless-proxy:1080
Сети hermes_taiga_net, ha_defaultс caddy_default (xray-admin) не пересекается
vless-proxy teddysun/xray:latest, SOCKS :1080 + HTTP :1081, outbound → v.qentra.top:443 (мёртв, VPS удалён)
user1 ce320965-… — клиент in-10095-tcp, hermes-taiga к нему не обращается

Следствие: Telegram/Discord у hermes-taiga сейчас без сети. Как чинить (НЕ выполнено): переписать outbound /mnt/RED_2TB/docker/vless-proxy/config.json:

Поле Сейчас Станет
address v.qentra.top vpn.mallexxx.duckdns.org
users[0].id 2D9F24C4-… ce320965-6956-4759-84bb-7cb71cfc6252 (user1)
tlsSettings.serverName v.qentra.top vpn.mallexxx.duckdns.org
wsSettings.headers.Host v.qentra.top vpn.mallexxx.duckdns.org
wsSettings.path /qentra /vless

⚠️ Побочный эффект: трафик пойдёт vless-proxy → vpn.mallexxx.duckdns.org:443 → Caddy → xray-admin на том же хосте → egress = IP TrueNAS 90.189.160.148. Петля внутри TrueNAS.

2. Compose-файл xray-admin утрачен — разбор

rm -f /etc/x-ui/docker-compose.yml внутри контейнера 2026-09-01 05:40:08 UTC снёс файл на хосте через bind-mount. Восстановлен из docker inspect (536 б).

3. «Auto» в Xray-контейнере = только скрипт

Xray-бинарник подписки не умеет. Умеет статические outbound'ы + routing.balancers. Авто-подписка — только GUI-клиенты (Happ/v2rayN/NekoBox) или штатная outbound_subscriptions 3x-ui через панель. Для docker-контейнера (hermes-taiga, vless-proxy) «auto» = cron-скрипт.