VPN Config Service¶
Централизованный сервис сбора данных с VPN серверов. Устраняет необходимость SSH-запросов в реальном времени для каждого клиентского запроса.
Сервер: 10.99.87.249 | API: :8000 | GitLab: project #55
Назначение:
- Backend API — конфиги клиентов без прямого SSH к XUI
- Grafana — метрики и дашборды через PostgreSQL datasource
- Abuse Detection — выявление злоупотреблений по IP
Контейнеры¶
| Контейнер | Порт | Назначение |
|---|---|---|
vpn-config-service |
8000 | FastAPI — API |
vpn-config-worker |
— | Sync scheduler |
vpn-config-checker |
8001 | VLESS probe — Prometheus метрики |
vpn-config-postgres |
5434:5432 | PostgreSQL 16 |
vpn-config-redis |
6379 (internal) | Redis кэш |
vpn-config-checker¶
Контейнер запускается из того же образа, что и основной сервис. Порт: 8001. Деплоится через CI/CD вместе с app и worker.
- Spawns xray внутри контейнера, проверяет каждый сервер раз в 5 минут (concurrency 20)
- Метрики доступны на
:8001/metrics
| Метрика | Описание |
|---|---|
vpn_probe_up |
1 если сервер доступен, 0 если нет |
vpn_probe_latency_ms |
Задержка VLESS probe, мс |
vpn_probe_servers_healthy |
Количество доступных серверов |
vpn_probe_servers_unhealthy |
Количество недоступных серверов |
Расположение на сервере: /opt/vpn-config-service/
Pipelines и периодические задачи¶
Worker работает в отдельном процессе (vpn-config-worker) и не блокирует API.
Pipelines¶
| Pipeline | Интервал | Описание |
|---|---|---|
| Config Pipeline | ~2 мин/сервер | Rolling sync, priority queue, 3 параллельных SSH |
| Online Pipeline | 60 сек | Sweep всех серверов (TCP sessions) |
| Traffic Pipeline | flush 30 сек | Async consumer, batch flush → traffic_hourly |
Периодические задачи¶
| Задача | Интервал | Описание |
|---|---|---|
mysql_sync |
10 мин | Список серверов из MySQL → PostgreSQL |
transport_params |
30 мин | Reality params из XUI → MySQL |
fill_configs |
60 мин | Добавление клиентов через X-UI API (zero downtime) |
cleanup_expired |
6 ч | Удаление истёкших клиентов с панелей |
db_cleanup |
24 ч | Очистка старых данных в PostgreSQL |
grafana_sync |
10 мин | Синхронизация метрик для Grafana |
health_check |
5 мин | Быстрая проверка доступности серверов (первый запуск через +3 мин после старта). Таймауты: per-server 90s, main phase 240s, probe restart 120s, outer worker 270s |
xray_restart |
8 ч | Периодический рестарт xray на серверах (первый запуск через полный интервал) |
Ключевые параметры конфигурации: config_sync_concurrency=3, config_sync_base_interval=120, fill_max_per_server=50.
Memory limit worker: 4GB.
fill_configs скорость
Максимум 50 клиентов за цикл. Для нового inbound с ~6000 аккаунтов — ~120 циклов (5 дней при часовом интервале). Для ускорения — ручной запуск через API.
API¶
Аутентификация: заголовок X-API-Key (значение — в Vaultwarden).
Base URL: http://10.99.87.249:8000/api/v1
# Health (без ключа)
curl http://10.99.87.249:8000/health
# Swagger документация
http://10.99.87.249:8000/docs
# Статистика
curl http://10.99.87.249:8000/api/v1/stats -H "X-API-Key: KEY"
# Sync серверов из MySQL (после изменений в БД)
curl -X POST http://10.99.87.249:8000/api/v1/sync/mysql-servers -H "X-API-Key: KEY"
# Добавить отсутствующие конфиги
curl -X POST "http://10.99.87.249:8000/api/v1/sync/fill-missing-configs?dry_run=false" \
-H "X-API-Key: KEY"
GET /clients/{account_id}/config¶
Конфиг клиента по 16-значному account_id. Основной endpoint для backend.
{
"account_id": "1330611587659340",
"servers": [
{
"server": {
"id": 92,
"hostname": "vpn-146-70-35-201",
"ip": "146.70.35.201",
"location": "NL"
},
"inbound": {
"id": 6,
"port": 443,
"protocol": "vless",
"network": "tcp",
"security": "reality",
"tag": "inbound-443",
"stream_settings": {
"network": "tcp",
"security": "reality",
"tcpSettings": {
"header": {"type": "none"},
"acceptProxyProtocol": false
},
"realitySettings": {
"dest": "dl.google.com:443",
"show": true,
"xver": 0,
"serverNames": ["dl.google.com", "www.dl.google.com"],
"privateKey": "gEEIFIrafaxEXfub4DgJJZDkuOG8nag2Own6aEOkykY",
"shortIds": ["7fe4f8", "fe", "8c47", "a6b64d49"],
"settings": {
"publicKey": "o3PRZsW1CEpRkjS9ZmYUmREhfn9jOMRB6QJpBSKYlls",
"fingerprint": "chrome",
"serverName": "",
"spiderX": "/"
}
},
"sockopt": {"tcpMptcp": true, "tcpcongestion": "bbr", "tcpFastOpen": false}
}
},
"client": {
"email": "1330611587659340@vpn",
"enable": true,
"flow": "xtls-rprx-vision",
"limit_ip": 0,
"total_gb": 0,
"expiry_time": null,
"traffic_up": 0,
"traffic_down": 0
}
}
],
"total_servers": 48,
"total_traffic_bytes": 0,
"is_blocked": false,
"block_reason": null
}
stream_settings содержит все данные для генерации конфига: Reality ключи, SNI, shortIds, TCP оптимизации.
POST /clients/batch¶
Массовый поиск клиентов.
// Request
{"account_ids": ["1234567890123456", "2345678901234567", "3456789012345678"]}
// Response
{
"clients": {
"1234567890123456": { "...client data..." },
"2345678901234567": { "...client data..." },
"3456789012345678": null
},
"found": 2,
"not_found": 1
}
GET /servers¶
Список серверов с пагинацией.
Query params: page (default=1), size (default=50, max=200), is_active (bool), location (код страны), search (hostname/IP).
{
"items": [
{
"id": 99,
"hostname": "vpn-130-195-222-139",
"ip": "130.195.222.139",
"ssh_port": 22,
"location": "AT",
"is_active": true,
"total_clients": 11556,
"total_inbounds": 1,
"last_sync": "2026-01-09T12:00:00Z",
"sync_error": null,
"created_at": "2025-12-01T00:00:00Z"
}
],
"total": 48,
"page": 1,
"size": 50,
"pages": 1
}
GET /servers/{server_id}/inbounds¶
{
"items": [
{
"id": 65,
"server_id": 99,
"xui_id": 1,
"port": 443,
"protocol": "vless",
"tag": "inbound-443",
"listen": "",
"enable": true,
"network": "tcp",
"security": "reality",
"clients_count": 11556,
"last_seen": "2026-01-09T12:00:00Z",
"created_at": "2025-12-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"size": 50,
"pages": 1
}
GET /clients/online/all¶
Query param: limit (default=100, max=1000).
[
{
"client_id": 12345,
"server_id": 99,
"server_hostname": "vpn-130-195-222-139",
"client_ip": "176.197.161.178",
"tcp_count": 5,
"updated_at": "2026-01-09T12:00:00Z"
}
]
Grafana datasource (PostgreSQL)¶
| Параметр | Значение |
|---|---|
| Host | 10.99.87.249:5434 |
| Database | vpnconfig |
| SSL | disabled |
| Auth | md5 |
md5 обязательно
PostgreSQL 16 использует scram-sha-256 по умолчанию, но Grafana его не поддерживает. Настроен md5.
Ключевые таблицы PostgreSQL¶
| Таблица | Что хранит |
|---|---|
online_sessions |
Активные IP-сессии (client_id, server_id, tcp_count, country) |
online_history |
История онлайна по времени |
traffic_hourly |
Почасовой трафик по клиентам/серверам |
client_ips |
IP-адреса клиентов с GeoIP |
servers |
Статус серверов, load, bandwidth |
sync_runs |
История sync-запусков |
clients |
Клиенты (account_id, трафик, блокировки) |
Интеграция с Backend¶
- 96% запросов — через VPN Config Service (
GET /api/v1/clients/{account_id}/config) - 4% — fallback на XrayPanel (новые клиенты до первой синхронизации)
- Feature flag:
vpn-config-service.enabled=true - Latency: 1 клиент 15–50ms, 100 клиентов batch 497ms
VpnConfigServiceClient — retry логика¶
@Retryable(
retryFor = {ResourceAccessException.class, SocketTimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2) // 1s → 2s → 4s
)
public Optional<VpnConfigClientResponse> getClientConfig(String accountId) { ... }
3 попытки с экспоненциальным backoff. Ретраи: только на сетевые ошибки (timeout, connection refused).
Стратегия fallback¶
public VpnConfigDataSource getDataSource() {
if (vpnConfigServiceClient.isEnabled() && vpnConfigServiceClient.isHealthy()) {
return vpnConfigServiceDataSource; // VPN Config Service
}
if (properties.isFallbackEnabled()) {
return xrayPanelDataSource; // прямые вызовы X-UI
}
throw new ServiceUnavailableException();
}
Health check: GET /health. Fallback включён через vpn-config-service.fallback-enabled=true.
Конфигурация (application-prod.properties)¶
vpn-config-service.url=http://10.99.87.249:8000/api/v1
vpn-config-service.api-key=<см. Vaultwarden>
vpn-config-service.enabled=true
vpn-config-service.fallback-enabled=true
vpn-config-service.connect-timeout=5000
vpn-config-service.read-timeout=30000
HTTP коды: 404 — клиент не создан на серверах; 401 — неверный API key; 500 — retry с backoff; 503 — fallback к XrayService.
Ключевые классы Backend¶
| Класс | Описание |
|---|---|
VpnConfigServiceClient |
HTTP-клиент с retry (3 попытки, backoff 1s→2s→4s) |
VpnConfigServiceProperties |
Конфигурация (url, api-key, enabled, fallback) |
VpnConfigDataSourceStrategy |
Выбор источника данных (VCS vs XrayPanel) |
VpnConfigServiceDataSource |
Реализация через VPN Config Service |
XrayPanelDataSource |
Fallback на прямые вызовы X-UI |
VpnConfigServiceMapper |
Маппинг DTO → internal models |
Деплой¶
# 1. Push в GitLab
cd vpn-config-service && git push origin main
# 2. CI/CD: автоматически test → build
# 3. Триггер деплоя — вручную через GitLab UI (кнопка deploy_prod)
Никогда не деплоить через volume mounts или scp
Только через CI/CD pipeline. docker compose up без IMAGE_TAG возьмёт latest — может быть устаревший образ.
После деплоя¶
# Применить миграции
docker exec vpn-config-service alembic upgrade head
# Загрузить серверы из MySQL (если БД пустая)
curl -X POST http://localhost:8000/api/v1/sync/mysql-servers -H "X-API-Key: KEY"
Мониторинг и диагностика¶
Операции: ручное управление sync¶
При добавлении нового сервера или WL записи — данные подхватываются автоматически, но с задержкой до 70 минут (worst case). Для ускорения — ручной запуск через API.
Все команды выполняются через SSH к backend (10.99.87.249)
API key для VCS: Vaultwarden → Infrastructure → VPN Config Service.1. Принудительный mysql_sync (серверы из MySQL → PostgreSQL)¶
Когда: после INSERT/UPDATE в MySQL server таблице (новый сервер, новый WL, изменение is_active).
Проверка: VCS увидел сервер?
2. Принудительный full_sync (SSH → SQLite → PostgreSQL)¶
Когда: после создания inbound'ов на XUI панели (нужны stream_settings для transport_params).
Проверка: inbound данные в PostgreSQL?
curl -s http://localhost:8000/api/v1/servers/<SERVER_ID>/inbounds -H "X-API-Key: KEY" | python3 -m json.tool
3. Ожидание transport_params_sync (Reality params → MySQL)¶
Когда: после full_sync — transport_params строятся из inbounds.stream_settings.
Автоматически запускается после full_sync. Интервал periodic: 30 мин.
Проверка: transport_params заполнен в MySQL?
docker exec vpn-db mysql -uvpn -p'PASSWORD' vpn \
-e "SELECT name, ip, port, LENGTH(transport_params) as tp_len FROM server WHERE ip='<IP>'"
Если tp_len = NULL — transport_params ещё не синхронизирован, ждать или перезапустить full_sync.
4. Принудительный fill_configs (добавить клиентов на XUI)¶
Когда: нужно добавить клиентов на новый сервер/inbound быстрее чем за 60 мин.
# Dry run (показать что будет сделано)
curl -X POST "http://localhost:8000/api/v1/sync/fill-missing-configs?dry_run=true" -H "X-API-Key: KEY"
# Реальный запуск
curl -X POST "http://localhost:8000/api/v1/sync/fill-missing-configs?dry_run=false" -H "X-API-Key: KEY"
# Fill для конкретного аккаунта (быстро)
curl -X POST "http://localhost:8000/api/v1/sync/fill-account/<ACCOUNT_ID>" -H "X-API-Key: KEY"
fill_configs: максимум 50 клиентов за цикл на сервер
Для нового сервера с ~6000 аккаунтов потребуется ~120 циклов. При ручном запуске — каждый вызов добавляет 50.
5. Проверить статус worker¶
# Health (возвращает возраст sync, ошибки, статус DB/Redis)
curl -s http://localhost:8000/health | python3 -m json.tool
# Логи worker — последние операции
docker logs vpn-config-worker --tail 30
# Ожидаемый вывод: "Online sync done: X sessions" каждые 60 сек
# Если нет — worker завис, перезапустить:
docker restart vpn-config-worker
6. Backend: принудительный sync аккаунта¶
Когда: нужно пересобрать конфиги для конкретного пользователя.
# Получить admin token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login/admin \
-H "Content-Type: application/json" \
-d '{"login":"admin@shivavpn.io","password":"PASSWORD"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
# Sync аккаунта на все серверы
curl -X POST "http://localhost:8080/api/admin/maintenance/sync/account/by-login/<ACCOUNT_16DIGIT>" \
-H "Authorization: Bearer $TOKEN"
Пароль admin: Vaultwarden → Infrastructure → Admin Panel.
Порядок операций при добавлении сервера (шпаргалка)¶
1. INSERT в MySQL (is_active=0)
2. Ansible deploy (playbooks/vpn.yml)
3. Создать inbound'ы на XUI панели
│
▼ [ручное ускорение]
4. curl -X POST .../sync/mysql-servers ← VCS видит сервер
5. curl -X POST .../sync/fast?force=true ← VCS читает inbounds по SSH
6. Ждать ~5мин: transport_params_sync ← Reality params → MySQL
│
▼ [проверка]
7. SELECT transport_params FROM server WHERE ip='...' ← должен быть NOT NULL
│
▼
8. UPDATE server SET is_active=1 ← Backend активирует
9. curl -X POST .../sync/fill-missing-configs ← клиенты на XUI (50/цикл)
│
▼ [проверка]
10. python3 scripts/check_vpn_server.py <IP> ← полная проверка
Worst case без ручного ускорения: ~70 мин. С ручными curl: ~10-15 мин.
# Проверить работу worker
docker logs vpn-config-worker --tail 20
# Должно показывать "Online sync done: X sessions" каждые 60 сек
# API logs
docker logs vpn-config-service -f --tail 100
# PostgreSQL logs
docker logs vpn-config-postgres -f --tail 100
Частые проблемы¶
Grafana: password authentication failed
# Проверить pg_hba.conf — должно быть md5
docker exec vpn-config-postgres \
cat /var/lib/postgresql/data/pg_hba.conf | tail -1
Sync показывает 0 серверов
# Серверы грузятся из MySQL вручную
curl -X POST http://localhost:8000/api/v1/sync/mysql-servers -H "X-API-Key: KEY"
Port already allocated (5434)
См. также: Config Delivery Flow · DevOps скрипты · Репозитории · Worker unhealthy · Runbooks