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