--- 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": "", "port": 443, "users": [ { "id": "c67ce742-…", "encryption": "none", "flow": "xtls-rprx-vision" } ] } ] }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "serverName": "", "fingerprint": "firefox", "publicKey": "", "shortId": "", "spiderX": "" } } } ``` **Итого:** было 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-/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`, получит ключ. Кандидат на включение подписки-по-токену в панели (не сделано). ## Что НЕ сделано / следующие шаги - [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` мёртв)