14 KiB
title, created, updated, type, namespace, tags, confidence, status, related
| title | created | updated | type | namespace | tags | confidence | status | related | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| vless-space — клиент 3x-ui с egress через external-подписку | 2026-09-15 | 2026-09-15 | tech | personal |
|
high | in-progress |
|
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'ы. Тогда сервер может: принять клиента → отправить его трафик через сервер из подписки. Это и есть нужный механизм.
Схема:
[клиент 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
Параметры клиента
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):
{
"type": "field",
"user": ["vless-space"],
"outboundTag": "space-balancer",
"ruleTag": "vless-space-via-subscription"
}
2. Балансировщик:
"balancers": [
{
"tag": "space-balancer",
"selector": ["space-"],
"strategy": { "type": "leastPing" }
}
]
3. Observatory (нужен для leastPing):
"observatory": {
"subjectSelector": ["space-"],
"probeUrl": "https://www.google.com/generate_204",
"probeInterval": "30s",
"enableConcurrency": true
}
4. 10 outbound'ов space-01…space-10, формат:
{
"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):
- SQL применялся к копии базы (
/tmp/x-ui.db.work), созданной без её-wal/-shm. - Копия клалась обратно как
/data/x-ui.db. - Рядом на хосте оставались СТАРЫЕ
x-ui.db-walиx-ui.db-shmот предыдущего процесса. - 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-uiINSERT OR REPLACEвместоINSERT(идемпотентность)- Работа идёт НАПРЯМУЮ с
/data/x-ui.db, не через копию
Команды
# 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.
Откат
# 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 — откат нужен только если сломалась БД.
Открытые риски (не проверено)
leastPing+observatory— могут не поддерживаться 3x-ui 3.7.0 (Xray 26.7.28). Если контейнер не поднимется →docker logs xray-admin, откат по A.- Если ругается на
subjectSelector— попробовать старый ключselector. - Ссылка подписки
vpn-panel…/sub/<subId>открыта БЕЗ авторизации — любой, кто знает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мёртв)