tailscale/prober/derp_test.go
Percy Wegmann 5a3b3f460f
cmd/derpprobe,prober: add ability to restrict specific kind of derp probes to specific regions
This introduces the ability to configure the derprobe command using a yaml config file.
See config_test.go for a complete example of such a file.

Updates tailscale/corp#24522

Co-authored-by: Mario Minardi <mario@tailscale.com>
Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-11-15 08:33:09 -06:00

244 lines
6.2 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prober
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func TestDerpProber(t *testing.T) {
dm := &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
0: {
RegionID: 0,
RegionCode: "zero",
Nodes: []*tailcfg.DERPNode{
{
Name: "n1",
RegionID: 0,
HostName: "derpn1.tailscale.test",
IPv4: "1.1.1.1",
IPv6: "::1",
},
{
Name: "n2",
RegionID: 0,
HostName: "derpn2.tailscale.test",
IPv4: "1.1.1.1",
IPv6: "::1",
},
},
},
1: {
RegionID: 1,
RegionCode: "one",
Nodes: []*tailcfg.DERPNode{
{
Name: "n3",
RegionID: 0,
HostName: "derpn3.tailscale.test",
IPv4: "1.1.1.1",
IPv6: "::1",
},
},
},
},
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp, err := json.Marshal(dm)
if err != nil {
t.Fatal(err)
}
w.Write(resp)
}))
defer srv.Close()
clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker)
dp := &derpProber{
p: p,
derpMapURL: srv.URL,
tlsInterval: time.Second,
tlsProbeFn: func(_ string) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
tlsRegions: []string{"zero"},
udpInterval: time.Second,
udpProbeFn: func(_ string, _ int) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
udpRegions: []string{"zero"},
meshInterval: time.Second,
meshProbeFn: func(_, _ string) ProbeClass { return FuncProbe(func(context.Context) error { return nil }) },
meshRegions: []string{"zero"},
nodes: make(map[string]*tailcfg.DERPNode),
probes: make(map[string]*Probe),
}
if err := dp.probeMapFn(context.Background()); err != nil {
t.Errorf("unexpected probeMapFn() error: %s", err)
}
if len(dp.nodes) != 3 || dp.nodes["n1"] == nil || dp.nodes["n2"] == nil || dp.nodes["n3"] == nil {
t.Errorf("unexpected nodes: %+v", dp.nodes)
}
// Probes expected for two nodes:
// - 3 regular probes per node (TLS, UDPv4, UDPv6)
// - 4 mesh probes (N1->N2, N1->N1, N2->N1, N2->N2)
if len(dp.probes) != 10 {
t.Errorf("unexpected probes: %+v", dp.probes)
}
// Add one more node and check that probes got created.
dm.Regions[0].Nodes = append(dm.Regions[0].Nodes, &tailcfg.DERPNode{
Name: "n4",
RegionID: 0,
HostName: "derpn4.tailscale.test",
IPv4: "1.1.1.1",
IPv6: "::1",
})
if err := dp.probeMapFn(context.Background()); err != nil {
t.Errorf("unexpected probeMapFn() error: %s", err)
}
if len(dp.nodes) != 4 {
t.Errorf("unexpected nodes: %+v", dp.nodes)
}
// 9 regular probes + 9 mesh probes
if len(dp.probes) != 18 {
t.Errorf("unexpected probes: %+v", dp.probes)
}
// Remove 2 nodes and check that probes have been destroyed.
dm.Regions[0].Nodes = dm.Regions[0].Nodes[:1]
if err := dp.probeMapFn(context.Background()); err != nil {
t.Errorf("unexpected probeMapFn() error: %s", err)
}
if len(dp.nodes) != 2 {
t.Errorf("unexpected nodes: %+v", dp.nodes)
}
// 3 regular probes + 1 mesh probes
if len(dp.probes) != 4 {
t.Errorf("unexpected probes: %+v", dp.probes)
}
// Stop filtering regions.
dp.tlsRegions = nil
dp.udpRegions = nil
dp.meshRegions = nil
if err := dp.probeMapFn(context.Background()); err != nil {
t.Errorf("unexpected probeMapFn() error: %s", err)
}
if len(dp.nodes) != 2 {
t.Errorf("unexpected nodes: %+v", dp.nodes)
}
// 6 regular probes + 2 mesh probe
if len(dp.probes) != 8 {
t.Errorf("unexpected probes: %+v", dp.probes)
}
}
func TestRunDerpProbeNodePair(t *testing.T) {
// os.Setenv("DERP_DEBUG_LOGS", "true")
serverPrivateKey := key.NewNode()
s := derp.NewServer(serverPrivateKey, t.Logf)
defer s.Close()
httpsrv := &http.Server{
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: derphttp.Handler(s),
}
ln, err := net.Listen("tcp4", "localhost:0")
if err != nil {
t.Fatal(err)
}
serverURL := "http://" + ln.Addr().String()
t.Logf("server URL: %s", serverURL)
go func() {
if err := httpsrv.Serve(ln); err != nil {
if err == http.ErrServerClosed {
return
}
panic(err)
}
}()
newClient := func() *derphttp.Client {
c, err := derphttp.NewClient(key.NewNode(), serverURL, t.Logf, netmon.NewStatic())
if err != nil {
t.Fatalf("NewClient: %v", err)
}
m, err := c.Recv()
if err != nil {
t.Fatalf("Recv: %v", err)
}
switch m.(type) {
case derp.ServerInfoMessage:
default:
t.Fatalf("unexpected first message type %T", m)
}
return c
}
c1 := newClient()
defer c1.Close()
c2 := newClient()
defer c2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
err = runDerpProbeNodePair(ctx, &tailcfg.DERPNode{Name: "c1"}, &tailcfg.DERPNode{Name: "c2"}, c1, c2, 100_000_000)
if err != nil {
t.Error(err)
}
}
func Test_packetsForSize(t *testing.T) {
tests := []struct {
name string
size int
wantPackets int
wantUnique bool
}{
{"small_unqiue", 8, 1, true},
{"8k_unique", 8192, 1, true},
{"full_size_packet", derp.MaxPacketSize, 1, true},
{"larger_than_one", derp.MaxPacketSize + 1, 2, false},
{"large", 500000, 8, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hashes := make(map[string]int)
for range 5 {
pkts := packetsForSize(int64(tt.size))
if len(pkts) != tt.wantPackets {
t.Errorf("packetsForSize(%d) got %d packets, want %d", tt.size, len(pkts), tt.wantPackets)
}
var total int
hash := sha256.New()
for _, p := range pkts {
hash.Write(p)
total += len(p)
}
hashes[string(hash.Sum(nil))]++
if total != tt.size {
t.Errorf("packetsForSize(%d) returned %d bytes total", tt.size, total)
}
}
unique := len(hashes) > 1
if unique != tt.wantUnique {
t.Errorf("packetsForSize(%d) is unique=%v (returned %d different answers); want unique=%v", tt.size, unique, len(hashes), unique)
}
})
}
}