summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorClaire Wang <claire@tailscale.com>2024-05-13 15:11:25 -0400
committerClaire Wang <claire@tailscale.com>2024-05-24 15:38:34 -0400
commitd541079e787054b2b73b14b724328053ce3bec94 (patch)
treead70124ca26df020ade60291f061bad4ac8d9986
parent9a64c06a202598ac9a102831b68fc998e3fbeb49 (diff)
downloadtailscale-clairew/handle-auto-exit-node-value.tar.xz
tailscale-clairew/handle-auto-exit-node-value.zip
ipn/ipnlocal: handle auto value for ExitNodeID syspolicyclairew/handle-auto-exit-node-value
Updates tailscale/corp#19681 Signed-off-by: Claire Wang <claire@tailscale.com>
-rw-r--r--cmd/tailscaled/depaware.txt2
-rw-r--r--ipn/ipnlocal/local.go156
-rw-r--r--ipn/ipnlocal/local_test.go216
-rw-r--r--ipn/localapi/localapi.go2
4 files changed, 300 insertions, 76 deletions
diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index 54605d950..3b2c38090 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -296,7 +296,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/dnsfallback from tailscale.com/cmd/tailscaled+
tailscale.com/net/flowtrack from tailscale.com/net/packet+
tailscale.com/net/netaddr from tailscale.com/ipn+
- tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock+
+ tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
tailscale.com/net/netknob from tailscale.com/logpolicy+
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 5ec03c505..bafdeee99 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -59,7 +59,6 @@ import (
"tailscale.com/net/dns"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
- "tailscale.com/net/netcheck"
"tailscale.com/net/netkernelconf"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
@@ -333,6 +332,9 @@ type LocalBackend struct {
// lastSuggestedExitNode stores the last suggested exit node ID and name.
// lastSuggestedExitNode updates whenever the suggestion changes.
lastSuggestedExitNode lastSuggestedExitNode
+ // refreshReportForExitNodeSuggestion indicates whether to update the netcheck report for an exit node suggestion.
+ // Value is set to true when a rebind happens.
+ refreshReportForExitNodeSuggestion bool
}
// HealthTracker returns the health tracker for the backend.
@@ -1167,7 +1169,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
prefs.WantRunning = true
prefs.LoggedOut = false
}
- if setExitNodeID(prefs, st.NetMap) {
+ if b.setExitNodeIDLocked(prefs, st.NetMap) {
prefsChanged = true
}
if applySysPolicy(prefs) {
@@ -1379,6 +1381,15 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
for _, p := range b.peers {
nm.Peers = append(nm.Peers, p)
+ if !*p.Online() && p.StableID() == b.pm.CurrentPrefs().ExitNodeID() {
+ if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr == "auto" {
+ prefs := b.pm.CurrentPrefs().AsStruct()
+ b.setExitNodeIDLocked(prefs, nm)
+ if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{}); err != nil {
+ b.logf("UpdateNetmapDelta: unable to set auto exit node; err=%v", err)
+ }
+ }
+ }
}
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
return cmp.Compare(a.ID(), b.ID())
@@ -1438,13 +1449,29 @@ func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (hand
return true
}
-// setExitNodeID updates prefs to reference an exit node by ID, rather
+// setExitNodeIDLocked updates prefs to reference an exit node by ID, rather
// than by IP. It returns whether prefs was mutated.
-func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
+// b.mu must be held.
+func (b *LocalBackend) setExitNodeIDLocked(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
- exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
- changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
- prefs.ExitNodeID = exitNodeID
+ var changed bool
+ if exitNodeIDStr == "auto" {
+ if b.lastSuggestedExitNode.id != "" {
+ changed = prefs.ExitNodeID != b.lastSuggestedExitNode.id || prefs.ExitNodeIP.IsValid()
+ prefs.ExitNodeID = b.lastSuggestedExitNode.id
+ } else {
+ res, err := b.suggestExitNodeLocked(true, nil)
+ if err != nil {
+ b.logf("setExitNodeIDLocked: Unable to suggest exit node. Error=%v", err)
+ }
+ changed = prefs.ExitNodeID != res.ID || prefs.ExitNodeIP.IsValid()
+ prefs.ExitNodeID = res.ID
+ }
+ } else {
+ exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
+ changed = prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
+ prefs.ExitNodeID = exitNodeID
+ }
prefs.ExitNodeIP = netip.Addr{}
return changed
}
@@ -3278,10 +3305,11 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
if oldp.Valid() {
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
}
- // setExitNodeID returns whether it updated b.prefs, but
+ // setExitNodeIDLocked returns whether it updated b.prefs, but
// everything in this function treats b.prefs as completely new
// anyway. No-op if no exit node resolution is needed.
- setExitNodeID(newp, netMap)
+ b.setExitNodeIDLocked(newp, netMap)
+ //}
// applySysPolicy does likewise so we can also ignore its return value.
applySysPolicy(newp)
// We do this to avoid holding the lock while doing everything else.
@@ -4760,12 +4788,19 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
b.mu.Lock()
cc := b.cc
+ refresh := b.refreshReportForExitNodeSuggestion
+ b.refreshReportForExitNodeSuggestion = false
b.mu.Unlock()
if cc == nil {
return
}
cc.SetNetInfo(ni)
+ if refresh {
+ if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr == "auto" {
+ b.suggestExitNodeLocked(true, ni)
+ }
+ }
}
// setNetMapLocked updates the LocalBackend state to reflect the newly
@@ -6400,20 +6435,68 @@ var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
var ErrUnableToSuggestLastExitNode = errors.New("unable to suggest last exit node")
-// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
+// latencyInfo is used to extra needed information from a netcheck report or NetInfo to make an exit node suggestion.
+type latencyInfo struct {
+ preferredDERP int
+ regionLatency map[int]time.Duration
+}
+
+// processNetInfo extracts the needed values from tailcfg.NetInfo to make an exit node suggestion.
+func processNetInfo(netInfo *tailcfg.NetInfo) (info latencyInfo, err error) {
+ if netInfo == nil {
+ return info, ErrCannotSuggestExitNode
+ }
+ if netInfo.PreferredDERP == 0 {
+ return info, ErrNoPreferredDERP
+ }
+ info.preferredDERP = netInfo.PreferredDERP
+ info.regionLatency = make(map[int]time.Duration)
+ for region, duration := range netInfo.DERPLatency {
+ parsed := strings.Split(region, "-")
+ r, _ := strconv.Atoi(parsed[0])
+ val, ok := info.regionLatency[r]
+ converted := time.Duration(duration * float64(time.Second))
+ if !ok {
+ info.regionLatency[r] = converted
+ } else {
+ if converted < val {
+ info.regionLatency[r] = converted
+ }
+ }
+ }
+ return info, nil
+}
+
+// suggestExitNodeIDLocked 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
// 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).
// 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.
-func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
- b.mu.Lock()
- lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
+// b.mu must be held.
+func (b *LocalBackend) suggestExitNodeLocked(isAuto bool, netInfo *tailcfg.NetInfo) (response apitype.ExitNodeSuggestionResponse, err error) {
+ var latency latencyInfo
+ if netInfo != nil {
+ latency, err = processNetInfo(netInfo)
+ if err != nil {
+ return response, err
+ }
+ } else {
+ lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
+ if lastReport == nil {
+ last, err := b.lastSuggestedExitNode.asAPIType()
+ if err != nil {
+ return response, ErrCannotSuggestExitNode
+ }
+ return last, err
+ }
+ latency.preferredDERP = lastReport.PreferredDERP
+ latency.regionLatency = lastReport.RegionLatency
+ }
netMap := b.netMap
lastSuggestedExitNode := b.lastSuggestedExitNode
- b.mu.Unlock()
- if lastReport == nil || netMap == nil {
+ if netMap == nil {
last, err := lastSuggestedExitNode.asAPIType()
if err != nil {
return response, ErrCannotSuggestExitNode
@@ -6422,7 +6505,7 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
}
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
- res, err := suggestExitNode(lastReport, netMap, r)
+ res, err := suggestExitNode(&latency, netMap, r, isAuto, b.pm.prefs)
if err != nil {
last, err := lastSuggestedExitNode.asAPIType()
if err != nil {
@@ -6430,9 +6513,14 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
}
return last, err
}
- b.mu.Lock()
b.lastSuggestedExitNode.id = res.ID
b.lastSuggestedExitNode.name = res.Name
+ return res, err
+}
+
+func (b *LocalBackend) HandleSuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
+ b.mu.Lock()
+ res, err := b.suggestExitNodeLocked(false, nil)
b.mu.Unlock()
return res, err
}
@@ -6449,13 +6537,29 @@ func (n lastSuggestedExitNode) asAPIType() (res apitype.ExitNodeSuggestionRespon
return res, ErrUnableToSuggestLastExitNode
}
-func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand.Rand) (res apitype.ExitNodeSuggestionResponse, err error) {
- if report.PreferredDERP == 0 {
+func suggestExitNode(latencyInfo *latencyInfo, netMap *netmap.NetworkMap, r *rand.Rand, isAuto bool, prefs ipn.PrefsView) (res apitype.ExitNodeSuggestionResponse, err error) {
+ if latencyInfo.preferredDERP == 0 {
return res, ErrNoPreferredDERP
}
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
for _, peer := range netMap.Peers {
- if peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
+ var isCandidate bool
+ if isAuto {
+ isCandidate = peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode) && peer.CapMap().Has(tailcfg.NodeAttrAutoExitNode)
+ if peer.StableID() == prefs.ExitNodeID() && isCandidate {
+ if hi := peer.Hostinfo(); hi.Valid() {
+ if loc := hi.Location(); loc != nil {
+ res.Location = loc.View()
+ }
+ }
+ res.ID = peer.StableID()
+ res.Name = peer.Name()
+ return res, nil
+ }
+ } else {
+ isCandidate = peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode)
+ }
+ if isCandidate && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
candidates = append(candidates, peer)
}
}
@@ -6475,7 +6579,7 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
}
candidatesByRegion := make(map[int][]tailcfg.NodeView, len(netMap.DERPMap.Regions))
- var preferredDERP *tailcfg.DERPRegion = netMap.DERPMap.Regions[report.PreferredDERP]
+ var preferredDERP *tailcfg.DERPRegion = netMap.DERPMap.Regions[latencyInfo.preferredDERP]
var minDistance float64 = math.MaxFloat64
type nodeDistance struct {
nv tailcfg.NodeView
@@ -6522,7 +6626,7 @@ func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand
// First, try to select an exit node that has the closest DERP home, based on lastReport's DERP latency.
// If there are no latency values, it returns an arbitrary region
if len(candidatesByRegion) > 0 {
- minRegion := minLatencyDERPRegion(xmaps.Keys(candidatesByRegion), report)
+ minRegion := minLatencyDERPRegion(xmaps.Keys(candidatesByRegion), latencyInfo.regionLatency)
if minRegion == 0 {
minRegion = randomRegion(xmaps.Keys(candidatesByRegion), r)
}
@@ -6605,14 +6709,14 @@ func randomRegion(regions []int, r *rand.Rand) int {
// minLatencyDERPRegion returns the region with the lowest latency value given the last netcheck report.
// If there are no latency values, it returns 0.
-func minLatencyDERPRegion(regions []int, report *netcheck.Report) int {
+func minLatencyDERPRegion(regions []int, regionLatency map[int]time.Duration) int {
min := slices.MinFunc(regions, func(i, j int) int {
const largeDuration time.Duration = math.MaxInt64
- iLatency, ok := report.RegionLatency[i]
+ iLatency, ok := regionLatency[i]
if !ok {
iLatency = largeDuration
}
- jLatency, ok := report.RegionLatency[j]
+ jLatency, ok := regionLatency[j]
if !ok {
jLatency = largeDuration
}
@@ -6621,7 +6725,7 @@ func minLatencyDERPRegion(regions []int, report *netcheck.Report) int {
}
return cmp.Compare(i, j)
})
- latency, ok := report.RegionLatency[min]
+ latency, ok := regionLatency[min]
if !ok || latency == 0 {
return 0
} else {
diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go
index cf706e928..f3ea576d9 100644
--- a/ipn/ipnlocal/local_test.go
+++ b/ipn/ipnlocal/local_test.go
@@ -1855,7 +1855,9 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
pm.prefs = test.prefs.View()
b.netMap = test.nm
b.pm = pm
- changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm)
+ b.mu.Lock()
+ changed := b.setExitNodeIDLocked(b.pm.prefs.AsStruct(), test.nm)
+ b.mu.Unlock()
b.SetPrefsForTest(pm.CurrentPrefs().AsStruct())
if got := b.pm.prefs.ExitNodeID(); got != tailcfg.StableNodeID(test.exitNodeIDWant) {
@@ -2710,7 +2712,7 @@ func (b *LocalBackend) SetPrefsForTest(newp *ipn.Prefs) {
func TestSuggestExitNode(t *testing.T) {
tests := []struct {
name string
- lastReport netcheck.Report
+ latencyInfo latencyInfo
netMap netmap.NetworkMap
wantID tailcfg.StableNodeID
wantName string
@@ -2719,13 +2721,13 @@ func TestSuggestExitNode(t *testing.T) {
}{
{
name: "2 exit nodes in same region",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 10 * time.Millisecond,
2: 20 * time.Millisecond,
3: 30 * time.Millisecond,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -2773,13 +2775,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "2 derp based exit nodes, different regions, no latency measurements",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -2827,13 +2829,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "2 derp based exit nodes, different regions, same latency",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 10,
2: 10,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -2881,13 +2883,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "mullvad nodes, no derp based exit nodes",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -2955,13 +2957,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "mullvad nodes close to each other, different priorities",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -3029,13 +3031,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "mullvad nodes, no preferred derp region exit nodes",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -3110,13 +3112,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "no mullvad nodes; no derp nodes",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -3136,8 +3138,8 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "no preferred derp region",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: -1,
3: 0,
@@ -3162,13 +3164,13 @@ func TestSuggestExitNode(t *testing.T) {
},
{
name: "derp exit node and mullvad exit node both with no suggest exit node attribute",
- lastReport: netcheck.Report{
- RegionLatency: map[int]time.Duration{
+ latencyInfo: latencyInfo{
+ regionLatency: map[int]time.Duration{
1: 0,
2: 0,
3: 0,
},
- PreferredDERP: 1,
+ preferredDERP: 1,
},
netMap: netmap.NetworkMap{
SelfNode: (&tailcfg.Node{
@@ -3217,7 +3219,7 @@ func TestSuggestExitNode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rand.New(rand.NewSource(100))
- got, err := suggestExitNode(&tt.lastReport, &tt.netMap, r)
+ got, err := suggestExitNode(&tt.latencyInfo, &tt.netMap, r, false, ipn.PrefsView{})
if got.Name != tt.wantName {
t.Errorf("name=%v, want %v", got.Name, tt.wantName)
}
@@ -3391,46 +3393,41 @@ func TestSuggestExitNodeLongLatDistance(t *testing.T) {
func TestMinLatencyDERPregion(t *testing.T) {
tests := []struct {
- name string
- regions []int
- report *netcheck.Report
- wantRegion int
+ name string
+ regions []int
+ regionLatency map[int]time.Duration
+ wantRegion int
}{
{
name: "regions, no latency values",
regions: []int{1, 2, 3},
wantRegion: 0,
- report: &netcheck.Report{},
},
{
name: "regions, different latency values",
regions: []int{1, 2, 3},
wantRegion: 2,
- report: &netcheck.Report{
- RegionLatency: map[int]time.Duration{
- 1: 10 * time.Millisecond,
- 2: 5 * time.Millisecond,
- 3: 30 * time.Millisecond,
- },
+ regionLatency: map[int]time.Duration{
+ 1: 10 * time.Millisecond,
+ 2: 5 * time.Millisecond,
+ 3: 30 * time.Millisecond,
},
},
{
name: "regions, same values",
regions: []int{1, 2, 3},
wantRegion: 1,
- report: &netcheck.Report{
- RegionLatency: map[int]time.Duration{
- 1: 10 * time.Millisecond,
- 2: 10 * time.Millisecond,
- 3: 10 * time.Millisecond,
- },
+ regionLatency: map[int]time.Duration{
+ 1: 10 * time.Millisecond,
+ 2: 10 * time.Millisecond,
+ 3: 10 * time.Millisecond,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- got := minLatencyDERPRegion(tt.regions, tt.report)
+ got := minLatencyDERPRegion(tt.regions, tt.regionLatency)
if got != tt.wantRegion {
t.Errorf("got region %v want region %v", got, tt.wantRegion)
}
@@ -3468,11 +3465,60 @@ func TestLastSuggestedExitNodeAsAPIType(t *testing.T) {
}
}
+func TestProcessNetInfo(t *testing.T) {
+ tests := []struct {
+ name string
+ netinfo *tailcfg.NetInfo
+ wantInfo latencyInfo
+ wantError error
+ }{
+ {
+ name: "valid net info",
+ netinfo: &tailcfg.NetInfo{
+ PreferredDERP: 1,
+ DERPLatency: map[string]float64{
+ "1-v4": 1.0,
+ "1-v6": 2.0,
+ },
+ },
+ wantInfo: latencyInfo{
+ preferredDERP: 1,
+ regionLatency: map[int]time.Duration{
+ 1: time.Duration(1 * float64(time.Second)),
+ },
+ },
+ },
+ {
+ name: "no preferred derp",
+ netinfo: &tailcfg.NetInfo{
+ PreferredDERP: 0,
+ },
+ wantError: ErrNoPreferredDERP,
+ },
+ {
+ name: "nil netinfo",
+ wantError: ErrCannotSuggestExitNode,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := processNetInfo(tt.netinfo)
+ if !reflect.DeepEqual(got, tt.wantInfo) {
+ t.Errorf("got %v want %v", got, tt.wantInfo)
+ }
+ if err != tt.wantError {
+ t.Errorf("got error %v want error %v", err, tt.wantError)
+ }
+ })
+ }
+}
+
func TestLocalBackendSuggestExitNode(t *testing.T) {
tests := []struct {
name string
lastSuggestedExitNode lastSuggestedExitNode
report *netcheck.Report
+ netInfo *tailcfg.NetInfo
netMap netmap.NetworkMap
wantID tailcfg.StableNodeID
wantName string
@@ -3766,6 +3812,78 @@ func TestLocalBackendSuggestExitNode(t *testing.T) {
},
wantErr: ErrCannotSuggestExitNode,
},
+ {
+ name: "use netinfo instead of last report",
+ netInfo: &tailcfg.NetInfo{
+ PreferredDERP: 1,
+ DERPLatency: map[string]float64{
+ "1-v4": 10,
+ "2-v6": 10,
+ "3-v4": 5,
+ },
+ },
+ netMap: netmap.NetworkMap{
+ SelfNode: (&tailcfg.Node{
+ Addresses: []netip.Prefix{
+ netip.MustParsePrefix("100.64.1.1/32"),
+ netip.MustParsePrefix("fe70::1/128"),
+ },
+ }).View(),
+ DERPMap: &tailcfg.DERPMap{
+ Regions: map[int]*tailcfg.DERPRegion{
+ 1: {},
+ 2: {},
+ 3: {},
+ },
+ },
+ Peers: []tailcfg.NodeView{
+ (&tailcfg.Node{
+ ID: 1,
+ StableID: "test",
+ Name: "test",
+ DERP: "127.3.3.40:1",
+ AllowedIPs: []netip.Prefix{
+ netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
+ },
+ CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
+ tailcfg.NodeAttrSuggestExitNode: {},
+ }),
+ }).View(),
+ (&tailcfg.Node{
+ ID: 2,
+ StableID: "test",
+ Name: "test",
+ DERP: "127.3.3.40:2",
+ AllowedIPs: []netip.Prefix{
+ netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
+ },
+ CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
+ tailcfg.NodeAttrSuggestExitNode: {},
+ }),
+ }).View(),
+ (&tailcfg.Node{
+ ID: 3,
+ StableID: "foo",
+ Name: "foo",
+ DERP: "127.3.3.40:3",
+ AllowedIPs: []netip.Prefix{
+ netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
+ },
+ CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
+ tailcfg.NodeAttrSuggestExitNode: {},
+ }),
+ }).View(),
+ },
+ },
+ wantID: "foo",
+ wantName: "foo",
+ wantLastSuggestedExitNode: lastSuggestedExitNode{name: "foo", id: "foo"},
+ },
+ {
+ name: "invalid netinfo returns error",
+ netInfo: &tailcfg.NetInfo{},
+ wantErr: ErrCannotSuggestExitNode,
+ },
}
for _, tt := range tests {
@@ -3773,7 +3891,9 @@ func TestLocalBackendSuggestExitNode(t *testing.T) {
lb.lastSuggestedExitNode = tt.lastSuggestedExitNode
lb.netMap = &tt.netMap
lb.sys.MagicSock.Get().SetLastNetcheckReportForTest(context.Background(), tt.report)
- got, err := lb.SuggestExitNode()
+ lb.mu.Lock()
+ got, err := lb.suggestExitNodeLocked(false, tt.netInfo)
+ lb.mu.Unlock()
if got.ID != tt.wantID {
t.Errorf("ID=%v, want=%v", got.ID, tt.wantID)
}
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index a822aad69..648a72432 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -2884,7 +2884,7 @@ func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
- res, err := h.b.SuggestExitNode()
+ res, err := h.b.HandleSuggestExitNode()
if err != nil {
writeErrorJSON(w, err)
return