mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-07 14:42:10 +03:00
fb7418f7bd
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.
138 lines
5.6 KiB
Go
138 lines
5.6 KiB
Go
package service
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/mtproto"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
// startQuotaSidecar runs a local MTProto inbound for mtga and mtgb under the fake
|
|
// mtg and returns its API log once the sidecar answers.
|
|
func startQuotaSidecar(t *testing.T, port int, mtga model.Client) (*model.Inbound, string) {
|
|
t.Helper()
|
|
setupConflictDB(t)
|
|
pidFile, logPath := installFakeMtgAPI(t)
|
|
runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
|
|
t.Cleanup(func() { runtime.SetManager(nil) })
|
|
|
|
mtga.Email, mtga.Secret = "mtga", mtprotoTestSecretA
|
|
clients := []model.Client{mtga, {Email: "mtgb", Secret: mtprotoTestSecretB, Enable: true}}
|
|
ib := &model.Inbound{Tag: "mt-quota", Enable: true, Port: port, Protocol: model.MTProto, Settings: clientsSettings(t, clients)}
|
|
if err := database.GetDB().Create(ib).Error; err != nil {
|
|
t.Fatalf("create inbound: %v", err)
|
|
}
|
|
if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
|
|
t.Fatalf("SyncInbound: %v", err)
|
|
}
|
|
for _, c := range clients {
|
|
row := xray.ClientTraffic{InboundId: ib.Id, Email: c.Email, Enable: true, Up: 5, Total: c.TotalGB, ExpiryTime: c.ExpiryTime, Reset: c.Reset}
|
|
if err := database.GetDB().Create(&row).Error; err != nil {
|
|
t.Fatalf("seed traffic: %v", err)
|
|
}
|
|
}
|
|
// A running sidecar needs a served client, so prime with the healthy set.
|
|
inst, ok := mtproto.InstanceFromInbound(&model.Inbound{
|
|
Id: ib.Id, Tag: ib.Tag, Port: port, Protocol: model.MTProto,
|
|
Settings: clientsSettings(t, []model.Client{{Email: "mtgb", Secret: mtprotoTestSecretB, Enable: true}}),
|
|
})
|
|
if !ok {
|
|
t.Fatal("seed inbound must produce an mtg instance")
|
|
}
|
|
if err := mtproto.GetManager().Ensure(inst); err != nil {
|
|
t.Fatalf("start mtg: %v", err)
|
|
}
|
|
t.Cleanup(func() { mtproto.GetManager().Remove(ib.Id) })
|
|
waitForSpawns(t, pidFile, 1)
|
|
waitFakeMtgLog(t, logPath, "ready")
|
|
return ib, logPath
|
|
}
|
|
|
|
func quotaResets(t *testing.T, logPath string) []string {
|
|
t.Helper()
|
|
var out []string
|
|
for _, line := range fakeMtgLog(t, logPath) {
|
|
if name, ok := strings.CutPrefix(line, "reset:"); ok {
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
slices.Sort(out)
|
|
return out
|
|
}
|
|
|
|
// Every path that zeroes a client's panel counters must zero the sidecar's own
|
|
// quota counter too, or the sidecar keeps refusing the client.
|
|
func TestPanelResetsZeroSidecarQuota(t *testing.T) {
|
|
t.Run("bulk reset", func(t *testing.T) {
|
|
_, logPath := startQuotaSidecar(t, 46201, model.Client{Enable: true})
|
|
if _, err := (&ClientService{}).BulkResetTraffic(&InboundService{}, []string{"mtga"}); err != nil {
|
|
t.Fatalf("BulkResetTraffic: %v", err)
|
|
}
|
|
if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga"}) {
|
|
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 {
|
|
t.Fatalf("ResetAllTraffics: %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("auto renew", func(t *testing.T) {
|
|
expired := time.Now().Add(-time.Hour).UnixMilli()
|
|
_, logPath := startQuotaSidecar(t, 46203, model.Client{Enable: true, Reset: 30, ExpiryTime: expired})
|
|
if _, _, err := (&InboundService{}).AddTraffic(nil, nil); err != nil {
|
|
t.Fatalf("AddTraffic: %v", err)
|
|
}
|
|
if got := quotaResets(t, logPath); !slices.Equal(got, []string{"mtga"}) {
|
|
t.Fatalf("sidecar quota resets %v, want [mtga]", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|