feat(node-sync): push global client usage to nodes for display and local enforcement

A client attached to several panels has one aggregated row on each
master, but a node only ever saw its local share: the node UI
under-reported usage, and the node kept serving a client whose
cross-panel total had already exceeded its quota — the master's disable
push doesn't kill established connections unless the node restarts xray
itself.

Masters now push their aggregated per-client counters to each node from
NodeTrafficSyncJob (throttled, scoped to the clients that node hosts).
The node stores them in the new client_global_traffics side table keyed
by (masterGuid, email), overwritten on every push so a master-side
reset propagates, and:

- overlays max(local, pushed) onto UI read paths (slim inbound list,
  inbound detail, clients list, WS stats, per-email lookups). The full
  /panel/api/inbounds/list stays un-overlaid on purpose: it doubles as
  the traffic snapshot masters poll, and overlaying it would corrupt
  every master's delta accounting;
- trips disableInvalidClients when any master's pushed total exceeds
  the client's quota, so the existing RestartXrayOnClientDisable flow
  disconnects the client locally;
- clears the side rows on traffic reset, auto-renew, and client
  delete, keeping a renewed quota window clean.

Supersedes #5204, which folded pushed globals into client_traffics and
compensated with read-back baselines — that double-counted first-sight
emails and could not work with several masters sharing one node.
This commit is contained in:
MHSanaei
2026-06-11 15:14:08 +02:00
parent 8258a26fbf
commit 58905d81a4
15 changed files with 604 additions and 4 deletions
+1
View File
@@ -72,6 +72,7 @@ func initModels() error {
&model.ClientGroup{},
&model.InboundFallback{},
&model.NodeClientTraffic{},
&model.ClientGlobalTraffic{},
&model.OutboundSubscription{},
}
for _, mdl := range models {
@@ -0,0 +1,20 @@
package model
// ClientGlobalTraffic mirrors a master panel's aggregated (global) usage for a
// client hosted on this panel. Masters push one row per (master, email) so the
// node can display the client's true cross-panel total and enforce its quota
// locally. The values never feed back into client_traffics — that table keeps
// this panel's local-only counters, which is what keeps every master's
// delta-baseline accounting over our snapshot correct.
//
// Rows are overwritten in place on every push (not max-merged), so a traffic
// reset on the master propagates here within one push cycle. Readers that need
// a single number fold the per-master rows with MAX.
type ClientGlobalTraffic struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
MasterGuid string `json:"masterGuid" gorm:"uniqueIndex:idx_master_email,priority:1;not null"`
Email string `json:"email" gorm:"uniqueIndex:idx_master_email,priority:2;not null"`
Up int64 `json:"up"`
Down int64 `json:"down"`
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
}