ipn/ipnlocal: plumb nodeBackend into suggestExitNode to support delta updates, such as online status changes

Now that (*LocalBackend).suggestExitNodeLocked is never called with a non-current netmap
(the netMap parameter is always nil, indicating that the current netmap should be used),
we can remove the unused parameter.

Additionally, instead of suggestExitNodeLocked passing the most recent full netmap to suggestExitNode,
we now pass the current nodeBackend so it can access peers with delta updates applied.

Finally, with that fixed, we no longer need to skip TestUpdateNetmapDeltaAutoExitNode.

Updates tailscale/corp#29969
Fixes #16455

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2025-07-03 14:32:28 -05:00
committed by Nick Khyl
parent 3e01652e4d
commit 4c1c0bac8d
2 changed files with 22 additions and 28 deletions

View File

@@ -1947,10 +1947,7 @@ func (b *LocalBackend) sysPolicyChanged(policy *rsop.PolicyChange) {
if policy.HasChanged(syspolicy.AllowedSuggestedExitNodes) {
b.refreshAllowedSuggestions()
// Re-evaluate exit node suggestion now that the policy setting has changed.
b.mu.Lock()
_, err := b.suggestExitNodeLocked(nil)
b.mu.Unlock()
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
if _, err := b.SuggestExitNode(); err != nil && !errors.Is(err, ErrNoPreferredDERP) {
b.logf("failed to select auto exit node: %v", err)
}
// If [syspolicy.ExitNodeID] is set to `auto:any`, the suggested exit node ID
@@ -4490,7 +4487,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
// anyway, so its return value can be ignored here.
applySysPolicy(newp, b.overrideAlwaysOn)
if newp.AutoExitNode.IsSet() {
if _, err := b.suggestExitNodeLocked(nil); err != nil {
if _, err := b.suggestExitNodeLocked(); err != nil {
b.logf("failed to select auto exit node: %v", err)
}
}
@@ -5918,7 +5915,7 @@ func (b *LocalBackend) resolveExitNode() (changed bool) {
nm := b.currentNode().NetMap()
prefs := b.pm.CurrentPrefs().AsStruct()
if prefs.AutoExitNode.IsSet() {
_, err := b.suggestExitNodeLocked(nil)
_, err := b.suggestExitNodeLocked()
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
b.logf("failed to select auto exit node: %v", err)
}
@@ -7445,19 +7442,12 @@ var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
// Peers are selected based on having a DERP home that is the lowest latency to this device. For peers
// without a DERP home, we look for geographic proximity to this device's DERP home.
//
// netMap is an optional netmap to use that overrides b.netMap (needed for SetControlClientStatus before b.netMap is updated).
// If netMap is nil, then b.netMap is used.
//
// b.mu.lock() must be held.
func (b *LocalBackend) suggestExitNodeLocked(netMap *netmap.NetworkMap) (response apitype.ExitNodeSuggestionResponse, err error) {
// netMap is an optional netmap to use that overrides b.netMap (needed for SetControlClientStatus before b.netMap is updated). If netMap is nil, then b.netMap is used.
if netMap == nil {
netMap = b.NetMap()
}
func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggestionResponse, err error) {
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
prevSuggestion := b.lastSuggestedExitNode
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, b.getAllowedSuggestions())
res, err := suggestExitNode(lastReport, b.currentNode(), prevSuggestion, randomRegion, randomNode, b.getAllowedSuggestions())
if err != nil {
return res, err
}
@@ -7468,7 +7458,7 @@ func (b *LocalBackend) suggestExitNodeLocked(netMap *netmap.NetworkMap) (respons
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.suggestExitNodeLocked(nil)
return b.suggestExitNodeLocked()
}
// getAllowedSuggestions returns a set of exit nodes permitted by the most recent
@@ -7512,22 +7502,23 @@ func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
return s
}
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
func suggestExitNode(report *netcheck.Report, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
netMap := nb.NetMap()
if report == nil || report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
return res, ErrNoPreferredDERP
}
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
for _, peer := range netMap.Peers {
// Use [nodeBackend.AppendMatchingPeers] instead of the netmap directly,
// since the netmap doesn't include delta updates (e.g., home DERP or Online
// status changes) from the control plane since the last full update.
candidates := nb.AppendMatchingPeers(nil, func(peer tailcfg.NodeView) bool {
if !peer.Valid() || !peer.Online().Get() {
continue
return false
}
if allowList != nil && !allowList.Contains(peer.StableID()) {
continue
return false
}
if peer.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
candidates = append(candidates, peer)
}
}
return peer.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs())
})
if len(candidates) == 0 {
return res, nil
}

View File

@@ -57,6 +57,7 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/util/set"
@@ -2327,8 +2328,6 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
}
func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
t.Skip("TODO(tailscale/tailscale#16455): suggestExitNode does not check for online status of exit nodes")
peer1 := makePeer(1, withCap(26), withSuggest(), withOnline(true), withExitRoutes())
peer2 := makePeer(2, withCap(26), withSuggest(), withOnline(true), withExitRoutes())
derpMap := &tailcfg.DERPMap{
@@ -4278,7 +4277,11 @@ func TestSuggestExitNode(t *testing.T) {
allowList = set.SetOf(tt.allowPolicy)
}
got, err := suggestExitNode(tt.lastReport, tt.netMap, tt.lastSuggestion, selectRegion, selectNode, allowList)
nb := newNodeBackend(t.Context(), eventbus.New())
defer nb.shutdown(errShutdown)
nb.SetNetMap(tt.netMap)
got, err := suggestExitNode(tt.lastReport, nb, tt.lastSuggestion, selectRegion, selectNode, allowList)
if got.Name != tt.wantName {
t.Errorf("name=%v, want %v", got.Name, tt.wantName)
}