Three layered fixes targeting the distinct causes of overage:
Bucket C (catastrophic): flush pending Xray stats to DB before every
scheduled Xray restart so in-memory counters are never silently zeroed.
- web/service/xray.go: add FlushTrafficToDB()
- web/web.go: call FlushTrafficToDB() in the 30 s restart cron before
RestartXray(false)
Bucket A (in-flight gap): drain per-user Xray stats counters immediately
after RemoveUser() succeeds, capturing bytes accumulated since the last
bulk GetTraffic(reset=true) cycle.
- xray/api.go: add DrainUserTraffic(email) using GetStats gRPC with reset
- web/service/inbound.go: call DrainUserTraffic and persist delta in
disableInvalidClients()
Bucket B (active TCP connections survive removal): insert iptables DROP
rules for each known client IP on the inbound port so established
connections are killed immediately, not just new ones.
- util/iptables/iptables.go: new package managing the 3X-UI-BLOCK chain
(EnsureChain, FlushChain, BlockIP, UnblockIP, ListRules); gracefully
degrades when iptables is unavailable
- web/job/unblock_ips_job.go: @every 5m cleanup job removes rules older
than maxBlockAgeSecs
- web/service/inbound.go: blockClientIPs() called after successful
RemoveUser(); unblockClientIPs() called after successful AddUser() in
autoRenewClients() so renewed clients can reconnect
- web/web.go: EnsureChain + FlushChain on startup; register unblock job