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

346 lines
20 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: vless-space — клиент 3x-ui с egress через external-подписку
created: 2026-09-15
updated: 2026-09-15
type: tech
namespace: personal
tags: [xray, 3x-ui, x-ui, subscription, outbound, profilegrid, truenas, space]
confidence: high
status: blocked-on-null-fields
related:
- "[[family/plans/reverse-xray-3xui-kraken]]"
- "[[personal/tech/xray-reverse-tunnel-kraken-truenas]]"
- "[[family/how-to/truenas-infrastructure]]"
---
# `vless-space` — клиент 3x-ui, egress через внешнюю подписку
> Задача поставлена Alex 2026-09-15. **Статус: ЧАСТИЧНО ПРИМЕНЕНО — БД корректна, Xray клиента не видит.** Шаблон (13 outbound + балансировщик) применён и работает; клиент `vless-space` есть во всех трёх местах БД, но в сгенерированный `config.json` не попадает. Корень найден: `auth`/`reverse` = `NULL` (см. «Корень найден»). Исправление — 4 команды, ждут выполнения.
## Что хотели
Добавить в 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** — откат нужен только если сломалась БД.
## ✅ ПОПЫТКА 2 (2026-09-15, ночь-4) — SQL применён, БД корректна, но клиент не в конфиге
**Выполнено (Alex сам, вручную на TrueNAS, строки без `ssh`-обёртки — так ему удобнее):**
Ошибка экранирования в моей инструкции: `\"` внутри `ssh '... sh -c \"...\"'` ломается на `sh`. Результат: диагностические `echo "integrity_..."` упали с `unrecognized token: ""PRAGMA"`, но **сам SQL прошёл** (`sql_exit=0`, вывод `delete` + `wal` = сработали оба `PRAGMA journal_mode`).
**Факт-состояние БД после применения (все три места корректны):**
| Где | `vless-space` |
|---|---|
| таблица `clients` | ✅ `id=5`, `sub_id=24df9391356b48ff`, `enable=1`, `security=auto` |
| `inbounds.settings` JSON (id=1) | ✅ `enable=true`, `id=a792c483-…` |
| `PRAGMA integrity_check` | ✅ `ok` |
| таблица `client_traffics` | ❌ **пустая** (но и у `user1`/`kraken-user` тоже пустая — не критерий) |
**Сгенерированный `/app/bin/config.json` после рестарта:**
```json
{"inbounds":[{"tag":"api","port":62789,"clients":[]},
{"tag":"in-10095-tcp","port":10095,"clients":["user1","kraken-user"]}],
"outcount":13,
"bal":[{"tag":"space-balancer","selector":["space-"],"strategy":{"type":"leastPing"}}]}
```
**шаблон применился** (13 outbound, балансировщик на месте), **клиент — нет**.
**Таймстампы опровергли гипотезу «не перечитал БД»:** `config.json` = 15:56, `x-ui.db` = 15:53, т.е. 3x-ui сгенерировал конфиг ПОСЛЕ правки и всё равно клиента выкинул.
### 🔑 КОРЕНЬ НАЙДЕН — `NULL` вместо `''` в полях `auth`/`reverse`
Построчное сравнение `clients` показало единственное расхождение:
```
user1 auth="" reverse="" ← живые, созданы 3x-ui
kraken-user auth="" reverse=""
vless-space auth=NULL reverse=NULL ← мой INSERT, эти колонки не заполнены
```
Xray при генерации конфига спотыкается на `NULL` и **молча выкидывает** клиента из списка. SQLite ставит `NULL` вместо `''` для колонок, не указанных в `INSERT ... VALUES`.
> 🔴 **ПИТФОЛЛ (повторяемый):** при ручном `INSERT` в таблицу `clients` (3x-ui) **заполнять ВСЕ текстовые колонки** — `auth`, `reverse`, `flow`, `password`, `group_name` — пустой строкой `''`, а не оставлять их вне списка. `NULL` ≠ `''` и ломает генерацию конфига без единой ошибки в логах.
### Исправление (4 команды, ждут выполнения)
```bash
docker stop xray-admin
```
```bash
docker run --rm -v /mnt/RED_2TB/docker/xray-admin:/data alpine sh -c 'apk add --no-cache sqlite >/dev/null 2>&1; cd /data; rm -f x-ui.db-wal x-ui.db-shm; sqlite3 x-ui.db "UPDATE clients SET auth=\"\", reverse=\"\", flow=\"\" WHERE email=\"vless-space\";"; sqlite3 -header x-ui.db "select email, quote(auth), quote(reverse), quote(flow) from clients;"; chown 950:root x-ui.db'
```
```bash
docker start xray-admin
```
```bash
sleep 8; docker exec xray-admin cat /app/bin/config.json | /usr/bin/jq -r '.inbounds[].settings.clients[]?.email'
```
**Ожидаемо:** `user1`, `kraken-user`, `vless-space`.
> ⚠️ **Правило взаимодействия (Alex, 2026-09-15):** команды выдаются **строками без `ssh truenas_admin@… '…'`-обёртки** — Alex выполняет их сам, сидя на хосте. Вложенные кавычки внутри `ssh '... sh -c \"...\"'` ломают передачу (`unrecognized token`), поэтому диагностику и правки писать **плоскими однострочниками**, без экранирования внутри `sh -c`.
### Откат
```bash
docker stop xray-admin
```
```bash
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-before-vless-space-20260915-012057/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'
```
```bash
docker start xray-admin
```
Возвращает состояние «только `user1` + `kraken-user`, 3 outbound».
## Открытые риски (не проверено)
1. **`leastPing` + `observatory`** — ⚠️ **СНЯТО (2026-09-15): 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 при ручном INSERT** — см. «Корень найден», это и есть реальный блокер.
3. **Ссылка подписки `vpn-panel…/sub/<subId>` открыта БЕЗ авторизации** — любой, кто знает `subId`, получит ключ. Кандидат на включение подписки-по-токену в панели (не сделано).
## Что НЕ сделано / следующие шаги
- [x] ~~Применить `apply_space_v2.sql` по исправленной процедуре~~**применён 2026-09-15, БД корректна** (шаблон + 3 клиента)
- [ ] **Исправить `auth`/`reverse`/`flow` с `NULL` на `''`** у `vless-space` (4 команды в «Исправление») — **текущий блокер**
- [ ] Проверить, что `config.json` содержит `vless-space` после фикса
- [ ] Проверить end-to-end: тестовый клиент с UUID `vless-space``curl` → egress IP ≠ `90.189.160.148`
- [ ] Проверить, что `user1` и `kraken-user` продолжают работать
- [ ] Решить вопрос с авторизацией на sub-ссылках
- [ ] Обновить [[family/how-to/truenas-infrastructure]] (новый клиент + балансировщик)
- [ ] Отдельная задача: `hermes-taiga` ходит через `vless-proxy`, чей outbound указывает на **мёртвый** `v.qentra.top` (VPS удалён). Нужно переключить на `vpn.mallexxx.duckdns.org` (клиент `user1`) или на `vless-space`/Kraken. **Alex спрашивал, можно ли переключить — ответ: да**, правка 5 полей в `/mnt/RED_2TB/docker/vless-proxy/config.json` (`address``vpn.mallexxx.duckdns.org`, `users[0].id``ce320965-6956-4759-84bb-7cb71cfc6252`, `tlsSettings.serverName``vpn.mallexxx.duckdns.org`, `wsSettings.headers.Host``vpn.mallexxx.duckdns.org`, `wsSettings.path``/vless`). Не начато.
## Связанные заметки
- [[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` мёртв)