fix(api): cap the client traffic list a master pushes by entry count

pushClientTraffics decoded the pushed list whole, and each "{}" became a
full traffic row, so a body under the size cap could cost the panel
close to a gigabyte. The list is now decoded entry by entry and refused
past 262144 entries. The capped decoder the node runtime already used
moves to json_util so both share it.
This commit is contained in:
MHSanaei
2026-10-31 14:25:21 +01:00
parent 44a502a407
commit fe5d704d67
5 changed files with 82 additions and 38 deletions
+7 -6
View File
@@ -22,6 +22,7 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
"github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
"github.com/mhsanaei/3x-ui/v3/internal/util/wirecodec"
"github.com/mhsanaei/3x-ui/v3/internal/web/entity"
@@ -423,7 +424,7 @@ func (r *Remote) ListInboundOptions(ctx context.Context) ([]RemoteInboundOption,
if err != nil {
return nil, err
}
list, err := decodeCappedList[RemoteInboundOption](env.Obj, maxNodeInbounds)
list, err := json_util.DecodeCappedList[RemoteInboundOption](env.Obj, maxNodeInbounds)
if err != nil {
return nil, fmt.Errorf("decode inbound list: %w", err)
}
@@ -435,7 +436,7 @@ func (r *Remote) refreshRemoteIDs(ctx context.Context) error {
if err != nil {
return err
}
list, err := decodeCappedList[struct {
list, err := json_util.DecodeCappedList[struct {
Id int `json:"id"`
Tag string `json:"tag"`
}](env.Obj, maxNodeInbounds)
@@ -768,7 +769,7 @@ func (r *Remote) GetDescendants(ctx context.Context) ([]model.NodeSummary, error
if err != nil {
return nil, err
}
out, err := decodeCappedList[model.NodeSummary](env.Obj, maxNodeDescendants)
out, err := json_util.DecodeCappedList[model.NodeSummary](env.Obj, maxNodeDescendants)
if err != nil {
return nil, fmt.Errorf("decode descendants: %w", err)
}
@@ -829,7 +830,7 @@ func (r *Remote) FetchHostGroups(ctx context.Context) ([]*entity.HostGroup, erro
if err != nil {
return nil, err
}
groups, err := decodeCappedList[*entity.HostGroup](env.Obj, maxNodeHostGroups)
groups, err := json_util.DecodeCappedList[*entity.HostGroup](env.Obj, maxNodeHostGroups)
if err != nil {
return nil, fmt.Errorf("decode host groups: %w", err)
}
@@ -843,7 +844,7 @@ func (r *Remote) FetchTrafficSnapshot(ctx context.Context) (*TrafficSnapshot, er
if err != nil {
return nil, err
}
if snap.Inbounds, err = decodeCappedList[*model.Inbound](envList.Obj, maxNodeInbounds); err != nil {
if snap.Inbounds, err = json_util.DecodeCappedList[*model.Inbound](envList.Obj, maxNodeInbounds); err != nil {
return nil, fmt.Errorf("decode inbound list: %w", err)
}
@@ -1003,7 +1004,7 @@ func (r *Remote) FetchAllClientIps(ctx context.Context) ([]model.InboundClientIp
if err != nil {
return nil, err
}
ips, err := decodeCappedList[model.InboundClientIps](env.Obj, maxNodeClientIPs)
ips, err := json_util.DecodeCappedList[model.InboundClientIps](env.Obj, maxNodeClientIPs)
if err != nil {
return nil, fmt.Errorf("decode client ips: %w", err)
}
+3 -29
View File
@@ -3,9 +3,10 @@ package runtime
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"unicode/utf8"
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
)
// Per-endpoint caps on node-reported lists: each "{}" decodes into a whole struct, so the
@@ -19,34 +20,7 @@ const (
maxNodeMapValues = 1 << 20
)
var errRemoteListTooLong = errors.New("remote list exceeds entry limit")
// decodeCappedList decodes a JSON array entry by entry and stops at the first entry
// past maxEntries; null decodes to a nil slice.
func decodeCappedList[T any](raw json.RawMessage, maxEntries int) ([]T, error) {
raw = bytes.TrimSpace(raw)
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
return nil, nil
}
dec := json.NewDecoder(bytes.NewReader(raw))
if tok, err := dec.Token(); err != nil {
return nil, err
} else if delim, ok := tok.(json.Delim); !ok || delim != '[' {
return nil, fmt.Errorf("expected a JSON array, got %v", tok)
}
var out []T
for dec.More() {
if len(out) >= maxEntries {
return nil, fmt.Errorf("%w (%d)", errRemoteListTooLong, maxEntries)
}
var item T
if err := dec.Decode(&item); err != nil {
return nil, err
}
out = append(out, item)
}
return out, nil
}
var errRemoteListTooLong = json_util.ErrListTooLong
// decodeCappedValues unmarshals raw into v after checking it holds at most maxValues
// JSON values in all, nested ones included.