WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -792,7 +792,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/memnet from tailscale.com/tsnet
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
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+
|
||||
|
||||
@@ -95,7 +95,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/neterror from tailscale.com/net/batching+
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
|
||||
@@ -112,7 +112,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/neterror from tailscale.com/net/batching+
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
|
||||
@@ -191,7 +191,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/memnet from tailscale.com/tsnet
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
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+
|
||||
|
||||
+33
-20
@@ -58,7 +58,6 @@ import (
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/ipset"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/netkernelconf"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
@@ -8643,10 +8642,19 @@ func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggest
|
||||
if !buildfeatures.HasUseExitNode {
|
||||
return response, feature.ErrUnavailable
|
||||
}
|
||||
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
||||
mc := b.MagicConn()
|
||||
var preferredDERP int
|
||||
if lastReport := mc.GetLastNetcheckReport(b.ctx); lastReport != nil {
|
||||
preferredDERP = lastReport.PreferredDERP
|
||||
}
|
||||
// Use netcheck's recent per-region latency rather than only the latest report:
|
||||
// the latest report may be an incremental netcheck that didn't re-probe the
|
||||
// regions where far-away candidate exit nodes live, which would otherwise force
|
||||
// a random choice.
|
||||
regionLatency := mc.GetDERPRegionLatency()
|
||||
prevSuggestion := b.lastSuggestedExitNode
|
||||
|
||||
res, err := suggestExitNode(lastReport, b.currentNode(), prevSuggestion, randomRegion, randomNode, b.getAllowedSuggestions())
|
||||
res, err := suggestExitNode(preferredDERP, regionLatency, b.currentNode(), prevSuggestion, randomRegion, randomNode, b.getAllowedSuggestions())
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
@@ -8718,7 +8726,11 @@ func fillAllowedSuggestions(polc policyclient.Client) (set.Set[tailcfg.StableNod
|
||||
|
||||
// suggestExitNode returns a suggestion for reasonably good exit node based on
|
||||
// the current netmap and the previous suggestion.
|
||||
func suggestExitNode(report *netcheck.Report, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
//
|
||||
// preferredDERP is this device's home DERP region (0 if unknown) and
|
||||
// regionLatency is the best recent latency to each DERP region; both come from
|
||||
// netcheck and are only used by the DERP-based algorithm.
|
||||
func suggestExitNode(preferredDERP int, regionLatency map[int]time.Duration, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
switch {
|
||||
case nb.SelfHasCap(tailcfg.NodeAttrTrafficSteering):
|
||||
// The traffic-steering feature flag is enabled on this tailnet.
|
||||
@@ -8727,7 +8739,7 @@ func suggestExitNode(report *netcheck.Report, nb *nodeBackend, prevSuggestion ta
|
||||
// The control plane will always strip the `traffic-steering`
|
||||
// node attribute if it isn’t enabled for this tailnet, even if
|
||||
// it is set in the policy file: tailscale/corp#34401
|
||||
res, err = suggestExitNodeUsingDERP(report, nb, prevSuggestion, selectRegion, selectNode, allowList)
|
||||
res, err = suggestExitNodeUsingDERP(preferredDERP, regionLatency, nb, prevSuggestion, selectRegion, selectNode, allowList)
|
||||
}
|
||||
if err != nil {
|
||||
nb.logf("netmap: suggested exit node: %v", err)
|
||||
@@ -8742,22 +8754,23 @@ func suggestExitNode(report *netcheck.Report, nb *nodeBackend, prevSuggestion ta
|
||||
// before traffic steering was implemented. This handles the plain failover
|
||||
// case, in addition to the optional Regional Routing.
|
||||
//
|
||||
// It 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.
|
||||
// It computes a suggestion based on the current netmap, this device's preferred
|
||||
// DERP region, and the recent measured latency to each DERP region (regionLatency).
|
||||
// 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 suggestExitNodeUsingDERP(report *netcheck.Report, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
func suggestExitNodeUsingDERP(preferredRegionID int, regionLatency map[int]time.Duration, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
// TODO(sfllaw): Context needs to be plumbed down here to support
|
||||
// reachability testing.
|
||||
ctx := context.TODO()
|
||||
|
||||
netMap := nb.NetMap()
|
||||
if report == nil || report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
if preferredRegionID == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
return res, ErrNoPreferredDERP
|
||||
}
|
||||
// Use [nodeBackend.AppendMatchingPeers] instead of the netmap directly,
|
||||
@@ -8788,7 +8801,7 @@ func suggestExitNodeUsingDERP(report *netcheck.Report, nb *nodeBackend, prevSugg
|
||||
}
|
||||
|
||||
candidatesByRegion := make(map[int][]tailcfg.NodeView, len(netMap.DERPMap.Regions))
|
||||
preferredDERP, ok := netMap.DERPMap.Regions[report.PreferredDERP]
|
||||
preferredDERP, ok := netMap.DERPMap.Regions[preferredRegionID]
|
||||
if !ok {
|
||||
return res, ErrNoPreferredDERP
|
||||
}
|
||||
@@ -8824,10 +8837,10 @@ func suggestExitNodeUsingDERP(report *netcheck.Report, nb *nodeBackend, prevSugg
|
||||
}
|
||||
distances = append(distances, nodeDistance{nv: c, distance: distance})
|
||||
}
|
||||
// First, try to select an exit node that has the closest DERP home, based on lastReport's DERP latency.
|
||||
// First, try to select an exit node that has the closest DERP home, based on recent DERP latency.
|
||||
// If there are no latency values, it returns an arbitrary region
|
||||
if len(candidatesByRegion) > 0 {
|
||||
minRegion := minLatencyDERPRegion(slicesx.MapKeys(candidatesByRegion), report)
|
||||
minRegion := minLatencyDERPRegion(slicesx.MapKeys(candidatesByRegion), regionLatency)
|
||||
if minRegion == 0 {
|
||||
minRegion = selectRegion(views.SliceOf(slicesx.MapKeys(candidatesByRegion)))
|
||||
}
|
||||
@@ -9016,16 +9029,16 @@ func randomNode(nodes views.Slice[tailcfg.NodeView], prefer tailcfg.StableNodeID
|
||||
return nodes.At(rand.IntN(nodes.Len()))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// minLatencyDERPRegion returns the region with the lowest latency value given
|
||||
// the per-region latency map. If there are no latency values, it returns 0.
|
||||
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
|
||||
}
|
||||
@@ -9034,7 +9047,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 {
|
||||
|
||||
+113
-26
@@ -1421,9 +1421,11 @@ func TestConfigureExitNode(t *testing.T) {
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
lb.SetPrefsForTest(tt.prefs.Clone())
|
||||
|
||||
// Then set the netcheck report and netmap, if any.
|
||||
// Then set the netcheck report and netmap, if any. Clone the shared
|
||||
// report because AddNetcheckReportForTest mutates it and subtests run
|
||||
// in parallel.
|
||||
if tt.report != nil {
|
||||
lb.MagicConn().SetLastNetcheckReportForTest(t.Context(), tt.report)
|
||||
lb.MagicConn().AddNetcheckReportForTest(clientNetmap.DERPMap, tt.report, time.Now())
|
||||
}
|
||||
if tt.netMap != nil {
|
||||
lb.SetControlClientStatus(lb.cc, controlclient.Status{NetMap: tt.netMap})
|
||||
@@ -1681,7 +1683,7 @@ func TestExitNodeNotifyOrder(t *testing.T) {
|
||||
clientNetmap := buildNetmapWithPeers(selfNode, exitNode1, exitNode2)
|
||||
|
||||
lb := newTestLocalBackend(t)
|
||||
lb.sys.MagicSock.Get().SetLastNetcheckReportForTest(lb.ctx, report)
|
||||
lb.sys.MagicSock.Get().AddNetcheckReportForTest(clientNetmap.DERPMap, report, time.Now())
|
||||
lb.SetPrefsForTest(&ipn.Prefs{
|
||||
ControlURL: controlURL,
|
||||
AutoExitNode: ipn.AnyExitNode,
|
||||
@@ -4090,7 +4092,7 @@ func TestUpdateNetmapDeltaAutoExitNode(t *testing.T) {
|
||||
b := newTestLocalBackendWithSys(t, sys)
|
||||
b.currentNode().SetNetMap(tt.netmap)
|
||||
b.lastSuggestedExitNode = tt.lastSuggestedExitNode
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, tt.report)
|
||||
b.sys.MagicSock.Get().AddNetcheckReportForTest(derpMap, tt.report, time.Now())
|
||||
b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
||||
|
||||
allDone := make(chan bool, 1)
|
||||
@@ -4224,14 +4226,14 @@ func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
|
||||
t.Errorf("got initial exit node %v, want %v", eid, peer1.StableID())
|
||||
}
|
||||
b.refreshAutoExitNode = true
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, &netcheck.Report{
|
||||
b.sys.MagicSock.Get().AddNetcheckReportForTest(defaultDERPMap, &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 5 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 2,
|
||||
})
|
||||
}, time.Now())
|
||||
b.setNetInfo(&ni)
|
||||
if eid := b.Prefs().ExitNodeID(); eid != peer2.StableID() {
|
||||
t.Errorf("got final exit node %v, want %v", eid, peer2.StableID())
|
||||
@@ -4288,7 +4290,7 @@ func TestSetControlClientStatusAutoExitNode(t *testing.T) {
|
||||
// Peer 2 should be the initial exit node, as it's better than peer 1
|
||||
// in terms of latency and DERP region.
|
||||
b.lastSuggestedExitNode = peer2.StableID()
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, report)
|
||||
b.sys.MagicSock.Get().AddNetcheckReportForTest(derpMap, report, time.Now())
|
||||
b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
||||
offlinePeer2 := makePeer(2, withCap(26), withSuggest(), withExitRoutes(), withOnline(false), withNodeKey())
|
||||
updatedNetmap := &netmap.NetworkMap{
|
||||
@@ -6058,7 +6060,14 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
defer nb.shutdown(errShutdown)
|
||||
nb.SetNetMap(tt.netMap)
|
||||
|
||||
got, err := suggestExitNode(tt.lastReport, nb, tt.lastSuggestion, selectRegion, selectNode, allowList)
|
||||
var preferredDERP int
|
||||
var regionLatency map[int]time.Duration
|
||||
if tt.lastReport != nil {
|
||||
preferredDERP = tt.lastReport.PreferredDERP
|
||||
regionLatency = tt.lastReport.RegionLatency
|
||||
}
|
||||
|
||||
got, err := suggestExitNode(preferredDERP, regionLatency, nb, tt.lastSuggestion, selectRegion, selectNode, allowList)
|
||||
if got.Name != tt.wantName {
|
||||
t.Errorf("name=%v, want %v", got.Name, tt.wantName)
|
||||
}
|
||||
@@ -6078,6 +6087,89 @@ func TestSuggestExitNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuggestExitNodeUsesRecentDERPLatency exercises DERP latency-based exit node
|
||||
// suggestion when the most recent netcheck report is incremental.
|
||||
func TestSuggestExitNodeUsesRecentDERPLatency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
derpMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {Nodes: []*tailcfg.DERPNode{{Name: "1a", RegionID: 1}}},
|
||||
2: {Nodes: []*tailcfg.DERPNode{{Name: "2a", RegionID: 2}}},
|
||||
3: {Nodes: []*tailcfg.DERPNode{{Name: "3a", RegionID: 3}}},
|
||||
4: {Nodes: []*tailcfg.DERPNode{{Name: "4a", RegionID: 4}}},
|
||||
5: {Nodes: []*tailcfg.DERPNode{{Name: "5a", RegionID: 5}}},
|
||||
},
|
||||
}
|
||||
|
||||
// Two candidate exit nodes, each homed in a far region (4 and 5) that the most
|
||||
// recent (incremental) netcheck won't re-probe.
|
||||
exitRegion4 := makePeer(4, withDERP(4), withExitRoutes(), withSuggest())
|
||||
exitRegion5 := makePeer(5, withDERP(5), withExitRoutes(), withSuggest())
|
||||
|
||||
selfNode := tailcfg.Node{
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.1.1/32")},
|
||||
}
|
||||
netMap := &netmap.NetworkMap{
|
||||
SelfNode: selfNode.View(),
|
||||
DERPMap: derpMap,
|
||||
Peers: []tailcfg.NodeView{exitRegion4, exitRegion5},
|
||||
}
|
||||
|
||||
// A full netcheck measured every region: region 4 (100ms) is closer than
|
||||
// region 5 (200ms).
|
||||
fullReport := &netcheck.Report{
|
||||
PreferredDERP: 1,
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
4: 100 * time.Millisecond,
|
||||
5: 200 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
// A later incremental netcheck only re-probed the home and fastest regions, so
|
||||
// it has no latency for regions 4 or 5.
|
||||
incrementalReport := &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
},
|
||||
}
|
||||
|
||||
b := newTestLocalBackend(t)
|
||||
b.currentNode().SetNetMap(netMap)
|
||||
mc := b.sys.MagicSock.Get()
|
||||
|
||||
// Without any netcheck reports, SuggestExitNode returns an error because no
|
||||
// preferred DERP can be determined.
|
||||
if _, err := b.SuggestExitNode(); !errors.Is(err, ErrNoPreferredDERP) {
|
||||
t.Fatalf("SuggestExitNode() error = %v, want ErrNoPreferredDERP", err)
|
||||
}
|
||||
|
||||
// Seed the full report, then the incremental one a minute later, so both
|
||||
// remain in netcheck's recent history.
|
||||
now := time.Now()
|
||||
mc.AddNetcheckReportForTest(derpMap, fullReport, now.Add(-time.Minute))
|
||||
mc.AddNetcheckReportForTest(derpMap, incrementalReport, now)
|
||||
|
||||
// suggestExitNodeLocked falls back to a random region when it cannot order the
|
||||
// candidates by latency, so query repeatedly to make sure it always returns the closer region-4 node.
|
||||
const iterations = 64
|
||||
got := make(map[tailcfg.StableNodeID]int)
|
||||
for range iterations {
|
||||
res, err := b.SuggestExitNode()
|
||||
if err != nil {
|
||||
t.Fatalf("SuggestExitNode() error = %v", err)
|
||||
}
|
||||
got[res.ID]++
|
||||
}
|
||||
if len(got) != 1 || got[exitRegion4.StableID()] != iterations {
|
||||
t.Errorf("expected all %v suggestions to be for %v, got %+v", iterations, exitRegion4.StableID(), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuggestExitNodePickWeighted(t *testing.T) {
|
||||
location10 := tailcfg.Location{
|
||||
Priority: 10,
|
||||
@@ -6565,46 +6657,41 @@ func TestSuggestExitNodeTrafficSteering(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",
|
||||
regions: []int{1, 2, 3},
|
||||
wantRegion: 0,
|
||||
report: &netcheck.Report{},
|
||||
},
|
||||
{
|
||||
name: "regions-different-latency",
|
||||
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-latency",
|
||||
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)
|
||||
}
|
||||
|
||||
+60
-23
@@ -45,6 +45,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// Debugging and experimentation tweakables.
|
||||
@@ -1352,6 +1353,26 @@ const (
|
||||
PreferredDERPKeepAliveTimeout = 2 * derp.KeepAlive
|
||||
)
|
||||
|
||||
// addReportAndPruneExpired adds r to the set of recent Reports, and drops
|
||||
// reports that are outside of the retention window.
|
||||
func (c *Client) addReportAndPruneExpired(now time.Time, r *Report) {
|
||||
if c.prev == nil {
|
||||
c.prev = map[time.Time]*Report{}
|
||||
}
|
||||
r.Now = now.UTC()
|
||||
c.prev[now] = r
|
||||
c.last = r
|
||||
|
||||
// maxAge is the retention window for report history, based on fullReportInterval
|
||||
// to make sure that at least one full report is always retained.
|
||||
const maxAge = fullReportInterval + ReportTimeout
|
||||
for t := range c.prev {
|
||||
if now.Sub(t) > maxAge {
|
||||
delete(c.prev, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addReportHistoryAndSetPreferredDERP adds r to the set of recent Reports
|
||||
// and mutates r.PreferredDERP to contain the best recent one.
|
||||
func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report, dm tailcfg.DERPMapView) {
|
||||
@@ -1362,30 +1383,12 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
|
||||
if c.last != nil {
|
||||
prevDERP = c.last.PreferredDERP
|
||||
}
|
||||
if c.prev == nil {
|
||||
c.prev = map[time.Time]*Report{}
|
||||
}
|
||||
|
||||
// Add report to history, enforce retention window, then take the best (lowest)
|
||||
// latency seen per region across what remains.
|
||||
now := c.timeNow()
|
||||
r.Now = now.UTC()
|
||||
c.prev[now] = r
|
||||
c.last = r
|
||||
|
||||
const maxAge = 5 * time.Minute
|
||||
|
||||
// region ID => its best recent latency in last maxAge
|
||||
bestRecent := map[int]time.Duration{}
|
||||
|
||||
for t, pr := range c.prev {
|
||||
if now.Sub(t) > maxAge {
|
||||
delete(c.prev, t)
|
||||
continue
|
||||
}
|
||||
for regionID, d := range pr.RegionLatency {
|
||||
if bd, ok := bestRecent[regionID]; !ok || d < bd {
|
||||
bestRecent[regionID] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
c.addReportAndPruneExpired(now, r)
|
||||
bestRecent := c.bestRecentLatencyLocked()
|
||||
|
||||
// Scale each region's best latency by any provided scores from the
|
||||
// DERPMap, for use in comparison below.
|
||||
@@ -1480,6 +1483,40 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(rs *reportState, r *Report,
|
||||
}
|
||||
}
|
||||
|
||||
// bestRecentLatencyLocked returns the lowest latency seen per DERP region across
|
||||
// the reports currently retained in history (c.prev), keyed by region ID. These
|
||||
// latencies are used for determining preferred DERP and suggesting an exit node.
|
||||
func (c *Client) bestRecentLatencyLocked() map[int]time.Duration {
|
||||
best := make(map[int]time.Duration)
|
||||
for _, pr := range c.prev {
|
||||
for regionID, d := range pr.RegionLatency {
|
||||
if bd, ok := best[regionID]; !ok || d < bd {
|
||||
best[regionID] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// RecentRegionLatency returns the lowest latency seen per DERP region over the
|
||||
// recent history window, keyed by region ID.
|
||||
func (c *Client) RecentRegionLatency() map[int]time.Duration {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.bestRecentLatencyLocked()
|
||||
}
|
||||
|
||||
// AddReportHistoryForTest records r in the client's recent-report history
|
||||
// (prev/last) as if GetReport had produced it at time now, and recomputes
|
||||
// r.PreferredDERP from that history.
|
||||
func (c *Client) AddReportHistoryForTest(dm *tailcfg.DERPMap, r *Report, now time.Time) {
|
||||
testenv.AssertInTest()
|
||||
defer func(prev func() time.Time) { c.TimeNow = prev }(c.TimeNow)
|
||||
c.TimeNow = func() time.Time { return now }
|
||||
rs := &reportState{c: c, start: now}
|
||||
c.addReportHistoryAndSetPreferredDERP(rs, r, dm.View())
|
||||
}
|
||||
|
||||
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {
|
||||
if prev, ok := m[regionID]; !ok || d < prev {
|
||||
m[regionID] = d
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/memnet from tailscale.com/tsnet
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
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+
|
||||
|
||||
@@ -4345,10 +4345,35 @@ func (c *Conn) GetLastNetcheckReport(ctx context.Context) *netcheck.Report {
|
||||
return c.lastNetCheckReport.Load()
|
||||
}
|
||||
|
||||
// SetLastNetcheckReportForTest sets the magicsock conn's last netcheck report.
|
||||
// Used for testing purposes.
|
||||
func (c *Conn) SetLastNetcheckReportForTest(ctx context.Context, report *netcheck.Report) {
|
||||
c.lastNetCheckReport.Store(report)
|
||||
// AddNetcheckReportForTest records report in the conn's netcheck client's
|
||||
// recent-report history as if it had been produced at time now, seeding the
|
||||
// netcheck client's per-region latency history. If report is newer than the
|
||||
// currently stored last netcheck report, it also becomes the last netcheck
|
||||
// report.
|
||||
func (c *Conn) AddNetcheckReportForTest(dm *tailcfg.DERPMap, report *netcheck.Report, now time.Time) {
|
||||
testenv.AssertInTest()
|
||||
rep := report.Clone() // netchecker mutates the report, so create a copy
|
||||
c.netChecker.AddReportHistoryForTest(dm, rep, now)
|
||||
for {
|
||||
if cur := c.lastNetCheckReport.Load(); cur == nil || rep.Now.After(cur.Now) {
|
||||
if c.lastNetCheckReport.CompareAndSwap(cur, rep) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDERPRegionLatency returns the lowest latency seen per DERP region over
|
||||
// netcheck's recent history, keyed by region ID. Unlike the most recent report
|
||||
// from GetLastNetcheckReport (which for an incremental netcheck covers only a
|
||||
// few regions), netcheck's history retains every region measured by the most
|
||||
// recent full netcheck, so this can rank regions the latest report did not
|
||||
// re-probe. It returns nil if the netcheck client is not yet initialized.
|
||||
func (c *Conn) GetDERPRegionLatency() map[int]time.Duration {
|
||||
if c.netChecker == nil {
|
||||
return nil
|
||||
}
|
||||
return c.netChecker.RecentRegionLatency()
|
||||
}
|
||||
|
||||
// lazyEndpoint is a wireguard [conn.Endpoint] for when magicsock received a
|
||||
|
||||
Reference in New Issue
Block a user