diff --git a/sub/subClashService.go b/sub/subClashService.go index c94ea467..51509ef3 100644 --- a/sub/subClashService.go +++ b/sub/subClashService.go @@ -91,6 +91,9 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e } } + // Override total with shared sub-quota when it is the active limiter. + s.SubService.resolveSubTraffic(subId, inbounds, &traffic) + proxyNames := make([]string, 0, len(proxies)+1) for _, proxy := range proxies { if name, ok := proxy["name"].(string); ok && name != "" { diff --git a/sub/subJsonService.go b/sub/subJsonService.go index 3b34ed68..f1c661b0 100644 --- a/sub/subJsonService.go +++ b/sub/subJsonService.go @@ -154,6 +154,9 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err } } + // Override total with shared sub-quota when it is the active limiter. + s.SubService.resolveSubTraffic(subId, inbounds, &traffic) + // Combile outbounds var finalJson []byte if len(configArray) == 1 { diff --git a/sub/subService.go b/sub/subService.go index d769bf5a..83943b2c 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -137,6 +137,8 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C } } } + // Override total with shared sub-quota when it is the active limiter. + s.resolveSubTraffic(subId, inbounds, &traffic) traffic.Enable = hasEnabledClient return result, lastOnline, traffic, nil } @@ -182,6 +184,52 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri return xray.ClientTraffic{} } +// resolveSubTraffic replaces the per-client totalGB-based total with the +// shared subTotalGB quota when it is the active limiter. Client apps +// (v2rayNG, Clash, Hiddify, etc.) read the Subscription-Userinfo header +// to show the user their remaining data — without this, they would see +// "unlimited" even when the admin set a shared sub-quota. +// +// Logic: +// +// subTotalGB > 0 && totalGB == 0 → use subTotalGB (shared is the limiter) +// subTotalGB > 0 && totalGB > 0 → use min(subTotalGB, sum(totalGB)) — whichever hits first +// subTotalGB == 0 → use sum(totalGB) (no shared quota, original behavior) +func (s *SubService) resolveSubTraffic(subId string, inbounds []*model.Inbound, traffic *xray.ClientTraffic) { + if subId == "" { + return + } + + var maxSubTotal int64 + for _, inbound := range inbounds { + clients, err := s.inboundService.GetClients(inbound) + if err != nil || clients == nil { + continue + } + for _, c := range clients { + if c.SubID == subId && c.SubTotalGB > maxSubTotal { + maxSubTotal = c.SubTotalGB + } + } + } + + if maxSubTotal <= 0 { + return // no shared quota — keep original aggregated total + } + + if traffic.Total == 0 { + // Individual totalGB is 0 (unlimited per-client), so subTotalGB + // is the only limiter. + traffic.Total = maxSubTotal + } else { + // Both individual and shared quotas are set — the effective + // limit is whichever fires first (the smaller one). + if maxSubTotal < traffic.Total { + traffic.Total = maxSubTotal + } + } +} + func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) { db := database.GetDB() var inbound *model.Inbound