ipn/ipnlocal: handle auto value for ExitNodeID syspolicy

Updates tailscale/corp#19681

Signed-off-by: Claire Wang <claire@tailscale.com>
This commit is contained in:
Claire Wang 2024-05-13 15:11:25 -04:00
parent 9a64c06a20
commit d541079e78
4 changed files with 300 additions and 76 deletions

View File

@ -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+

View File

@ -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 {

View File

@ -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)
}

View File

@ -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