3x-ui/web/job/unblock_ips_job.go
WatchDogsDev bef6b45848 fix: prevent multi-GB traffic overages after bandwidth limit is reached
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
2026-03-22 06:21:05 +03:30

40 lines
1.2 KiB
Go

package job
import (
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/iptables"
)
const maxBlockAgeSecs int64 = 600 // 10 minutes
// UnblockIPsJob removes expired iptables DROP rules from the 3X-UI-BLOCK chain.
// Rules older than maxBlockAgeSecs are removed to prevent the firewall table
// from growing unbounded and to unblock IPs that may have been re-assigned.
type UnblockIPsJob struct{}
// NewUnblockIPsJob creates a new instance of the IP unblock cleanup job.
func NewUnblockIPsJob() *UnblockIPsJob {
return &UnblockIPsJob{}
}
// Run enumerates all rules in the 3X-UI-BLOCK chain and removes any that are
// older than maxBlockAgeSecs.
func (j *UnblockIPsJob) Run() {
rules, err := iptables.ListRules()
if err != nil {
logger.Debug("UnblockIPsJob: failed to list iptables rules:", err)
return
}
now := time.Now().Unix()
for _, rule := range rules {
if rule.InsertedAt > 0 && (now-rule.InsertedAt) > maxBlockAgeSecs {
if err := iptables.UnblockIP(rule.IP, rule.Port); err != nil {
logger.Warning("UnblockIPsJob: failed to unblock", rule.IP, rule.Port, err)
} else {
logger.Debug("UnblockIPsJob: unblocked expired rule", rule.IP, rule.Port)
}
}
}
}