ipn/ipnlocal: add (*LocalBackend).RefreshExitNode

In this PR, we add (*LocalBackend).RefreshExitNode which determines which exit node
to use based on the current prefs and netmap and switches to it if needed. It supports
both scenarios when an exit node is specified by IP (rather than ID) and needs to be resolved
once the netmap is ready as well as auto exit nodes.

We then use it in (*LocalBackend).SetControlClientStatus when the netmap changes,
and wherever (*LocalBackend).pickNewAutoExitNode was previously used.

Updates tailscale/corp#29969

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2025-07-03 14:25:33 -05:00 committed by Nick Khyl
parent 04d24cdbd4
commit 3e01652e4d

View File

@ -1627,16 +1627,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
if applySysPolicy(prefs, b.overrideAlwaysOn) { if applySysPolicy(prefs, b.overrideAlwaysOn) {
prefsChanged = true prefsChanged = true
} }
if prefs.AutoExitNode.IsSet() {
// Re-evaluate exit node suggestion in case circumstances have changed.
_, err := b.suggestExitNodeLocked(curNetMap)
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
b.logf("SetControlClientStatus failed to select auto exit node: %v", err)
}
}
if setExitNodeID(prefs, b.lastSuggestedExitNode, curNetMap) {
prefsChanged = true
}
// Until recently, we did not store the account's tailnet name. So check if this is the case, // Until recently, we did not store the account's tailnet name. So check if this is the case,
// and backfill it on incoming status update. // and backfill it on incoming status update.
@ -1653,6 +1643,8 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
}); err != nil { }); err != nil {
b.logf("Failed to save new controlclient state: %v", err) b.logf("Failed to save new controlclient state: %v", err)
} }
b.sendToLocked(ipn.Notify{Prefs: ptr.To(prefs.View())}, allClients)
} }
// initTKALocked is dependent on CurrentProfile.ID, which is initialized // initTKALocked is dependent on CurrentProfile.ID, which is initialized
@ -1695,16 +1687,17 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.mu.Unlock() b.mu.Unlock()
// Now complete the lock-free parts of what we started while locked. // Now complete the lock-free parts of what we started while locked.
if prefsChanged {
b.send(ipn.Notify{Prefs: ptr.To(prefs.View())})
}
if st.NetMap != nil { if st.NetMap != nil {
// Check and update the exit node if needed, now that we have a new netmap.
b.RefreshExitNode()
if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) {
msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line." msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line."
b.health.SetLocalLogConfigHealth(errors.New(msg)) b.health.SetLocalLogConfigHealth(errors.New(msg))
// Connecting to this tailnet without logging is forbidden; boot us outta here. // Connecting to this tailnet without logging is forbidden; boot us outta here.
b.mu.Lock() b.mu.Lock()
// Get the current prefs again, since we unlocked above.
prefs := b.pm.CurrentPrefs().AsStruct()
prefs.WantRunning = false prefs.WantRunning = false
p := prefs.View() p := prefs.View()
if err := b.pm.SetPrefs(p, ipn.NetworkProfile{ if err := b.pm.SetPrefs(p, ipn.NetworkProfile{
@ -1999,7 +1992,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
if !ok || n.StableID() != exitNodeID { if !ok || n.StableID() != exitNodeID {
continue continue
} }
b.goTracker.Go(b.pickNewAutoExitNode) b.goTracker.Go(b.RefreshExitNode)
break break
} }
} }
@ -5898,30 +5891,50 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
} }
cc.SetNetInfo(ni) cc.SetNetInfo(ni)
if refresh { if refresh {
b.pickNewAutoExitNode() b.RefreshExitNode()
} }
} }
// pickNewAutoExitNode picks a new automatic exit node if needed. // RefreshExitNode determines which exit node to use based on the current
func (b *LocalBackend) pickNewAutoExitNode() { // prefs and netmap and switches to it if needed.
unlock := b.lockAndGetUnlock() func (b *LocalBackend) RefreshExitNode() {
defer unlock() if b.resolveExitNode() {
b.authReconfig()
}
}
newSuggestion, err := b.suggestExitNodeLocked(nil) // resolveExitNode determines which exit node to use based on the current
if err != nil { // prefs and netmap. It updates the exit node ID in the prefs if needed,
b.logf("setAutoExitNodeID: %v", err) // sends a notification to clients, and returns true if the exit node has changed.
return //
// It is the caller's responsibility to reconfigure routes and actually
// start using the selected exit node, if needed.
//
// b.mu must not be held.
func (b *LocalBackend) resolveExitNode() (changed bool) {
b.mu.Lock()
defer b.mu.Unlock()
nm := b.currentNode().NetMap()
prefs := b.pm.CurrentPrefs().AsStruct()
if prefs.AutoExitNode.IsSet() {
_, err := b.suggestExitNodeLocked(nil)
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
b.logf("failed to select auto exit node: %v", err)
} }
if b.pm.CurrentPrefs().ExitNodeID() == newSuggestion.ID {
return
} }
_, err = b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{ if !setExitNodeID(prefs, b.lastSuggestedExitNode, nm) {
Prefs: ipn.Prefs{ExitNodeID: newSuggestion.ID}, return false // no changes
ExitNodeIDSet: true,
}, unlock)
if err != nil {
b.logf("setAutoExitNodeID: failed to apply exit node ID preference: %v", err)
} }
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
MagicDNSName: nm.MagicDNSSuffix(),
DomainName: nm.DomainName(),
}); err != nil {
b.logf("failed to save exit node changes: %v", err)
}
b.sendToLocked(ipn.Notify{Prefs: ptr.To(prefs.View())}, allClients)
return true
} }
// setNetMapLocked updates the LocalBackend state to reflect the newly // setNetMapLocked updates the LocalBackend state to reflect the newly