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

39 KiB
Raw Blame History

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-15T00:00:00.000Z 2026-09-15T23:59:00.000Z tech personal
xray
3x-ui
x-ui
subscription
outbound
outbound_subscriptions
profilegrid
truenas
balancer
leastLoad
balancerTag
sampling
burstObservatory
vless-proxy
hermes-taiga
wal
high done-egress-via-subscription-working
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 команды, без остановки контейнера):

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-правки жили в -walcp перезаписал файл → запись outbound_subscriptions потеряна. Alex восстанавливал подписку в UI заново. Правило: docker stop ДО скачивания БД. Либо читать WAL через strings x-ui.db-wal (а не sqlite3 на файле).

🧱 ГРАБЛИ — 4 условия работы балансировщика (все обязательны, каждое ловилось по отдельному провалу): balancerTag в правиле (НЕ outboundTag) + strategy: leastLoad (НЕ leastPing — в Xray 26.x балансировщик молча не создаётся) + burstObservatory с sampling (без него то же самое) + subjectSelector observatory = тот же префикс, что у selector балансировщика (сейчас sub1-). ⚠️ xray -test печатает Configuration OK при всех этих ошибках — проверять только рантайм-логом на non existing outTag и живым запросом.

🧱 ГРАБЛИ — 3 условия правки БД: инбаунд НЕ внутри xrayTemplateConfig (иначе шаблон перебивает панельный и ломает чужих клиентов) + править локально на Mac (scp -> sqlite3/jq/integrity_check -> валидация живым xray -test -> заливка готового файла) + docker stop ДО скачивания (UI-записи живут в -wal, cp при живом контейнере их теряет). Клиенты инбаунда через SQLite добавить НЕЛЬЗЯ — только панель/API-токен.

Что хотели

Добавить в 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

Параметры клиента

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):

{
  "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-01space-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):

  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, не через копию

Команды

# 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 — откат нужен только если сломалась БД.

🔴 ПИТФОЛЛ 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-токен не выдан.

Что НЕ сделано / как довести до конца

  • Применить apply_space_v2.sql → применён, БД была корректна
  • Исправить auth/reverse/flow с NULL → исправлено, не помогло (теория опровергнута)
  • Дописать security:"auto" в inbounds.settings → применено, не помогло
  • Восстановить compose-файлсделано (см. family/plans/reverse-xray-3xui-kraken)
  • Откатить БДсделано 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-ссылках (открыты без авторизации)
  • Обновить family/how-to/truenas-infrastructure (новый клиент + балансировщик)сделано 2026-09-15
  • Отдельная задача: 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.1890.189.160.148. См. §«vless-proxyxray-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).

Команда проверки (минимальный конфиг, доказательство):

# 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.jsonConfiguration 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 — вытащить и разобрать подписку

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)

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 — клиент в таблицы

# 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 — заливка

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 — проверка (все три обязательны)

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 до старта

Откат (любой версии)

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-proxyxray-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.1890.189.160.148

🔴 ПИТФОЛЛ ПРОВЕРКИ: wget не умеет SOCKS5

# ❌ ЛОЖНЫЙ РЕЗУЛЬТАТ — 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-стека):

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.

Проверка прав (факт):

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-taigano such service

Сервис в /mnt/RED_2TB/docker/hermes/docker-compose.yml называется taiga, не hermes-taiga (это container_name). Правильно: docker compose up -d taiga.

Связанные заметки