mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
8b47322acc
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>
327 lines
6.7 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|