fix(node): adopt a matching deployed inbound instead of recreating it (#6197)

* fix(node): adopt compatible origin inbounds without mutation

* fix(nodes): preserve ambiguous and adopted aliases

---------

Co-authored-by: n0ctal <293235942+n0ctal@users.noreply.github.com>
This commit is contained in:
n0ctal
2026-08-14 23:01:35 +05:00
committed by GitHub
parent 3b19091547
commit bb29b6afec
7 changed files with 241 additions and 10 deletions
+55 -3
View File
@@ -96,13 +96,15 @@ func (s *InboundService) ReconcileNode(ctx context.Context, rt *runtime.Remote,
if err := db.Model(model.Inbound{}).Where("node_id = ?", nodeID).Find(&inbounds).Error; err != nil {
return err
}
remoteTags, err := rt.ListRemoteTags(ctx)
remoteInbounds, err := rt.ListInboundOptions(ctx)
if err != nil {
return err
}
remoteTags := make([]string, 0, len(remoteInbounds))
remoteTagSet := make(map[string]struct{}, len(remoteTags))
for _, tag := range remoteTags {
remoteTagSet[tag] = struct{}{}
for _, remoteIb := range remoteInbounds {
remoteTags = append(remoteTags, remoteIb.Tag)
remoteTagSet[remoteIb.Tag] = struct{}{}
}
prefix := nodeTagPrefix(&nodeID)
desiredTags := make(map[string]struct{}, len(inbounds)*2)
@@ -129,6 +131,33 @@ func (s *InboundService) ReconcileNode(ctx context.Context, rt *runtime.Remote,
if built, bErr := s.buildInboundForNodePush(db, ib); bErr == nil {
runtimeIb = built
}
if !existsOnNode && n.Guid != "" && ib.OriginNodeGuid == n.Guid {
var compatible []runtime.RemoteInboundOption
for _, remoteIb := range remoteInbounds {
if remoteIb.Port == runtimeIb.Port &&
remoteIb.Protocol == runtimeIb.Protocol &&
strings.TrimSpace(remoteIb.Listen) == strings.TrimSpace(runtimeIb.Listen) {
compatible = append(compatible, remoteIb)
}
}
switch len(compatible) {
case 1:
alias := compatible[0]
desiredTags[alias.Tag] = struct{}{}
rt.AdoptInboundAlias(runtimeIb, alias)
existsOnNode = true
logger.Infof("adopted compatible inbound %q on node %s as %q", alias.Tag, n.Name, ib.Tag)
case 0:
// No compatible occupant: keep the normal create path, which
// leaves a real port/protocol drift loud.
default:
for _, candidate := range compatible {
desiredTags[candidate.Tag] = struct{}{}
}
errs = append(errs, fmt.Errorf("reconcile inbound %q: ambiguous compatible remote inbounds", ib.Tag))
continue
}
}
if _, err := rt.ReconcileInbound(ctx, runtimeIb, existsOnNode); err != nil {
errs = append(errs, fmt.Errorf("reconcile inbound %q: %w", ib.Tag, err))
}
@@ -514,6 +543,29 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
}
c, ok := tagToCentral[snapIb.Tag]
if !ok {
origin := originGuidFor(snapIb)
var compatible []*model.Inbound
for i := range central {
candidate := &central[i]
if candidate.OriginNodeGuid == origin &&
candidate.Port == snapIb.Port &&
candidate.Protocol == snapIb.Protocol &&
strings.TrimSpace(candidate.Listen) == strings.TrimSpace(snapIb.Listen) {
compatible = append(compatible, candidate)
}
}
switch len(compatible) {
case 1:
c, ok = compatible[0], true
tagToCentral[snapIb.Tag] = c
snapTags[c.Tag] = struct{}{}
case 0:
// A genuinely new inbound follows the normal adoption path.
default:
return false, fmt.Errorf("setRemoteTraffic: inbound %q has ambiguous compatible central aliases", snapIb.Tag)
}
}
if !ok {
if dirty {
continue