mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-15 15:29:33 -04:00
Instead of accessing the key and string directly, put it behind a method to make it easier to use that as a proxy when we add multiple keys (control and TSMP origin). Instead of only updating a single origin for disco keys, teach peermap how to work with multiple disco key sources so a key can originate from either control or TSMP. This sets the work up for switching dynamically between disco keys later. As of this commit, the keys still arrive for the most part via the controlClient, but this commit sets us up for: - Switching dynamically between received keys. - Route TSMP keys directly into magicsock, not via controlClient. - Potentially revert controlClient to the state before any TSMP changes, making it single threaded. - Use switching keys as trigger for optimistic WG handshakes, instead of tearing down the full connection and setting it up again. Updates #20494 Signed-off-by: Claus Lensbøl <claus@tailscale.com>
856 lines
25 KiB
Go
856 lines
25 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package magicsock
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"tailscale.com/disco"
|
|
"tailscale.com/envknob"
|
|
"tailscale.com/net/packet"
|
|
"tailscale.com/net/stun"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/tstime/mono"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/util/ringlog"
|
|
)
|
|
|
|
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 lowerPriv, higherPriv key.DiscoPrivate
|
|
var lower, higher key.DiscoPublic
|
|
privA := key.NewDisco()
|
|
privB := key.NewDisco()
|
|
a := privA.Public()
|
|
b := privB.Public()
|
|
if a.String() < b.String() {
|
|
lower = a
|
|
higher = b
|
|
lowerPriv = privA
|
|
higherPriv = privB
|
|
} else {
|
|
lower = b
|
|
higher = a
|
|
lowerPriv = privB
|
|
higherPriv = privA
|
|
}
|
|
addr := addrQuality{epAddr: epAddr{ap: netip.MustParseAddrPort("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
|
|
}{
|
|
{
|
|
name: "nil-probeUDPLifetime",
|
|
localDisco: higher,
|
|
remoteDisco: &lower,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
return nil
|
|
},
|
|
bestAddr: addr,
|
|
},
|
|
{
|
|
name: "local-higher-disco-key",
|
|
localDisco: higher,
|
|
remoteDisco: &lower,
|
|
probeUDPLifetimeFn: newProbeUDPLifetime,
|
|
bestAddr: addr,
|
|
},
|
|
{
|
|
name: "remote-no-disco-key",
|
|
localDisco: higher,
|
|
remoteDisco: nil,
|
|
probeUDPLifetimeFn: newProbeUDPLifetime,
|
|
bestAddr: addr,
|
|
},
|
|
{
|
|
name: "invalid-bestAddr",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: newProbeUDPLifetime,
|
|
bestAddr: addrQuality{},
|
|
},
|
|
{
|
|
name: "cycle-started-too-recently",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
lt := newProbeUDPLifetime()
|
|
lt.cycleActive = false
|
|
lt.cycleStartedAt = time.Now()
|
|
return lt
|
|
},
|
|
bestAddr: addr,
|
|
},
|
|
{
|
|
name: "maybe-cliff-0-cycle-not-active",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
lt := newProbeUDPLifetime()
|
|
lt.cycleActive = false
|
|
lt.cycleStartedAt = time.Now().Add(-lt.config.CycleCanStartEvery).Add(-time.Second)
|
|
return lt
|
|
},
|
|
bestAddr: addr,
|
|
wantAfterInactivityForFn: func(lifetime *probeUDPLifetime) time.Duration {
|
|
return lifetime.config.Cliffs[0] - udpLifetimeProbeCliffSlack
|
|
},
|
|
wantMaybe: true,
|
|
},
|
|
{
|
|
name: "maybe-cliff-0",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
lt := newProbeUDPLifetime()
|
|
lt.cycleActive = true
|
|
lt.currentCliff = 0
|
|
return lt
|
|
},
|
|
bestAddr: addr,
|
|
wantAfterInactivityForFn: func(lifetime *probeUDPLifetime) time.Duration {
|
|
return lifetime.config.Cliffs[0] - udpLifetimeProbeCliffSlack
|
|
},
|
|
wantMaybe: true,
|
|
},
|
|
{
|
|
name: "maybe-cliff-1",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
lt := newProbeUDPLifetime()
|
|
lt.cycleActive = true
|
|
lt.currentCliff = 1
|
|
return lt
|
|
},
|
|
bestAddr: addr,
|
|
wantAfterInactivityForFn: func(lifetime *probeUDPLifetime) time.Duration {
|
|
return lifetime.config.Cliffs[1] - udpLifetimeProbeCliffSlack
|
|
},
|
|
wantMaybe: true,
|
|
},
|
|
{
|
|
name: "maybe-cliff-2",
|
|
localDisco: lower,
|
|
remoteDisco: &higher,
|
|
probeUDPLifetimeFn: func() *probeUDPLifetime {
|
|
lt := newProbeUDPLifetime()
|
|
lt.cycleActive = true
|
|
lt.currentCliff = 2
|
|
return lt
|
|
},
|
|
bestAddr: addr,
|
|
wantAfterInactivityForFn: func(lifetime *probeUDPLifetime) time.Duration {
|
|
return lifetime.config.Cliffs[2] - udpLifetimeProbeCliffSlack
|
|
},
|
|
wantMaybe: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Conn{}
|
|
if tt.localDisco.IsZero() {
|
|
c.discoAtomic.Set(key.NewDisco())
|
|
} else if tt.localDisco.Compare(lower) == 0 {
|
|
c.discoAtomic.Set(lowerPriv)
|
|
} else if tt.localDisco.Compare(higher) == 0 {
|
|
c.discoAtomic.Set(higherPriv)
|
|
} else {
|
|
t.Fatalf("unexpected localDisco value")
|
|
}
|
|
de := &endpoint{
|
|
c: c,
|
|
bestAddr: tt.bestAddr,
|
|
}
|
|
if tt.remoteDisco != nil {
|
|
de.updateDiscoKey(*tt.remoteDisco)
|
|
}
|
|
p := tt.probeUDPLifetimeFn()
|
|
de.probeUDPLifetime = p
|
|
gotAfterInactivityFor, gotMaybe := de.maybeProbeUDPLifetimeLocked()
|
|
var wantAfterInactivityFor time.Duration
|
|
if tt.wantAfterInactivityForFn != nil {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_epAddr_isDirectUDP(t *testing.T) {
|
|
vni := packet.VirtualNetworkID{}
|
|
vni.Set(7)
|
|
tests := []struct {
|
|
name string
|
|
ap netip.AddrPort
|
|
vni packet.VirtualNetworkID
|
|
want bool
|
|
}{
|
|
{
|
|
name: "true",
|
|
ap: netip.MustParseAddrPort("192.0.2.1:7"),
|
|
vni: packet.VirtualNetworkID{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "false-derp-magic-addr",
|
|
ap: netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, 0),
|
|
vni: packet.VirtualNetworkID{},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "false-vni-set",
|
|
ap: netip.MustParseAddrPort("192.0.2.1:7"),
|
|
vni: vni,
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
e := epAddr{
|
|
ap: tt.ap,
|
|
vni: tt.vni,
|
|
}
|
|
if got := e.isDirect(); got != tt.want {
|
|
t.Errorf("isDirect() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_endpoint_udpRelayEndpointReady(t *testing.T) {
|
|
directAddrQuality := addrQuality{epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.1:7")}}
|
|
peerRelayAddrQuality := addrQuality{epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.2:77")}, latency: time.Second}
|
|
peerRelayAddrQuality.vni.Set(1)
|
|
peerRelayAddrQualityHigherLatencySameServer := addrQuality{
|
|
epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.3:77"), vni: peerRelayAddrQuality.vni},
|
|
latency: peerRelayAddrQuality.latency * 10,
|
|
}
|
|
peerRelayAddrQualityHigherLatencyDiffServer := addrQuality{
|
|
epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.3:77"), vni: peerRelayAddrQuality.vni},
|
|
latency: peerRelayAddrQuality.latency * 10,
|
|
relayServerDisco: key.NewDisco().Public(),
|
|
}
|
|
peerRelayAddrQualityLowerLatencyDiffServer := addrQuality{
|
|
epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.4:77"), vni: peerRelayAddrQuality.vni},
|
|
latency: peerRelayAddrQuality.latency / 10,
|
|
relayServerDisco: key.NewDisco().Public(),
|
|
}
|
|
peerRelayAddrQualityEqualLatencyDiffServer := addrQuality{
|
|
epAddr: epAddr{ap: netip.MustParseAddrPort("192.0.2.4:77"), vni: peerRelayAddrQuality.vni},
|
|
latency: peerRelayAddrQuality.latency,
|
|
relayServerDisco: key.NewDisco().Public(),
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
curBestAddr addrQuality
|
|
trustBestAddrUntil mono.Time
|
|
maybeBest addrQuality
|
|
wantBestAddr addrQuality
|
|
}{
|
|
{
|
|
name: "bestAddr-trusted-direct",
|
|
curBestAddr: directAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(1 * time.Hour),
|
|
maybeBest: peerRelayAddrQuality,
|
|
wantBestAddr: directAddrQuality,
|
|
},
|
|
{
|
|
name: "bestAddr-untrusted-direct",
|
|
curBestAddr: directAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(-1 * time.Hour),
|
|
maybeBest: peerRelayAddrQuality,
|
|
wantBestAddr: peerRelayAddrQuality,
|
|
},
|
|
{
|
|
name: "maybeBest-same-relay-higher-latency-trusted",
|
|
curBestAddr: peerRelayAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(1 * time.Hour),
|
|
maybeBest: peerRelayAddrQualityHigherLatencySameServer,
|
|
wantBestAddr: peerRelayAddrQualityHigherLatencySameServer,
|
|
},
|
|
{
|
|
name: "maybeBest-diff-relay-higher-latency-trusted",
|
|
curBestAddr: peerRelayAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(1 * time.Hour),
|
|
maybeBest: peerRelayAddrQualityHigherLatencyDiffServer,
|
|
wantBestAddr: peerRelayAddrQuality,
|
|
},
|
|
{
|
|
name: "maybeBest-diff-relay-lower-latency-trusted",
|
|
curBestAddr: peerRelayAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(1 * time.Hour),
|
|
maybeBest: peerRelayAddrQualityLowerLatencyDiffServer,
|
|
wantBestAddr: peerRelayAddrQualityLowerLatencyDiffServer,
|
|
},
|
|
{
|
|
name: "maybeBest-diff-relay-equal-latency-trusted",
|
|
curBestAddr: peerRelayAddrQuality,
|
|
trustBestAddrUntil: mono.Now().Add(1 * time.Hour),
|
|
maybeBest: peerRelayAddrQualityEqualLatencyDiffServer,
|
|
wantBestAddr: peerRelayAddrQuality,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
de := &endpoint{
|
|
c: &Conn{logf: func(msg string, args ...any) { return }},
|
|
bestAddr: tt.curBestAddr,
|
|
trustBestAddrUntil: tt.trustBestAddrUntil,
|
|
}
|
|
de.udpRelayEndpointReady(tt.maybeBest)
|
|
if de.bestAddr != tt.wantBestAddr {
|
|
t.Errorf("de.bestAddr = %v, want %v", de.bestAddr, tt.wantBestAddr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_endpoint_discoPingTimeout(t *testing.T) {
|
|
expired := -1 * time.Hour
|
|
valid := 1 * time.Hour
|
|
directAddrA := epAddr{ap: netip.MustParseAddrPort("192.0.2.1:7")}
|
|
relayAddrA := epAddr{ap: netip.MustParseAddrPort("192.0.2.2:77")}
|
|
relayAddrA.vni.Set(1)
|
|
directAddrB := epAddr{ap: netip.MustParseAddrPort("192.0.2.3:7")}
|
|
relayAddrB := epAddr{ap: netip.MustParseAddrPort("192.0.2.4:77")}
|
|
relayAddrB.vni.Set(1)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
bestAddr addrQuality
|
|
trustBestAddrUntil time.Duration
|
|
pingTo epAddr
|
|
wantBestAddrCleared bool
|
|
}{
|
|
{
|
|
name: "relay-path-trust-expired",
|
|
bestAddr: addrQuality{epAddr: relayAddrA},
|
|
trustBestAddrUntil: expired,
|
|
pingTo: relayAddrA,
|
|
wantBestAddrCleared: true,
|
|
},
|
|
{
|
|
name: "direct-udp-path-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA},
|
|
trustBestAddrUntil: expired,
|
|
pingTo: directAddrA,
|
|
wantBestAddrCleared: true,
|
|
},
|
|
{
|
|
name: "direct-udp-path-trust-valid",
|
|
bestAddr: addrQuality{epAddr: directAddrA},
|
|
trustBestAddrUntil: valid,
|
|
pingTo: directAddrA,
|
|
wantBestAddrCleared: false,
|
|
},
|
|
{
|
|
name: "relay-path-trust-valid",
|
|
bestAddr: addrQuality{epAddr: relayAddrA},
|
|
trustBestAddrUntil: valid,
|
|
pingTo: relayAddrA,
|
|
wantBestAddrCleared: false,
|
|
},
|
|
{
|
|
name: "ping-to-different-direct-addr-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA},
|
|
trustBestAddrUntil: expired,
|
|
pingTo: directAddrB,
|
|
wantBestAddrCleared: false,
|
|
},
|
|
{
|
|
name: "ping-to-different-relay-addr-trust-expired",
|
|
bestAddr: addrQuality{epAddr: relayAddrA},
|
|
trustBestAddrUntil: expired,
|
|
pingTo: relayAddrB,
|
|
wantBestAddrCleared: false,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
now := mono.Now() // synctest to match this to the internal 'now'
|
|
c := &Conn{
|
|
logf: func(msg string, args ...any) {},
|
|
}
|
|
c.discoAtomic.Set(key.NewDisco())
|
|
de := &endpoint{
|
|
c: c,
|
|
bestAddr: tc.bestAddr,
|
|
trustBestAddrUntil: now.Add(tc.trustBestAddrUntil),
|
|
sentPing: make(map[stun.TxID]sentPing),
|
|
}
|
|
txid := stun.NewTxID()
|
|
timer := time.NewTimer(time.Hour)
|
|
timer.Stop()
|
|
de.sentPing[txid] = sentPing{
|
|
to: tc.pingTo,
|
|
at: now.Add(-100 * time.Millisecond),
|
|
timer: timer,
|
|
purpose: pingDiscovery,
|
|
}
|
|
|
|
de.discoPingTimeout(txid)
|
|
if tc.wantBestAddrCleared {
|
|
if de.bestAddr.ap.IsValid() {
|
|
t.Errorf("expected bestAddr to be cleared, but bestAddr.ap is valid: %v", de.bestAddr.ap)
|
|
}
|
|
if de.trustBestAddrUntil != 0 {
|
|
t.Errorf("expected trustBestAddrUntil to be cleared, but got: %v", de.trustBestAddrUntil)
|
|
}
|
|
} else {
|
|
if de.bestAddr != tc.bestAddr {
|
|
t.Errorf("expected bestAddr to be unchanged, got: %v, want: %v", de.bestAddr, tc.bestAddr)
|
|
}
|
|
}
|
|
if _, ok := de.sentPing[txid]; ok {
|
|
t.Errorf("expected sentPing[txid] to be removed, but it still exists")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_endpoint_sendDiscoPingsLocked_neverDirectUDP(t *testing.T) {
|
|
directAddr := netip.MustParseAddrPort("192.0.2.1:7")
|
|
for _, neverDirectUDP := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("neverDirectUDP=%v", neverDirectUDP), func(t *testing.T) {
|
|
prev := envknob.String("TS_DEBUG_NEVER_DIRECT_UDP")
|
|
if neverDirectUDP {
|
|
envknob.SetenvForTest(t, "TS_DEBUG_NEVER_DIRECT_UDP", "true")
|
|
} else {
|
|
envknob.SetenvForTest(t, "TS_DEBUG_NEVER_DIRECT_UDP", "")
|
|
}
|
|
t.Cleanup(func() { envknob.Setenv("TS_DEBUG_NEVER_DIRECT_UDP", prev) })
|
|
now := mono.Now()
|
|
c := &Conn{
|
|
logf: func(msg string, args ...any) {},
|
|
}
|
|
c.discoAtomic.Set(key.NewDisco())
|
|
de := &endpoint{
|
|
c: c,
|
|
sentPing: make(map[stun.TxID]sentPing),
|
|
endpointState: make(map[netip.AddrPort]*endpointState),
|
|
}
|
|
de.updateDiscoKey(key.NewDisco().Public())
|
|
de.endpointState[directAddr] = &endpointState{}
|
|
de.sendDiscoPingsLocked(now, true)
|
|
|
|
wantPing := !neverDirectUDP
|
|
if gotPing := de.lastFullPing == now; gotPing != wantPing {
|
|
t.Errorf("lastFullPing set = %v, want %v", gotPing, wantPing)
|
|
}
|
|
if gotPing := de.endpointState[directAddr].lastPing == now; gotPing != wantPing {
|
|
t.Errorf("direct endpoint lastPing set = %v, want %v", gotPing, wantPing)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_endpoint_updateFromNodeAfterDiscoKeyChange(t *testing.T) {
|
|
relayAddr := epAddr{ap: netip.MustParseAddrPort("192.0.2.2:77")}
|
|
relayAddr.vni.Set(1)
|
|
directAddr := epAddr{ap: netip.MustParseAddrPort("192.0.2.1:7")}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
bestAddr epAddr
|
|
keyChanges bool
|
|
}{
|
|
{
|
|
name: "relay-bestaddr-key-changed",
|
|
bestAddr: relayAddr,
|
|
keyChanges: true,
|
|
},
|
|
{
|
|
name: "direct-bestaddr-key-changed",
|
|
bestAddr: directAddr,
|
|
keyChanges: true,
|
|
},
|
|
{
|
|
name: "relay-bestaddr-key-unchanged",
|
|
bestAddr: relayAddr,
|
|
keyChanges: false,
|
|
},
|
|
{
|
|
name: "direct-bestaddr-key-unchanged",
|
|
bestAddr: directAddr,
|
|
keyChanges: false,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
now := mono.Now()
|
|
c := &Conn{
|
|
logf: func(msg string, args ...any) {},
|
|
}
|
|
c.discoAtomic.Set(key.NewDisco())
|
|
c.relayManager.hasPeerRelayServers.Store(true)
|
|
|
|
oldKey := key.NewDisco().Public()
|
|
de := &endpoint{
|
|
c: c,
|
|
publicKey: key.NewNode().Public(),
|
|
bestAddr: addrQuality{epAddr: tc.bestAddr},
|
|
trustBestAddrUntil: now.Add(time.Hour),
|
|
sentPing: make(map[stun.TxID]sentPing),
|
|
endpointState: make(map[netip.AddrPort]*endpointState),
|
|
debugUpdates: ringlog.New[EndpointChange](10),
|
|
}
|
|
de.lastUDPRelayPathDiscovery = mono.Now()
|
|
de.updateDiscoKey(oldKey)
|
|
|
|
incomingKey := oldKey
|
|
if tc.keyChanges {
|
|
incomingKey = key.NewDisco().Public()
|
|
}
|
|
nv := (&tailcfg.Node{
|
|
ID: 1,
|
|
Key: key.NewNode().Public(),
|
|
DiscoKey: incomingKey,
|
|
HomeDERP: 1,
|
|
Cap: 121, // capVerIsRelayCapable
|
|
}).View()
|
|
de.updateFromNode(nv, false, false)
|
|
|
|
de.mu.Lock()
|
|
got := de.wantUDPRelayPathDiscoveryLocked(now)
|
|
de.mu.Unlock()
|
|
if got != tc.keyChanges {
|
|
t.Errorf("wantUDPRelayPathDiscoveryLocked = %v, want %v", got, tc.keyChanges)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_endpoint_handlePongConnLocked(t *testing.T) {
|
|
goodLatency := 50 * time.Millisecond
|
|
badLatency := 100 * time.Millisecond
|
|
expired := -1 * time.Hour
|
|
valid := 1 * time.Hour
|
|
directAddrA := epAddr{ap: netip.MustParseAddrPort("192.0.2.1:7")}
|
|
directAddrB := epAddr{ap: netip.MustParseAddrPort("192.0.2.2:8")}
|
|
derpAddr := epAddr{ap: netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, 0)}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
bestAddr addrQuality
|
|
trustBestAddrUntil time.Duration
|
|
pongFrom epAddr
|
|
pongLatency time.Duration
|
|
wantBestAddr epAddr
|
|
}{
|
|
{
|
|
name: "better-latency-trust-valid",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: badLatency},
|
|
trustBestAddrUntil: valid,
|
|
pongFrom: directAddrB,
|
|
pongLatency: goodLatency,
|
|
wantBestAddr: directAddrB,
|
|
},
|
|
{
|
|
name: "worse-latency-trust-valid",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: goodLatency},
|
|
trustBestAddrUntil: valid,
|
|
pongFrom: directAddrB,
|
|
pongLatency: badLatency,
|
|
wantBestAddr: directAddrA,
|
|
},
|
|
{
|
|
name: "worse-latency-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: goodLatency},
|
|
trustBestAddrUntil: expired,
|
|
pongFrom: directAddrB,
|
|
pongLatency: badLatency,
|
|
wantBestAddr: directAddrB,
|
|
},
|
|
{
|
|
name: "same-path-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: badLatency},
|
|
trustBestAddrUntil: expired,
|
|
pongFrom: directAddrA,
|
|
pongLatency: goodLatency, // updated latency
|
|
wantBestAddr: directAddrA,
|
|
},
|
|
{
|
|
name: "derp-pong-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: badLatency},
|
|
trustBestAddrUntil: expired,
|
|
pongFrom: derpAddr,
|
|
pongLatency: goodLatency,
|
|
wantBestAddr: directAddrA,
|
|
},
|
|
{
|
|
name: "better-latency-trust-expired",
|
|
bestAddr: addrQuality{epAddr: directAddrA, latency: badLatency},
|
|
trustBestAddrUntil: expired,
|
|
pongFrom: directAddrB,
|
|
pongLatency: goodLatency,
|
|
wantBestAddr: directAddrB,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
now := mono.Now() // synctest to match this to the internal 'now'
|
|
pm := newPeerMap()
|
|
c := &Conn{
|
|
logf: func(msg string, args ...any) {},
|
|
peerMap: pm,
|
|
}
|
|
c.discoAtomic.Set(key.NewDisco())
|
|
de := &endpoint{
|
|
c: c,
|
|
bestAddr: tc.bestAddr,
|
|
bestAddrAt: now.Add(-5 * time.Minute),
|
|
trustBestAddrUntil: now.Add(tc.trustBestAddrUntil),
|
|
sentPing: make(map[stun.TxID]sentPing),
|
|
endpointState: make(map[netip.AddrPort]*endpointState),
|
|
debugUpdates: ringlog.New[EndpointChange](10),
|
|
}
|
|
txid := stun.NewTxID()
|
|
pong := &disco.Pong{
|
|
TxID: txid,
|
|
Src: tc.pongFrom.ap,
|
|
}
|
|
timer := time.NewTimer(time.Hour)
|
|
timer.Stop()
|
|
de.sentPing[txid] = sentPing{
|
|
to: tc.pongFrom,
|
|
at: now.Add(-tc.pongLatency),
|
|
timer: timer,
|
|
purpose: pingDiscovery,
|
|
}
|
|
if tc.pongFrom.ap.Addr() != tailcfg.DerpMagicIPAddr && !tc.pongFrom.vni.IsSet() {
|
|
de.endpointState[tc.pongFrom.ap] = &endpointState{}
|
|
}
|
|
di := &discoInfo{
|
|
discoKey: key.NewDisco().Public(),
|
|
discoShort: "test",
|
|
}
|
|
|
|
knownTxID := de.handlePongConnLocked(pong, di, tc.pongFrom)
|
|
if !knownTxID {
|
|
t.Errorf("expected knownTxID to be true, got false")
|
|
}
|
|
if de.bestAddr.epAddr != tc.wantBestAddr {
|
|
t.Errorf("expected bestAddr.epAddr to be %v, got: %v", tc.wantBestAddr, de.bestAddr.epAddr)
|
|
}
|
|
if tc.pongFrom == tc.bestAddr.epAddr && de.bestAddr.latency-tc.pongLatency > 0 {
|
|
t.Errorf("expected latency to be %v, got: %v", tc.pongLatency, de.bestAddr.latency)
|
|
}
|
|
if tc.pongFrom != derpAddr && de.trustBestAddrUntil.Before(now) {
|
|
t.Errorf("expected trustBestAddrUntil to be refreshed, but it's in the past: %v", de.trustBestAddrUntil)
|
|
}
|
|
if _, ok := de.sentPing[txid]; ok {
|
|
t.Errorf("expected sentPing[txid] to be removed, but it still exists")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUpdateFromNodeUsesControlKeyForComparison verifies that updateFromNode
|
|
// compares the netmap-provided disco key against the endpoint's control-learned
|
|
// key (keyFromControl), not the currently active key (key()).
|
|
// Some of this is scaffold for later changes.
|
|
func TestUpdateFromNodeUsesControlKeyForComparison(t *testing.T) {
|
|
dk1 := key.NewDisco().Public() // initial control key
|
|
dk2 := key.NewDisco().Public() // TSMP key
|
|
|
|
tests := []struct {
|
|
name string
|
|
netmapDiscoKey key.DiscoPublic
|
|
wantControlKey key.DiscoPublic
|
|
wantActiveKey key.DiscoPublic
|
|
wantTsmpActive bool
|
|
}{
|
|
{
|
|
name: "control_catches_up_to_tsmp_key",
|
|
netmapDiscoKey: dk2,
|
|
wantControlKey: dk2,
|
|
wantActiveKey: dk2,
|
|
wantTsmpActive: false,
|
|
},
|
|
{
|
|
name: "unchanged_netmap_key_preserves_active",
|
|
netmapDiscoKey: dk1,
|
|
wantControlKey: dk1,
|
|
wantActiveKey: dk2,
|
|
wantTsmpActive: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
de := &endpoint{c: &Conn{logf: func(msg string, args ...any) {}}}
|
|
de.updateDiscoKey(dk1)
|
|
de.updateTSMPDiscoKey(dk2)
|
|
|
|
// Calls updateDiscoKey() internally and sets the endpoint `disco` field.
|
|
de.updateFromNode(
|
|
(&tailcfg.Node{Key: de.publicKey, DiscoKey: tt.netmapDiscoKey}).View(),
|
|
false, false)
|
|
|
|
epDisco := de.disco.Load()
|
|
if got := epDisco.keyFromControl(); got != tt.wantControlKey {
|
|
t.Errorf("keyFromControl: got %v, want %v", got, tt.wantControlKey)
|
|
}
|
|
if got := epDisco.tsmpActive; got != tt.wantTsmpActive {
|
|
t.Errorf("tsmpActive: got %t, want %t", got, tt.wantTsmpActive)
|
|
}
|
|
if got := epDisco.key(); got != tt.wantActiveKey {
|
|
t.Errorf("key(): got %v, want %v", got, tt.wantActiveKey)
|
|
}
|
|
})
|
|
}
|
|
}
|