From fb7418f7bd74526a06992f11e8248102c0a31aa6 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 28 Sep 2026 13:02:50 +0200 Subject: [PATCH] fix(mtproto): zero sidecar quotas only for clients whose usage was reset Invariant: the sidecar's quota counter for a client is zeroed exactly when the panel zeroes that client's usage. InboundService.ResetAllTraffics resets only inbound counters, yet it cleared every MTProto client's sidecar quota - on the master and, through its node propagation, on every node - handing out a fresh quota while the panel still counted the old usage. ResetAllClientTraffics for one inbound likewise cleared the quotas of MTProto clients on every other inbound. The inbound-level reset no longer touches sidecar quotas, and the per-inbound client reset zeroes only the clients it reset. --- internal/web/service/client_traffic.go | 13 ++++--- internal/web/service/inbound_traffic.go | 1 - .../web/service/mtproto_quota_reset_test.go | 37 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/internal/web/service/client_traffic.go b/internal/web/service/client_traffic.go index 5082d9525..93016eeac 100644 --- a/internal/web/service/client_traffic.go +++ b/internal/web/service/client_traffic.go @@ -126,22 +126,24 @@ func (s *ClientService) BulkResetTraffic(inboundSvc *InboundService, emails []st func (s *ClientService) ResetAllClientTraffics(inboundSvc *InboundService, id int) error { var resetNodes []int + var resetEmails []string err := submitTrafficWrite(func() error { var inner error - resetNodes, inner = s.resetAllClientTrafficsLocked(id) + resetEmails, resetNodes, inner = s.resetAllClientTrafficsLocked(id) return inner }) if err == nil { - inboundSvc.resetAllMtprotoQuotas() + inboundSvc.resetMtprotoClientQuotas(resetEmails) inboundSvc.deliverNodeResetsNow(resetNodes) } return err } -func (s *ClientService) resetAllClientTrafficsLocked(id int) ([]int, error) { +func (s *ClientService) resetAllClientTrafficsLocked(id int) ([]string, []int, error) { db := database.GetDB() now := time.Now().Unix() * 1000 var resetNodes []int + var reset []string if err := db.Transaction(func(tx *gorm.DB) error { // client_traffics.inbound_id is stale: it reflects the inbound the row was @@ -164,6 +166,7 @@ func (s *ClientService) resetAllClientTrafficsLocked(id int) ([]int, error) { if len(resetEmails) == 0 { return nil } + reset = resetEmails if err := adjustGroupBaselinesForRemovedTraffic(tx, resetEmails); err != nil { return err @@ -204,9 +207,9 @@ func (s *ClientService) resetAllClientTrafficsLocked(id int) ([]int, error) { return result.Error }); err != nil { - return nil, err + return nil, nil, err } - return resetNodes, nil + return reset, resetNodes, nil } func (s *ClientService) ResetAllTraffics() (bool, error) { diff --git a/internal/web/service/inbound_traffic.go b/internal/web/service/inbound_traffic.go index 904499b27..366af1a97 100644 --- a/internal/web/service/inbound_traffic.go +++ b/internal/web/service/inbound_traffic.go @@ -783,7 +783,6 @@ func (s *InboundService) ResetAllTraffics() error { }) if err == nil { s.propagateResetAllTrafficsToNodes() - s.resetAllMtprotoQuotas() } return err } diff --git a/internal/web/service/mtproto_quota_reset_test.go b/internal/web/service/mtproto_quota_reset_test.go index 0ae1e41a0..efcfec0ef 100644 --- a/internal/web/service/mtproto_quota_reset_test.go +++ b/internal/web/service/mtproto_quota_reset_test.go @@ -78,6 +78,15 @@ func TestPanelResetsZeroSidecarQuota(t *testing.T) { t.Fatalf("sidecar quota resets %v, want [mtga]", got) } }) + t.Run("inbound clients", func(t *testing.T) { + ib, logPath := startQuotaSidecar(t, 46207, model.Client{Enable: true}) + if err := (&ClientService{}).ResetAllClientTraffics(&InboundService{}, ib.Id); err != nil { + t.Fatalf("ResetAllClientTraffics: %v", err) + } + if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga", "mtgb"}) { + t.Fatalf("sidecar quota resets %v, want [mtga mtgb]", got) + } + }) t.Run("reset all", func(t *testing.T) { _, logPath := startQuotaSidecar(t, 46202, model.Client{Enable: true}) if _, err := (&ClientService{}).ResetAllTraffics(); err != nil { @@ -98,3 +107,31 @@ func TestPanelResetsZeroSidecarQuota(t *testing.T) { } }) } + +// Resetting inbound counters leaves every client's usage in place, so the +// sidecar's quota counters must stay too or clients get their quota again free. +func TestInboundResetAllKeepsSidecarQuota(t *testing.T) { + _, logPath := startQuotaSidecar(t, 46204, model.Client{Enable: true}) + if err := (&InboundService{}).ResetAllTraffics(); err != nil { + t.Fatalf("ResetAllTraffics: %v", err) + } + if got := quotaResets(t, logPath); len(got) != 0 { + t.Fatalf("inbound reset zeroed sidecar quotas %v, want none", got) + } +} + +// Resetting one inbound's clients zeroes only their sidecar quotas, not those +// of MTProto clients whose usage the reset left in place. +func TestInboundClientResetKeepsOtherSidecarQuotas(t *testing.T) { + _, logPath := startQuotaSidecar(t, 46205, model.Client{Enable: true}) + other := mkInbound(t, 46206, model.VLESS, clientsSettings(t, []model.Client{{Email: "vless-only", ID: "11111111-1111-1111-1111-1111111111ab", Enable: true}})) + if err := (&ClientService{}).SyncInbound(nil, other.Id, []model.Client{{Email: "vless-only", ID: "11111111-1111-1111-1111-1111111111ab", Enable: true}}); err != nil { + t.Fatalf("SyncInbound: %v", err) + } + if err := (&ClientService{}).ResetAllClientTraffics(&InboundService{}, other.Id); err != nil { + t.Fatalf("ResetAllClientTraffics: %v", err) + } + if got := quotaResets(t, logPath); len(got) != 0 { + t.Fatalf("resetting another inbound zeroed sidecar quotas %v, want none", got) + } +}