ipn/ipnlocal: use the live peer map, not the netmap's stale Peers slice

The nodeBackend's netMap.Peers slice is frozen at the last full netmap
install; the live per-peer state lives in the nodeBackend.peers map,
updated by delta mutations. Three spots still read the stale slice or
paid to materialize a fresh one:

AppendMatchingPeers iterated netMap.Peers and re-looked-up each ID in
the peers map (with a lock round-trip per peer), so peers added by a
delta since the last full netmap were invisible to it. That affected
its callers: taildrop's file-target list, exit node suggestions, and
conn25's connector discovery. It now snapshots the peers map directly
(sorted by node ID, matching the old netmap ordering).

DebugPeerDiscoKeys read netMap.Peers and so returned stale disco keys
after deltas. It now reads the peers map via the new
nodeBackend.peerDiscoKeys.

pingPeerAPI called NetMapWithPeers, building and sorting the full
O(n) peer slice, just to linearly scan it for one IP. It now uses the
nodeBackend's existing by-address index and the new O(1) PeerByID
accessor, and passes the peers-free netmap to peerAPIBase, which only
reads the self node's addresses.

Updates #12542

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I2e57527d64733b4eb17006f896faaa907b1d128c
This commit is contained in:
Brad Fitzpatrick
2026-07-15 13:29:26 -04:00
committed by Brad Fitzpatrick
parent 0bae201912
commit 65fd320aa6
2 changed files with 41 additions and 34 deletions
+7 -18
View File
@@ -4710,19 +4710,18 @@ func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer tai
var zero tailcfg.NodeView
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// PeerByTailscaleIP needs an up-to-date Peers slice.
nm := b.NetMapWithPeers()
if nm == nil {
return zero, "", errors.New("no netmap")
cn := b.currentNode()
var ok bool
if nid, addrOK := cn.NodeByAddr(ip); addrOK {
peer, ok = cn.PeerByID(nid)
}
peer, ok := nm.PeerByTailscaleIP(ip)
if !ok {
return zero, "", fmt.Errorf("no peer found with Tailscale IP %v", ip)
}
if peer.Expired() {
return zero, "", errors.New("peer's node key has expired")
}
base := peerAPIBase(nm, peer)
base := peerAPIBase(cn.NetMap(), peer)
if base == "" {
return zero, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID(), ip)
}
@@ -7968,22 +7967,12 @@ func (b *LocalBackend) DebugPeerRelayServers() set.Set[netip.Addr] {
}
// DebugPeerDiscoKeys returns the disco public keys this node has learned for
// each of its peers from the most recent network map. Intended for tests
// each of its current peers. Intended for tests
// (the production [ipnstate.PeerStatus] purposefully does not surface disco
// keys; surfacing them via the [ipnstate.Status] API would also pollute
// every PeerStatus consumer with a non-comparable struct field).
func (b *LocalBackend) DebugPeerDiscoKeys() map[key.NodePublic]key.DiscoPublic {
nm := b.currentNode().NetMap()
if nm == nil {
return nil
}
m := make(map[key.NodePublic]key.DiscoPublic, len(nm.Peers))
for _, p := range nm.Peers {
if dk := p.DiscoKey(); !dk.IsZero() {
m[p.Key()] = dk
}
}
return m
return b.currentNode().peerDiscoKeys()
}
// ControlKnobs returns the node's control knobs.
+34 -16
View File
@@ -345,6 +345,30 @@ func (nb *nodeBackend) Peers() []tailcfg.NodeView {
return slicesx.MapValues(nb.peers)
}
// PeerByID returns the current state of the peer (not self) node with
// the given ID, or ok=false if it is not a current peer.
func (nb *nodeBackend) PeerByID(id tailcfg.NodeID) (_ tailcfg.NodeView, ok bool) {
nb.mu.Lock()
defer nb.mu.Unlock()
n, ok := nb.peers[id]
return n, ok
}
// peerDiscoKeys returns the disco public keys of all current peers,
// keyed by their node public keys. Peers without a disco key are
// omitted.
func (nb *nodeBackend) peerDiscoKeys() map[key.NodePublic]key.DiscoPublic {
nb.mu.Lock()
defer nb.mu.Unlock()
m := make(map[key.NodePublic]key.DiscoPublic, len(nb.peers))
for _, p := range nb.peers {
if dk := p.DiscoKey(); !dk.IsZero() {
m[p.Key()] = dk
}
}
return m
}
func (nb *nodeBackend) PeersForTest() []tailcfg.NodeView {
nb.mu.Lock()
defer nb.mu.Unlock()
@@ -363,28 +387,22 @@ func (nb *nodeBackend) CollectServices() bool {
// AppendMatchingPeers returns base with all peers that match pred appended.
//
// It acquires b.mu to read the netmap but releases it before calling pred.
// It acquires nb.mu to snapshot the peers but releases it before
// calling pred.
func (nb *nodeBackend) AppendMatchingPeers(base []tailcfg.NodeView, pred func(tailcfg.NodeView) bool) []tailcfg.NodeView {
var peers []tailcfg.NodeView
nb.mu.Lock()
if nb.netMap != nil {
// All fields on b.netMap are immutable, so this is
// safe to copy and use outside the lock.
peers = nb.netMap.Peers
}
peers := slicesx.MapValues(nb.peers)
nb.mu.Unlock()
// Sort by node ID for deterministic results; the map iteration
// above is randomly ordered.
slices.SortFunc(peers, func(a, b tailcfg.NodeView) int {
return cmp.Compare(a.ID(), b.ID())
})
ret := base
for _, peer := range peers {
// The peers in b.netMap don't contain updates made via
// UpdateNetmapDelta. So only use PeerView in b.netMap for its NodeID,
// and then look up the latest copy in b.peers which is updated in
// response to UpdateNetmapDelta edits.
nb.mu.Lock()
peer, ok := nb.peers[peer.ID()]
nb.mu.Unlock()
if ok && pred(peer) {
if pred(peer) {
ret = append(ret, peer)
}
}