Перейти к содержанию

Маппинг аккаунтов и VPN-конфигов

Как MySQL-аккаунт связан с клиентами на XRAY-серверах. Критично для поддержки и отладки.

Полный источник: docs/development/ACCOUNT-VPN-MAPPING-ARCHITECTURE.md + docs/development/BACKEND-ARCHITECTURE-KEY-FACTS.md (monorepo).


Ключевое правило

account.account (16-значный ключ) = clients[].id на XRAY
clients[].email ({account}@vpn)   = client_traffics.email — учёт трафика

Это разные значения с разными целями. account.id (UUID в MySQL) — на XRAY серверах не используется.


Структура клиента на XRAY

{
  "id":    "7231707254629351",      // = account.account (16 цифр)
  "email": "7231707254629351@vpn", // для учёта трафика
  "expiryTime": 1764799199000,
  "enable": true,
  "limitIp": 5
}

Исторически (до Feb 2026) email был случайным UUID, разным на каждом сервере. На production-серверах оба формата сосуществуют: новые клиенты — {account}@vpn, старые — legacy UUID. При поиске всегда ищите по id (16 цифр), не по email.


Полная цепочка

MySQL: account.account = "7231707254629351"
   ↓ backend buildClient()
XRAY client:
   clients[].id    = "7231707254629351"   ← ищем по этому
   clients[].email = "7231707254629351@vpn"
SQLite client_traffics:
   email = "7231707254629351@vpn"  → up/down байты

Что происходит при логине

  1. POST /api/auth/login — backend ищет аккаунт по 16-значному ключу
  2. Генерирует JWT-токены
  3. Асинхронно создаёт клиента на всех активных VPN-серверах
  4. Клиент может отсутствовать сразу после логина — backend ждёт до 20 сек (5 попыток)

При несоответствии срока действия (>1 ч между MySQL valid_to и XRAY expiryTime) клиент пересоздаётся.


Получение конфигов (API)

GET /api/v2/countries?lang=RU
  → возвращает список стран со VLESS-ссылками
  → кэшируется в Redis: country-configs:lkg:{account}:{lang}:v4, TTL ~12ч

Если конфиг не готов — backend ждёт появления клиента на сервере (до 20 сек).


Диагностика: как найти клиента

На VPN-сервере через SSH

ssh -i ~/.ssh/id_ed25519_shivavpn root@<SERVER_IP> "
  sqlite3 /app/xray/data/config/x-ui.db \"
    SELECT json_extract(value, '$.id') as id,
           json_extract(value, '$.email') as email,
           json_extract(value, '$.expiryTime') as expiry,
           json_extract(value, '$.enable') as enabled
    FROM inbounds, json_each(json_extract(settings, '$.clients'))
    WHERE json_extract(value, '$.id') = '7231707254629351'
  \"
"

Трафик клиента

# Получить email клиента из SQLite (шаг выше), затем:
sqlite3 /app/xray/data/config/x-ui.db \
  "SELECT email, (up+down)/1024/1024/1024 as gb FROM client_traffics WHERE email='7231707254629351@vpn'"

Через backend API

curl -k "https://<SERVER_IP>:1443/panel/inbound/list" \
  -H "Cookie: session=<TOKEN>" | \
  jq '.obj[] | select(.protocol=="vless") | .settings.clients[] | select(.id=="7231707254629351")'

Redis-сессии

JWT и сессии хранятся в Redis. Ключи используют MySQL UUID аккаунта (не 16-значный номер):

sess:{accountId}:{deviceId}     — данные сессии
devices:{accountId}             — SET deviceId-ов

Перевод HEX → UUID: AC1100029AB41B28819AB42CFCCB0001ac110002-9ab4-1b28-819a-b42cfccb0001

Найти сессии аккаунта:

# 1. Получить HEX UUID из MySQL
./scripts/ssh-internal.sh 10.99.87.249 'docker exec vpn-db mysql -uvpn -p... vpn -N -e "SELECT HEX(id) FROM account WHERE account=7231707254629351"'

# 2. Искать в Redis
./scripts/ssh-internal.sh 10.99.87.249 'docker exec redis redis-cli -a ... SMEMBERS "devices:{uuid}"'


Частые ошибки поддержки

Ошибка Последствие
Искать по account.id (UUID) на VPN-серверах Ничего не найти — это внутренний MySQL UUID
Считать, что email на разных серверах одинаковый Legacy-данные: email был случайным UUID на каждом сервере
Сравнивать account.id с clients[].email 100% ложные срабатывания (разные UUID для разных целей)

Правильно: искать клиента по clients[].id = account.account (16-значный ключ).


Связанные страницы


См. также: DevOps скрипты → audit_account · Config Delivery Flow · VPN Config Service · Навигация по проблемам