From fd3fa869820366da14f12665d5f2b5130e0eeff0 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 31 Oct 2026 09:28:39 +0000 Subject: [PATCH] fix(node): drop a swept inbound's bookkeeping from the node runtime The node Remote keeps a fingerprint, a remote id and possibly an alias per inbound tag. When the master swept an inbound the node stopped reporting, those entries stayed, so a node rotating tags grew the maps for as long as the master ran. The sweep now hands every removed tag to Remote.ForgetInbound, which drops all of it in both tag forms, and a master-driven delete uses it too. The sweep's call is wiring; TestRemoteForgetInboundDropsEveryTrace covers what it drops. --- internal/web/node_contract_test.go | 1 + internal/web/runtime/remote.go | 19 ++++++++++++++++++- internal/web/runtime/remote_test.go | 16 ++++++++++++++++ internal/web/service/inbound_node.go | 7 ++++++- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/internal/web/node_contract_test.go b/internal/web/node_contract_test.go index 4b1bd9f9e..01819e495 100644 --- a/internal/web/node_contract_test.go +++ b/internal/web/node_contract_test.go @@ -363,6 +363,7 @@ var remoteMethodsOutsideContract = map[string]string{ "AdoptedInboundAliases": "local alias bookkeeping", "AdvancePushedInbound": "local fingerprint bookkeeping", "ForgetPushedInbound": "local fingerprint bookkeeping", + "ForgetInbound": "local bookkeeping for an inbound the node no longer has", "UpdatePanel": "replaces the node binary; node-sync is denied it on purpose (#6201)", } diff --git a/internal/web/runtime/remote.go b/internal/web/runtime/remote.go index 5a4b9a6f9..f65aa47c2 100644 --- a/internal/web/runtime/remote.go +++ b/internal/web/runtime/remote.go @@ -498,7 +498,7 @@ func (r *Remote) DelInbound(ctx context.Context, ib *model.Inbound) error { if _, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/del/"+strconv.Itoa(id), nil); err != nil { return err } - r.cacheDel(ib.Tag) + r.ForgetInbound(ib.Tag) return nil } @@ -581,6 +581,23 @@ func (r *Remote) ForgetPushedInbound(tag string) { r.mu.Unlock() } +// ForgetInbound drops everything kept under an inbound the node no longer has, in either +// tag form: a node rotating tags would otherwise grow these maps without bound. +func (r *Remote) ForgetInbound(tag string) { + prefix := nodeInboundTagPrefix(r.node.Id) + bare := strings.TrimPrefix(tag, prefix) + r.mu.Lock() + defer r.mu.Unlock() + for _, t := range []string{bare, prefix + bare} { + if alias, ok := r.adoptedAliases[t]; ok { + delete(r.remoteIDByTag, alias) + delete(r.adoptedAliases, t) + } + delete(r.remoteIDByTag, t) + delete(r.pushedFP, t) + } +} + // AdoptInboundAlias records a deployed alias without mutating either panel. // The runtime association is rediscovered after a master restart. func (r *Remote) AdoptInboundAlias(ib *model.Inbound, remote RemoteInboundOption) { diff --git a/internal/web/runtime/remote_test.go b/internal/web/runtime/remote_test.go index 1b79bd577..1720514a1 100644 --- a/internal/web/runtime/remote_test.go +++ b/internal/web/runtime/remote_test.go @@ -538,3 +538,19 @@ func TestRemoteNodeListsAreCappedByEntryCount(t *testing.T) { t.Fatalf("GetDescendants = %d entries, %v; want errRemoteListTooLong", len(got), err) } } + +// A node rotating inbound tags must not grow the per-tag maps forever: an inbound the +// master swept leaves nothing behind in either tag form, its adopted alias included. +func TestRemoteForgetInboundDropsEveryTrace(t *testing.T) { + n := &model.Node{Id: 7, Name: "n"} + r := NewRemote(n, nil) + ib := &model.Inbound{Tag: "n7-in-443", Port: 443, Protocol: model.VLESS} + r.AdoptInboundAlias(ib, RemoteInboundOption{Id: 3, Tag: "in-443"}) + r.RecordAdoptedInbound(&model.Inbound{Tag: "in-443", Port: 443, Protocol: model.VLESS}) + r.ForgetInbound("in-443") + r.mu.RLock() + defer r.mu.RUnlock() + if len(r.pushedFP)+len(r.remoteIDByTag)+len(r.adoptedAliases) != 0 { + t.Fatalf("left behind: pushedFP=%v ids=%v aliases=%v", r.pushedFP, r.remoteIDByTag, r.adoptedAliases) + } +} diff --git a/internal/web/service/inbound_node.go b/internal/web/service/inbound_node.go index e830009ac..e0839f905 100644 --- a/internal/web/service/inbound_node.go +++ b/internal/web/service/inbound_node.go @@ -704,6 +704,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi structuralChange := false lifecycleLifted := false + var sweptTags []string var adoptedInbounds []*model.Inbound type pendingAdopt struct { @@ -1036,6 +1037,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi return false, err } delete(tagToCentral, c.Tag) + sweptTags = append(sweptTags, c.Tag) structuralChange = true } @@ -1480,13 +1482,16 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi } } - if len(adoptedInbounds) > 0 { + if len(adoptedInbounds) > 0 || len(sweptTags) > 0 { if mgr := runtime.GetManager(); mgr != nil { if rt, rtErr := mgr.RuntimeFor(&nodeID); rtErr == nil { if rem, ok := rt.(*runtime.Remote); ok { for _, ib := range adoptedInbounds { rem.RecordAdoptedInbound(ib) } + for _, tag := range sweptTags { + rem.ForgetInbound(tag) + } } } }