mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(hwid): re-check the device limit where an unlimited fetch registers
EnforceHwidForSubID read the subscription's device limit and, finding none, recorded the device without looking again. A fetch racing an operator who had just set a limit could therefore register its device right after the limit was written and trimmed, leaving more devices registered than the limit and serving all of them. The tracking now re-reads the limit and writes the row in one transaction, which serializes with the operator's, and a limit that has appeared sends the request through the limited gate instead.
This commit is contained in:
@@ -128,19 +128,18 @@ func (s *ClientService) EnforceHwidForSubID(subID string, req HwidRequest) (Hwid
|
|||||||
}
|
}
|
||||||
req = normalizeHwidRequest(req)
|
req = normalizeHwidRequest(req)
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
res.Allowed = true
|
|
||||||
if len(req.Hwid) < minHwidLength {
|
if len(req.Hwid) < minHwidLength {
|
||||||
return res, nil
|
return HwidGateResult{Allowed: true}, nil
|
||||||
}
|
}
|
||||||
// The gate runs before the sub lookup; a subId no client owns gets no row.
|
// The gate runs before the sub lookup; a subId no client owns gets no row.
|
||||||
known, err := subIDHasEnabledClient(db, subID)
|
known, err := subIDHasEnabledClient(db, subID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
if known {
|
// A limit set since the read above sends the request through the gate below.
|
||||||
trackUnlimitedHwid(db, subID, req)
|
if !known || trackUnlimitedHwid(db, subID, req) {
|
||||||
|
return HwidGateResult{Allowed: true}, nil
|
||||||
}
|
}
|
||||||
return res, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Active = true
|
res.Active = true
|
||||||
@@ -206,15 +205,29 @@ func (s *ClientService) EnforceHwidForSubID(subID string, req HwidRequest) (Hwid
|
|||||||
|
|
||||||
// trackUnlimitedHwid lists devices of a sub with no HWID limit in the panel. It is
|
// trackUnlimitedHwid lists devices of a sub with no HWID limit in the panel. It is
|
||||||
// best-effort: a failed write must not deny a subscription nothing restricts.
|
// best-effort: a failed write must not deny a subscription nothing restricts.
|
||||||
func trackUnlimitedHwid(db *gorm.DB, subID string, req HwidRequest) {
|
// False means the sub has a limit by now and no row was written.
|
||||||
|
func trackUnlimitedHwid(db *gorm.DB, subID string, req HwidRequest) bool {
|
||||||
|
limited := false
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
err := db.Clauses(clause.OnConflict{
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
Columns: []clause.Column{{Name: "sub_id"}, {Name: "hwid_hash"}},
|
// A row landing behind setClientLimitHwidByEmailTx's trim would stay registered
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"last_seen", "user_agent", "device_os", "os_version", "device_model"}),
|
// above the limit, so the caller's read does not count here.
|
||||||
}).Create(&model.ClientHwid{SubID: subID, HwidHash: hashHwid(req.Hwid), FirstSeen: now, LastSeen: now, UserAgent: req.UserAgent, DeviceOS: req.DeviceOS, OsVersion: req.OsVersion, DeviceModel: req.DeviceModel}).Error
|
limit, err := effectiveHwidLimitForSubID(tx, subID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if limited = limit > 0; limited {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "sub_id"}, {Name: "hwid_hash"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"last_seen", "user_agent", "device_os", "os_version", "device_model"}),
|
||||||
|
}).Create(&model.ClientHwid{SubID: subID, HwidHash: hashHwid(req.Hwid), FirstSeen: now, LastSeen: now, UserAgent: req.UserAgent, DeviceOS: req.DeviceOS, OsVersion: req.OsVersion, DeviceModel: req.DeviceModel}).Error
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("track HWID for unlimited subscription failed:", err)
|
logger.Warning("track HWID for unlimited subscription failed:", err)
|
||||||
}
|
}
|
||||||
|
return !limited
|
||||||
}
|
}
|
||||||
|
|
||||||
// HwidSlotStatusForSubID is SELECT-only: it must never write client_hwids or
|
// HwidSlotStatusForSubID is SELECT-only: it must never write client_hwids or
|
||||||
|
|||||||
@@ -96,6 +96,53 @@ func TestClientHwidTxRejectsUnserializedHandle(t *testing.T) {
|
|||||||
assertHwidState(t, db, rec.Email, 1, 0)
|
assertHwidState(t, db, rec.Email, 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnlimitedHwidTrackingCannotLandBehindNewLimit(t *testing.T) {
|
||||||
|
initClientHwidTestDB(t)
|
||||||
|
db := database.GetDB()
|
||||||
|
rec := seedHwidClient(t, 0)
|
||||||
|
svc := &ClientService{}
|
||||||
|
if _, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: "device-one"}); err != nil {
|
||||||
|
t.Fatalf("track first device: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The limit write and its trim stay uncommitted while the gate reads "no limit".
|
||||||
|
written := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
limitDone := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
limitDone <- runSerializedTx(func(tx *gorm.DB) error {
|
||||||
|
err := svc.setClientLimitHwidByEmailTx(tx, rec.Email, 1)
|
||||||
|
close(written)
|
||||||
|
<-release
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
<-written
|
||||||
|
|
||||||
|
type gate struct {
|
||||||
|
res HwidGateResult
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
gateDone := make(chan gate, 1)
|
||||||
|
go func() {
|
||||||
|
res, err := svc.EnforceHwidForSubID(rec.SubID, HwidRequest{Hwid: "device-two"})
|
||||||
|
gateDone <- gate{res, err}
|
||||||
|
}()
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
close(release)
|
||||||
|
if err := <-limitDone; err != nil {
|
||||||
|
t.Fatalf("set limit: %v", err)
|
||||||
|
}
|
||||||
|
g := <-gateDone
|
||||||
|
if g.err != nil {
|
||||||
|
t.Fatalf("gate: %v", g.err)
|
||||||
|
}
|
||||||
|
if g.res.Allowed || !g.res.MaxDevicesReached || g.res.Limit != 1 {
|
||||||
|
t.Fatalf("second device must meet the new limit: %+v", g.res)
|
||||||
|
}
|
||||||
|
assertHwidState(t, db, rec.Email, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBulkAdjustHwidRollsBackFailedTrim(t *testing.T) {
|
func TestBulkAdjustHwidRollsBackFailedTrim(t *testing.T) {
|
||||||
setupBulkDB(t)
|
setupBulkDB(t)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|||||||
Reference in New Issue
Block a user