fix: prevent multi-GB traffic overages after bandwidth limit is reached#3958
Open
WatchDogsDev wants to merge 1 commit intoMHSanaei:mainfrom
Open
fix: prevent multi-GB traffic overages after bandwidth limit is reached#3958WatchDogsDev wants to merge 1 commit intoMHSanaei:mainfrom
WatchDogsDev wants to merge 1 commit intoMHSanaei:mainfrom
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a client exceeds their bandwidth limit, the 10-second traffic check job often fails to stop them in time. Clients can overshoot their limit by 10 GB or more. There are three distinct causes:
Bucket A — in-flight gap (~10 s of traffic):
Between
GetTraffic(reset=true)zeroing the Xray counter andRemoveUser()taking effect, the client keeps generating traffic. Those bytes land in the freshly-zeroed counter and are counted one cycle late — a minor but real overage.Bucket B — active TCP connections survive removal (primary cause of multi-GB overages):
RemoveUser()removes the client from Xray's auth table but does not close existing established connections. A mid-download client on a persistent TLS tunnel keeps transferring until the connection closes naturally.Bucket C — restart race (catastrophic, permanent data loss):
When
RemoveUser()fails,needRestart=trueis set and Xray restarts up to 30 seconds later. On restart all in-memory stats counters are zeroed — bytes transferred during that window are permanently lost and never credited to the client.Changes
Fix 1 — Flush traffic to DB before Xray restart (Bucket C)
web/service/xray.go— NewFlushTrafficToDB()method that fetches current Xray stats and persists them viainboundService.AddTraffic().web/web.go— CallFlushTrafficToDB()inside the 30 s restart cron beforeRestartXray(false), so in-memory counters are saved before Xray zeros them.Fix 2 — Drain residual per-user traffic after RemoveUser (Bucket A)
xray/api.go— NewDrainUserTraffic(email)method that callsGetStatswithReset_: truefor bothuser>>>email>>>traffic>>>uplinkand>>>downlink, capturing bytes accumulated since the last bulk reset.web/service/inbound.godisableInvalidClients()— After a successfulRemoveUser(), callsDrainUserTrafficand adds the delta to the client's DB row withgorm.Expr("up + ?", ...).Fix 3 — iptables DROP block for active TCP connections (Bucket B)
util/iptables/iptables.go(new) — Utility package managing a dedicated3X-UI-BLOCKiptables chain:EnsureChain()— creates the chain and adds a jump fromINPUT(idempotent)FlushChain()— removes all rules (used on startup)BlockIP(ip, port)— inserts a DROP rule with a3xui:block:<timestamp>commentUnblockIP(ip, port)— removes the DROP ruleListRules()— parsesiptables -S 3X-UI-BLOCKinto structuredRuleEntryvaluesAll rules are isolated to the custom chain — no OS/admin rules are touched. If
iptablesis not available, functions log a warning and return gracefully.web/job/unblock_ips_job.go(new) — Background job scheduled@every 5mthat enumerates rules in3X-UI-BLOCKand removes any older thanmaxBlockAgeSecs(10 minutes).web/service/inbound.go— Two helpers:blockClientIPs(email)— reads known IPs frominbound_client_ips, gets the inbound port, callsiptables.BlockIPfor each IP. Called after a successfulRemoveUser()indisableInvalidClients().unblockClientIPs(email)— mirror of the above usingiptables.UnblockIP. Called after a successfulAddUser()inautoRenewClients()so renewed clients can reconnect.web/web.go— On startup callsiptables.EnsureChain()theniptables.FlushChain()to initialize the chain and clear any stale rules from a prior crash. RegistersNewUnblockIPsJob()at@every 5m.Testing
client_traffics.enable = 0in DBiptables -L 3X-UI-BLOCK -nshows DROP rules for the client's IPs on the inbound portRemoveUser()gRPC failure → verifyclient_trafficstotals are accurate after the subsequent Xray restart (no bytes permanently lost).Notes
iptablesto be available on the host (standard on all Linux distributions used with 3x-ui). Absence is handled gracefully — normal disable logic continues, only the IP block step is skipped with a warning log.ip6tables) can be added as a follow-up.maxBlockAgeSecsconstant inunblock_ips_job.gocontrols the auto-expiry window (default: 10 minutes).