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) + } } } }