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

308 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/<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`) — **строго по порядку**:
```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-скрипт.