mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-04 04:05:04 +03:00
fix(clients): collapse repeated inbound ids before fanning a client out
Create, BulkCreate, Attach and BulkAttach built one marshalled copy of the client per entry of the request's inboundIds and held all of them until the fan-out returned, with repeated ids never collapsed. An id costs two bytes of request body, so a large client named on the same inbound a few thousand times needed gigabytes and the panel died. Every fan-out entry point now collapses the id list first, keeping the order of first occurrence so credentials are still minted on the first inbound.
This commit is contained in:
@@ -30,6 +30,7 @@ type BulkAttachResult struct {
|
|||||||
// all clients to a target in a single AddInboundClient call, and reports clients already
|
// all clients to a target in a single AddInboundClient call, and reports clients already
|
||||||
// present on a target as skipped.
|
// present on a target as skipped.
|
||||||
func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkAttachResult, bool, error) {
|
func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkAttachResult, bool, error) {
|
||||||
|
inboundIds = uniqueInts(inboundIds)
|
||||||
result := &BulkAttachResult{}
|
result := &BulkAttachResult{}
|
||||||
if len(emails) == 0 || len(inboundIds) == 0 {
|
if len(emails) == 0 || len(inboundIds) == 0 {
|
||||||
return result, false, nil
|
return result, false, nil
|
||||||
@@ -1357,7 +1358,7 @@ func (s *ClientService) bulkCreate(inboundSvc *InboundService, payloads []Client
|
|||||||
seenEmail[le] = struct{}{}
|
seenEmail[le] = struct{}{}
|
||||||
seenSubID[client.SubID] = le
|
seenSubID[client.SubID] = le
|
||||||
|
|
||||||
prep = append(prep, prepared{client: client, inboundIds: payloads[i].InboundIds, limitHwid: payloads[i].LimitHwid, payloadIdx: i})
|
prep = append(prep, prepared{client: client, inboundIds: uniqueInts(payloads[i].InboundIds), limitHwid: payloads[i].LimitHwid, payloadIdx: i})
|
||||||
emails = append(emails, email)
|
emails = append(emails, email)
|
||||||
subIDs = append(subIDs, client.SubID)
|
subIDs = append(subIDs, client.SubID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
goruntime "runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -338,3 +340,54 @@ func TestFanoutReportsEveryFailingInbound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An inbound id costs two bytes of request body, so a repeated one must not cost
|
||||||
|
// the panel another copy of the client: 2000 ids x 8 MB of comment was 16 GB.
|
||||||
|
func TestRepeatedInboundIdCostsOneCopyOfTheClient(t *testing.T) {
|
||||||
|
const commentBytes = 1 << 20
|
||||||
|
client := func(email string) model.Client {
|
||||||
|
return model.Client{Email: email, Comment: strings.Repeat("x", commentBytes), Enable: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := map[string]func(svc *ClientService, inboundSvc *InboundService, spare int, ids []int) error{
|
||||||
|
"Create": func(svc *ClientService, inboundSvc *InboundService, _ int, ids []int) error {
|
||||||
|
_, err := svc.Create(inboundSvc, &ClientCreatePayload{Client: client("repeated@x"), InboundIds: ids})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
"BulkCreate": func(svc *ClientService, inboundSvc *InboundService, _ int, ids []int) error {
|
||||||
|
res, _, err := svc.BulkCreate(inboundSvc, []ClientCreatePayload{{Client: client("repeated@x"), InboundIds: ids}})
|
||||||
|
if err == nil && res.Created != 1 {
|
||||||
|
t.Fatalf("bulk create: created %d, skipped %+v", res.Created, res.Skipped)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
"Attach": func(svc *ClientService, inboundSvc *InboundService, spare int, ids []int) error {
|
||||||
|
if _, err := svc.CreateOne(inboundSvc, spare, client("repeated@x")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := svc.AttachByEmail(inboundSvc, "repeated@x", ids)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for name, run := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
setupBulkDB(t)
|
||||||
|
ib := mkInbound(t, 21011, model.VLESS, `{"clients":[]}`)
|
||||||
|
spare := mkInbound(t, 21012, model.VLESS, `{"clients":[]}`)
|
||||||
|
|
||||||
|
var before, after goruntime.MemStats
|
||||||
|
goruntime.ReadMemStats(&before)
|
||||||
|
if err := run(&ClientService{}, &InboundService{}, spare.Id, slices.Repeat([]int{ib.Id}, 300)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
goruntime.ReadMemStats(&after)
|
||||||
|
|
||||||
|
if allocated := after.TotalAlloc - before.TotalAlloc; allocated > 100*commentBytes {
|
||||||
|
t.Fatalf("allocated %d MiB for a 1 MiB client named on one inbound", allocated>>20)
|
||||||
|
}
|
||||||
|
if emails := settingsClientEmails(t, ib.Id); len(emails) != 1 {
|
||||||
|
t.Fatalf("clients on the inbound = %v, want the one", emails)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
|
|||||||
if payload == nil {
|
if payload == nil {
|
||||||
return false, common.NewError("empty payload")
|
return false, common.NewError("empty payload")
|
||||||
}
|
}
|
||||||
|
// A repeated id adds nothing but another copy of the client held through the fan-out.
|
||||||
|
payload.InboundIds = uniqueInts(payload.InboundIds)
|
||||||
client := payload.Client
|
client := payload.Client
|
||||||
if strings.TrimSpace(client.Email) == "" {
|
if strings.TrimSpace(client.Email) == "" {
|
||||||
return false, common.NewError("client email is required")
|
return false, common.NewError("client email is required")
|
||||||
@@ -1138,6 +1140,7 @@ func addressesFitAmneziaWGInbound(addrs []string, ib *model.Inbound) bool {
|
|||||||
// Attach applies the client to every requested inbound: one failing inbound no
|
// Attach applies the client to every requested inbound: one failing inbound no
|
||||||
// longer aborts the others, so the error can name several and needRestart holds.
|
// longer aborts the others, so the error can name several and needRestart holds.
|
||||||
func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) {
|
func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []int) (bool, error) {
|
||||||
|
inboundIds = uniqueInts(inboundIds)
|
||||||
existing, err := s.GetByID(id)
|
existing, err := s.GetByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func (s *InboundService) AnyNodePending(inboundIds []int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
nodeSvc := NodeService{}
|
nodeSvc := NodeService{}
|
||||||
for _, id := range inboundIds {
|
for _, id := range uniqueInts(inboundIds) {
|
||||||
ib, err := s.GetInbound(id)
|
ib, err := s.GetInbound(id)
|
||||||
if err != nil || ib.NodeID == nil {
|
if err != nil || ib.NodeID == nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user