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

324 lines
26 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: done-egress-via-subscription-working
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`)
> ## 📱 КЛИЕНТСКАЯ СТОРОНА: как включить авто-обновление (вопрос Alex 2026-09-15)
>
> **Симптом вопроса:** «можно поменять на xray subscription channel (auto), чтобы он автоматом обновлял сервера?» — типичная ошибка: в клиент вбит **готовый `vless://…` ключ** вместо **ссылки подписки**. Ключ — статика, он не обновляется никогда.
>
> **Что отдаёт сервер (проверено HTTP 200):**
>
> | Клиент | Ссылка подписки |
> |---|---|
> | `user1` | `https://vpn-panel.mallexxx.duckdns.org/sub/68c5cy5n5ui138yh` |
> | `kraken-user` | `https://vpn-panel.mallexxx.duckdns.org/sub/f43074a029dc656e` |
> | `vless-space` | `https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff` |
>
> Отдаётся **base64-список** `vless://` — именно это клиент тянет как «subscription channel».
>
> **Включение авто в клиенте — 3 шага:**
> 1. **Удалить** вручную вбитый профиль/сервер (напр. `vless-ws-user1`).
> 2. **Add subscription / «Подписка»** → вставить **полный URL с `/sub/<subId>`** (без пути `/sub` не сработает).
> 3. В настройках подписки включить **auto update** (v2rayN / Happ / NekoBox — галочка «Auto update interval», ставить 612 ч; Streisand / sing-box — свой путь в меню).
>
> ⚠️ **Три разных «авто» — не путать** (см. таблицу в [[family/how-to/truenas-infrastructure]] §«Sub-канал»): GUI-клиент тянет `/sub/<subId>`; **`xray-admin` (сервер) сам тянет внешнюю `profilegrid` через `outbound_subscriptions` (600 с)**; `vless-proxy` авто не умеет.
>
> 🔴 **Одно неверное утверждение, которое из-за этого убрано из доки:** «Xray в docker подписки не умеет → авто только в GUI» — верно только для **контейнера-клиента** (`vless-proxy`), но **НЕ для сервера `xray-admin`**, у которого штатное авто через `outbound_subscriptions`. Причина путаницы: оба — «Xray в docker».
>
> **Безопасность:** `/sub/<subId>` отдаётся **без авторизации** — кто знает `subId`, получает рабочий ключ. Возможный фикс — подписка по токену/`subUpdates`. Решения нет, вопрос открыт с 2026-09-15.
> ## 🏁 2026-09-15 (ЗАКРЫТО) — EGRESS РАБОТАЕТ через штатную подписку
>
> **Статус:** всё собрано и проверено живым запросом. `space-` вычищен, 28 серверов подписки `sub1-*` работают.
>
> | Проверка (факт) | Результат |
> |---|---|
> | `vless-proxy:1080` egress | ✅ `104.28.219.140` / `188.239.191.18` — НЕ `90.189.160.148` |
> | `outbounds` | 31 = 3 базовых + **28 × `sub1-*`** |
> | `balancers` | `space-balancer`, `selector: ["sub1-"]`, `leastLoad` |
> | `burstObservatory.subjectSelector` | `["sub1-"]` |
> | `non existing outTag` в логе | 0 |
> | клиенты 10095 | `user1`, `kraken-user`, `vless-space` — целы |
>
> 🔴 **ГЛАВНЫЙ ПИТФОЛЛ СЕССИИ — имя поля:** в UI правится **`burstObservatory`**, а не `observatory`. Я искал `observatory`, получил `has("observatory")` → `false` и **ошибочно объявил секцию отсутствующей** — она была под другим именем. Диагностика:
> ```bash
> docker exec xray-admin cat /app/bin/config.json | jq -r 'keys[]'
> docker exec xray-admin cat /app/bin/config.json | jq -c '.burstObservatory // .observatory'
> ```
> ⚠️ **Обе секции валидны для Xray 26.x.** `observatory` требует `sampling: 3`; `burstObservatory` использует `pingConfig{sampling, interval, destination}`. Панель при сохранении может удалить `observatory` и создать `burstObservatory` — проверять ОБА.
>
> 🔴 **Правило:** `routing.balancers[].selector` и `observatory|burstObservatory.subjectSelector` — **ДВА разных поля, менять ПАРОЙ.**
>
> **Историческая формулировка (до фикса) — оставлена ниже для контекста.**
> ## 🏁 Архитектура (собрана и подтверждена; 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, решения нет.
## Статус: ✅ ВСЁ ЗАКРЫТО 2026-09-15
**1. `observatory.subjectSelector` = `["sub1-"]`** — ✅ **СДЕЛАНО.** Причина «трафика в `direct`» устранена. Факт: `bal == obs == ["sub1-"]`.
> 🔴 **Поправка к формулировке плана:** правило писать не `observatory`, а **`burstObservatory`** (Alex правил в UI, панель создала `burstObservatory` с `pingConfig`). Правка по `$.observatory.subjectSelector` в этом случае **не находит цель**. Проверять оба имени.
**2. Дубль балансировщика `vless-space-balancer`** — ✅ **УДАЛЁН Alex'ом в UI.** В конфиге ровно один балансировщик. *(Мой прошлый отчёт о «дубле» строился на устаревшем замере — актуальный факт: дубля нет.)*
**3. Живой тест egress** — ✅ **ОК:** `vless-proxy:1080``104.28.219.140` / `188.239.191.18`, ≠ `90.189.160.148`.
**Проверка одним запросом (все 4 условия сразу, актуальная):**
```bash
docker exec xray-admin cat /app/bin/config.json | jq -c '{bal:.routing.balancers[0].selector, obs:(.burstObservatory // .observatory).subjectSelector, sub1:([.outbounds[].tag|select(startswith("sub1-"))]|length), rule:([.routing.rules[]|select(.balancerTag!=null)]|length)}'
# ожидается: bal==obs==["sub1-"], sub1==28, rule==1
```
⚠️ `.burstObservatory // .observatory` в `jq`**обязателен**, иначе на живом конфиге получите `null` и ложный вывод «секции нет».
## Связанные заметки
- [[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`)
- [[personal/tech/vault-doc-pruning]] — метод чистки устаревших разделов в доках
## 🔴 Попутные находки (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` |
| `user1` | `ce320965-…` — клиент `in-10095-tcp`, `hermes-taiga` к нему **не обращается** |
**✅ ФИНАЛ: `vless-proxy` переключён с мёртвого `v.qentra.top` на `vpn.mallexxx.duckdns.org`.**
| Поле в `/mnt/RED_2TB/docker/vless-proxy/config.json` | Стало |
|---|---|
| `address` | `vpn.mallexxx.duckdns.org` |
| `users[0].id` | `a792c483-07e2-4723-9c50-78054c0abc07`**клиент `vless-space`, НЕ `user1`** |
| `tlsSettings.serverName` | `vpn.mallexxx.duckdns.org` |
| `wsSettings.headers.Host` | `vpn.mallexxx.duckdns.org` |
| `wsSettings.path` | `/vless` |
> 🔑 **Решение Alex: брать `vless-space`, а не `user1`** — иначе egress был бы IP TrueNAS `90.189.160.148` (петля внутри TrueNAS). С `vless-space` трафик уходит через подписку (нероссийский IP).
> ⚠️ **Файл агенту недоступен:** `/mnt/RED_2TB/docker/vless-proxy/` — `root:root 755`, `config.json` примонтирован `:ro`. Правку делает **Alex от root**; агент готовит конфиг локально (`~/tmp-xray-space/vless-proxy-config-new.json`).
> ⚠️ **Питфолл проверки:** `wget` **не умеет SOCKS5** — `docker exec vless-proxy wget -qO- https://api.ipify.org` вернёт IP контейнера и даст ложный вывод «direct». Проверять только curl через SOCKS (см. §Проверка в шапке).
### 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-скрипт.