prober: add a DERP bandwidth probe

Updates tailscale/corp#17912

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov 2024-02-28 20:27:44 +00:00 committed by Anton Tolchanov
parent 5018683d58
commit f12d2557f9
3 changed files with 392 additions and 104 deletions

View File

@ -19,18 +19,31 @@
) )
var ( var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)") derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address") listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag") probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
spread = flag.Bool("spread", true, "whether to spread probing over time") spread = flag.Bool("spread", true, "whether to spread probing over time")
interval = flag.Duration("interval", 15*time.Second, "probe interval") interval = flag.Duration("interval", 15*time.Second, "probe interval")
meshInterval = flag.Duration("mesh-interval", 15*time.Second, "mesh probe interval")
stunInterval = flag.Duration("stun-interval", 15*time.Second, "STUN probe interval")
tlsInterval = flag.Duration("tls-interval", 15*time.Second, "TLS probe interval")
bwInterval = flag.Duration("bw-interval", 0, "bandwidth probe interval (0 = no bandwidth probing)")
bwSize = flag.Int64("bw-probe-size-bytes", 1_000_000, "bandwidth probe size")
) )
func main() { func main() {
flag.Parse() flag.Parse()
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe") p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval) opts := []prober.DERPOpt{
prober.WithMeshProbing(*meshInterval),
prober.WithSTUNProbing(*stunInterval),
prober.WithTLSProbing(*tlsInterval),
}
if *bwInterval > 0 {
opts = append(opts, prober.WithBandwidthProbing(*bwInterval, *bwSize))
}
dp, err := prober.DERP(p, *derpMapURL, opts...)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -53,6 +66,7 @@ func main() {
mux := http.NewServeMux() mux := http.NewServeMux()
tsweb.Debugger(mux) tsweb.Debugger(mux)
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p))) mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
log.Printf("Listening on %s", *listen)
log.Fatal(http.ListenAndServe(*listen, mux)) log.Fatal(http.ListenAndServe(*listen, mux))
} }

View File

@ -5,12 +5,14 @@
import ( import (
"bytes" "bytes"
"cmp"
"context" "context"
crand "crypto/rand" crand "crypto/rand"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log"
"maps"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -21,6 +23,7 @@
"tailscale.com/derp" "tailscale.com/derp"
"tailscale.com/derp/derphttp" "tailscale.com/derp/derphttp"
"tailscale.com/net/stun" "tailscale.com/net/stun"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
@ -35,10 +38,15 @@ type derpProber struct {
meshInterval time.Duration meshInterval time.Duration
tlsInterval time.Duration tlsInterval time.Duration
// Optional bandwidth probing.
bwInterval time.Duration
bwProbeSize int64
// Probe functions that can be overridden for testing. // Probe functions that can be overridden for testing.
tlsProbeFn func(string) ProbeFunc tlsProbeFn func(string) ProbeFunc
udpProbeFn func(string, int) ProbeFunc udpProbeFn func(string, int) ProbeFunc
meshProbeFn func(string, string) ProbeFunc meshProbeFn func(string, string) ProbeFunc
bwProbeFn func(string, string, int64) ProbeFunc
sync.Mutex sync.Mutex
lastDERPMap *tailcfg.DERPMap lastDERPMap *tailcfg.DERPMap
@ -47,20 +55,57 @@ type derpProber struct {
probes map[string]*Probe probes map[string]*Probe
} }
type DERPOpt func(*derpProber)
// WithBandwidthProbing enables bandwidth probing. When enabled, a payload of
// `size` bytes will be regularly transferred through each DERP server, and each
// pair of DERP servers in every region.
func WithBandwidthProbing(interval time.Duration, size int64) DERPOpt {
return func(d *derpProber) {
d.bwInterval = interval
d.bwProbeSize = size
}
}
// WithMeshProbing enables mesh probing. When enabled, a small message will be
// transferred through each DERP server and each pair of DERP servers.
func WithMeshProbing(interval time.Duration) DERPOpt {
return func(d *derpProber) {
d.meshInterval = interval
}
}
// WithSTUNProbing enables STUN/UDP probing, with a STUN request being sent
// to each DERP server every `interval`.
func WithSTUNProbing(interval time.Duration) DERPOpt {
return func(d *derpProber) {
d.udpInterval = interval
}
}
// WithTLSProbing enables TLS probing that will check TLS certificate on port
// 443 of each DERP server every `interval`.
func WithTLSProbing(interval time.Duration) DERPOpt {
return func(d *derpProber) {
d.tlsInterval = interval
}
}
// DERP creates a new derpProber. // DERP creates a new derpProber.
func DERP(p *Prober, derpMapURL string, udpInterval, meshInterval, tlsInterval time.Duration) (*derpProber, error) { func DERP(p *Prober, derpMapURL string, opts ...DERPOpt) (*derpProber, error) {
d := &derpProber{ d := &derpProber{
p: p, p: p,
derpMapURL: derpMapURL, derpMapURL: derpMapURL,
udpInterval: udpInterval, tlsProbeFn: TLS,
meshInterval: meshInterval, nodes: make(map[string]*tailcfg.DERPNode),
tlsInterval: tlsInterval, probes: make(map[string]*Probe),
tlsProbeFn: TLS, }
nodes: make(map[string]*tailcfg.DERPNode), for _, o := range opts {
probes: make(map[string]*Probe), o(d)
} }
d.udpProbeFn = d.ProbeUDP d.udpProbeFn = d.ProbeUDP
d.meshProbeFn = d.probeMesh d.meshProbeFn = d.probeMesh
d.bwProbeFn = d.probeBandwidth
return d, nil return d, nil
} }
@ -84,42 +129,59 @@ func (d *derpProber) ProbeMap(ctx context.Context) error {
"hostname": server.HostName, "hostname": server.HostName,
} }
n := fmt.Sprintf("derp/%s/%s/tls", region.RegionCode, server.Name) if d.tlsInterval > 0 {
wantProbes[n] = true n := fmt.Sprintf("derp/%s/%s/tls", region.RegionCode, server.Name)
if d.probes[n] == nil {
log.Printf("adding DERP TLS probe for %s (%s)", server.Name, region.RegionName)
derpPort := 443
if server.DERPPort != 0 {
derpPort = server.DERPPort
}
d.probes[n] = d.p.Run(n, d.tlsInterval, labels, d.tlsProbeFn(fmt.Sprintf("%s:%d", server.HostName, derpPort)))
}
for idx, ipStr := range []string{server.IPv6, server.IPv4} {
n = fmt.Sprintf("derp/%s/%s/udp", region.RegionCode, server.Name)
if idx == 0 {
n = n + "6"
}
if ipStr == "" || server.STUNPort == -1 {
continue
}
wantProbes[n] = true wantProbes[n] = true
if d.probes[n] == nil { if d.probes[n] == nil {
log.Printf("adding DERP UDP probe for %s (%s)", server.Name, n) log.Printf("adding DERP TLS probe for %s (%s) every %v", server.Name, region.RegionName, d.tlsInterval)
d.probes[n] = d.p.Run(n, d.udpInterval, labels, d.udpProbeFn(ipStr, server.STUNPort)) derpPort := cmp.Or(server.DERPPort, 443)
d.probes[n] = d.p.Run(n, d.tlsInterval, labels, d.tlsProbeFn(fmt.Sprintf("%s:%d", server.HostName, derpPort)))
}
}
if d.udpInterval > 0 {
for idx, ipStr := range []string{server.IPv6, server.IPv4} {
n := fmt.Sprintf("derp/%s/%s/udp", region.RegionCode, server.Name)
if idx == 0 {
n += "6"
}
if ipStr == "" || server.STUNPort == -1 {
continue
}
wantProbes[n] = true
if d.probes[n] == nil {
log.Printf("adding DERP UDP probe for %s (%s) every %v", server.Name, n, d.udpInterval)
d.probes[n] = d.p.Run(n, d.udpInterval, labels, d.udpProbeFn(ipStr, server.STUNPort))
}
} }
} }
for _, to := range region.Nodes { for _, to := range region.Nodes {
n = fmt.Sprintf("derp/%s/%s/%s/mesh", region.RegionCode, server.Name, to.Name) if d.meshInterval > 0 {
wantProbes[n] = true n := fmt.Sprintf("derp/%s/%s/%s/mesh", region.RegionCode, server.Name, to.Name)
if d.probes[n] == nil { wantProbes[n] = true
log.Printf("adding DERP mesh probe for %s->%s (%s)", server.Name, to.Name, region.RegionName) if d.probes[n] == nil {
d.probes[n] = d.p.Run(n, d.meshInterval, labels, d.meshProbeFn(server.HostName, to.HostName)) log.Printf("adding DERP mesh probe for %s->%s (%s) every %v", server.Name, to.Name, region.RegionName, d.meshInterval)
d.probes[n] = d.p.Run(n, d.meshInterval, labels, d.meshProbeFn(server.Name, to.Name))
}
}
if d.bwInterval > 0 && d.bwProbeSize > 0 {
bwLabels := maps.Clone(labels)
bwLabels["probe_size_bytes"] = fmt.Sprintf("%d", d.bwProbeSize)
if server.Name == to.Name {
bwLabels["derp_path"] = "single"
} else {
bwLabels["derp_path"] = "mesh"
}
n := fmt.Sprintf("derp/%s/%s/%s/bw", region.RegionCode, server.Name, to.Name)
wantProbes[n] = true
if d.probes[n] == nil {
log.Printf("adding DERP bandwidth probe for %s->%s (%s) %v bytes every %v", server.Name, to.Name, region.RegionName, d.bwProbeSize, d.bwInterval)
d.probes[n] = d.p.Run(n, d.bwInterval, bwLabels, d.bwProbeFn(server.Name, to.Name, d.bwProbeSize))
}
} }
} }
} }
@ -136,26 +198,52 @@ func (d *derpProber) ProbeMap(ctx context.Context) error {
return nil return nil
} }
// probeMesh returs a probe func that sends a test packet through a pair of DERP
// servers (or just one server, if 'from' and 'to' are the same). 'from' and 'to'
// are expected to be names (DERPNode.Name) of two DERP servers in the same region.
func (d *derpProber) probeMesh(from, to string) ProbeFunc { func (d *derpProber) probeMesh(from, to string) ProbeFunc {
return func(ctx context.Context) error { return func(ctx context.Context) error {
d.Lock() fromN, toN, err := d.getNodePair(from, to)
dm := d.lastDERPMap if err != nil {
fromN, ok := d.nodes[from] return err
if !ok {
d.Unlock()
return fmt.Errorf("could not find derp node %s", from)
} }
toN, ok := d.nodes[to]
if !ok {
d.Unlock()
return fmt.Errorf("could not find derp node %s", to)
}
d.Unlock()
dm := d.lastDERPMap
return derpProbeNodePair(ctx, dm, fromN, toN) return derpProbeNodePair(ctx, dm, fromN, toN)
} }
} }
// probeBandwidth returs a probe func that sends a payload of a given size
// through a pair of DERP servers (or just one server, if 'from' and 'to' are
// the same). 'from' and 'to' are expected to be names (DERPNode.Name) of two
// DERP servers in the same region.
func (d *derpProber) probeBandwidth(from, to string, size int64) ProbeFunc {
return func(ctx context.Context) error {
fromN, toN, err := d.getNodePair(from, to)
if err != nil {
return err
}
return derpProbeBandwidth(ctx, d.lastDERPMap, fromN, toN, size)
}
}
// getNodePair returns DERPNode objects for two DERP servers based on their
// short names.
func (d *derpProber) getNodePair(n1, n2 string) (ret1, ret2 *tailcfg.DERPNode, _ error) {
d.Lock()
defer d.Unlock()
ret1, ok := d.nodes[n1]
if !ok {
return nil, nil, fmt.Errorf("could not find derp node %s", n1)
}
ret2, ok = d.nodes[n2]
if !ok {
return nil, nil, fmt.Errorf("could not find derp node %s", n2)
}
return ret1, ret2, nil
}
// updateMap refreshes the locally-cached DERP map.
func (d *derpProber) updateMap(ctx context.Context) error { func (d *derpProber) updateMap(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", d.derpMapURL, nil) req, err := http.NewRequestWithContext(ctx, "GET", d.derpMapURL, nil)
if err != nil { if err != nil {
@ -189,13 +277,13 @@ func (d *derpProber) updateMap(ctx context.Context) error {
d.nodes = make(map[string]*tailcfg.DERPNode) d.nodes = make(map[string]*tailcfg.DERPNode)
for _, reg := range d.lastDERPMap.Regions { for _, reg := range d.lastDERPMap.Regions {
for _, n := range reg.Nodes { for _, n := range reg.Nodes {
if existing, ok := d.nodes[n.HostName]; ok { if existing, ok := d.nodes[n.Name]; ok {
return fmt.Errorf("derpmap has duplicate nodes: %+v and %+v", existing, n) return fmt.Errorf("derpmap has duplicate nodes: %+v and %+v", existing, n)
} }
// Allow the prober to monitor nodes marked as // Allow the prober to monitor nodes marked as
// STUN only in the default map // STUN only in the default map
n.STUNOnly = false n.STUNOnly = false
d.nodes[n.HostName] = n d.nodes[n.Name] = n
} }
} }
return nil return nil
@ -257,13 +345,17 @@ func derpProbeUDP(ctx context.Context, ipStr string, port int) error {
return nil return nil
} }
func derpProbeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (err error) { // derpProbeBandwidth sends a payload of a given size between two local
fromc, err := newConn(ctx, dm, from) // DERP clients connected to two DERP servers.
func derpProbeBandwidth(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode, size int64) (err error) {
// This probe uses clients with isProber=false to avoid spamming the derper logs with every packet
// sent by the bandwidth probe.
fromc, err := newConn(ctx, dm, from, false)
if err != nil { if err != nil {
return err return err
} }
defer fromc.Close() defer fromc.Close()
toc, err := newConn(ctx, dm, to) toc, err := newConn(ctx, dm, to, false)
if err != nil { if err != nil {
return err return err
} }
@ -276,71 +368,147 @@ func derpProbeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailc
time.Sleep(100 * time.Millisecond) // pretty arbitrary time.Sleep(100 * time.Millisecond) // pretty arbitrary
} }
if err := runDerpProbeNodePair(ctx, from, to, fromc, toc); err != nil { if err := runDerpProbeNodePair(ctx, from, to, fromc, toc, size); err != nil {
// Record pubkeys on failed probes to aid investigation. // Record pubkeys on failed probes to aid investigation.
return fmt.Errorf("%s -> %s: %w", return fmt.Errorf("%s -> %s: %w",
fromc.SelfPublicKey().ShortString(), fromc.SelfPublicKey().ShortString(),
toc.SelfPublicKey().ShortString(), err) toc.SelfPublicKey().ShortString(), err)
} }
return err return nil
} }
func runDerpProbeNodePair(ctx context.Context, from, to *tailcfg.DERPNode, fromc, toc *derphttp.Client) error { // derpProbeNodePair sends a small packet between two local DERP clients
// Make a random packet // connected to two DERP servers.
pkt := make([]byte, 8) func derpProbeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (err error) {
crand.Read(pkt) fromc, err := newConn(ctx, dm, from, true)
if err != nil {
return err
}
defer fromc.Close()
toc, err := newConn(ctx, dm, to, true)
if err != nil {
return err
}
defer toc.Close()
// Send the random packet. // Wait a bit for from's node to hear about to existing on the
sendc := make(chan error, 1) // other node in the region, in the case where the two nodes
go func() { // are different.
sendc <- fromc.Send(toc.SelfPublicKey(), pkt) if from.Name != to.Name {
}() time.Sleep(100 * time.Millisecond) // pretty arbitrary
select {
case <-ctx.Done():
return fmt.Errorf("timeout sending via %q: %w", from.Name, ctx.Err())
case err := <-sendc:
if err != nil {
return fmt.Errorf("error sending via %q: %w", from.Name, err)
}
} }
// Receive the random packet. const meshProbePacketSize = 8
recvc := make(chan any, 1) // either derp.ReceivedPacket or error if err := runDerpProbeNodePair(ctx, from, to, fromc, toc, meshProbePacketSize); err != nil {
// Record pubkeys on failed probes to aid investigation.
return fmt.Errorf("%s -> %s: %w",
fromc.SelfPublicKey().ShortString(),
toc.SelfPublicKey().ShortString(), err)
}
return nil
}
// probePackets stores a pregenerated slice of probe packets keyed by their total size.
var probePackets syncs.Map[int64, [][]byte]
// packetsForSize returns a slice of packet payloads with a given total size.
func packetsForSize(size int64) [][]byte {
// For a small payload, create a unique random packet.
if size <= derp.MaxPacketSize {
pkt := make([]byte, size)
crand.Read(pkt)
return [][]byte{pkt}
}
// For a large payload, create a bunch of packets once and re-use them
// across probes.
pkts, _ := probePackets.LoadOrInit(size, func() [][]byte {
const packetSize = derp.MaxPacketSize
var pkts [][]byte
for remaining := size; remaining > 0; remaining -= packetSize {
pkt := make([]byte, min(remaining, packetSize))
crand.Read(pkt)
pkts = append(pkts, pkt)
}
return pkts
})
return pkts
}
// runDerpProbeNodePair takes two DERP clients (fromc and toc) connected to two
// DERP servers (from and to) and sends a test payload of a given size from one
// to another.
func runDerpProbeNodePair(ctx context.Context, from, to *tailcfg.DERPNode, fromc, toc *derphttp.Client, size int64) error {
// To avoid derper dropping enqueued packets, limit the number of packets in flight.
// The value here is slightly smaller than perClientSendQueueDepth in derp_server.go
inFlight := syncs.NewSemaphore(30)
pkts := packetsForSize(size)
// Send the packets.
sendc := make(chan error, 1)
go func() { go func() {
for idx, pkt := range pkts {
inFlight.AcquireContext(ctx)
if err := fromc.Send(toc.SelfPublicKey(), pkt); err != nil {
sendc <- fmt.Errorf("sending packet %d: %w", idx, err)
return
}
}
}()
// Receive the packets.
recvc := make(chan error, 1)
go func() {
defer close(recvc) // to break out of 'select' below.
idx := 0
for { for {
m, err := toc.Recv() m, err := toc.Recv()
if err != nil { if err != nil {
recvc <- err recvc <- fmt.Errorf("after %d data packets: %w", idx, err)
return return
} }
switch v := m.(type) { switch v := m.(type) {
case derp.ReceivedPacket: case derp.ReceivedPacket:
recvc <- v inFlight.Release()
if v.Source != fromc.SelfPublicKey() {
recvc <- fmt.Errorf("got data packet %d from unexpected source, %v", idx, v.Source)
return
}
if got, want := v.Data, pkts[idx]; !bytes.Equal(got, want) {
recvc <- fmt.Errorf("unexpected data packet %d (out of %d)", idx, len(pkts))
return
}
idx += 1
if idx == len(pkts) {
return
}
case derp.KeepAliveMessage:
// Silently ignore.
default: default:
log.Printf("%v: ignoring Recv frame type %T", to.Name, v) log.Printf("%v: ignoring Recv frame type %T", to.Name, v)
// Loop. // Loop.
} }
} }
}() }()
select { select {
case <-ctx.Done(): case <-ctx.Done():
return fmt.Errorf("timeout receiving from %q: %w", to.Name, ctx.Err()) return fmt.Errorf("timeout: %w", ctx.Err())
case v := <-recvc: case err := <-sendc:
if err, ok := v.(error); ok { if err != nil {
return fmt.Errorf("error sending via %q: %w", from.Name, err)
}
case err := <-recvc:
if err != nil {
return fmt.Errorf("error receiving from %q: %w", to.Name, err) return fmt.Errorf("error receiving from %q: %w", to.Name, err)
} }
p := v.(derp.ReceivedPacket)
if p.Source != fromc.SelfPublicKey() {
return fmt.Errorf("got data packet from unexpected source, %v", p.Source)
}
if !bytes.Equal(p.Data, pkt) {
return fmt.Errorf("unexpected data packet %q", p.Data)
}
} }
return nil return nil
} }
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) { func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode, isProber bool) (*derphttp.Client, error) {
// To avoid spamming the log with regular connection messages. // To avoid spamming the log with regular connection messages.
l := logger.Filtered(log.Printf, func(s string) bool { l := logger.Filtered(log.Printf, func(s string) bool {
return !strings.Contains(s, "derphttp.Client.Connect: connecting to") return !strings.Contains(s, "derphttp.Client.Connect: connecting to")
@ -355,7 +523,7 @@ func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*de
Nodes: []*tailcfg.DERPNode{n}, Nodes: []*tailcfg.DERPNode{n},
} }
}) })
dc.IsProber = true dc.IsProber = isProber
err := dc.Connect(ctx) err := dc.Connect(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -5,12 +5,19 @@
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls"
"encoding/json" "encoding/json"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key"
) )
func TestDerpProber(t *testing.T) { func TestDerpProber(t *testing.T) {
@ -50,18 +57,21 @@ func TestDerpProber(t *testing.T) {
clk := newFakeTime() clk := newFakeTime()
p := newForTest(clk.Now, clk.NewTicker) p := newForTest(clk.Now, clk.NewTicker)
dp := &derpProber{ dp := &derpProber{
p: p, p: p,
derpMapURL: srv.URL, derpMapURL: srv.URL,
tlsProbeFn: func(_ string) ProbeFunc { return func(context.Context) error { return nil } }, tlsInterval: time.Second,
udpProbeFn: func(_ string, _ int) ProbeFunc { return func(context.Context) error { return nil } }, tlsProbeFn: func(_ string) ProbeFunc { return func(context.Context) error { return nil } },
meshProbeFn: func(_, _ string) ProbeFunc { return func(context.Context) error { return nil } }, udpInterval: time.Second,
nodes: make(map[string]*tailcfg.DERPNode), udpProbeFn: func(_ string, _ int) ProbeFunc { return func(context.Context) error { return nil } },
probes: make(map[string]*Probe), meshInterval: time.Second,
meshProbeFn: func(_, _ string) ProbeFunc { return func(context.Context) error { return nil } },
nodes: make(map[string]*tailcfg.DERPNode),
probes: make(map[string]*Probe),
} }
if err := dp.ProbeMap(context.Background()); err != nil { if err := dp.ProbeMap(context.Background()); err != nil {
t.Errorf("unexpected ProbeMap() error: %s", err) t.Errorf("unexpected ProbeMap() error: %s", err)
} }
if len(dp.nodes) != 2 || dp.nodes["derpn1.tailscale.test"] == nil || dp.nodes["derpn2.tailscale.test"] == nil { if len(dp.nodes) != 2 || dp.nodes["n1"] == nil || dp.nodes["n2"] == nil {
t.Errorf("unexpected nodes: %+v", dp.nodes) t.Errorf("unexpected nodes: %+v", dp.nodes)
} }
// Probes expected for two nodes: // Probes expected for two nodes:
@ -103,3 +113,99 @@ func TestDerpProber(t *testing.T) {
t.Errorf("unexpected probes: %+v", dp.probes) 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)
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 i := 0; i < 5; i++ {
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)
}
})
}
}