tailscale/wgengine/magicsock/endpoint_test.go
Jordan Whited 8b47322acc
wgengine/magicsock: implement probing of UDP path lifetime (#10844)
This commit implements probing of UDP path lifetime on the tail end of
an active direct connection. Probing configuration has two parts -
Cliffs, which are various timeout cliffs of interest, and
CycleCanStartEvery, which limits how often a probing cycle can start,
per-endpoint. Initially a statically defined default configuration will
be used. The default configuration has cliffs of 10s, 30s, and 60s,
with a CycleCanStartEvery of 24h. Probing results are communicated via
clientmetric counters. Probing is off by default, and can be enabled
via control knob. Probing is purely informational and does not yet
drive any magicsock behaviors.

Updates #540

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-01-23 09:37:32 -08:00

327 lines
6.7 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"testing"
"time"
"github.com/dsnet/try"
"tailscale.com/types/key"
)
func TestProbeUDPLifetimeConfig_Equals(t *testing.T) {
tests := []struct {
name string
a *ProbeUDPLifetimeConfig
b *ProbeUDPLifetimeConfig
want bool
}{
{
"both sides nil",
nil,
nil,
true,
},
{
"equal pointers",
defaultProbeUDPLifetimeConfig,
defaultProbeUDPLifetimeConfig,
true,
},
{
"a nil",
nil,
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second},
CycleCanStartEvery: time.Hour,
},
false,
},
{
"b nil",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second},
CycleCanStartEvery: time.Hour,
},
nil,
false,
},
{
"Cliffs unequal",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second},
CycleCanStartEvery: time.Hour,
},
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second * 2},
CycleCanStartEvery: time.Hour,
},
false,
},
{
"CycleCanStartEvery unequal",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second},
CycleCanStartEvery: time.Hour,
},
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second},
CycleCanStartEvery: time.Hour * 2,
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.a.Equals(tt.b); got != tt.want {
t.Errorf("Equals() = %v, want %v", got, tt.want)
}
})
}
}
func TestProbeUDPLifetimeConfig_Valid(t *testing.T) {
tests := []struct {
name string
p *ProbeUDPLifetimeConfig
want bool
}{
{
"default config valid",
defaultProbeUDPLifetimeConfig,
true,
},
{
"no cliffs",
&ProbeUDPLifetimeConfig{
CycleCanStartEvery: time.Hour,
},
false,
},
{
"zero CycleCanStartEvery",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second * 10},
CycleCanStartEvery: 0,
},
false,
},
{
"cliff too small",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{min(udpLifetimeProbeCliffSlack*2, heartbeatInterval)},
CycleCanStartEvery: time.Hour,
},
false,
},
{
"duplicate Cliffs values",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second * 2, time.Second * 2},
CycleCanStartEvery: time.Hour,
},
false,
},
{
"Cliffs not ascending",
&ProbeUDPLifetimeConfig{
Cliffs: []time.Duration{time.Second * 2, time.Second * 1},
CycleCanStartEvery: time.Hour,
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.Valid(); got != tt.want {
t.Errorf("Valid() = %v, want %v", got, tt.want)
}
})
}
}
func Test_endpoint_maybeProbeUDPLifetimeLocked(t *testing.T) {
var lower, higher key.DiscoPublic
a := key.NewDisco().Public()
b := key.NewDisco().Public()
if a.String() < b.String() {
lower = a
higher = b
} else {
lower = b
higher = a
}
addr := addrQuality{AddrPort: try.E1[netip.AddrPort](netip.ParseAddrPort("1.1.1.1:1"))}
newProbeUDPLifetime := func() *probeUDPLifetime {
return &probeUDPLifetime{
config: *defaultProbeUDPLifetimeConfig,
}
}
tests := []struct {
name string
localDisco key.DiscoPublic
remoteDisco *key.DiscoPublic
probeUDPLifetimeFn func() *probeUDPLifetime
bestAddr addrQuality
wantAfterInactivityForFn func(*probeUDPLifetime) time.Duration
wantMaybe bool
}{
{
"nil probeUDPLifetime",
higher,
&lower,
func() *probeUDPLifetime {
return nil
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return 0
},
false,
},
{
"local higher disco key",
higher,
&lower,
newProbeUDPLifetime,
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return 0
},
false,
},
{
"remote no disco key",
higher,
nil,
newProbeUDPLifetime,
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return 0
},
false,
},
{
"invalid bestAddr",
lower,
&higher,
newProbeUDPLifetime,
addrQuality{},
func(lifetime *probeUDPLifetime) time.Duration {
return 0
},
false,
},
{
"cycle started too recently",
lower,
&higher,
func() *probeUDPLifetime {
l := newProbeUDPLifetime()
l.cycleActive = false
l.cycleStartedAt = time.Now()
return l
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return 0
},
false,
},
{
"maybe cliff 0 cycle not active",
lower,
&higher,
func() *probeUDPLifetime {
l := newProbeUDPLifetime()
l.cycleActive = false
l.cycleStartedAt = time.Now().Add(-l.config.CycleCanStartEvery).Add(-time.Second)
return l
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return lifetime.config.Cliffs[0] - udpLifetimeProbeCliffSlack
},
true,
},
{
"maybe cliff 0",
lower,
&higher,
func() *probeUDPLifetime {
l := newProbeUDPLifetime()
l.cycleActive = true
l.currentCliff = 0
return l
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return lifetime.config.Cliffs[0] - udpLifetimeProbeCliffSlack
},
true,
},
{
"maybe cliff 1",
lower,
&higher,
func() *probeUDPLifetime {
l := newProbeUDPLifetime()
l.cycleActive = true
l.currentCliff = 1
return l
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return lifetime.config.Cliffs[1] - udpLifetimeProbeCliffSlack
},
true,
},
{
"maybe cliff 2",
lower,
&higher,
func() *probeUDPLifetime {
l := newProbeUDPLifetime()
l.cycleActive = true
l.currentCliff = 2
return l
},
addr,
func(lifetime *probeUDPLifetime) time.Duration {
return lifetime.config.Cliffs[2] - udpLifetimeProbeCliffSlack
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
de := &endpoint{
c: &Conn{
discoPublic: tt.localDisco,
},
bestAddr: tt.bestAddr,
}
if tt.remoteDisco != nil {
remote := &endpointDisco{
key: *tt.remoteDisco,
}
de.disco.Store(remote)
}
p := tt.probeUDPLifetimeFn()
de.probeUDPLifetime = p
gotAfterInactivityFor, gotMaybe := de.maybeProbeUDPLifetimeLocked()
wantAfterInactivityFor := tt.wantAfterInactivityForFn(p)
if gotAfterInactivityFor != wantAfterInactivityFor {
t.Errorf("maybeProbeUDPLifetimeLocked() gotAfterInactivityFor = %v, want %v", gotAfterInactivityFor, wantAfterInactivityFor)
}
if gotMaybe != tt.wantMaybe {
t.Errorf("maybeProbeUDPLifetimeLocked() gotMaybe = %v, want %v", gotMaybe, tt.wantMaybe)
}
})
}
}