--- title: Xray outbound — внешняя подписка в 3x-ui (vless-space) created: 2026-09-15T00:00:00.000Z updated: '2026-09-15T23:59:00.000Z' type: tech namespace: personal tags: - xray - 3x-ui - subscription - outbound - outbound_subscriptions - truenas - balancer - leastLoad - balancerTag - sampling - observatory - subjectSelector - wal confidence: high status: blocked-egress-direct-observatory-mismatch related: - '[[family/how-to/truenas-infrastructure]]' - '[[personal/tech/xray-reverse-tunnel-kraken-truenas]]' - '[[personal/tech/vless-space-subscription-egress]]' - '[[family/how-to/vps-qentra]]' --- # 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-"]`. > > ```jsonc > // ФАКТИЧЕСКОЕ состояние 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` — **ДВА РАЗНЫХ ПОЛЯ, менять ВМЕСТЕ.** Сменил префикс подписки — проверь ОБА. Проверка: > ```bash > 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 → ЗАПИСЬ ПОДПИСКИ УНИЧТОЖЕНА ``` **Правильный порядок (единственный безопасный):** ```bash # 1. СНАЧАЛА остановить docker stop xray-admin # 2. СКАЧАТЬ с checkpoint — после stop WAL уже слит, данные на месте sqlite3 x-ui.db "PRAGMA wal_checkpoint(TRUNCATE);" # если контейнер НЕ останавливали # 3. только потом править/подменять ``` **Если контейнер нельзя останавливать** — читать данные через `strings` из `-wal`, а не через `sqlite3` на файле: ```bash 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` идёт через существующий путь `/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` ```jsonc // ✅ РАБОТАЕТ в 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 } ``` ```jsonc // ❌ НЕ РАБОТАЕТ — тег не регистрируется, трафик молча теряется "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 молча не создаёт балансировщик. ## Питфоллы (все проверены фактом) 1. **🔴 Подмена БД при живом контейнере теряет панельные данные** — см. шапку. `docker stop` ДО скачивания БД. 2. **🔴 SSH к TrueNAS обрывается на `docker stop xray-admin`** — `closed 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 команды) ```bash 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/` отдаётся **без авторизации** (`/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`) — **строго по порядку**: ```bash # 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 условия сразу):** ```bash 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 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-скрипт.