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

Config Sync Rules

Rules and pitfalls for VPN config synchronization. Derived from the Felis incident (January 2026).


The Felis Incident (20 Jan 2026)

A user received only 26/49 configs (53%) because:

  1. 46 servers were deactivated on Jan 17 — no resync was triggered
  2. On Jan 20, the backend returned partial data (26/49) and the desktop client cached it for 1 hour
  3. Background sync completed in ~8 min, but the cache already held stale data
  4. User experienced a crash ~2.5 hours later when attempting to use VPN

Key timeline:

11:42 — CONFIG_FROM_DB_INCOMPLETE: healthyConfigs=26 activeServers=49
11:42 — Background sync started for 23 missing servers
11:50 — CONFIG_FROM_DB_SUCCESS: 27 countries, 49 servers ✓
12:00 — XUI panel errors, MySQL connection pool issues
14:27 — User tries VPN → app crashes
14:39 — After restart: SUCCESS (27/49)


Client Cache TTL

Client Cache TTL Notes
Desktop (Rust) 1 hour shiva-api/src/rest/relay.rsCACHE_HOURS = 1
Android Room DB Updated on each request
iOS FileManager Updated on each request

Desktop caches for 1 hour

If a user gets partial configs, the desktop app will use them for up to 1 hour even after the backend has recovered. A restart clears the cache.


Rule 1: After Server Deactivation — Trigger Resync

When deactivating servers in bulk, always clean up stale configs:

-- Option 1: Reset sync status for affected accounts
UPDATE vpn_user_server_account
SET status = 'PENDING'
WHERE server_id IN (SELECT id FROM server WHERE is_active = 0);

-- Option 2: Delete stale configs (forces rebuild on next request)
DELETE FROM vpn_user_server_config
WHERE server_id IN (SELECT id FROM server WHERE is_active = 0);

After running, wait ~10 minutes for background sync to complete.


Rule 2: Partial Data Response

Current behavior (problematic):

When healthyConfigs / activeServers < threshold, the backend returns partial data and starts background sync. The client receives and may cache an incomplete list.

Recommendation: If ratio < 0.8, return an error or empty response — let the client use its old cache rather than caching the partial response.

File: backend-app/.../CountryServiceImpl.java:808-820


Dependency Checklist

Before activating a server:

  • server.transport_params filled (check: SELECT transport_params FROM server WHERE id = X)
  • Clients exist on XUI panel (check via SQLite on the VPN server)
  • server.is_active = 1 set only after above are confirmed
  • For whitelist servers: transport_params MUST be pre-filled (no fallback to XUI HTTP)

After deactivating servers:

  • Run resync query (see Rule 1 above)
  • Wait ~10 min
  • Verify ratio: SELECT COUNT(*) FROM vpn_user_server_config c JOIN server s ON c.server_id=s.id WHERE s.is_active=1 vs total active servers

What Works / What Doesn't

Works: - Background sync recovers missing configs within ~8 minutes - After app restart, fresh data is loaded - Config pipeline continuously syncs (rolling, ~2 min per server)

Doesn't work: - No mechanism to force-invalidate desktop client cache - Backend returns partial data instead of error when ratio is low - No automated trigger for resync after bulk deactivation


Verifying Config State

# Count configs per account 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 total_configs,
    SUM(CASE WHEN s.is_active=1 THEN 1 ELSE 0 END) as active_configs,
    (SELECT COUNT(*) FROM server WHERE is_active=1) as total_active_servers
   FROM vpn_user_server_config c
   JOIN server s ON c.server_id = s.id
   WHERE c.account_id = UNHEX('ACCOUNT_UUID_HEX')\""

# Check accounts missing configs entirely (backend's background sync trigger)
./scripts/ssh-internal.sh 10.99.87.62 "docker exec vpn-db mysql -uvpn -p'$(см. Vaultwarden  Databases)' vpn -e \
  \"SELECT COUNT(DISTINCT a.id) FROM account a
   WHERE a.deleted_at IS NULL
   AND a.pay_status NOT IN ('POOL')
   AND NOT EXISTS (
     SELECT 1 FROM vpn_user_server_config c WHERE c.account_id = a.id
   )\""