--- 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: in-progress 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. **Статус: НЕ применено** — SQL готов и проверен в песочнице, применение на живой БД провалилось (см. «Провал попытки 1») и требует повтора по исправленной процедуре. ## Что хотели Добавить в 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** — откат нужен только если сломалась БД. ## Открытые риски (не проверено) 1. **`leastPing` + `observatory`** — могут не поддерживаться 3x-ui 3.7.0 (Xray 26.7.28). Если контейнер не поднимется → `docker logs xray-admin`, откат по A. 2. Если ругается на `subjectSelector` — попробовать старый ключ `selector`. 3. **Ссылка подписки `vpn-panel…/sub/` открыта БЕЗ авторизации** — любой, кто знает `subId`, получит ключ. Кандидат на включение подписки-по-токену в панели (не сделано). ## Что НЕ сделано / следующие шаги - [ ] Применить `apply_space_v2.sql` по исправленной процедуре - [ ] Проверить 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. ## Связанные заметки - [[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` мёртв)