Files
obsidian-vault/personal/tech/vless-space-subscription-egress.md.bak-before-squeeze2
T

639 lines
45 KiB
Plaintext
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: vless-space — клиент 3x-ui с egress через external-подписку
created: 2026-09-15T00:00:00.000Z
updated: '2026-09-15T23:59:00.000Z'
type: tech
namespace: personal
tags:
- xray
- 3x-ui
- x-ui
- subscription
- outbound
- outbound_subscriptions
- profilegrid
- truenas
- balancer
- leastLoad
- balancerTag
- sampling
- burstObservatory
- vless-proxy
- hermes-taiga
- wal
confidence: high
status: done-egress-via-subscription-working
related:
- '[[family/plans/reverse-xray-3xui-kraken]]'
- '[[personal/tech/xray-reverse-tunnel-kraken-truenas]]'
- '[[personal/tech/xray-outbound-subscription-3xui]]'
- '[[family/how-to/truenas-infrastructure]]'
---
# `vless-space` — клиент 3x-ui, egress через внешнюю подписку
> # 🏁 2026-09-15 (ЗАКРЫТО) — РАБОТАЕТ на штатной подписке. `space-` вычищен.
>
> **Итог сессии:** статика `space-01…10` удалена, egress идёт через 28 серверов штатной подписки `sub1-*`.
>
> | Проверка (факт) | Результат |
> |---|---|
> | `vless-proxy:1080` egress | ✅ `104.28.219.140` / `188.239.191.18` — НЕ `90.189.160.148` |
> | `outbounds` | 31 = `direct`+`blocked`+`via-kraken` + **28 × `sub1-*`** |
> | `space-*` | ❌ 0 — вычищено |
> | `routing.balancers` | `space-balancer`, `selector: ["sub1-"]`, `leastLoad` |
> | `burstObservatory.subjectSelector` | `["sub1-"]` |
> | `routing.rules` | `{"user":["vless-space"], "balancerTag":"space-balancer"}` |
> | ошибки `non existing outTag` | 0 |
> | `in-10095-tcp` clients | `user1`, `kraken-user`, `vless-space` — целы |
>
> ---
>
> ## 🔴 ПИТФОЛЛ (стоил сессии): поле observatory в UI = `burstObservatory`, НЕ `observatory`
>
> Xray 26.x принимает **обе** секции, но ведут они себя по-разному, и панель путает:
>
> | Что | Где живёт | Примечание |
> |---|---|---|
> | `observatory` | `xrayTemplateConfig.observatory` | классическая секция, требует `sampling: 3` |
> | `burstObservatory` | `xrayTemplateConfig.burstObservatory` | `pingConfig{sampling, interval, destination}` — **рабочий вариант** |
>
> **Симптом ошибки:** правка секции в UI **удалила** `observatory` целиком (`jq 'has("observatory")'` → `false`), а `subjectSelector` остался `["space-"]` → балансировщик не пингует никого → **трафик молча падает в `direct`, ошибок в логе НЕТ**.
>
> **Как диагностировать правильно (3 команды, без остановки контейнера):**
> ```bash
> docker exec xray-admin cat /app/bin/config.json | jq -r 'keys[]' # ищем observatory ИЛИ burstObservatory
> docker exec xray-admin cat /app/bin/config.json | jq -c '.burstObservatory // .observatory'
> docker exec xray-admin cat /app/bin/config.json | jq -c '.routing.balancers' # selector должен матчить префикс подписки
> ```
> ⚠️ Я искал `observatory` и объявил секцию отсутствующей — она была, но под именем `burstObservatory`. **Проверять оба имени.**
>
> ## 🔴 ПИТФОЛЛ: селектор балансировщика И `subjectSelector` observatory надо менять ПАРОЙ
>
> При переходе `space-` → `sub1-` недостаточно поменять `routing.balancers[].selector`. `observatory/burstObservatory.subjectSelector` — **отдельное поле**, и если его забыть, балансировщик не находит живых кандидатов.
>
> ## 🔴 ПИТФОЛЛ: подмена `x-ui.db` при живом контейнере ТЕРЯЕТ панельные данные
>
> Скачал БД при работающем контейнере → UI-правки жили в `-wal` → `cp` перезаписал файл → **запись `outbound_subscriptions` потеряна**. Alex восстанавливал подписку в UI заново.
> **Правило:** `docker stop` ДО скачивания БД. Либо читать WAL через `strings x-ui.db-wal` (а не `sqlite3` на файле).
> # 🏁 ИСТОРИЯ: 2026-09-15 22:30 — на статике `space-01…10` egress РАБОТАЛ (`xui_v9.db`)
>
> | Проверка (факт, `xui_v9.db`) | Результат |
> |---|---|
> | IP через `vless-space` | ✅ `104.28.225.223` / `195.72.61.192` (**Стокгольм, SE**) |
> | IP TrueNAS напрямую | `90.189.160.148` — **отличается** → трафик идёт через подписку |
> | Стабильность | ✅ 3/3 запроса подряд через клиент в докере |
> | `in-10095-tcp` clients | ✅ `user1,kraken-user,vless-space` (один инбаунд, порта 10096 нет) |
> | outbounds | 13, balancer `space-balancer` (leastLoad) — **статика, позже удалена** |
> | подписка `/sub/24df9391356b48ff` | ✅ HTTP 200 |
> | `kraken-user`, `user1` | ✅ целы, не тронуты |
>
> ## 🔑 ТРИ ПИТФОЛЛА + ЧЕТВЁРТЫЙ (все обязательны)
>
> **⚠️ 4-е условие (2026-09-15, финал): `observatory.subjectSelector` обязан совпадать с `routing.balancers[].selector`.** Проверка: `jq -c '{bal:.routing.balancers[0].selector, obs:.observatory.subjectSelector}'` → значения равны. Рассинхрон = `direct` без единой ошибки в логе.
>
> **1. `balancerTag`, а НЕ `outboundTag`.** Правило маршрутизации обязано ссылаться на балансировщик через `balancerTag` — с `outboundTag` 3x-ui/Xray не распознаёт его как балансировщик, и на первом же соединении: `app/dispatcher: non existing outTag: space-balancer`.
>
> ```jsonc
> // ❌ НЕ РАБОТАЕТ
> { "type":"field", "user":["vless-space"], "outboundTag":"space-balancer", "ruleTag":"..." }
> // ✅ РАБОТАЕТ
> { "type":"field", "user":["vless-space"], "balancerTag":"space-balancer", "ruleTag":"..." }
> ```
>
> **2. `strategy: leastLoad`, а НЕ `leastPing`.** В Xray 26.x `leastPing` балансировщик **не создаёт** (проверено A/B на минимальном конфиге; `burstObservatory` — то же).
>
> **3. `observatory` ОБЯЗАТЕЛЬНО с `sampling`.** Без `sampling` Xray 26.7.28 **молча** не создаёт балансировщик — `-test` при этом печатает `Configuration OK`.
>
> ```jsonc
> "observatory": { "subjectSelector":["space-"], "probeUrl":"https://www.google.com/generate_204",
> "probeInterval":"30s", "enableConcurrency":true, "sampling":3 } // ← sampling обязателен
> ```
>
> **Итоговый рабочий шаблон (все три фикса):**
> ```jsonc
> "routing": {
> "rules": [ ..., { "type":"field","user":["vless-space"],
> "balancerTag":"space-balancer","ruleTag":"vless-space-via-subscription" } ],
> "balancers": [ { "tag":"space-balancer","selector":["space-"],
> "strategy":{"type":"leastLoad"} } ]
> },
> "observatory": { ..., "sampling": 3 }
> ```
>
> **Артефакт, который сработал:** `~/tmp-xray-space/xui_v9.db` (md5 `8113add5b1d43a54f54253324eed2f1c`), залита на NAS.
> **Бэкапы:** `/mnt/RED_2TB/docker/backups/xray-admin-v9-20260915-102126/` (и v8, v7load, v7, `…-before-vless-space-20260915-012057` — исходная).
>
> **Как проверялось (Xray-клиент в докере на NAS):** конфиг из sub-ссылки → `docker run --name vless-test --network host -v /tmp/client_test_vless_space.json:/etc/xray/config.json:ro ghcr.io/xtls/xray-core:latest` → `curl --socks5-hostname 127.0.0.1:10808 https://api.ipify.org`. Питфолл: `chmod 644` конфига перед `docker run`, иначе `permission denied`.
> ## ✅ ПРЕДЫДУЩИЙ ЭТАП: `xui_v7.db` ЗАЛИТА И РАБОТАЕТ ЧАСТИЧНО
>
> | Проверка после заливки `xui_v7.db` | Результат |
> |---|---|
> | `in-10095-tcp` clients | ✅ `user1,kraken-user,vless-space` — **один инбаунд, без дубля, без порта 10096** |
> | outbounds | ✅ 13 |
> | balancer | ✅ `space-balancer` в конфиге |
> | подписка `/sub/24df9391356b48ff` | ✅ **HTTP 200** (QR теперь есть — впервые за 7 попыток) |
> | `kraken-user` | ✅ **не отвалился** (исправление урока попытки 6 сработало) |
> | `user1` | ✅ на месте |
> | **egress `vless-space`** | ❌ **НЕ работает** — `non existing outTag: space-balancer`, туннель рвётся `websocket: close 1000` |
>
> **🔴 Корень найден и доказан:** стратегия **`leastPing` не работает в Xray 26.x** — балансировщик не создаётся. Лечится заменой на **`leastLoad`**. Подробности — §«ГЛАВНАЯ НАХОДКА СЕССИИ» ниже.
>
> **Собран `xui_v7_load.db`** (md5 `855ff3ac2d2319b17b20fe5c788ddc59`, integrity `ok`) — `xui_v7.db` с `leastPing` → `leastLoad`. **Залит не был** (апрувы истекли) — но это уже неважно: финальная сборка `xui_v9.db` залита и работает (см. ниже).
>
> **Ключевой вывод, который опроверг прежний пессимизм:** правка клиентов 3x-ui через SQLite **СРАБОТАЛА** в попытке 7 — клиент появился в конфиге и подписка отдала 200. Барьер был **только** в том, что инбаунд лежал внутри шаблона (урок 6). При исправленной сборке SQLite-путь рабочий.
## Что хотели
Добавить в 3x-ui (`xray-admin` на TrueNAS) **третьего клиента** `vless-space`, чей трафик уходит в интернет **не** через TrueNAS (`direct`) и **не** через Кра́кена (`via-kraken`), а через **внешнюю подписку** провайдера `profilegrid.net` (28 строк → 10 уникальных серверов).
Требование Alex (дословно): «не трогая `user1` и `kraken-user` (reverse)», «авто брать» — т.е. **балансировщик по всем серверам подписки**, а не один конкретный.
## Ключевое различие, которое я сначала понял неверно
**Подписка — функция КЛИЕНТА, не сервера.** 3x-ui — это сервер (принимает входящие), он **не может** «ходить через чужую подписку» в inbound-режиме.
**НО** у 3x-ui есть `outbound_subscriptions` — функция, которая **скачивает подписку и превращает её серверы в свои outbound'ы**. Тогда сервер может: принять клиента → отправить его трафик через сервер из подписки. Это и есть нужный механизм.
**Схема:**
```text
[клиент vless-space] → vpn.mallexxx.duckdns.org:443 → Caddy → xray-admin:10095
routing: user=[vless-space]
space-balancer (leastPing)
10 × VLESS+REALITY outbound (mirrorgrid.net)
интернет
```
## Подписка (источник серверов)
- **URL:** `https://go.profilegrid.net/sub/djMsNDc3MjgsMTc4OTQ1OTg5NA.WjZEVaps9Xl3s5dkD6x7KRyNIGi7MlDUNAVPMKBUOdk`
- **Проверено 2026-09-15 с TrueNAS:** `HTTP 200`, 12 412 байт, `text/plain`, base64
- **28 строк** `vless://`, из них **10 уникальных серверов** (остальное — дубли одного UUID на тех же хостах)
- **Один UUID на весь список:** `c67ce742-d94f-4e56-889e-9f894ae59940` (на разных хостах встречаются варианты `…-4e56-…` и `…-0008-…`)
- **Формат:** VLESS + `security=reality` + `type=tcp` + `flow=xtls-rprx-vision` + `fp=firefox`
- **Уникальность** определяется парой `host` + `pbk` (publicKey) + `sid` (shortId) — у каждого сервера свои.
### 10 уникальных серверов
| tag | имя | host |
|---|---|---|
| `space-01` | 🇩🇪 Germany | `edge-de.mirrorgrid.net:443` |
| `space-02` | 🇱🇻 Latvia \| YT | `edge-lv.mirrorgrid.net:443` |
| `space-03` | 🇳🇱 Netherlands \| YT | `edge-nl.mirrorgrid.net:443` |
| `space-04` | 🇳🇱 Netherlands #2 | `packages-nl.repocache.com:443` |
| `space-05` | 🇪🇪 Estonia \| YT | `edge-ee.mirrorgrid.net:443` |
| `space-06` | 🇸🇪 Sweden \| YT | `packages-se.repocache.com:443` |
| `space-07` | 🇲🇩 Moldova \| Torrent | `md.repodelivery.com:443` |
| `space-08` | 🇵🇱 Poland \| YT | `edge-pl.mirrorgrid.net:443` |
| `space-09` | 🇫🇷 France \| YT | `edge-fr.mirrorgrid.net:443` |
| `space-10` | 🇺🇸 USA \| YT | `edge-us.mirrorgrid.net:443` |
## Артефакты (на Mac)
```
~/tmp-xray-space/
├── apply_space_v2.sql ← ИСПРАВЛЕННЫЙ SQL (применять этот)
├── apply_space.sql ← первая (битая) версия — НЕ использовать
├── make_sql.py ← генератор v1 (битый)
├── make_sql_v2.py ← генератор v2 (с PRAGMA journal_mode)
├── build_template.py ← сборка xrayTemplateConfig из подписки
├── sub_decoded.txt ← развёрнутая подписка (28 строк vless://)
├── space_outbounds.json ← 10 новых outbound (справочно)
├── template_config.new.json ← новый xrayTemplateConfig (13 outbound)
├── template_config.json ← исходный шаблон из БД
├── runtime_config.json ← снимок /app/bin/config.json
├── xui_copy.db ← копия БД до правок
└── docker-compose.yml ← восстановленный compose xray-admin
```
## Параметры клиента
| | |
|---|---|
| email | `vless-space` |
| UUID | `a792c483-07e2-4723-9c50-78054c0abc07` |
| subId | `24df9391356b48ff` |
| sub-ссылка | `https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff` |
| inbound | `in-10095-tcp` (id=1), port 10095, VLESS-WS |
| egress | `space-balancer` → 10 серверов подписки |
## Изменения в `xrayTemplateConfig`
Шаблон хранится в БД: таблица `settings`, ключ **`xrayTemplateConfig`**. 3x-ui генерирует из него `/app/bin/config.json` при старте.
Добавлено (существующее не тронуто):
**1. Routing-правило** (после `kraken-user-via-reverse`):
```json
{
"type": "field",
"user": ["vless-space"],
"outboundTag": "space-balancer",
"ruleTag": "vless-space-via-subscription"
}
```
**2. Балансировщик:**
```json
"balancers": [
{
"tag": "space-balancer",
"selector": ["space-"],
"strategy": { "type": "leastPing" }
}
]
```
**3. Observatory** (нужен для `leastPing`):
```json
"observatory": {
"subjectSelector": ["space-"],
"probeUrl": "https://www.google.com/generate_204",
"probeInterval": "30s",
"enableConcurrency": true
}
```
**4. 10 outbound'ов** `space-01`…`space-10`, формат:
```json
{
"tag": "space-NN",
"protocol": "vless",
"settings": { "vnext": [ { "address": "<host>", "port": 443, "users": [
{ "id": "c67ce742-…", "encryption": "none", "flow": "xtls-rprx-vision" } ] } ] },
"streamSettings": {
"network": "tcp",
"security": "reality",
"realitySettings": {
"serverName": "<host>", "fingerprint": "firefox",
"publicKey": "<pbk>", "shortId": "<sid>", "spiderX": "<spx>"
}
}
}
```
**Итого:** было 3 outbound (`direct`, `blocked`, `via-kraken`) → стало 13. Правил было 4 → стало 5.
## 🔴 ПРОВАЛ ПОПЫТКИ 1 (2026-09-15) — `database disk image is malformed`
**Симптом:** после применения `apply_space.sql` запрос к БД вернул `Parse error in 2nd command line argument: database disk image is malformed (11)`.
**Причина (моя ошибка в процедуре, не в SQL):**
1. SQL применялся к **копии** базы (`/tmp/x-ui.db.work`), созданной **без** её `-wal`/`-shm`.
2. Копия клалась обратно как `/data/x-ui.db`.
3. Рядом на хосте оставались **СТАРЫЕ** `x-ui.db-wal` и `x-ui.db-shm` от предыдущего процесса.
4. SQLite при открытии увидела WAL-файл, попыталась применить **чужой журнал** к новой базе → `malformed`.
**Дополнительно:** в SQL не было `PRAGMA journal_mode=DELETE` — WAL-режим не отключался на время записи.
> 🔴 **ПИТФОЛЛ (повторяемый):** при правке SQLite-базы, работающей в WAL-режиме, **нельзя** копировать только `.db` и класть рядом со старыми `-wal`/`-shm`. Либо работать **напрямую** с базой при остановленном процессе, либо удалять `-wal`/`-shm` до открытия.
## ✅ ИСПРАВЛЕННАЯ ПРОЦЕДУРА (`apply_space_v2.sql`)
Отличия от v1:
- `PRAGMA journal_mode=DELETE;` в начале → WAL отключён на время сессии
- `PRAGMA journal_mode=WAL;` в конце → возврат к режиму, которого ждёт 3x-ui
- `INSERT OR REPLACE` вместо `INSERT` (идемпотентность)
- **Работа идёт НАПРЯМУЮ с `/data/x-ui.db`**, не через копию
### Команды
```bash
# 1) залить SQL
scp ~/tmp-xray-space/apply_space_v2.sql truenas_admin@mallexxx.duckdns.org:/tmp/
# 2) стоп (3x-ui сам сбросит WAL)
ssh truenas_admin@mallexxx.duckdns.org 'docker stop xray-admin'
# 3) бэкап
ssh truenas_admin@mallexxx.duckdns.org '
TS=$(date +%Y%m%d-%H%M%S)
docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/d -v /mnt/RED_2TB/docker/backups:/b alpine sh -c "
mkdir -p /b/xray-admin-v2-\$TS && cp -av /d/x-ui.db /b/xray-admin-v2-\$TS/
echo BACKUP_DIR=/b/xray-admin-v2-\$TS"'
# 4) применить — ВАЖНО: rm WAL до SQL, работа напрямую
ssh truenas_admin@mallexxx.duckdns.org '
docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/data -v /tmp:/src alpine sh -c "
apk add --no-cache sqlite >/dev/null 2>&1
cd /data
rm -f x-ui.db-wal x-ui.db-shm
echo \"before=\$(sqlite3 x-ui.db \\\"PRAGMA integrity_check;\\\")\"
sqlite3 x-ui.db < /src/apply_space_v2.sql
echo \"sql_exit=\$?\"
echo \"after=\$(sqlite3 x-ui.db \\\"PRAGMA integrity_check;\\\")\"
sqlite3 x-ui.db \"select email, sub_id from clients order by id;\"
chown 950:root x-ui.db"'
# 5) старт
ssh truenas_admin@mallexxx.duckdns.org 'docker start xray-admin && sleep 6 && docker logs --tail 12 xray-admin'
# 6) проверка
ssh truenas_admin@mallexxx.duckdns.org '
docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r ".inbounds[].settings.clients[]?.email"
docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r ".outbounds | length"
docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -c ".routing.balancers"
curl -sk -o /dev/null -w "HTTP %{http_code}\n" "https://vpn-panel.mallexxx.duckdns.org/sub/24df9391356b48ff"'
```
**Ожидаемо:** клиенты `user1`, `kraken-user`, `vless-space` / outbounds `13` / balancer `space-balancer` / `HTTP 200`.
### Откат
```bash
# A — из бэкапа шага 3
ssh truenas_admin@mallexxx.duckdns.org '
docker stop xray-admin
docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/d -v /mnt/RED_2TB/docker/backups:/b alpine sh -c "
cp -av /b/xray-admin-v2-<TS>/x-ui.db /d/x-ui.db
rm -f /d/x-ui.db-wal /d/x-ui.db-shm && chown 950:root /d/x-ui.db"
docker start xray-admin'
# B — из исходного бэкапа (до всех правок)
# /mnt/RED_2TB/docker/backups/xray-admin-before-vless-space-20260915-012057/
# (только x-ui.db; -wal/-shm удалить, не копировать)
```
`user1` и `kraken-user` **не изменяются ни в одной версии SQL** — откат нужен только если сломалась БД.
## 🔴 ИСТОРИЯ (промежуточная попытка) ФИНАЛ 2026-09-15: ОТКАЧЕНО, ЗАДАЧА НЕ ВЫПОЛНЕНА
> ⚠️ **Не текущий статус.** Это срез промежуточной попытки. Задача закрыта позже: залита `xui_v9.db`, egress работает, далее — штатная подписка `sub1-*`. Актуальный статус: `status: done-egress-via-subscription-working`.
Alex восстановил БД из бэкапа. **Проверено фактом после отката:**
| Проверка | Результат |
|---|---|
| `xray-admin` | Up |
| панель `vpn-panel.mallexxx.duckdns.org` | HTTP 200 |
| `vpn.mallexxx.duckdns.org` (Xray-сервер) | HTTP 200 |
| подписка `user1` (`/sub/68c5cy5n5ui138yh`) | HTTP 200 |
| подписка `kraken-user` (`/sub/f43074a029dc656e`) | HTTP 200 |
| клиенты в конфиге | `user1`, `kraken-user` |
| outbound'ов | 3 |
**Пережило откат (единственный полезный артефакт в системе):** `/mnt/RED_2TB/docker/xray-admin/docker-compose.yml` — восстановленный compose, `docker compose config` валиден, `docker compose ps` распознаёт контейнер.
> 🔴 **Хард-вывод для будущих сессий: НЕ тратить попытки на правку клиентов 3x-ui через SQLite. Сразу просить доступ к панели (пароль или API-токен).** Шесть подходов в этой сессии (копия БД, прямой SQL, `auth`/`reverse`, `security`, `password`, инбаунд-в-шаблоне) — все отбились, при том что БД каждый раз была полностью корректна. Провал не в данных, а в архитектуре 3x-ui.
## 🔴 ПИТФОЛЛ 2026-09-15 (найден на 6-й попытке): инбаунд ВНУТРИ `xrayTemplateConfig` ломает своих клиентов
**Что делалось:** собрана БД `xui_v6.db`, где `vless-space` положен в **существующий** `in-10095-tcp` — но не в таблицу `inbounds`, а **в сам `settings.xrayTemplateConfig`** (инбаунд `in-10095-tcp` объявлен внутри шаблона рядом с `api`, с тремя клиентами).
**Симптом у Alex после заливки:** `kraken-user` отвалился. Вывод `jq` показывал `in-10095-tcp` **дважды** — с `user1,kraken-user,vless-space` (из шаблона) и с `user1,kraken-user` (из таблицы `inbounds`).
**Причина:** 3x-ui **мёржит** шаблон с таблицей `inbounds`. Инбаунд с тем же тегом (`in-10095-tcp`) из шаблона **перебивает** тот, которым управляет панель: панель рендерит клиентов из своего кэша (`client_traffics`/`client_inbounds`), а шаблонная версия инбаунда их **перетирает** — egress-правила привязываются к email, которых в активном инбаунде не остаётся.
**Как выглядела рабочая (живая) БД — эталон:**
```
шаблон (xrayTemplateConfig): api + 13 outbounds + 5 routing-rules + balancer + observatory
⛔ in-10095-tcp В ШАБЛОНЕ НЕТ ВООБЩЕ
таблица inbounds (id=1): in-10095-tcp port=10095 clients=user1,kraken-user
```
**Вывод — как правильно:**
| Что | Где держать |
|---|---|
| `space-01…10`, правило `vless-space-via-subscription`, `space-balancer`, `observatory` | ✅ **в `xrayTemplateConfig`** (работает) |
| `in-10095-tcp` **сам инбаунд** | ⛔ **НЕ в шаблоне** — только в таблице `inbounds`, которой владеет панель |
| клиент `vless-space` | ⛔ извне не добавляется (см. выше) — **только панель/API** |
> 🔴 **Сухой остаток сессии: не мешать два механизма.** Шаблон = только «дополнение» (outbound'ы/правила). Всё, что касается инбаундов и их клиентов, — эксклюзивно панель. Попытка протащить инбаунд через шаблон ломает чужих клиентов (`kraken-user`), а не решает задачу.
**Откат:** Alex восстановил БД из бэкапа; система вернулась к `user1` + `kraken-user`, 3 outbound'а.
## Открытые риски (проверено)
1. **`leastPing` + `observatory`** — ✅ **принимаются** (3x-ui 3.7.0 / Xray 26.7.28). Проверено фактом: после применения шаблона контейнер поднялся, `config.json` содержит `balancers` с `leastPing`, Xray-процесс запущен (`bin/xray-linux-amd64 -c bin/config.json`), в логах ошибок нет — только безобидные `WARNING common/protocol/http: received "X-Forwarded-For" … "sockopt.trustedXForwardedFor" is not configured` (Caddy шлёт XFF, на работу не влияет).
2. **~~`auth`/`reverse` = NULL~~** — ❌ **опровергнуто**, не было причиной. См. «Корень опровергнут».
3. **Ссылка подписки `vpn-panel…/sub/<subId>` открыта БЕЗ авторизации** — любой, кто знает `subId`, получит ключ. Кандидат на включение подписки-по-токену в панели (не сделано).
4. **🔴 Доступа к панели 3x-ui у агента нет** — единственный реальный блокер задачи. Пароль bcrypt, API-токен не выдан.
## Что НЕ сделано / как довести до конца
- [x] ~~Применить `apply_space_v2.sql`~~ → применён, БД была корректна
- [x] ~~Исправить `auth`/`reverse`/`flow` с `NULL`~~ → исправлено, **не помогло** (теория опровергнута)
- [x] ~~Дописать `security:"auto"` в `inbounds.settings`~~ → применено, **не помогло**
- [x] ~~Восстановить compose-файл~~ → **сделано** (см. [[family/plans/reverse-xray-3xui-kraken]])
- [x] ~~Откатить БД~~ → **сделано Alex'ом**, система в исходном состоянии
- [ ] **❌ ТЕКУЩИЙ БЛОКЕР: нет доступа к панели 3x-ui.** Логин `vpn-admin`, пароль — bcrypt-хэш (`$2a$10$JJscByJjUHJyqmp1a8LZ7egLY0g.XbIoY…`), нечитаем. `secret` из БД (`RuERf2DTzPTw3CoMcVjk7tGXKCQOk0Z4`) для логина не подошёл (403), API-путь вернул 404.
- [ ] **Довести задачу одним из двух:** (а) Alex добавляет клиента в панели — Inbounds → `vless-ws` (10095) → Edit → Add Client (`vless-space` / `a792c483-07e2-4723-9c50-78054c0abc07` / subId `24df9391356b48ff`) → Save → **Restart Xray**; (б) Alex создаёт API-токен (Settings → API Tokens) и агент делает всё сам через `/panel/api/inbounds/update/1`.
- [ ] После появления клиента — routing-правило на `space-balancer` **тоже через панель** (Xray → Routing)
- [ ] Проверить end-to-end: egress IP ≠ `90.189.160.148` (TrueNAS) и ≠ `92.62.70.41` (Kraken)
- [ ] Решить вопрос с авторизацией на sub-ссылках (открыты без авторизации)
- [x] ~~Обновить [[family/how-to/truenas-infrastructure]] (новый клиент + балансировщик)~~ → **сделано 2026-09-15**
- [x] ~~**Отдельная задача:** `hermes-taiga` ходит через `vless-proxy`, чей outbound указывает на **мёртвый** `v.qentra.top`~~ → **✅ ВЫПОЛНЕНО 2026-09-15.** `vless-proxy` переключён на `vpn.mallexxx.duckdns.org:443` + `/vless`, клиент **`vless-space`** (`a792c483-07e2-4723-9c50-78054c0abc07`). Проверено фактом: egress `104.28.219.140` / `188.239.191.18` ≠ `90.189.160.148`. См. §«`vless-proxy` → `xray-admin`» ниже.
- [ ] ⏳ **Осталось (не закрыто):** Telegram-адаптер `hermes-taiga` уходил в ветку `TelegramFallbackTransport` (прямой коннект по fallback-IP) вместо SOCKS, несмотря на `TELEGRAM_PROXY`. Симптом — `Connecting (attempt 1/8)` и тишина, без `Proxy detected` в логе. Попытка фикса `HERMES_TELEGRAM_DISABLE_FALLBACK_IPS=1` **не помогла**, переменная убрана. **Telegram РАБОТАЕТ** (подтверждено Alex) — саморазрешалось, вероятно тайминг рестартов.
## 🔴🔴 ГЛАВНАЯ НАХОДКА СЕССИИ (2026-09-15, позже в тот же день): `leastPing` В XRAY 26.x НЕ РАБОТАЕТ — НУЖЕН `leastLoad`
> **Это объясняет ошибку `non existing outTag: space-balancer`.** Балансировщик в конфиге есть, Xray конфиг принимает (`Configuration OK`), но **тег не регистрируется**, и правило `vless-space-via-subscription` уходит в никуда → клиент подключается, туннель строится, трафик обрывается (`websocket: close 1000`).
**Проверено на минимальном конфиге в докере на TrueNAS (тройной A/B):**
| Конфиг | Результат |
|---|---|
| `strategy: leastPing` + `observatory` | ❌ `app/dispatcher: non existing outTag: space-balancer` |
| `burstObservatory` + `pingConfig` + `leastPing` | ❌ то же самое |
| **`strategy: leastLoad` + `observatory`** | ✅ **работает:** `app/observatory: the outbound space-01 is alive:0.249334909`, `app/dispatcher: taking platform initialized detour [space-02]` |
**Вывод:** в Xray 26.x стратегия `leastPing` **устарела/несовместима** — балансировщик с ней не создаётся вообще. Рабочая стратегия — **`leastLoad`** (нативная для `observatory`).
**Команда проверки (минимальный конфиг, доказательство):**
```bash
# outbounds: [{"tag":"space-01","protocol":"freedom"},{"tag":"space-02","protocol":"freedom"}]
# routing.balancers: [{"tag":"space-balancer","selector":["space-"],"strategy":{"type":"leastLoad"}}]
# routing.rules: [{"type":"field","inboundTag":["socks-in"],"outboundTag":"space-balancer"}]
# observatory: {"subjectSelector":["space-"],"probeUrl":"https://www.google.com/generate_204","probeInterval":"30s","enableConcurrency":true}
docker run --name probe-load -d --network host -v /tmp/probe_load.json:/etc/xray/config.json:ro ghcr.io/xtls/xray-core:latest -c /etc/xray/config.json
docker logs probe-load 2>&1 | grep -iE 'observatory|detour|non existing'
```
> ⚠️ **Питфолл:** `xray -test -c config.json` → `Configuration OK` **НЕ гарантирует**, что балансировщик создастся. Ошибка `non existing outTag` видна **только в рантайм-логе** при первом реальном соединении. Проверять всегда живым запросом, не только `-test`.
> ⚠️ **Питфолл docker volume:** файл конфига, залитый через `scp` в `/tmp`, при монтировании в контейнер даёт `permission denied` — нужен `chmod 644` перед `docker run`.
## Артефакты на Mac — итоговые (`~/tmp-xray-space/`)
| Файл | Назначение |
|---|---|
| **`apply_final.sql`** | **ФИНАЛЬНЫЙ SQL** (13 447 б): клиент + инбаунд + шаблон с 10 серверами + правило + балансировщик + запись в `outbound_subscriptions`. Не применён. |
| **`xui_v7.db`** | **Сборка попытки 7** (историческая): оригинал + шаблон без инбаунда (13 outbound + balancer + observatory) + `vless-space` в таблице `inbounds`. `integrity ok`, 270 336 б. **Тогда не залита; позже заменена на `xui_v9.db` (рабочая).** |
| `xui_v3.db` / `xui_v4.db` / `xui_v5.db` / `xui_v6.db` | предыдущие сборки (в v3 — отдельный инбаунд `in-10096-space`; в v5/v6 — инбаунд внутри шаблона, сломало `kraken-user`) |
| `xui_live.db` / `xui_live2.db` | промежуточные копии (обе уже содержат правки — НЕ эталон) |
| `tpl_v5.json` / `tpl_v6.json` / `tpl_v7_final.json` | извлечённые шаблоны соответствующих сборок |
| `make_final.py` | генератор `apply_final.sql` |
| `inbound_settings.json` | снимок `inbounds.settings` из живой БД (2 клиента) |
| `tpl_live.json` / `template_config.json` | шаблон из живой БД (2129 б, 3 outbound, 4 правила) |
| `template_config.new.json` | шаблон с 13 outbound (v1, без правки инбаунда) |
| `apply_space_v2.sql` / `apply_space.sql` | SQL v2 (с `PRAGMA journal_mode`) / v1 (битый) |
| `make_sql_v2.py` / `make_sql.py` | генераторы v2 / v1 |
| `fix_security.sql` | правка `security` в `inbounds.settings` (применена, не помогла) |
| `make_fix_security.py` | генератор |
| `build_template.py` | парсер `vless://` → xray-outbound |
| `sub_decoded.txt` | подписка развёрнутая (28 строк) |
| `space_outbounds.json` | 10 outbound'ов (отчёт) |
| `runtime_config.json` | снимок `/app/bin/config.json` (3 клиента, 3 outbound) |
| `xui_copy.db` / `test_apply.db` | копия живой БД / песочница |
| `docker-compose.yml` | **восстановленный compose `xray-admin`** |
| `inbound_settings.json` | снимок инбаунда для финального SQL |
## 🏁 MILESTONE 2026-09-15 — точная процедура, которая сработала (повторяемо)
**Задача:** новый клиент 3x-ui с egress через внешнюю подписку, не трогая существующих.
### Шаг 1 — вытащить и разобрать подписку
```bash
curl -s "https://go.profilegrid.net/sub/<TOKEN>" | base64 -d > sub_decoded.txt
# дедуп по host+publicKey+shortId → 10 уникальных серверов
# парсер vless:// → xray-outbound: build_template.py
```
### Шаг 2 — собрать шаблон ЛОКАЛЬНО (на Mac)
```bash
cp xui_copy.db xui_vN.db # собирать ВСЕГДА от оригинала
sqlite3 xui_copy.db "SELECT value FROM settings WHERE key='xrayTemplateConfig';" > /tmp/base_orig.json
# ДОБАВИТЬ: 10 outbound space-01…10, правило, balancer, observatory
jq --slurpfile sp /tmp/space_outs.json '.outbounds += $sp[0]
| .routing.rules += [{"type":"field","user":["vless-space"],
"balancerTag":"space-balancer", # ← КЛЮЧ 1
"ruleTag":"vless-space-via-subscription"}]
| .routing.balancers = [{"tag":"space-balancer","selector":["space-"],
"strategy":{"type":"leastLoad"}}] # ← КЛЮЧ 2
| .observatory = {"subjectSelector":["space-"],
"probeUrl":"https://www.google.com/generate_204",
"probeInterval":"30s","enableConcurrency":true,
"sampling":3}' # ← КЛЮЧ 3
/tmp/base_orig.json > /tmp/tpl_final.json
```
⛔ **`.inbounds` в шаблоне НЕ трогать** — там остаётся только `api`.
### Шаг 3 — клиент в таблицы
```bash
# inbounds.settings JSON (id=1) + clients + client_inbounds, id=5
# client_traffics НЕ трогать — 3x-ui заполняет сам
sqlite3 xui_vN.db "PRAGMA journal_mode=DELETE;"; rm -f xui_vN.db-wal xui_vN.db-shm
sqlite3 xui_vN.db "PRAGMA integrity_check;" # → ok
```
### Шаг 4 — заливка
```bash
scp xui_vN.db truenas_admin@mallexxx.duckdns.org:/tmp/
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-vN-$TS; cp -av /data/x-ui.db /b/xray-admin-vN-$TS/; rm -f /data/x-ui.db-wal /data/x-ui.db-shm; cp -av /src/xui_vN.db /data/x-ui.db; chown 950:root /data/x-ui.db; echo BACKUP=/b/xray-admin-vN-$TS'
docker start xray-admin
```
### Шаг 5 — проверка (все три обязательны)
```bash
docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r '.inbounds[].settings.clients[]?.email'
docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -c '{bal:.routing.balancers[0], ob:.observatory.sampling}'
docker logs --since 1m xray-admin 2>&1 | grep -c 'non existing' # → 0
```
**Плюс end-to-end через Xray-клиент в докере** — `api.ipify.org` должен вернуть НЕ `90.189.160.148`.
### Диагностика: как отличить три дефекта по симптому
| Симптом в рантайм-логе | Причина | Фикс |
|---|---|---|
| `non existing outTag: space-balancer` | правило ссылается через `outboundTag` | → `balancerTag` |
| то же, без `alive` в логе | нет `sampling` в observatory | → `"sampling": 3` |
| то же, `alive` нет, `sampling` есть | стратегия `leastPing` | → `leastLoad` |
| клиент в `config.json` отсутствует | инбаунд лежит **внутри** шаблона | убрать из шаблона |
| `in-10095-tcp` **дважды** в `jq` | то же (шаблонная копия + панельная) | то же |
| `database disk image is malformed` | копию БД положили рядом со старыми `-wal`/`-shm` | `rm -f *.db-wal *.db-shm` до старта |
### Откат (любой версии)
```bash
docker stop xray-admin
docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/d -v /mnt/RED_2TB/docker/backups:/b alpine sh -c 'cp -av /b/xray-admin-v9-<TS>/x-ui.db /d/x-ui.db; rm -f /d/x-ui.db-wal /d/x-ui.db-shm; chown 950:root /d/x-ui.db'
docker start xray-admin
```
**Исходное состояние (до всей работы):** `/mnt/RED_2TB/docker/backups/xray-admin-before-vless-space-20260915-012057/`
## 🔗 `vless-proxy` → `xray-admin` (2026-09-15) — ВЫПОЛНЕНО
**Задача:** `vless-proxy` (SOCKS `:1080` + HTTP `:1081`) смотрел на **мёртвый** `v.qentra.top` (VPS удалён) → `hermes-taiga` (Telegram/Discord) без сети. Переключить на живой `xray-admin`.
**Что изменено** — `/mnt/RED_2TB/docker/vless-proxy/config.json`:
| Поле | Было | Стало |
|---|---|---|
| `outbounds[0].settings.vnext[0].address` | `v.qentra.top` | `vpn.mallexxx.duckdns.org` |
| `…vnext[0].users[0].id` | `2D9F24C4-21FE-4784-9843-F11C384DA67A` | `a792c483-07e2-4723-9c50-78054c0abc07` (`vless-space`) |
| `streamSettings.tlsSettings.serverName` | `v.qentra.top` | `vpn.mallexxx.duckdns.org` |
| `streamSettings.wsSettings.path` | `/qentra` | `/vless` |
| `streamSettings.wsSettings.headers.Host` | `v.qentra.top` | `vpn.mallexxx.duckdns.org` |
`port: 443`, `security: tls`, `network: ws` — без изменений.
**Выбор клиента — `vless-space`, НЕ `user1`.** Через `user1` egress = IP TrueNAS (`90.189.160.148`); через `vless-space` egress = IP подписки (нероссийский). Для Telegram/Discord нужен второй.
**Итоговая цепочка:**
```
hermes-taiga → vless-proxy:1080 → vpn.mallexxx.duckdns.org:443 → xray-admin → 28 × sub1-* → интернет
```
Env `hermes-taiga` (`TELEGRAM_PROXY`, `DISCORD_PROXY` = `socks5://vless-proxy:1080`) **менять не нужно** — SOCKS-соединения устанавливаются на каждый запрос, рестарт не требуется.
**Проверено фактом:** `104.28.219.140` / `188.239.191.18` ≠ `90.189.160.148` ✅
### 🔴 ПИТФОЛЛ ПРОВЕРКИ: `wget` не умеет SOCKS5
```bash
# ❌ ЛОЖНЫЙ РЕЗУЛЬТАТ — wget идёт напрямую, минуя SOCKS
docker exec vless-proxy wget -qO- https://api.ipify.org # → 90.189.160.148 (обман!)
# ✅ ПРАВИЛЬНО — только через SOCKS-прокси
docker run --rm --network hermes_taiga_net alpine sh -c \
"apk add -q curl; curl -s --max-time 20 --socks5-hostname vless-proxy:1080 https://api.ipify.org"
```
Также проверять `httpx` из venv taiga (доказательство, что SOCKS работает из python-стека):
```bash
docker exec hermes-taiga /opt/hermes/.venv/bin/python3 -c "
import asyncio, httpx
async def m():
async with httpx.AsyncClient(proxy='socks5://vless-proxy:1080', timeout=15) as c:
r = await c.get('https://api.telegram.org'); print('OK', r.status_code)
asyncio.run(m())"
# → OK 302
```
### 🔴 ПИТФОЛЛ ДОСТУПА: правку делает только Alex от root
`/mnt/RED_2TB/docker/vless-proxy/` — `root:root 755`, `config.json` примонтирован `:ro`, внутри контейнера `/etc/xray/config.json` тоже `Read-only file system`. **`truenas_admin` править НЕ может:** `touch` → Permission denied; `sudo` требует пароль; `root@192.168.2.197` → publickey denied.
Проверка прав (факт):
```bash
ssh truenas_admin@192.168.2.197 'ls -ld /mnt/RED_2TB/docker/vless-proxy; touch /mnt/RED_2TB/docker/vless-proxy/.wtest 2>&1 || echo CANNOT_WRITE'
ssh truenas_admin@192.168.2.197 'docker exec vless-proxy sh -c "touch /etc/xray/config.json 2>&1 || echo READONLY"'
```
**Рабочий приём:** агент готовит конфиг **локально на Mac** (`~/tmp-xray-space/vless-proxy-config-new.json`), отдаёт целиком; Alex копирует и делает `docker restart vless-proxy`.
### ⚠️ ПИТФОЛЛ: `docker compose up -d hermes-taiga` → `no such service`
Сервис в `/mnt/RED_2TB/docker/hermes/docker-compose.yml` называется **`taiga`**, не `hermes-taiga` (это `container_name`). Правильно: `docker compose up -d taiga`.
## Связанные заметки
- [[family/plans/reverse-xray-3xui-kraken]] — reverse-туннель к Кра́кену, история compose-инцидента
- [[personal/tech/xray-reverse-tunnel-kraken-truenas]] — архитектура reverse
- [[family/how-to/truenas-infrastructure]] — инфраструктура TrueNAS, контейнер `xray-admin`
- [[family/how-to/vps-qentra]] — удалённый VPS (объясняет, почему `vless-proxy` мёртв)