mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
44b07aa708
If Australia's far away and not going to be used, it's still going to be far away a minute later. No need to send backup just-in-case-UDP-gets-lost STUN packets to the known far away destinations. Those are the ones most likely to trigger retries due to delay anyway (in random 50-250ms, currently). But we'll keep sending 1 packet to them, just in case our airplane landed. Likewise, be less aggressive with IPv6. The main point is just to see whether IPv6 works. No need to send up to 10 packets every round. Max two is enough (except for the first round). This does mean our STUN traffic graphs for IPv4-vs-IPv6 will change shape. Oh well. It was a weird eyeball metric for IPv6 connectivity anyway and we have better metrics. We can tweak this policy over time. It's factored out and has tests now.
293 lines
6.4 KiB
Go
293 lines
6.4 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package netcheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/derp/derpmap"
|
|
"tailscale.com/stun"
|
|
"tailscale.com/stun/stuntest"
|
|
)
|
|
|
|
func TestHairpinSTUN(t *testing.T) {
|
|
c := &Client{
|
|
hairTX: stun.NewTxID(),
|
|
gotHairSTUN: make(chan *net.UDPAddr, 1),
|
|
}
|
|
req := stun.Request(c.hairTX)
|
|
if !stun.Is(req) {
|
|
t.Fatal("expected STUN message")
|
|
}
|
|
if !c.handleHairSTUN(req, nil) {
|
|
t.Fatal("expected true")
|
|
}
|
|
select {
|
|
case <-c.gotHairSTUN:
|
|
default:
|
|
t.Fatal("expected value")
|
|
}
|
|
}
|
|
|
|
func TestBasic(t *testing.T) {
|
|
stunAddr, cleanup := stuntest.Serve(t)
|
|
defer cleanup()
|
|
|
|
c := &Client{
|
|
DERP: derpmap.NewTestWorld(stunAddr),
|
|
Logf: t.Logf,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
r, err := c.GetReport(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !r.UDP {
|
|
t.Error("want UDP")
|
|
}
|
|
if len(r.DERPLatency) != 1 {
|
|
t.Errorf("expected 1 key in DERPLatency; got %+v", r.DERPLatency)
|
|
}
|
|
if _, ok := r.DERPLatency[stunAddr]; !ok {
|
|
t.Errorf("expected key %q in DERPLatency; got %+v", stunAddr, r.DERPLatency)
|
|
}
|
|
if r.GlobalV4 == "" {
|
|
t.Error("expected GlobalV4 set")
|
|
}
|
|
if r.PreferredDERP != 1 {
|
|
t.Errorf("PreferredDERP = %v; want 1", r.PreferredDERP)
|
|
}
|
|
}
|
|
|
|
func TestWorksWhenUDPBlocked(t *testing.T) {
|
|
blackhole, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("failed to open blackhole STUN listener: %v", err)
|
|
}
|
|
defer blackhole.Close()
|
|
|
|
stunAddr := blackhole.LocalAddr().String()
|
|
|
|
c := &Client{
|
|
DERP: derpmap.NewTestWorld(stunAddr),
|
|
Logf: t.Logf,
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
|
|
defer cancel()
|
|
|
|
r, err := c.GetReport(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := &Report{
|
|
DERPLatency: map[string]time.Duration{},
|
|
}
|
|
|
|
if !reflect.DeepEqual(r, want) {
|
|
t.Errorf("mismatch\n got: %+v\nwant: %+v\n", r, want)
|
|
}
|
|
}
|
|
|
|
func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
|
|
derps := derpmap.NewTestWorldWith(
|
|
&derpmap.Server{
|
|
ID: 1,
|
|
STUN4: "d1:1",
|
|
},
|
|
&derpmap.Server{
|
|
ID: 2,
|
|
STUN4: "d2:1",
|
|
},
|
|
&derpmap.Server{
|
|
ID: 3,
|
|
STUN4: "d3:1",
|
|
},
|
|
)
|
|
// report returns a *Report from (DERP host, time.Duration)+ pairs.
|
|
report := func(a ...interface{}) *Report {
|
|
r := &Report{DERPLatency: map[string]time.Duration{}}
|
|
for i := 0; i < len(a); i += 2 {
|
|
k := a[i].(string) + ":1"
|
|
switch v := a[i+1].(type) {
|
|
case time.Duration:
|
|
r.DERPLatency[k] = v
|
|
case int:
|
|
r.DERPLatency[k] = time.Second * time.Duration(v)
|
|
default:
|
|
panic(fmt.Sprintf("unexpected type %T", v))
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
type step struct {
|
|
after time.Duration
|
|
r *Report
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
steps []step
|
|
wantDERP int // want PreferredDERP on final step
|
|
wantPrevLen int // wanted len(c.prev)
|
|
}{
|
|
{
|
|
name: "first_reading",
|
|
steps: []step{
|
|
{0, report("d1", 2, "d2", 3)},
|
|
},
|
|
wantPrevLen: 1,
|
|
wantDERP: 1,
|
|
},
|
|
{
|
|
name: "with_two",
|
|
steps: []step{
|
|
{0, report("d1", 2, "d2", 3)},
|
|
{1 * time.Second, report("d1", 4, "d2", 3)},
|
|
},
|
|
wantPrevLen: 2,
|
|
wantDERP: 1, // t0's d1 of 2 is still best
|
|
},
|
|
{
|
|
name: "but_now_d1_gone",
|
|
steps: []step{
|
|
{0, report("d1", 2, "d2", 3)},
|
|
{1 * time.Second, report("d1", 4, "d2", 3)},
|
|
{2 * time.Second, report("d2", 3)},
|
|
},
|
|
wantPrevLen: 3,
|
|
wantDERP: 2, // only option
|
|
},
|
|
{
|
|
name: "d1_is_back",
|
|
steps: []step{
|
|
{0, report("d1", 2, "d2", 3)},
|
|
{1 * time.Second, report("d1", 4, "d2", 3)},
|
|
{2 * time.Second, report("d2", 3)},
|
|
{3 * time.Second, report("d1", 4, "d2", 3)}, // same as 2 seconds ago
|
|
},
|
|
wantPrevLen: 4,
|
|
wantDERP: 1, // t0's d1 of 2 is still best
|
|
},
|
|
{
|
|
name: "things_clean_up",
|
|
steps: []step{
|
|
{0, report("d1", 1, "d2", 2)},
|
|
{1 * time.Second, report("d1", 1, "d2", 2)},
|
|
{2 * time.Second, report("d1", 1, "d2", 2)},
|
|
{3 * time.Second, report("d1", 1, "d2", 2)},
|
|
{10 * time.Minute, report("d3", 3)},
|
|
},
|
|
wantPrevLen: 1, // t=[0123]s all gone. (too old, older than 10 min)
|
|
wantDERP: 3, // only option
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fakeTime := time.Unix(123, 0)
|
|
c := &Client{
|
|
DERP: derps,
|
|
TimeNow: func() time.Time { return fakeTime },
|
|
}
|
|
for _, s := range tt.steps {
|
|
fakeTime = fakeTime.Add(s.after)
|
|
c.addReportHistoryAndSetPreferredDERP(s.r)
|
|
}
|
|
lastReport := tt.steps[len(tt.steps)-1].r
|
|
if got, want := len(c.prev), tt.wantPrevLen; got != want {
|
|
t.Errorf("len(prev) = %v; want %v", got, want)
|
|
}
|
|
if got, want := lastReport.PreferredDERP, tt.wantDERP; got != want {
|
|
t.Errorf("PreferredDERP = %v; want %v", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPickSubset(t *testing.T) {
|
|
derps := derpmap.NewTestWorldWith(
|
|
&derpmap.Server{
|
|
ID: 1,
|
|
STUN4: "d1:4",
|
|
STUN6: "d1:6",
|
|
},
|
|
&derpmap.Server{
|
|
ID: 2,
|
|
STUN4: "d2:4",
|
|
STUN6: "d2:6",
|
|
},
|
|
&derpmap.Server{
|
|
ID: 3,
|
|
STUN4: "d3:4",
|
|
STUN6: "d3:6",
|
|
},
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
last *Report
|
|
want4 []string
|
|
want6 []string
|
|
wantTries map[string]int
|
|
}{
|
|
{
|
|
name: "fresh",
|
|
last: nil,
|
|
want4: []string{"d1:4", "d2:4", "d3:4"},
|
|
want6: []string{"d1:6", "d2:6", "d3:6"},
|
|
wantTries: map[string]int{
|
|
"d1:4": 2,
|
|
"d2:4": 2,
|
|
"d3:4": 2,
|
|
"d1:6": 1,
|
|
"d2:6": 1,
|
|
"d3:6": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "1_and_3_closest",
|
|
last: &Report{
|
|
DERPLatency: map[string]time.Duration{
|
|
"d1:4": 15 * time.Millisecond,
|
|
"d2:4": 300 * time.Millisecond,
|
|
"d3:4": 25 * time.Millisecond,
|
|
},
|
|
},
|
|
want4: []string{"d1:4", "d2:4", "d3:4"},
|
|
want6: []string{"d1:6", "d3:6"},
|
|
wantTries: map[string]int{
|
|
"d1:4": 2,
|
|
"d3:4": 2,
|
|
"d2:4": 1,
|
|
"d1:6": 1,
|
|
"d3:6": 1,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Client{DERP: derps, last: tt.last}
|
|
got4, got6, gotTries, err := c.pickSubset()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got4, tt.want4) {
|
|
t.Errorf("stuns4 = %q; want %q", got4, tt.want4)
|
|
}
|
|
if !reflect.DeepEqual(got6, tt.want6) {
|
|
t.Errorf("stuns6 = %q; want %q", got6, tt.want6)
|
|
}
|
|
if !reflect.DeepEqual(gotTries, tt.wantTries) {
|
|
t.Errorf("tries = %v; want %v", gotTries, tt.wantTries)
|
|
}
|
|
})
|
|
}
|
|
}
|