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.
This commit is contained in:
MHSanaei
2026-10-31 09:28:39 +00:00
parent d4b80b5d80
commit fd3fa86982
4 changed files with 41 additions and 2 deletions
+1
View File
@@ -363,6 +363,7 @@ var remoteMethodsOutsideContract = map[string]string{
"AdoptedInboundAliases": "local alias bookkeeping", "AdoptedInboundAliases": "local alias bookkeeping",
"AdvancePushedInbound": "local fingerprint bookkeeping", "AdvancePushedInbound": "local fingerprint bookkeeping",
"ForgetPushedInbound": "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)", "UpdatePanel": "replaces the node binary; node-sync is denied it on purpose (#6201)",
} }
+18 -1
View File
@@ -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 { if _, err := r.do(ctx, http.MethodPost, "panel/api/inbounds/del/"+strconv.Itoa(id), nil); err != nil {
return err return err
} }
r.cacheDel(ib.Tag) r.ForgetInbound(ib.Tag)
return nil return nil
} }
@@ -581,6 +581,23 @@ func (r *Remote) ForgetPushedInbound(tag string) {
r.mu.Unlock() 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. // AdoptInboundAlias records a deployed alias without mutating either panel.
// The runtime association is rediscovered after a master restart. // The runtime association is rediscovered after a master restart.
func (r *Remote) AdoptInboundAlias(ib *model.Inbound, remote RemoteInboundOption) { func (r *Remote) AdoptInboundAlias(ib *model.Inbound, remote RemoteInboundOption) {
+16
View File
@@ -538,3 +538,19 @@ func TestRemoteNodeListsAreCappedByEntryCount(t *testing.T) {
t.Fatalf("GetDescendants = %d entries, %v; want errRemoteListTooLong", len(got), err) 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)
}
}
+6 -1
View File
@@ -704,6 +704,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
structuralChange := false structuralChange := false
lifecycleLifted := false lifecycleLifted := false
var sweptTags []string
var adoptedInbounds []*model.Inbound var adoptedInbounds []*model.Inbound
type pendingAdopt struct { type pendingAdopt struct {
@@ -1036,6 +1037,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
return false, err return false, err
} }
delete(tagToCentral, c.Tag) delete(tagToCentral, c.Tag)
sweptTags = append(sweptTags, c.Tag)
structuralChange = true 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 mgr := runtime.GetManager(); mgr != nil {
if rt, rtErr := mgr.RuntimeFor(&nodeID); rtErr == nil { if rt, rtErr := mgr.RuntimeFor(&nodeID); rtErr == nil {
if rem, ok := rt.(*runtime.Remote); ok { if rem, ok := rt.(*runtime.Remote); ok {
for _, ib := range adoptedInbounds { for _, ib := range adoptedInbounds {
rem.RecordAdoptedInbound(ib) rem.RecordAdoptedInbound(ib)
} }
for _, tag := range sweptTags {
rem.ForgetInbound(tag)
}
} }
} }
} }