From c2250dbe57c1a59e500084b2fe744edfbd8e7f88 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 1 Nov 2026 00:45:21 +0100 Subject: [PATCH] fix(sub): keep the salamander warning state one entry per inbound genHysteriaLink remembered which salamander settings it had warned about in a set keyed by the setting names themselves, which a node chooses with no bound on length or number and the set never forgot, and it logged the names verbatim into the in-memory log buffer. A node renaming a setting on every poll grew the master's memory by twice the name's size per subscription fetch until the process ran out. The set is keyed by inbound id and holds a digest of the names, the warning repeats only when that set changes, and it logs at most eight names cut to 32 characters. --- internal/sub/salamander_uri_test.go | 40 +++++++++++++++++++++++++++++ internal/sub/service.go | 9 ++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/sub/salamander_uri_test.go b/internal/sub/salamander_uri_test.go index c7add43a6..869110528 100644 --- a/internal/sub/salamander_uri_test.go +++ b/internal/sub/salamander_uri_test.go @@ -1,6 +1,7 @@ package sub import ( + "fmt" "reflect" "strconv" "strings" @@ -86,3 +87,42 @@ func TestGenHysteriaLinkGeckoStillWarnsOnExtraKeys(t *testing.T) { t.Fatal("leftover salamander key did not warn alongside the gecko export") } } + +// The names may be a node's: 50 renames of one oversized setting must leave +// one fixed-size entry for the inbound and warning lines of bounded length. +func TestGenHysteriaLinkSalamanderWarningStateIsBounded(t *testing.T) { + const id = 910004 + for i := range 50 { + name := strconv.Itoa(i) + strings.Repeat("k", 4096) + in := &model.Inbound{ + Id: id, Listen: "203.0.113.1", Port: 443, Protocol: model.Hysteria, + Settings: `{"version":2,"clients":[{"auth":"secret","email":"user"}]}`, + StreamSettings: `{"security":"tls","finalmask":{"udp":[{"type":"salamander","settings":{"password":"pw","` + name + `":1}}]}}`, + } + (&SubService{}).genHysteriaLink(in, "user") + } + entries := 0 + salamanderWarningSeen.Range(func(key, _ any) bool { + if strings.HasPrefix(fmt.Sprint(key), strconv.Itoa(id)) { + entries++ + } + return true + }) + if entries != 1 { + t.Fatalf("salamanderWarningSeen holds %d entries for one inbound, want 1", entries) + } + needle := "inbound " + strconv.Itoa(id) + ": salamander settings" + found := 0 + for _, line := range logger.GetLogs(100, "warning") { + if !strings.Contains(line, needle) { + continue + } + found++ + if len(line) > 512 { + t.Fatalf("warning line is %d bytes long, want at most 512", len(line)) + } + } + if found == 0 { + t.Fatal("renamed salamander settings did not warn at all") + } +} diff --git a/internal/sub/service.go b/internal/sub/service.go index 4d6f95e5f..c1ed7bb93 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -31,6 +31,8 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/xray" ) +// salamanderWarningSeen maps an inbound id to a digest of the setting names +// last warned about: a node picks the names, so the entry must stay fixed-size. var salamanderWarningSeen sync.Map // SubService provides business logic for generating subscription links and managing subscription data. @@ -1610,9 +1612,10 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin // packetSize rides its own URI fields; anything else still // breaks standard clients and must warn even when gecko fires. if extra := extraSalamanderKeys(settings, gecko != ""); len(extra) > 0 { - warningKey := fmt.Sprintf("%d:%v", inbound.Id, extra) - if _, loaded := salamanderWarningSeen.LoadOrStore(warningKey, struct{}{}); !loaded { - logger.Warningf("SubService - inbound %d: salamander settings %v cannot be expressed in a hysteria2 URI; standard clients will fail the handshake", inbound.Id, extra) + // Warn again when the set changes; log a bounded sample of the names. + digest := sha256.Sum256([]byte(strings.Join(extra, "\x00"))) + if prev, _ := salamanderWarningSeen.Swap(inbound.Id, digest); prev != digest { + logger.Warningf("SubService - inbound %d: salamander settings %.32q cannot be expressed in a hysteria2 URI; standard clients will fail the handshake", inbound.Id, extra[:min(len(extra), 8)]) } } if params["obfs"] == "" {