24 KiB
title, created, updated, type, namespace, tags, confidence, status, related
| title | created | updated | type | namespace | tags | confidence | status | related | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Xray outbound — внешняя подписка в 3x-ui (vless-space) | 2026-09-15T00:00:00.000Z | 2026-09-15T23:59:00.000Z | tech | personal |
|
high | blocked-egress-direct-observatory-mismatch |
|
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) заменитьsubjectSelectorspace-→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_subscriptionsremark=vless-space,tag_prefix=sub1-,update_interval=600, enabledoutbound'ы 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 шага)
outbound_subscriptionsсоздаёт outbound'ы с префиксом (sub1-) и обновляет их каждыеupdate_intervalсекунд. Настраивается в панели:Xray → Outbound Subscriptions → Add.- Балансировщик создаётся ОТДЕЛЬНО (
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 идёт через существующий путь /vless → xray-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): leastPing → non existing outTag; burstObservatory + leastPing → то же; leastLoad + observatory → the outbound sub1-xx is alive:0.249, taking detour [...] ✅.
⚠️
xray -testпечатаетConfiguration OKи дляleastPing— валидация конфига ошибку НЕ ловит. Проверять только рантайм-логом + живым запросом. ⚠️sampling: 3в observatory обязателен — без него Xray 26.7.28 молча не создаёт балансировщик.
Питфоллы (все проверены фактом)
- 🔴 Подмена БД при живом контейнере теряет панельные данные — см. шапку.
docker stopДО скачивания БД. - 🔴 SSH к TrueNAS обрывается на
docker stop xray-admin—closed by remote host. Контейнер сам поднимается (restart: unless-stopped). Останавливать и применять одной короткой командой. sqlite3внутри контейнера ОТСУТСТВУЕТ. Использоватьdocker cpБД на хост, хостовыйsqlite3, либоstringsдля WAL.- Подписка даёт дубли: 28 строк → 10 уникальных хостов. Одна подписка отдаёт несколько записей на хост с разными
pbk/sid(перебор ключей). Балансировщик это учитывает сам — дедуп не нужен. - REALITY в outbound: параметры в
streamSettings.realitySettings(serverName,publicKey,shortId,spiderX,fingerprint),flow: xtls-rprx-vision— вusers[0].flow. spxв ссылке URL-энкодирован (%2F…) — обязателенunquote.routing.balancers.selector— префиксный матч:["sub1-"]подхватываетsub1-germany,sub1-latvia-yt, … и будущие.- Правки в
routing.rules— В КОНЕЦ — существующие (api,geoip:private→blocked,bittorrent→blocked,kraken-user→via-kraken) сохраняют приоритет. - Панель может создать дубль балансировщика — если при правке UI создаётся новый тег, старый остаётся. Проверять
jq '.routing.balancers'— должно быть ровно одна запись, на которую смотрит правило. (Факт 2026-09-15: в конфиге оказались ДВА —space-balancerиvless-space-balancer, обаselector:["sub1-"]. Правило смотрит наspace-balancer; второй — мусор.) - 🔴
observatory.subjectSelector— отдельное поле, не связанное сbalancers[].selector. Сменил префикс в балансировщике → обязательно смени и в observatory. Иначе: серверы в конфиге есть, ошибок в логе нет, а трафик уходит вdirect(см. шапку). Самый коварный питфолл сессии — тихий отказ без диагностических признаков. - Запись
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 |
копии бэкапов (проверка на наличие подписки) |
Параметры клиента:
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
Связанные заметки
- family/how-to/truenas-infrastructure — контейнер
xray-admin, клиенты, sub-канал - personal/tech/xray-reverse-tunnel-kraken-truenas — reverse-туннель (отдельный механизм)
- personal/tech/vless-space-subscription-egress — история отладки egress
- family/how-to/vps-qentra — удалённый VPS (источник мёртвого
v.qentra.top)
🔴 Попутные находки (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 TrueNAS90.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-скрипт.