2025-05-09 14:50:01 -07:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package magicsock
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"tailscale.com/disco"
|
2025-10-04 20:27:57 -07:00
|
|
|
udprelay "tailscale.com/net/udprelay/endpoint"
|
2025-05-16 13:51:40 -07:00
|
|
|
"tailscale.com/types/key"
|
2025-06-20 15:00:28 -07:00
|
|
|
"tailscale.com/util/set"
|
2025-05-09 14:50:01 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestRelayManagerInitAndIdle(t *testing.T) {
|
|
|
|
|
rm := relayManager{}
|
2025-06-30 12:12:57 -07:00
|
|
|
rm.startUDPRelayPathDiscoveryFor(&endpoint{}, addrQuality{}, false)
|
2025-05-09 14:50:01 -07:00
|
|
|
<-rm.runLoopStoppedCh
|
|
|
|
|
|
|
|
|
|
rm = relayManager{}
|
2025-05-16 13:51:40 -07:00
|
|
|
rm.stopWork(&endpoint{})
|
2025-05-09 14:50:01 -07:00
|
|
|
<-rm.runLoopStoppedCh
|
|
|
|
|
|
|
|
|
|
rm = relayManager{}
|
types/key,wgengine/magicsock,control/controlclient,ipn: add debug disco key rotation
Adds the ability to rotate discovery keys on running clients, needed for
testing upcoming disco key distribution changes.
Introduces key.DiscoKey, an atomic container for a disco private key,
public key, and the public key's ShortString, replacing the prior
separate atomic fields.
magicsock.Conn has a new RotateDiscoKey method, and access to this is
provided via localapi and a CLI debug command.
Note that this implementation is primarily for testing as it stands, and
regular use should likely introduce an additional mechanism that allows
the old key to be used for some time, to provide a seamless key rotation
rather than one that invalidates all sessions.
Updates tailscale/corp#34037
Signed-off-by: James Tucker <james@tailscale.com>
2025-11-03 16:41:37 -08:00
|
|
|
c1 := &Conn{}
|
|
|
|
|
c1.discoAtomic.Set(key.NewDisco())
|
|
|
|
|
rm.handleCallMeMaybeVia(&endpoint{c: c1}, addrQuality{}, false, &disco.CallMeMaybeVia{UDPRelayEndpoint: disco.UDPRelayEndpoint{ServerDisco: key.NewDisco().Public()}})
|
2025-05-09 14:50:01 -07:00
|
|
|
<-rm.runLoopStoppedCh
|
|
|
|
|
|
|
|
|
|
rm = relayManager{}
|
types/key,wgengine/magicsock,control/controlclient,ipn: add debug disco key rotation
Adds the ability to rotate discovery keys on running clients, needed for
testing upcoming disco key distribution changes.
Introduces key.DiscoKey, an atomic container for a disco private key,
public key, and the public key's ShortString, replacing the prior
separate atomic fields.
magicsock.Conn has a new RotateDiscoKey method, and access to this is
provided via localapi and a CLI debug command.
Note that this implementation is primarily for testing as it stands, and
regular use should likely introduce an additional mechanism that allows
the old key to be used for some time, to provide a seamless key rotation
rather than one that invalidates all sessions.
Updates tailscale/corp#34037
Signed-off-by: James Tucker <james@tailscale.com>
2025-11-03 16:41:37 -08:00
|
|
|
c2 := &Conn{}
|
|
|
|
|
c2.discoAtomic.Set(key.NewDisco())
|
|
|
|
|
rm.handleRxDiscoMsg(c2, &disco.BindUDPRelayEndpointChallenge{}, key.NodePublic{}, key.DiscoPublic{}, epAddr{})
|
2025-05-09 14:50:01 -07:00
|
|
|
<-rm.runLoopStoppedCh
|
2025-06-20 15:00:28 -07:00
|
|
|
|
|
|
|
|
rm = relayManager{}
|
2025-07-21 10:02:37 -07:00
|
|
|
rm.handleRelayServersSet(make(set.Set[candidatePeerRelay]))
|
2025-06-20 15:00:28 -07:00
|
|
|
<-rm.runLoopStoppedCh
|
2025-07-16 10:03:05 -07:00
|
|
|
|
|
|
|
|
rm = relayManager{}
|
|
|
|
|
rm.getServers()
|
|
|
|
|
<-rm.runLoopStoppedCh
|
2025-07-21 10:02:37 -07:00
|
|
|
|
|
|
|
|
rm = relayManager{}
|
|
|
|
|
rm.handleDERPHomeChange(key.NodePublic{}, 1)
|
|
|
|
|
<-rm.runLoopStoppedCh
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRelayManagerHandleDERPHomeChange(t *testing.T) {
|
|
|
|
|
rm := relayManager{}
|
|
|
|
|
servers := make(set.Set[candidatePeerRelay], 1)
|
|
|
|
|
c := candidatePeerRelay{
|
|
|
|
|
nodeKey: key.NewNode().Public(),
|
|
|
|
|
discoKey: key.NewDisco().Public(),
|
|
|
|
|
derpHomeRegionID: 1,
|
|
|
|
|
}
|
|
|
|
|
servers.Add(c)
|
|
|
|
|
rm.handleRelayServersSet(servers)
|
|
|
|
|
want := c
|
|
|
|
|
want.derpHomeRegionID = 2
|
|
|
|
|
rm.handleDERPHomeChange(c.nodeKey, 2)
|
|
|
|
|
got := rm.getServers()
|
|
|
|
|
if len(got) != 1 {
|
|
|
|
|
t.Fatalf("got %d servers, want 1", len(got))
|
|
|
|
|
}
|
|
|
|
|
_, ok := got[want]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("DERP home change failed to propagate")
|
|
|
|
|
}
|
2025-07-16 10:03:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRelayManagerGetServers(t *testing.T) {
|
|
|
|
|
rm := relayManager{}
|
2025-07-21 10:02:37 -07:00
|
|
|
servers := make(set.Set[candidatePeerRelay], 1)
|
|
|
|
|
c := candidatePeerRelay{
|
|
|
|
|
nodeKey: key.NewNode().Public(),
|
|
|
|
|
discoKey: key.NewDisco().Public(),
|
|
|
|
|
}
|
|
|
|
|
servers.Add(c)
|
2025-07-16 10:03:05 -07:00
|
|
|
rm.handleRelayServersSet(servers)
|
|
|
|
|
got := rm.getServers()
|
|
|
|
|
if !servers.Equal(got) {
|
|
|
|
|
t.Errorf("got %v != want %v", got, servers)
|
|
|
|
|
}
|
2025-05-09 14:50:01 -07:00
|
|
|
}
|
2025-10-04 20:27:57 -07:00
|
|
|
|
|
|
|
|
func TestRelayManager_handleNewServerEndpointRunLoop(t *testing.T) {
|
2025-10-06 15:43:42 -07:00
|
|
|
wantHandshakeWorkCount := func(t *testing.T, rm *relayManager, n int) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
byServerDiscoByEndpoint := 0
|
|
|
|
|
for _, v := range rm.handshakeWorkByServerDiscoByEndpoint {
|
|
|
|
|
byServerDiscoByEndpoint += len(v)
|
|
|
|
|
}
|
|
|
|
|
byServerDiscoVNI := len(rm.handshakeWorkByServerDiscoVNI)
|
|
|
|
|
if byServerDiscoByEndpoint != n ||
|
|
|
|
|
byServerDiscoVNI != n ||
|
|
|
|
|
byServerDiscoByEndpoint != byServerDiscoVNI {
|
|
|
|
|
t.Fatalf("want handshake work count %d byServerDiscoByEndpoint=%d byServerDiscoVNI=%d",
|
|
|
|
|
n,
|
|
|
|
|
byServerDiscoByEndpoint,
|
|
|
|
|
byServerDiscoVNI,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 20:27:57 -07:00
|
|
|
conn := newConn(t.Logf)
|
2025-10-06 15:43:42 -07:00
|
|
|
epA := &endpoint{c: conn}
|
|
|
|
|
epB := &endpoint{c: conn}
|
|
|
|
|
serverDiscoA := key.NewDisco().Public()
|
|
|
|
|
serverDiscoB := key.NewDisco().Public()
|
|
|
|
|
|
|
|
|
|
serverAendpointALamport1VNI1 := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epA},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoA, LamportID: 1, VNI: 1},
|
|
|
|
|
}
|
|
|
|
|
serverAendpointALamport1VNI1LastBestMatching := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epA, lastBestIsTrusted: true, lastBest: addrQuality{relayServerDisco: serverDiscoA}},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoA, LamportID: 1, VNI: 1},
|
|
|
|
|
}
|
|
|
|
|
serverAendpointALamport2VNI1 := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epA},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoA, LamportID: 2, VNI: 1},
|
|
|
|
|
}
|
|
|
|
|
serverAendpointALamport2VNI2 := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epA},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoA, LamportID: 2, VNI: 2},
|
|
|
|
|
}
|
|
|
|
|
serverAendpointBLamport1VNI2 := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epB},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoA, LamportID: 1, VNI: 2},
|
|
|
|
|
}
|
|
|
|
|
serverBendpointALamport1VNI1 := newRelayServerEndpointEvent{
|
|
|
|
|
wlb: endpointWithLastBest{ep: epA},
|
|
|
|
|
se: udprelay.ServerEndpoint{ServerDisco: serverDiscoB, LamportID: 1, VNI: 1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
events []newRelayServerEndpointEvent
|
|
|
|
|
want []newRelayServerEndpointEvent
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
// Test for http://go/corp/32978
|
|
|
|
|
name: "eq server+ep neq VNI higher lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "eq server+ep neq VNI lower lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "eq server+vni neq ep lower lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
serverAendpointBLamport1VNI2,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "eq server+vni neq ep higher lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointBLamport1VNI2,
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI2,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "eq server+endpoint+vni higher lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
serverAendpointALamport2VNI1,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI1,
|
|
|
|
|
},
|
2025-10-04 20:27:57 -07:00
|
|
|
},
|
2025-10-06 15:43:42 -07:00
|
|
|
{
|
|
|
|
|
name: "eq server+endpoint+vni lower lamport",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI1,
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport2VNI1,
|
|
|
|
|
},
|
2025-10-04 20:27:57 -07:00
|
|
|
},
|
2025-10-06 15:43:42 -07:00
|
|
|
{
|
|
|
|
|
name: "eq endpoint+vni+lamport neq server",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
serverBendpointALamport1VNI1,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport1VNI1,
|
|
|
|
|
serverBendpointALamport1VNI1,
|
|
|
|
|
},
|
2025-10-04 20:27:57 -07:00
|
|
|
},
|
2025-10-06 15:43:42 -07:00
|
|
|
{
|
|
|
|
|
name: "trusted last best with matching server",
|
|
|
|
|
events: []newRelayServerEndpointEvent{
|
|
|
|
|
serverAendpointALamport1VNI1LastBestMatching,
|
|
|
|
|
},
|
|
|
|
|
want: []newRelayServerEndpointEvent{},
|
2025-10-04 20:27:57 -07:00
|
|
|
},
|
2025-10-06 15:43:42 -07:00
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
rm := &relayManager{}
|
|
|
|
|
rm.init()
|
|
|
|
|
<-rm.runLoopStoppedCh // prevent runLoop() from starting
|
|
|
|
|
|
|
|
|
|
// feed events
|
|
|
|
|
for _, event := range tt.events {
|
|
|
|
|
rm.handleNewServerEndpointRunLoop(event)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validate state
|
|
|
|
|
wantHandshakeWorkCount(t, rm, len(tt.want))
|
|
|
|
|
for _, want := range tt.want {
|
|
|
|
|
byServerDisco, ok := rm.handshakeWorkByServerDiscoByEndpoint[want.wlb.ep]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("work not found by endpoint")
|
|
|
|
|
}
|
|
|
|
|
workByServerDiscoByEndpoint, ok := byServerDisco[want.se.ServerDisco]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("work not found by server disco by endpoint")
|
|
|
|
|
}
|
|
|
|
|
workByServerDiscoVNI, ok := rm.handshakeWorkByServerDiscoVNI[serverDiscoVNI{want.se.ServerDisco, want.se.VNI}]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatal("work not found by server disco + VNI")
|
|
|
|
|
}
|
|
|
|
|
if workByServerDiscoByEndpoint != workByServerDiscoVNI {
|
|
|
|
|
t.Fatal("workByServerDiscoByEndpoint != workByServerDiscoVNI")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cleanup
|
|
|
|
|
for _, event := range tt.events {
|
|
|
|
|
rm.stopWorkRunLoop(event.wlb.ep)
|
|
|
|
|
}
|
|
|
|
|
wantHandshakeWorkCount(t, rm, 0)
|
|
|
|
|
})
|
2025-10-04 20:27:57 -07:00
|
|
|
}
|
|
|
|
|
}
|