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

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)

./scripts/ssh-internal.sh 10.99.87.249
API key для VCS: Vaultwarden → Infrastructure → VPN Config Service.

1. Принудительный mysql_sync (серверы из MySQL → PostgreSQL)

Когда: после INSERT/UPDATE в MySQL server таблице (новый сервер, новый WL, изменение is_active).

curl -X POST http://localhost:8000/api/v1/sync/mysql-servers -H "X-API-Key: KEY"

Проверка: VCS увидел сервер?

curl -s http://localhost:8000/api/v1/servers?search=<IP> -H "X-API-Key: KEY" | python3 -m json.tool

2. Принудительный full_sync (SSH → SQLite → PostgreSQL)

Когда: после создания inbound'ов на XUI панели (нужны stream_settings для transport_params).

curl -X POST "http://localhost:8000/api/v1/sync/fast?force=true" -H "X-API-Key: KEY"

Проверка: 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)

fuser -k 5434/tcp
docker compose -f docker-compose.prod.yml up -d vpn-config-postgres


См. также: Config Delivery Flow · DevOps скрипты · Репозитории · Worker unhealthy · Runbooks