fix(node): bound what a node's answers can cost the master

Node responses are untrusted input on the master, and a few shapes still
cost it without bound under the 64 MiB body cap:
- a success=false message became an error (logged, stored as lastError)
  in full; it is now cut to the 8 KiB the non-200 diagnostic already uses;
- each "{}" of a reported list decodes into a whole struct, so lists are
  now decoded entry by entry with per-endpoint caps (1024 descendants,
  65536 inbounds and host groups, 1M client-IP rows), and the GUID-keyed
  trees and maps are capped at 1M values;
- a TLS handshake the node never answers kept a socket and goroutines
  alive forever, since the transport keeps dialing for its pool after the
  request gives up; node transports now give up after 10 s.
This commit is contained in:
MHSanaei
2026-10-31 09:19:51 +00:00
parent 08482154c2
commit 5a1ea42785
5 changed files with 172 additions and 26 deletions
+21 -26
View File
@@ -327,6 +327,7 @@ func (r *Remote) do(ctx context.Context, method, path string, body any) (*envelo
return nil, fmt.Errorf("decode envelope: %w", err)
}
if !env.Success {
env.Msg = truncateNodeMessage(env.Msg)
return &env, &remoteAPIError{msg: env.Msg}
}
return &env, nil
@@ -422,8 +423,8 @@ func (r *Remote) ListInboundOptions(ctx context.Context) ([]RemoteInboundOption,
if err != nil {
return nil, err
}
var list []RemoteInboundOption
if err := json.Unmarshal(env.Obj, &list); err != nil {
list, err := decodeCappedList[RemoteInboundOption](env.Obj, maxNodeInbounds)
if err != nil {
return nil, fmt.Errorf("decode inbound list: %w", err)
}
return list, nil
@@ -434,11 +435,11 @@ func (r *Remote) refreshRemoteIDs(ctx context.Context) error {
if err != nil {
return err
}
var list []struct {
list, err := decodeCappedList[struct {
Id int `json:"id"`
Tag string `json:"tag"`
}
if err := json.Unmarshal(env.Obj, &list); err != nil {
}](env.Obj, maxNodeInbounds)
if err != nil {
return fmt.Errorf("decode inbound list: %w", err)
}
next := make(map[string]int, len(list))
@@ -750,11 +751,9 @@ func (r *Remote) GetDescendants(ctx context.Context) ([]model.NodeSummary, error
if err != nil {
return nil, err
}
var out []model.NodeSummary
if len(env.Obj) > 0 {
if err := json.Unmarshal(env.Obj, &out); err != nil {
return nil, fmt.Errorf("decode descendants: %w", err)
}
out, err := decodeCappedList[model.NodeSummary](env.Obj, maxNodeDescendants)
if err != nil {
return nil, fmt.Errorf("decode descendants: %w", err)
}
return out, nil
}
@@ -813,11 +812,9 @@ func (r *Remote) FetchHostGroups(ctx context.Context) ([]*entity.HostGroup, erro
if err != nil {
return nil, err
}
var groups []*entity.HostGroup
if len(env.Obj) > 0 {
if err := json.Unmarshal(env.Obj, &groups); err != nil {
return nil, fmt.Errorf("decode host groups: %w", err)
}
groups, err := decodeCappedList[*entity.HostGroup](env.Obj, maxNodeHostGroups)
if err != nil {
return nil, fmt.Errorf("decode host groups: %w", err)
}
return groups, nil
}
@@ -829,7 +826,7 @@ func (r *Remote) FetchTrafficSnapshot(ctx context.Context) (*TrafficSnapshot, er
if err != nil {
return nil, err
}
if err := json.Unmarshal(envList.Obj, &snap.Inbounds); err != nil {
if snap.Inbounds, err = decodeCappedList[*model.Inbound](envList.Obj, maxNodeInbounds); err != nil {
return nil, fmt.Errorf("decode inbound list: %w", err)
}
@@ -837,14 +834,14 @@ func (r *Remote) FetchTrafficSnapshot(ctx context.Context) (*TrafficSnapshot, er
// node is an old build without the per-GUID endpoint (#4983).
envTree, err := r.do(ctx, http.MethodPost, "panel/api/clients/onlinesByGuid", nil)
if err == nil && len(envTree.Obj) > 0 {
_ = json.Unmarshal(envTree.Obj, &snap.OnlineTree)
_ = decodeCappedValues(envTree.Obj, &snap.OnlineTree, maxNodeMapValues)
}
if len(snap.OnlineTree) == 0 {
envOnlines, err := r.do(ctx, http.MethodPost, "panel/api/clients/onlines", nil)
if err != nil {
logger.Warning("remote", r.node.Name, "onlines fetch failed:", err)
} else if len(envOnlines.Obj) > 0 {
_ = json.Unmarshal(envOnlines.Obj, &snap.OnlineEmails)
_ = decodeCappedValues(envOnlines.Obj, &snap.OnlineEmails, maxNodeMapValues)
}
}
@@ -852,14 +849,14 @@ func (r *Remote) FetchTrafficSnapshot(ctx context.Context) (*TrafficSnapshot, er
if err != nil {
logger.Warning("remote", r.node.Name, "lastOnline fetch failed:", err)
} else if len(envLastOnline.Obj) > 0 {
_ = json.Unmarshal(envLastOnline.Obj, &snap.LastOnlineMap)
_ = decodeCappedValues(envLastOnline.Obj, &snap.LastOnlineMap, maxNodeMapValues)
}
envActiveInbounds, err := r.do(ctx, http.MethodPost, "panel/api/clients/activeInbounds", nil)
if err != nil {
logger.Debugf("remote %s active inbounds fetch failed: %v", r.node.Name, err)
} else if len(envActiveInbounds.Obj) > 0 {
_ = json.Unmarshal(envActiveInbounds.Obj, &snap.ActiveInboundTree)
_ = decodeCappedValues(envActiveInbounds.Obj, &snap.ActiveInboundTree, maxNodeMapValues)
}
return snap, nil
@@ -989,11 +986,9 @@ func (r *Remote) FetchAllClientIps(ctx context.Context) ([]model.InboundClientIp
if err != nil {
return nil, err
}
var ips []model.InboundClientIps
if len(env.Obj) > 0 {
if err := json.Unmarshal(env.Obj, &ips); err != nil {
return nil, fmt.Errorf("decode client ips: %w", err)
}
ips, err := decodeCappedList[model.InboundClientIps](env.Obj, maxNodeClientIPs)
if err != nil {
return nil, fmt.Errorf("decode client ips: %w", err)
}
return ips, nil
}
@@ -1014,7 +1009,7 @@ func (r *Remote) FetchClientIpsByGuid(ctx context.Context) (map[string]map[strin
}
out := map[string]map[string][]model.ClientIpEntry{}
if len(env.Obj) > 0 {
if err := json.Unmarshal(env.Obj, &out); err != nil {
if err := decodeCappedValues(env.Obj, &out, maxNodeMapValues); err != nil {
return nil, fmt.Errorf("decode client ips by guid: %w", err)
}
}