diff --git a/sub/subService.go b/sub/subService.go index 85bd7243..40aebe46 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -158,34 +158,61 @@ func (s *SubService) AggregateTrafficByEmails(emails []string) (xray.ClientTraff if len(emails) == 0 { return agg, 0 } + db := database.GetDB() var rows []xray.ClientTraffic - if err := database.GetDB(). + if err := db. Model(&xray.ClientTraffic{}). Where("email IN ?", emails). Find(&rows).Error; err != nil { logger.Warning("SubService - AggregateTrafficByEmails: load by email:", err) return agg, 0 } + + // total/expiry are configured limits owned by the clients table, not the + // runtime traffic rows. In a multi-node setup the node snapshot can reset + // client_traffics.total/expiry_time to 0, so fall back to the clients + // table to keep the Subscription-Userinfo header in sync with the UI (#4645). + limits := make(map[string][2]int64, len(emails)) + var records []model.ClientRecord + if err := db.Model(&model.ClientRecord{}).Where("email IN ?", emails).Find(&records).Error; err != nil { + logger.Warning("SubService - AggregateTrafficByEmails: load client limits:", err) + } else { + for _, r := range records { + limits[r.Email] = [2]int64{r.TotalGB, r.ExpiryTime} + } + } + now := time.Now().UnixMilli() - for i, ct := range rows { + first := true + for _, ct := range rows { if ct.LastOnline > lastOnline { lastOnline = ct.LastOnline } - if i == 0 { + total, expiry := ct.Total, ct.ExpiryTime + if lim, ok := limits[ct.Email]; ok { + if total == 0 { + total = lim[0] + } + if expiry == 0 { + expiry = lim[1] + } + } + if first { agg.Up = ct.Up agg.Down = ct.Down - agg.Total = ct.Total - agg.ExpiryTime = subscriptionExpiryFromClient(now, ct.ExpiryTime) + agg.Total = total + agg.ExpiryTime = subscriptionExpiryFromClient(now, expiry) + first = false continue } agg.Up += ct.Up agg.Down += ct.Down - if agg.Total == 0 || ct.Total == 0 { + if agg.Total == 0 || total == 0 { agg.Total = 0 } else { - agg.Total += ct.Total + agg.Total += total } - normalized := subscriptionExpiryFromClient(now, ct.ExpiryTime) + normalized := subscriptionExpiryFromClient(now, expiry) if normalized != agg.ExpiryTime { agg.ExpiryTime = 0 } @@ -581,6 +608,9 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin params["insecure"] = "1" } } + if pins, ok := pinnedSha256List(tlsSettings); ok { + params["pinSHA256"] = strings.Join(pins, ",") + } } // salamander obfs (Hysteria2). The panel-side link generator already diff --git a/sub/subService_userinfo_test.go b/sub/subService_userinfo_test.go new file mode 100644 index 00000000..698dadc6 --- /dev/null +++ b/sub/subService_userinfo_test.go @@ -0,0 +1,56 @@ +package sub + +import ( + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/database" + "github.com/mhsanaei/3x-ui/v3/database/model" + "github.com/mhsanaei/3x-ui/v3/xray" +) + +func TestAggregateTrafficByEmails_FallsBackToClientLimits(t *testing.T) { + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + + const email = "node-client@example.com" + const totalBytes = int64(300) * 1024 * 1024 * 1024 + const expiry = int64(1893456000000) + + db := database.GetDB() + if err := db.Create(&model.ClientRecord{ + Email: email, + TotalGB: totalBytes, + ExpiryTime: expiry, + Enable: true, + }).Error; err != nil { + t.Fatalf("seed client record: %v", err) + } + if err := db.Create(&xray.ClientTraffic{ + Email: email, + Up: 111, + Down: 222, + Total: 0, + ExpiryTime: 0, + Enable: true, + }).Error; err != nil { + t.Fatalf("seed client traffic: %v", err) + } + + var s SubService + agg, _ := s.AggregateTrafficByEmails([]string{email}) + + if agg.Up != 111 || agg.Down != 222 { + t.Errorf("usage = up %d/down %d, want 111/222", agg.Up, agg.Down) + } + if agg.Total != totalBytes { + t.Errorf("total = %d, want %d (fallback to clients table)", agg.Total, totalBytes) + } + if agg.ExpiryTime != expiry { + t.Errorf("expiry = %d, want %d (fallback to clients table)", agg.ExpiryTime, expiry) + } +}