|
@@ -338,6 +338,9 @@ type LocalBackend struct {
|
|
|
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
|
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
|
|
// avoid unnecessary churn between multiple equally-good options.
|
|
// avoid unnecessary churn between multiple equally-good options.
|
|
|
lastSuggestedExitNode tailcfg.StableNodeID
|
|
lastSuggestedExitNode tailcfg.StableNodeID
|
|
|
|
|
+
|
|
|
|
|
+ // refreshAutoExitNode indicates if the exit node should be recomputed when the next netcheck report is available.
|
|
|
|
|
+ refreshAutoExitNode bool
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// HealthTracker returns the health tracker for the backend.
|
|
// HealthTracker returns the health tracker for the backend.
|
|
@@ -640,7 +643,9 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
|
|
hadPAC := b.prevIfState.HasPAC()
|
|
hadPAC := b.prevIfState.HasPAC()
|
|
|
b.prevIfState = ifst
|
|
b.prevIfState = ifst
|
|
|
b.pauseOrResumeControlClientLocked()
|
|
b.pauseOrResumeControlClientLocked()
|
|
|
-
|
|
|
|
|
|
|
+ if delta.Major && shouldAutoExitNode() {
|
|
|
|
|
+ b.refreshAutoExitNode = true
|
|
|
|
|
+ }
|
|
|
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
|
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
|
|
// add/remove subnets.
|
|
// add/remove subnets.
|
|
|
if hadPAC != ifst.HasPAC() {
|
|
if hadPAC != ifst.HasPAC() {
|
|
@@ -1215,7 +1220,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
|
|
prefs.WantRunning = true
|
|
prefs.WantRunning = true
|
|
|
prefs.LoggedOut = false
|
|
prefs.LoggedOut = false
|
|
|
}
|
|
}
|
|
|
- if setExitNodeID(prefs, st.NetMap) {
|
|
|
|
|
|
|
+ if setExitNodeID(prefs, st.NetMap, b.lastSuggestedExitNode) {
|
|
|
prefsChanged = true
|
|
prefsChanged = true
|
|
|
}
|
|
}
|
|
|
if applySysPolicy(prefs) {
|
|
if applySysPolicy(prefs) {
|
|
@@ -1418,9 +1423,8 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
|
|
b.send(*notify)
|
|
b.send(*notify)
|
|
|
}
|
|
}
|
|
|
}()
|
|
}()
|
|
|
-
|
|
|
|
|
- b.mu.Lock()
|
|
|
|
|
- defer b.mu.Unlock()
|
|
|
|
|
|
|
+ unlock := b.lockAndGetUnlock()
|
|
|
|
|
+ defer unlock()
|
|
|
if !b.updateNetmapDeltaLocked(muts) {
|
|
if !b.updateNetmapDeltaLocked(muts) {
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
@@ -1428,8 +1432,14 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
|
|
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
|
|
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
|
|
|
nm := ptr.To(*b.netMap) // shallow clone
|
|
nm := ptr.To(*b.netMap) // shallow clone
|
|
|
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
|
|
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
|
|
|
|
|
+ shouldAutoExitNode := shouldAutoExitNode()
|
|
|
for _, p := range b.peers {
|
|
for _, p := range b.peers {
|
|
|
nm.Peers = append(nm.Peers, p)
|
|
nm.Peers = append(nm.Peers, p)
|
|
|
|
|
+ // If the auto exit node currently set goes offline, find another auto exit node.
|
|
|
|
|
+ if shouldAutoExitNode && b.pm.prefs.ExitNodeID() == p.StableID() && p.Online() != nil && !*p.Online() {
|
|
|
|
|
+ b.setAutoExitNodeIDLockedOnEntry(unlock)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
|
|
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
|
|
|
return cmp.Compare(a.ID(), b.ID())
|
|
return cmp.Compare(a.ID(), b.ID())
|
|
@@ -1491,9 +1501,14 @@ func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (hand
|
|
|
|
|
|
|
|
// setExitNodeID updates prefs to reference an exit node by ID, rather
|
|
// setExitNodeID updates prefs to reference an exit node by ID, rather
|
|
|
// than by IP. It returns whether prefs was mutated.
|
|
// than by IP. It returns whether prefs was mutated.
|
|
|
-func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
|
|
|
|
|
|
|
+func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap, lastSuggestedExitNode tailcfg.StableNodeID) (prefsChanged bool) {
|
|
|
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
|
|
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
|
|
|
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
|
|
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
|
|
|
|
|
+ if shouldAutoExitNode() && lastSuggestedExitNode != "" {
|
|
|
|
|
+ exitNodeID = lastSuggestedExitNode
|
|
|
|
|
+ }
|
|
|
|
|
+ // Note: when exitNodeIDStr == "auto" && lastSuggestedExitNode == "", then exitNodeID is now "auto" which will never match a peer's node ID.
|
|
|
|
|
+ // When there is no a peer matching the node ID, traffic will blackhole, preventing accidental non-exit-node usage when a policy is in effect that requires an exit node.
|
|
|
changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
|
|
changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
|
|
|
prefs.ExitNodeID = exitNodeID
|
|
prefs.ExitNodeID = exitNodeID
|
|
|
prefs.ExitNodeIP = netip.Addr{}
|
|
prefs.ExitNodeIP = netip.Addr{}
|
|
@@ -3357,7 +3372,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
|
|
// setExitNodeID returns whether it updated b.prefs, but
|
|
// setExitNodeID returns whether it updated b.prefs, but
|
|
|
// everything in this function treats b.prefs as completely new
|
|
// everything in this function treats b.prefs as completely new
|
|
|
// anyway. No-op if no exit node resolution is needed.
|
|
// anyway. No-op if no exit node resolution is needed.
|
|
|
- setExitNodeID(newp, netMap)
|
|
|
|
|
|
|
+ setExitNodeID(newp, netMap, b.lastSuggestedExitNode)
|
|
|
// applySysPolicy does likewise so we can also ignore its return value.
|
|
// applySysPolicy does likewise so we can also ignore its return value.
|
|
|
applySysPolicy(newp)
|
|
applySysPolicy(newp)
|
|
|
// We do this to avoid holding the lock while doing everything else.
|
|
// We do this to avoid holding the lock while doing everything else.
|
|
@@ -4850,12 +4865,44 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
|
|
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
|
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
|
|
b.mu.Lock()
|
|
b.mu.Lock()
|
|
|
cc := b.cc
|
|
cc := b.cc
|
|
|
|
|
+ refresh := b.refreshAutoExitNode
|
|
|
|
|
+ b.refreshAutoExitNode = false
|
|
|
b.mu.Unlock()
|
|
b.mu.Unlock()
|
|
|
|
|
|
|
|
if cc == nil {
|
|
if cc == nil {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
cc.SetNetInfo(ni)
|
|
cc.SetNetInfo(ni)
|
|
|
|
|
+ if refresh {
|
|
|
|
|
+ unlock := b.lockAndGetUnlock()
|
|
|
|
|
+ defer unlock()
|
|
|
|
|
+ b.setAutoExitNodeIDLockedOnEntry(unlock)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (b *LocalBackend) setAutoExitNodeIDLockedOnEntry(unlock unlockOnce) {
|
|
|
|
|
+ defer unlock()
|
|
|
|
|
+
|
|
|
|
|
+ prefs := b.pm.CurrentPrefs()
|
|
|
|
|
+ if !prefs.Valid() {
|
|
|
|
|
+ b.logf("[unexpected]: received tailnet exit node ID pref change callback but current prefs are nil")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ prefsClone := prefs.AsStruct()
|
|
|
|
|
+ newSuggestion, err := b.suggestExitNodeLocked()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ b.logf("setAutoExitNodeID: %v", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ prefsClone.ExitNodeID = newSuggestion.ID
|
|
|
|
|
+ _, err = b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
|
|
|
|
|
+ Prefs: *prefsClone,
|
|
|
|
|
+ ExitNodeIDSet: true,
|
|
|
|
|
+ }, unlock)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ b.logf("setAutoExitNodeID: failed to apply exit node ID preference: %v", err)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// setNetMapLocked updates the LocalBackend state to reflect the newly
|
|
// setNetMapLocked updates the LocalBackend state to reflect the newly
|
|
@@ -6526,30 +6573,33 @@ func mayDeref[T any](p *T) (v T) {
|
|
|
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
|
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
|
|
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
|
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
|
|
|
|
|
|
|
-// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
|
|
|
|
|
|
|
+// suggestExitNodeLocked computes a suggestion based on the current netmap and last netcheck report. If
|
|
|
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
|
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
|
|
// eligible for consideration, the peer must have NodeAttrSuggestExitNode in its CapMap.
|
|
// eligible for consideration, the peer must have NodeAttrSuggestExitNode in its CapMap.
|
|
|
//
|
|
//
|
|
|
// Currently, peers with a DERP home are preferred over those without (typically this means Mullvad).
|
|
// Currently, peers with a DERP home are preferred over those without (typically this means Mullvad).
|
|
|
// Peers are selected based on having a DERP home that is the lowest latency to this device. For peers
|
|
// 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.
|
|
// without a DERP home, we look for geographic proximity to this device's DERP home.
|
|
|
-func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
|
|
|
|
- b.mu.Lock()
|
|
|
|
|
|
|
+// b.mu.lock() must be held.
|
|
|
|
|
+func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggestionResponse, err error) {
|
|
|
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
|
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
|
|
netMap := b.netMap
|
|
netMap := b.netMap
|
|
|
prevSuggestion := b.lastSuggestedExitNode
|
|
prevSuggestion := b.lastSuggestedExitNode
|
|
|
- b.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, getAllowedSuggestions())
|
|
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, getAllowedSuggestions())
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return res, err
|
|
return res, err
|
|
|
}
|
|
}
|
|
|
- b.mu.Lock()
|
|
|
|
|
b.lastSuggestedExitNode = res.ID
|
|
b.lastSuggestedExitNode = res.ID
|
|
|
- b.mu.Unlock()
|
|
|
|
|
return res, err
|
|
return res, err
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
|
|
|
|
+ b.mu.Lock()
|
|
|
|
|
+ defer b.mu.Unlock()
|
|
|
|
|
+ return b.suggestExitNodeLocked()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// selectRegionFunc returns a DERP region from the slice of candidate regions.
|
|
// selectRegionFunc returns a DERP region from the slice of candidate regions.
|
|
|
// The value is returned, not the slice index.
|
|
// The value is returned, not the slice index.
|
|
|
type selectRegionFunc func(views.Slice[int]) int
|
|
type selectRegionFunc func(views.Slice[int]) int
|
|
@@ -6788,6 +6838,12 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
|
|
|
return earthRadiusMeters * c
|
|
return earthRadiusMeters * c
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// shouldAutoExitNode checks for the auto exit node MDM policy.
|
|
|
|
|
+func shouldAutoExitNode() bool {
|
|
|
|
|
+ exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, "")
|
|
|
|
|
+ return exitNodeIDStr == "auto:any"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// startAutoUpdate triggers an auto-update attempt. The actual update happens
|
|
// startAutoUpdate triggers an auto-update attempt. The actual update happens
|
|
|
// asynchronously. If another update is in progress, an error is returned.
|
|
// asynchronously. If another update is in progress, an error is returned.
|
|
|
func (b *LocalBackend) startAutoUpdate(logPrefix string) (retErr error) {
|
|
func (b *LocalBackend) startAutoUpdate(logPrefix string) (retErr error) {
|