Маппинг аккаунтов и 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 байты
Что происходит при логине¶
POST /api/auth/login— backend ищет аккаунт по 16-значному ключу- Генерирует JWT-токены
- Асинхронно создаёт клиента на всех активных VPN-серверах
- Клиент может отсутствовать сразу после логина — 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-значный номер):
Перевод HEX → UUID: AC1100029AB41B28819AB42CFCCB0001 → ac110002-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 · Навигация по проблемам