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
+28
View File
@@ -510,3 +510,31 @@ func TestRemoteAdoptedAliasYieldsToNodeReportedTag(t *testing.T) {
t.Fatalf("central tag resolved to %d via a stale alias, want 5 (the id the node reports)", id)
}
}
// A node's success=false message becomes an error the master logs and stores as the
// node's lastError; it must not carry the whole response body there.
func TestRemoteDo_BoundsTheNodeErrorMessage(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"success":false,"msg":"` + strings.Repeat("m", 1<<20) + `"}`))
}))
defer srv.Close()
r := NewRemote(nodeForServer(t, srv, "skip", ""), nil)
_, err := r.do(context.Background(), http.MethodGet, "/probe", nil)
if err == nil || len(err.Error()) > errBodyDiagBytes+64 {
t.Fatalf("error = %d bytes, want at most the %d-byte diagnostic", len(err.Error()), errBodyDiagBytes)
}
}
// Each "{}" of a node-reported list decodes into a whole struct, so a body under the
// byte cap could still expand into gigabytes; lists are capped by entry count.
func TestRemoteNodeListsAreCappedByEntryCount(t *testing.T) {
descendants := "[" + strings.TrimSuffix(strings.Repeat("{},", maxNodeDescendants+1), ",") + "]"
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"success":true,"obj":` + descendants + `}`))
}))
defer srv.Close()
r := NewRemote(nodeForServer(t, srv, "skip", ""), nil)
if got, err := r.GetDescendants(context.Background()); !errors.Is(err, errRemoteListTooLong) {
t.Fatalf("GetDescendants = %d entries, %v; want errRemoteListTooLong", len(got), err)
}
}