diff --git a/personal/tech/xray-outbound-subscription-3xui.md b/personal/tech/xray-outbound-subscription-3xui.md index e5611032..a7fd2c9a 100644 --- a/personal/tech/xray-outbound-subscription-3xui.md +++ b/personal/tech/xray-outbound-subscription-3xui.md @@ -1,65 +1,101 @@ ---- -title: Xray outbound — внешняя подписка в 3x-ui (vless-space) -created: '2026-09-15' -updated: '2026-09-15T22:30:00.000Z' -type: tech -namespace: personal -tags: - - xray - - 3x-ui - - subscription - - outbound - - truenas - - reality - - balancer - - leastLoad - - balancerTag - - sampling -confidence: high -status: done-egress-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`) -> ## 🏁 2026-09-15 22:30 — EGRESS РАБОТАЕТ. `xui_v9.db` ЗАЛИТА. +> ## 🔴 2026-09-15 (финал сессии) — EGRESS ВСЁ ЕЩЁ `direct`. НЕ ЗАКРЫТО. > -> IP через `vless-space` = `194.x / 104.28.225.223` (Стокгольм, SE) ≠ `90.189.160.148` (TrueNAS). Проверено Xray-клиентом в докере на NAS, 3 запроса подряд. +> **Статус на конец сессии:** архитектура собрана, но трафик `vless-space` уходит в `direct`, а не через подписку. Подтверждено Alex живым запросом — **IP = `90.189.160.148` (TrueNAS)**, т.е. egress НЕ работает. +> +> **Что осталось (одна правка, не выполнена):** `observatory.subjectSelector` = `["space-"]`, а должен быть `["sub1-"]`. > -> **Формула, которая работает (все три пункта обязательны):** > ```jsonc -> { "type":"field", "user":["vless-space"], "balancerTag":"space-balancer" } // ← balancerTag, НЕ outboundTag -> "balancers": [ { "tag":"space-balancer","selector":["space-"], -> "strategy":{"type":"leastLoad"} } ] // ← leastLoad, НЕ leastPing -> "observatory": { "subjectSelector":["space-"], ..., "sampling":3 } // ← sampling обязателен +> // ФАКТИЧЕСКОЕ состояние 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-*) // ✅ верно > ``` > -> Без `balancerTag` → `non existing outTag: space-balancer`. Без `sampling` → то же молча, `-test` = `Configuration OK`. Без `leastLoad` → то же. +> **Механизм отказа:** `observatory.subjectSelector` матчит `space-*` → после удаления статики **ни один сервер не пингуется** → `leastLoad` не находит живых кандидатов → трафик падает в **первый outbound списка** (`direct`). **Молча: ошибок в логе нет, `non existing outTag` не появляется.** > -> ⚠️ **`xray -test` НЕ ловит эти ошибки** — проверять только рантайм-логом и живым запросом. +> **Правка:** в панели `Xray → Observatory` (или Settings → Xray Config) заменить `subjectSelector` `space-` → `sub1-`. Либо через `xrayTemplateConfig` в БД — **обязательно** по безопасному порядку (см. §WAL ниже). > -> **Остаточный риск:** `strategy: leastLoad` выбран по A/B-тесту (единственный рабочий). Авто-обновление списка серверов подписки по-прежнему требует cron-скрипта — outbound'ы статические. **Отдельная задача, не начата.** +> 🔑 **ПРАВИЛО:** selector балансировщика и `observatory.subjectSelector` — **ДВА РАЗНЫХ ПОЛЯ, менять ВМЕСТЕ.** Сменил префикс подписки — проверь ОБА. Проверка: +> ```bash +> docker exec xray-admin cat /app/bin/config.json | jq -c '{bal:.routing.balancers[0].selector, obs:.observatory.subjectSelector}' +> # оба должны быть одинаковы, напр. ["sub1-"] +> ``` -## ✅ БАРЬЕР ПРЕОДОЛЁН — `xui_v7.db` ЗАЛИТА УСПЕШНО +> ## 🏁 Архитектура (собрана и подтверждена; egress — см. блок выше) > -> **Прежний вывод «клиентов через SQLite добавить нельзя» — ОПРОВЕРГНУТ.** `xui_v7.db` залита, и клиент появился: -> -> | Проверка | Результат | +> | Компонент | Значение | > |---|---| -> | `in-10095-tcp` clients | ✅ `user1,kraken-user,vless-space` **одной строкой** (без дубля, без порта 10096) | -> | outbounds | ✅ 13 | -> | подписка `/sub/24df9391356b48ff` | ✅ **HTTP 200** — QR появился | -> | `kraken-user` | ✅ **не отвалился** | +> | `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` — все целы | > -> **Что было настоящим барьером (а не «внутренняя память 3x-ui»):** в попытках 5-6 инбаунд `in-10095-tcp` лежал **внутри `xrayTemplateConfig`**. 3x-ui мёржит шаблон с таблицей `inbounds`, шаблонная версия того же тега перебивает панельную → клиенты исчезают. Как только инбаунд убран из шаблона (попытка 7) — SQLite-путь **работает штатно**, клиент и подписка в норме. +> **Формула (все 4 пункта обязательны):** `balancerTag` (не `outboundTag`) + `leastLoad` (не `leastPing`) + `observatory.sampling: 3` + **`observatory.subjectSelector` = тот же префикс, что у балансировщика**. +> ⚠️ `xray -test` печатает `Configuration OK` при всех четырёх ошибках — проверять только рантайм-логом `non existing outTag` **и живым запросом egress**. + +> ## 🔴 ВТОРОЙ КРИТИЧНЫЙ ПИТФОЛЛ: подписка попадает в `config.json` ТОЛЬКО после рестарта > -> **Остаточная проблема — НЕ в клиенте, а в стратегии балансировщика:** egress `vless-space` не идёт из-за `leastPing` (см. §«leastLoad»). Клиент создан, подписка работает, маршрут — нет. Собран `xui_v7_load.db` (`leastPing`→`leastLoad`), залит не был. +> Запись `outbound_subscriptions` в БД (в т.ч. видимая в `-wal`) **сама по себе конфиг не меняет**. Панель рендерит outbound'ы подписки при генерации `config.json` — на старте и по таймеру `update_interval`. > -> ⛔ **Правило взаимодействия (Alex, 2026-09-15):** команды — **плоские однострочники без `ssh … '…'`-обёртки**, **без `sleep N`**, **без кириллицы в bash**, без `execute_code`/python. Alex выполняет их сам. +> **Симптом:** `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 в клиентскую подписку), к серверному роутингу отношения не имеют. Не путать. + +--- ## Задача @@ -67,29 +103,31 @@ related: ``` [клиент vless-space] → vpn.mallexxx.duckdns.org:443 → xray-admin - └→ space-01…space-10 (leastPing) → интернет (DE/LV/NL/EE/PL/FR/US/SE/MD) + └→ 28 × sub1-* (leastLoad) → интернет ``` ## Ключевые решения и почему | Решение | Почему | |---|---| -| **Править `settings.xrayTemplateConfig` в `x-ui.db`, а не `config.json`** | ✅ **Подтверждено фактом:** 3x-ui генерирует `/app/bin/config.json` из БД. `/app/bin/config.json` править бесполезно (перезапишется). Из шаблона применились все 13 outbound'ов + балансировщик. | -| **Развернуть серверы подписки в обычные outbound'ы** | Не зависит от версии панели. Работает сразу после `docker start`. | -| **НЕ полагаться на `inbounds` в SQL — только панель** | 🔴 **Урок сессии:** клиенты инбаунда через SQLite снаружи **не применяются**. 3x-ui держит БД открытой, пишет в `-wal`, и при рестарте берёт список клиентов из своей внутренней памяти, а не из файла. Четыре подхода (INSERT в `clients`, правка `inbounds.settings`, фикс `NULL`, дописать `security`) — все отбились. | -| **НЕ класть существующий инбаунд в `xrayTemplateConfig`** | 🔴 **Урок 6-й попытки (2026-09-15):** попытка протащить `in-10095-tcp` с тремя клиентами внутрь шаблона **сломала `kraken-user`** — 3x-ui мёржит шаблон с таблицей `inbounds`, и шаблонная версия того же тега перебивает панельную (клиенты исчезают, egress-правило по email не срабатывает). В шаблоне — **только outbound'ы/правила/балансировщик**. Инбаунды — эксклюзив панели. | -| ~~**`leastPing` + `observatory`**~~ | 🔴 **ОПРОВЕРГНУТО 2026-09-15 (вечер).** `leastPing` в Xray 26.x **не работает** — балансировщик не создаётся, `non existing outTag: space-balancer` в рантайме. Рабочая стратегия: **`leastLoad`**. См. §«leastLoad» ниже. | -| **Правило по `user`, а не по клиентскому id** | Образец взят с рабочего `kraken-user-via-reverse`: Xray матчит клиента inbound'а по `email`. | +| **Править `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` (проверено 2026-09-15) +## 🔴 ОБЯЗАТЕЛЬНО: `leastLoad`, а НЕ `leastPing` ```jsonc -// ✅ РАБОТАЕТ в Xray 26.3.27 / 26.7.28 -"balancers": [ { "tag": "space-balancer", "selector": ["space-"], +// ✅ РАБОТАЕТ в Xray 26.7.28 +"balancers": [ { "tag": "space-balancer", "selector": ["sub1-"], "strategy": { "type": "leastLoad" } } ] -"observatory": { "subjectSelector": ["space-"], +"observatory": { "subjectSelector": ["sub1-"], "probeUrl": "https://www.google.com/generate_204", - "probeInterval": "30s", "enableConcurrency": true } + "probeInterval": "30s", "enableConcurrency": true, "sampling": 3 } ``` ```jsonc @@ -97,50 +135,42 @@ related: "strategy": { "type": "leastPing" } ``` -**A/B на минимальном конфиге (docker, TrueNAS):** `leastPing` → `non existing outTag`; `burstObservatory + leastPing` → то же; **`leastLoad` + `observatory` → `the outbound space-01 is alive:0.249`, `taking detour [space-02]`** ✅. +**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`** — валидация конфига ошибку НЕ ловит. Проверять только рантайм-логом + живым запросом. -> ⚠️ **`chmod 644` конфига** перед `docker run -v …` — иначе `permission denied`. +> ⚠️ **`sampling: 3` в observatory обязателен** — без него Xray 26.7.28 молча не создаёт балансировщик. ## Питфоллы (все проверены фактом) -1. **🔴 SSH к TrueNAS обрывается на `docker stop xray-admin`** — `closed by remote host`. Контейнер **сам поднялся** (`restart: unless-stopped`), БД не пострадала. Останавливать и применять SQL **одной короткой командой**, не серией вызовов. -2. **WAL:** `docker stop` у 3x-ui корректно закрывает БД и **сливает WAL в основной файл**. Удалять `-wal`/`-shm` можно только **после** остановки. Правка при живом WAL = риск порчи базы. -3. **`sqlite3` внутри контейнера ОТСУТСТВУЕТ** (`which sqlite3` пусто). Использовать: `docker cp` БД на хост, либо хостовый `sqlite3`, либо `alpine`-контейнер. -4. **Дедуп подписки:** 28 строк → **10 уникальных серверов**. Одна подписка отдаёт несколько записей на один хост с разными `pbk`/`sid` (перебор ключей). Ключ дедупа — `host + publicKey + shortId`. -5. **REALITY в outbound:** параметры кладутся в `streamSettings.realitySettings` (`serverName`, `publicKey`, `shortId`, `spiderX`, `fingerprint`), а `flow: xtls-rprx-vision` — в `users[0].flow` (в ссылке он приходит query-параметром). -6. **`spx` в ссылке URL-энкодирован** (`%2F…`) — обязателен `unquote`, иначе Xray падает на `spiderX`. -7. **`routing.balancers.selector` — префиксный матч:** `["space-"]` подхватывает `space-01`…`space-10`. -8. **Правки в `routing.rules` добавлять В КОНЕЦ** — существующие правила (`api`, `geoip:private→blocked`, `bittorrent→blocked`, `kraken-user→via-kraken`) должны сохранить приоритет. +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» -Существующее (outbound'ы `direct`/`blocked`/`via-kraken`, правила, inbound) **не трогается**. Добавляется: +| Признак | Причина | Проверка | +|---|---|---| +| 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'` | -```jsonc -// в outbounds[] -{ "tag": "space-01", "protocol": "vless", - "settings": { "vnext": [{ "address": "edge-de.mirrorgrid.net", "port": 443, - "users": [{ "id": "c67ce742-d94f-4e56-889e-9f894ae59940", - "encryption": "none", "flow": "xtls-rprx-vision" }] }] }, - "streamSettings": { "network": "tcp", "security": "reality", - "realitySettings": { "serverName": "edge-de.mirrorgrid.net", - "fingerprint": "firefox", "publicKey": "", - "shortId": "", "spiderX": "/" } } } -// … space-02 … space-10 -// в routing.rules[] (В КОНЕЦ) -{ "type": "field", "user": ["vless-space"], - "outboundTag": "space-balancer", "ruleTag": "vless-space-via-subscription" } +## Проверка после применения (3 команды) -// новый ключ routing.balancers -[ { "tag": "space-balancer", "selector": ["space-"], - "strategy": { "type": "leastPing" } } ] - -// новый ключ observatory (ОБЯЗАТЕЛЕН для leastPing) -{ "subjectSelector": ["space-"], - "probeUrl": "https://www.google.com/generate_204", - "probeInterval": "30s", "enableConcurrency": true } +```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) @@ -149,15 +179,14 @@ related: | Файл | Назначение | |---|---| -| `build_template.py` | парсер `vless://` → xray-outbound; сборка шаблона (outbounds + rule + balancer + observatory) | -| `make_sql.py` | генератор `apply_space.sql` по живой БД | -| `apply_space.sql` | **готовый SQL** для применения (3 операции) | -| `template_config.new.json` | итоговый шаблон, 11 140 б, JSON VALID | -| `space_outbounds.json` | 10 новых outbound'ов (отчёт) | -| `sub_decoded.txt` | подписка, base64 развёрнут — 28 строк | -| `xui_copy.db` / `test_apply.db` | копия живой БД / песочница с применённым SQL | +| `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` | копии бэкапов (проверка на наличие подписки) | -**Параметры клиента (сгенерированы):** +**Параметры клиента:** | | | |---|---| @@ -166,206 +195,85 @@ related: | `subId` | `24df9391356b48ff` | | sub-ссылка | `https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff` | -## 🔴 Почему внешняя правка НЕ работает (главный урок сессии) +## Бэкапы -Проверено четырьмя подходами, все отбились: - -| # | Подход | Результат | -|---|---|---| -| 1 | SQL в **копию** БД → копия на место старых `-wal`/`-shm` | `database disk image is malformed` | -| 2 | SQL v2 напрямую (`PRAGMA journal_mode=DELETE`, `rm -wal` до правки) | SQL прошёл, БД корректна → в `config.json` клиента нет | -| 3 | Фикс `auth`/`reverse` (NULL → `''`) | применено → клиента нет | -| 4 | Дописать `security:"auto"` в `inbounds.settings` | применено (`3 × security:auto`) → клиента нет | - -**Что закрыло вопрос:** - -``` -ls -la /app/bin/config.json /etc/x-ui/x-ui.db -→ config.json = 2978 б (16:06) ← схлопнулся с 11 990 -→ x-ui.db = 262144 б (15:00) ← СТАРАЯ дата, правки не попали -``` - -1. **`x-ui.db` не обновлялся** — правки, применённые в 15:53/16:02/16:06, в основной файл **не попадали**. 3x-ui держит базу и пишет в `-wal`; удаление `-wal` перед правкой уничтожало актуальные данные, после правки он перезаписывал файл из своего состояния. -2. **`config.json` перегенерирован из внутренней памяти** 3x-ui, а не из файла — отсюда парадокс: **13 outbound'ов применились** (шаблон `settings` он читает), **третий клиент исчез** (список клиентов берёт из своей памяти). - -> 🔴 **ВЫВОД (исправлен 2026-09-15 вечером): `inbounds` и клиентов 3x-ui через SQLite править МОЖНО — но при двух жёстких условиях.** Прежний вывод «нельзя вообще» был **неверен**: он был основан на сборках, где инбаунд лежал внутри `xrayTemplateConfig` (см. урок 6) и где копию БД клали рядом со старыми `-wal`/`-shm`. -> -> **Условия, при которых SQLite-путь РАБОТАЕТ (проверено заливкой `xui_v7.db`):** -> 1. Инбаунд `in-10095-tcp` **НЕ должен присутствовать в `xrayTemplateConfig`** (в шаблоне — только `api`). -> 2. БД правится **локально на Mac**, затем кладётся файлом при остановленном контейнере, с `rm -f x-ui.db-wal x-ui.db-shm` и `chown 950:root`. -> -> Результат: клиент появился в `config.json`, подписка отдала HTTP 200, `kraken-user` не пострадал. -> -> **Что при этом всё равно требует внимания:** `xray -test` не ловит семантические ошибки маршрутизации (пример: `leastPing` даёт `Configuration OK`, но в рантайме `non existing outTag`). Рантайм-лог обязателен. - -### Рецепт, который сработал (`xui_v7.db`, 2026-09-15) - -Сборка **от оригинала** (`xui_copy.db`), не от промежуточных копий: - -```bash -# 1) шаблон: ТОЛЬКО outbounds + rule + balancer + observatory (инбаунд api уже есть) -sqlite3 xui_v6.db "SELECT value FROM settings WHERE key='xrayTemplateConfig';" | \ - jq '[.outbounds[] | select(.tag | startswith("space-"))]' > /tmp/space_outs.json -jq --slurpfile sp /tmp/space_outs.json '.outbounds += $sp[0] - | .routing.rules += [{"type":"field","user":["vless-space"],"outboundTag":"space-balancer","ruleTag":"vless-space-via-subscription"}] - | .routing.balancers = [{"tag":"space-balancer","selector":["space-"],"strategy":{"type":"leastLoad"}}] - | .observatory = {"subjectSelector":["space-"],"probeUrl":"https://www.google.com/generate_204","probeInterval":"30s","enableConcurrency":true}' \ - /tmp/base_orig.json > /tmp/tpl_final.json -# ⛔ .inbounds НЕ трогать — там остаётся только api - -# 2) клиент в ТРИ места: inbounds.settings JSON, clients, client_inbounds -sqlite3 xui_v7.db "UPDATE inbounds SET settings = json_set(settings, '$.clients', json('<новый_массив>')) WHERE id=1;" -sqlite3 xui_v7.db "INSERT INTO clients (id,email,sub_id,uuid,password,auth,flow,security,reverse,...) VALUES (5,'vless-space',...);" -sqlite3 xui_v7.db "INSERT INTO client_inbounds (client_id,inbound_id,flow_override,created_at) VALUES (5,1,'',...);" -# client_traffics НЕ трогать — в оригинале пусто, 3x-ui заполняет сам - -# 3) обязательная гигиена перед заливкой -sqlite3 xui_v7.db "PRAGMA journal_mode=DELETE;"; rm -f xui_v7.db-wal xui_v7.db-shm -sqlite3 xui_v7.db "PRAGMA integrity_check;" # → ok -``` - -**Контроль качества:** `diff <(sqlite3 xui_copy.db .dump) <(sqlite3 xui_v7.db .dump)` — должно быть ровно 4 изменения (клиент в `inbounds.settings`, строка в `clients`, строка в `client_inbounds`, шаблон + `sqlite_sequence`). Всё лишнее — ошибка. - -**Заливка на TrueNAS:** -```bash -scp ~/tmp-xray-space/xui_v7.db truenas_admin@mallexxx.duckdns.org:/tmp/xui_v7.db -docker stop xray-admin -docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/data -v /mnt/RED_2TB/docker/backups:/b -v /tmp:/src alpine sh -c 'TS=$(date +%Y%m%d-%H%M%S); mkdir -p /b/xray-admin-v7-$TS; cp -av /data/x-ui.db /b/xray-admin-v7-$TS/; rm -f /data/x-ui.db-wal /data/x-ui.db-shm; cp -av /src/xui_v7.db /data/x-ui.db; chown 950:root /data/x-ui.db; echo BACKUP=/b/xray-admin-v7-$TS' -docker start xray-admin -``` - -**Проверка после старта:** -```bash -docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r '.inbounds[] | "\(.tag) port=\(.port) clients=\([.settings.clients[]?.email]|join(\",\"))"' -docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -c '{out:(.outbounds|length),bal:.routing.balancers[0].strategy}' -docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r '.inbounds[].tag' # в шаблоне должен быть ТОЛЬКО api -curl -sk -o /dev/null -w "sub: %{http_code}\n" https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff -``` - -### Историческая (ошибочная) формулировка — оставлена для контекста - -> Работает только `settings.xrayTemplateConfig` (outbound'ы/правила/балансировщики). Всё, что касается клиентов инбаунда, идёт **исключительно через панель** (`/panel/api/inbounds/update/:id`) — а для API нужен **API-токен**. - -**Почему это было неверно:** «внутреннюю память 3x-ui» обвиняли зря. Настоящая причина — инбаунд внутри шаблона. См. §«БАРЬЕР ПРЕОДОЛЁН» в шапке. - -> **Как это делали раньше:** `kraken-user` добавлен 2026-09-02 **при участии панели** — в Zulip-записи прямо сказано «Temporary full-admin API token **не создавался**; `api_tokens` осталась пустой» + «Verified with the local `xray-test-client`». Это и есть подтверждение, что путь был через API панели, а не через SQL. - -## §6 Как довести до конца — ждёт доступа к панели - -### 🔴 Попытка 7 (2026-09-15, финал) — `xui_v7.db`, шаблон БЕЗ инбаунда - -Исправление ошибки попытки 6: инбаунд `in-10095-tcp` **убран из шаблона**, собиралось от оригинала (`xui_copy.db`). - -| Что | Значение | +| Путь | Что | |---|---| -| `xrayTemplateConfig.inbounds` | **только `api`** ✅ | -| `xrayTemplateConfig.outbounds` | 13 ✅ | -| balancer / observatory | `space-balancer` (leastPing) / `selector: ["space-"]` ✅ | -| `inbounds` (таблица) | `vless-ws` port 10095, clients = `user1`, `kraken-user`, `vless-space` | -| `clients` / `client_inbounds` | `vless-space` id=5 (+ привязка `5→1`) | -| diff от `xui_copy.db` | ровно 4 изменения, все по делу | -| `integrity_check` / `journal_mode` | `ok` / `DELETE` | +| `/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` | БД до подмены | -**Статус: не залита.** Файл `~/tmp-xray-space/xui_v7.db` (270 336 б). +## Ограничение (снято) -> ⚠️ **Ожидание — низкое.** В оригинале таблица `client_traffics` **пуста**, хотя два клиента живые. Значит панель рендерит клиентов не из дисковых таблиц. Скорее всего барьер попыток 2-6 сохранится: шаблон применится, клиент — нет. - -### Диагностический признак: инбаунд «дважды» в выводе `jq` - -Если это: - -``` -in-10095-tcp port=10095 clients=user1,kraken-user,vless-space -in-10095-tcp port=10095 clients=user1,kraken-user -``` - -— то это **НЕ два инбаунда**, а шаблонная копия + панельная (симптом ошибки попытки 6). Проверка-опровержение: - -```bash -docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r '.inbounds[].tag' -``` - -Правильно — **только `api`** в списке инбаундов шаблона; сам `in-10095-tcp` приходит из таблицы `inbounds`. - -### Путь к цели - -**Блокер:** логин панели `vpn-admin`, пароль — bcrypt-хэш (нечитаем). `secret` из БД (`RuERf2DTzPTw3CoMcVjk7tGXKCQOk0Z4`) для логина дал 403, API-путь — 404. - -**Вариант А — панель вручную** (2 минуты): - -``` -https://vpn-panel.mallexxx.duckdns.org/ (логин vpn-admin) -→ Inbounds → vless-ws (10095) → Edit -→ Clients → Add Client: - Email: vless-space - ID: a792c483-07e2-4723-9c50-78054c0abc07 - Sub ID: 24df9391356b48ff - Enable: ✅ -→ Save → Restart Xray (в шапке) -``` -Затем routing-правило на `space-balancer` — тоже через панель (Xray → Routing). - -**Вариант Б — API-токен:** Settings → API Tokens → Create, отдать агенту → он делает всё сам через `/panel/api/inbounds/update/1`. - -**Готовый SQL на Mac (не применён):** `~/tmp-xray-space/apply_final.sql` (13 447 б) — делает 4 операции одной транзакцией: клиент в `clients`, клиент в `inbounds.settings`, шаблон с 10 серверами + правило + балансировщик, запись в `outbound_subscriptions`. ⚠️ Учитывая урок сессии, **сработавшей будет только 3-я операция** (шаблон) — клиента всё равно придётся добавлять через панель. - -**Откат:** `/mnt/RED_2TB/docker/backups/xray-admin-before-vless-space-20260915-012057/` → скопировать `x-ui.db` при остановленном контейнере → `docker start xray-admin`. - -## Ограничение: «auto» здесь НЕ автоматическое - -Выбранный путь разворачивает серверы подписки в **статические** outbound'ы. Если провайдер сменит список серверов, шаблон надо **пересобрать вручную**: `build_template.py` → `make_sql.py` → применить. - -**Для настоящего авто-обновления нужен cron-скрипт на TrueNAS:** -``` -curl подписки → base64 -d → build_template.py → пересобрать xrayTemplateConfig -→ стоп xray-admin → SQL → старт -``` -Не реализован. Альтернатива без скрипта — использовать штатную таблицу `outbound_subscriptions` через панель (даёт `update_interval` из коробки, но требует доступа к UI). +~~«auto» здесь НЕ автоматическое~~ — **СНЯТО 2026-09-15.** Штатная `outbound_subscriptions` обновляет список серверов каждые 600 с. Ручная пересборка шаблона больше не нужна. ## Открытый вопрос безопасности (не решён) -`/sub/` отдаётся **без авторизации** (`/sub/abc` тоже 200). Новая ссылка `vless-space` будет так же открыта. Варианты: подписка по токену, ротация `subId`. Задано Alex 2026-09-15, решения нет. +`/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: попутные находки этой сессии +## 🔴 Попутные находки (2026-09-15) ### 1. `hermes-taiga` ходит НЕ через `xray-admin`, а через `vless-proxy` (мёртвый) -Разбор путаницы «таига настроена через user1?» — **нет**: - | | | |---|---| | Env `hermes-taiga` | `TELEGRAM_PROXY=socks5://vless-proxy:1080`, `DISCORD_PROXY=socks5://vless-proxy:1080` | -| Сети `hermes-taiga` | `hermes_taiga_net`, `ha_default` | -| `vless-proxy` | образ `teddysun/xray:latest`, SOCKS `:1080` + HTTP `:1081`, outbound → **`v.qentra.top:443` (мёртв, VPS удалён)** | -| Сеть `xray-admin` | `caddy_default` — с `hermes-taiga` **не пересекается** | -| `user1` | id `ce320965-…` — клиент inbound `in-10095-tcp`, `hermes-taiga` к нему **не обращается** | +| Сети | `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` сейчас без сети (цепочка `hermes-taiga → vless-proxy:1080 → v.qentra.top → timeout`). - -**Как чинить (обсуждалось, НЕ выполнено):** переписать outbound `/mnt/RED_2TB/docker/vless-proxy/config.json` с мёртвого `v.qentra.top` на живой сервер. 4 поля: +**Следствие:** 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-21FE-4784-9843-F11C384DA67A` | `ce320965-6956-4759-84bb-7cb71cfc6252` (`user1`) | +| `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` | -Порт `443`, `security: tls`, `network: ws` — без изменений. `xray-admin` **не имеет SOCKS-входа** (он сервер) — поэтому `vless-proxy` нужен как локальный клиент-мостик, его нельзя просто «выкинуть». - > ⚠️ Побочный эффект: трафик пойдёт `vless-proxy → vpn.mallexxx.duckdns.org:443 → Caddy → xray-admin` на том же хосте → egress = IP TrueNAS `90.189.160.148`. Петля внутри TrueNAS. -### 2. Compose-файл `xray-admin` утрачен — подробный разбор +### 2. Compose-файл `xray-admin` утрачен — разбор -См. [[family/plans/reverse-xray-3xui-kraken]] (шапка). Кратко: `rm -f /etc/x-ui/docker-compose.yml` внутри контейнера 2026-09-01 05:40:08 UTC снёс файл на хосте через bind-mount. Восстанавливать из `docker inspect`. +`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`. Авто-подписка (`update_interval`) — только GUI-клиенты (Happ/v2rayN/NekoBox) **или** штатная таблица `outbound_subscriptions` 3x-ui через панель. Для docker-контейнера (`hermes-taiga`, `vless-proxy`) «auto» = cron-скрипт. +**Xray-бинарник подписки не умеет.** Умеет статические outbound'ы + `routing.balancers`. Авто-подписка — только GUI-клиенты (Happ/v2rayN/NekoBox) **или** штатная `outbound_subscriptions` 3x-ui через панель. Для docker-контейнера (`hermes-taiga`, `vless-proxy`) «auto» = cron-скрипт.