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

Config Delivery Flow

How a VPN config travels from MySQL activation to the user's device. Use this to understand delays and debug missing configs.


Systems Involved

System Location Role
MySQL (vpn-db) 10.99.87.62 server, vpn_user_server_account, vpn_user_server_config
VPN Config Service 10.99.87.249:8000 Worker: Config/Online/Traffic pipelines + periodic jobs
Backend (Java) 10.99.87.249:8080 NodeSyncService, VpnSyncScheduler, VpnConfigSyncEventListener
XUI Panel VPN servers SQLite, addClient API, xray hot-reload

Worker Schedule (VPN Config Service)

Pipelines (continuous):
  Config Pipeline    — rolling sync per server, priority queue, 3 parallel SSH
                       each server synced ~every 2 min
                       (config_sync_concurrency=3, base_interval=120s, max_backoff=1800s)
  Online Pipeline    — sweeps all servers every 60s (TCP sessions)
  Traffic Pipeline   — async consumer, flushes traffic_hourly batches every 30s

Periodic jobs:
  mysql_sync         — every 10 min  (MySQL servers → PostgreSQL)
  transport_params   — every 30 min  (Reality params from XUI → MySQL)
  fill_configs       — every 60 min  (add missing clients via XUI API)
  cleanup_expired    — every 6h      (remove expired clients from panels)
  db_cleanup         — every 24h     (clean old PostgreSQL data)
  grafana_sync       — every 10 min  (sync metrics for Grafana)
  health_check       — every 5 min   (fast server availability check, first run +3 min after startup)
  xray_restart       — every 8h      (periodic xray restart on all servers, first run after full interval)

Key files: config_pipeline.py, server_queue.py, traffic_pipeline.py, sync_fast.py, worker.py


Scenario A: New XUI Server (is_active=0 → 1)

1. [MANUAL] Admin: UPDATE server SET is_active=1
2. [Backend] ServerServiceImpl.updateServer()
       → afterCommit: ServerStatusChangedEvent(server, false→true)
3. [Backend] VpnConfigSyncEventListener.handleServerStatusChanged()
       → finds ALL active accounts
       → enqueues NodeSyncTask(CREATE, priority=3) for each
4. [Backend] NodeSyncWorker (every 500ms, batch=30)
       → NodeSyncService.syncUserOnServer()
       ├── transport_params present (fast path):
       │     recalculateConfigs() → buildVlessUrl()
       │     → creates vpn_user_server_account
       │     → creates vpn_user_server_config (VLESS URL)
       └── transport_params absent (slow path):
             → HTTP to XUI panel (addClient)
             → creates vpn_user_server_account (SYNCED)
             → buildAndSaveConfigs() → vpn_user_server_config
5. [VCS Worker] mysql_sync (≤10 min)
       → sees new active server in MySQL
       → INSERT into PostgreSQL servers
6. [VCS Worker] full_sync (rolling, ≤2 min per server)
       → SSH to server, reads SQLite
       → updates PostgreSQL: inbounds (stream_settings), client_presence
7. [VCS Worker] transport_params_sync (after full_sync)
       → builds transport_params JSON from inbounds.stream_settings
       → UPDATE MySQL server.transport_params
       → DELETES vpn_user_server_config for this server (forces rebuild)
8. [VCS Worker] fill_configs (≤60 min)
       → for new server: ALL accounts = missing
       → SSH → docker exec → reads SQLite (existing IDs)
       → POST addClient batches of 50 per inbound
       → XUI: SQLite write + xray gRPC hot-reload (zero downtime)

Dependency order:

MySQL is_active=1  →  Backend event  →  vpn_user_server_account
                                         + vpn_user_server_config (if tp present)
                   →  VCS mysql_sync →  VCS full_sync  →  VCS transport_params_sync
                                                              → MySQL transport_params
                                        VCS fill_configs → clients on XUI panels


Scenario B: New Whitelist Server (is_active=0 → 1)

Steps 1–3 are the same as Scenario A.

4. [Backend] NodeSyncService.syncUserOnServer() for WHITELIST:
       → recalculateConfigs() — NO XUI HTTP call
       → buildVlessUrl():
           host = server.change_ip (Russian proxy IP)
           port = server.port (proxy listen port: 2053, 2083, etc.)
           Reality params = from server.transport_params
       → vpn_user_server_account + vpn_user_server_config

transport_params must be pre-filled

For whitelist servers, transport_params must be populated before activation. Clients on upstream XUI panels are already present from XUI entries (port 2087/853). fill_configs does NOT SSH to WL servers — they are pure DNAT proxies.


Scenario C: transport_params Changed

1. [VCS] transport_params_sync detects mismatch
       → UPDATE MySQL server.transport_params
       → DELETE FROM vpn_user_server_config WHERE server_id = X
2. [Backend] VpnSyncScheduler (every 6h):
       → detects transport_params change
       → sets vpn_user_server_account = PENDING
       → enqueues UPDATE for all accounts on this server
3. [Backend] NodeSyncWorker → rebuilds VLESS URLs

Scenario D: New Inbound on Existing Server

1. [XUI Panel] New inbound created (port X, protocol Y)
2. [MySQL] INSERT server row: same IP, different port/inbound_id, is_active=0
3. [VCS] full_sync → SSH → finds new inbound in SQLite → INSERT PostgreSQL inbounds
4. [VCS] mysql_sync → sees new server row → INSERT PostgreSQL servers
5. [VCS] fill_configs → addClient for new inbound
       → email: account@i{N} (N>1)
       → flow: empty for xhttp, xtls-rprx-vision for tcp
6. [MANUAL] UPDATE server SET is_active=1 → Backend event → sync

Full Path: User Pays → Receives VLESS

User pays
  → AccountServiceImpl.claimFromPool()
  → afterCommit: AccountCreatedEvent
  → VpnConfigSyncEventListener: enqueue CREATE for all servers
  → NodeSyncWorker → syncUserOnServer() × N servers
  → vpn_user_server_config filled (VLESS URLs)

User app: GET /api/v2/countries
  → CountryServiceImpl.getCountriesWithConfigsFromDb()
  → SELECT FROM vpn_user_server_config WHERE account_id = X
  → Group by country, filter active servers
  → Response: {countries: [{code: "DE", servers: [{configString: "vless://..."}]}]}

Dependency Table

Must exist BEFORE Required for
MySQL is_active=1 VCS mysql_sync pickup
PostgreSQL inbounds (stream_settings) transport_params_sync can build tp
MySQL transport_params filled Backend fast path (no XUI HTTP call)
vpn_user_server_account created vpn_user_server_config can be built
Client added to XUI (addClient) Client can connect to VPN
vpn_user_server_config exists /api/v2/countries returns server to user

Maximum Delays

Step Delay
Backend event (ServerStatusChanged) instant (afterCommit)
NodeSyncWorker processing ≤ a few minutes (500ms poll, batch=30)
VCS mysql_sync ≤ 10 min
VCS config pipeline (full sync per server) ≤ ~2 min (rolling)
VCS transport_params periodic ≤ 30 min
VCS fill_configs ≤ 60 min
Worst case (all users) ~70 min
With pre-filled transport_params ~5 min

Debugging Missing Configs

# Check vpn_user_server_config for a specific account
./scripts/ssh-internal.sh 10.99.87.62 "docker exec vpn-db mysql -uvpn -p'$(см. Vaultwarden  Databases)' vpn -e \
  \"SELECT s.ip, s.is_active, c.created_at FROM vpn_user_server_config c
    JOIN server s ON c.server_id = s.id
    WHERE c.account_id = UNHEX('ACCOUNT_UUID_HEX')
    ORDER BY c.created_at DESC\""

# Check ratio of active configs vs active servers
./scripts/ssh-internal.sh 10.99.87.62 "docker exec vpn-db mysql -uvpn -p'$(см. Vaultwarden  Databases)' vpn -e \
  \"SELECT COUNT(*) as cfg_cnt,
    SUM(CASE WHEN s.is_active=1 THEN 1 ELSE 0 END) as active_cnt,
    (SELECT COUNT(*) FROM server WHERE is_active=1) as total_active
   FROM vpn_user_server_config c JOIN server s ON c.server_id = s.id
   WHERE c.account_id = UNHEX('ACCOUNT_UUID_HEX')\""

If ratio < 0.8, background sync should be triggered. If configs are still missing after 10 min, check VCS worker logs on 10.99.87.249.


См. также: VPN Config Service · Архитектура бэкенда · Маппинг аккаунтов · DevOps скрипты → check_backend_configs