ipn/ipnlocal: evict stale node indexes when a delta upsert replaces a peer

When a node is renamed in the admin console, control sends peers a
single MapResponse delta: a PeersChanged entry carrying the full
updated node with its new Name, and no new DNSConfig (MagicDNS
records are computed client-side from peer names). That arrives as a
NodeMutationUpsert, but nodeBackend's upsert path only added the new
node's index entries and never removed the replaced node's, so
nodeByName retained the old name, and nodeByAddr, nodeByKey, and
nodeByStableID could likewise go stale if those fields changed.

Since 7e609b258 the quad-100 resolver serves MagicDNS answers on
demand from those live indexes, so a renamed peer's old name kept
resolving until something rebuilt the indexes from a full netmap,
such as toggling Tailscale off and on.

Evict the replaced node's index entries before adding the new ones.
Also consolidate the natlab DNS coverage into a single TestMagicDNS
that boots one VM and exercises extra records, search domains, and
peer add/rename/remove end to end, injecting the same MapResponse
shapes that production control sends.

Updates tailscale/corp#45631

Change-Id: I8a418317d930ec8ce112f7bd19bfd5778117a65e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-27 19:43:11 -07:00
committed by Brad Fitzpatrick
parent d0b4d44963
commit c0c453334a
3 changed files with 163 additions and 24 deletions
+16
View File
@@ -1100,6 +1100,22 @@ func (nb *nodeBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (res netmap
} else {
delete(nb.tsmpLearnedDisco, old.Key())
}
// Evict index entries derived from the old node value
// before re-adding them from the new one below, so a
// changed name, address, or key doesn't leave a stale
// entry behind. Notably, a node rename in the admin
// console arrives as an upsert with a new Name, and a
// stale nodeByName entry would keep serving MagicDNS
// answers for the old name (tailscale/corp#45631).
for _, ipp := range old.Addresses().All() {
if ipp.IsSingleIP() {
delete(nb.nodeByAddr, ipp.Addr())
}
}
delete(nb.nodeByKey, old.Key())
delete(nb.nodeByWGString, old.Key().WireGuardGoString())
delete(nb.nodeByStableID, old.StableID())
nb.removeNodeNameLocked(old.Name())
}
mak.Set(&nb.peers, nid, m.Node)
for _, ipp := range m.Node.Addresses().All() {
+14
View File
@@ -641,4 +641,18 @@ func TestNodeBackendMagicDNSHosts(t *testing.T) {
t.Fatal("UpdateNetmapDelta not handled")
}
wantHost("p3.example.ts.net.", netip.MustParseAddr("100.64.0.3"))
// Renaming a peer arrives as an upsert of the full node with a
// new Name. The old name must stop resolving and the new one
// must start (tailscale/corp#45631).
p3renamed := p3.Clone()
p3renamed.Name = "p3-renamed.example.ts.net."
if _, handled := nb.UpdateNetmapDelta([]netmap.NodeMutation{netmap.NodeMutationUpsert{Node: p3renamed.View()}}); !handled {
t.Fatal("UpdateNetmapDelta not handled")
}
wantHost("p3-renamed.example.ts.net.", netip.MustParseAddr("100.64.0.3"))
wantHost("p3.example.ts.net.")
if fqdn, ok := nb.magicDNSPTR(netip.MustParseAddr("100.64.0.3")); !ok || fqdn != "p3-renamed.example.ts.net." {
t.Errorf("magicDNSPTR(100.64.0.3) after rename = %q, %v; want p3's new name", fqdn, ok)
}
}