tailscale/tstest/integration/integration_test.go
Alex Chan 63d07db733 cmd/tailscale/cli: fix race condition in up --force-reauth
This commit fixes a race condition where `tailscale up --force-reauth` would
exit prematurely on an already-logged in device.

Previously, the CLI would wait for IPN to report the "Running" state and then
exit. However, this could happen before the new auth URL was printed, leading
to two distinct issues:

*   **Without seamless key renewal:** The CLI could exit immediately after
    the `StartLoginInteractive` call, before IPN has time to switch into
    the "Starting" state or send a new auth URL back to the CLI.
*   **With seamless key renewal:** IPN stays in the "Running" state
    throughout the process, so the CLI exits immediately without performing
    any reauthentication.

The fix is to change the CLI's exit condition.

Instead of waiting for the "Running" state, if we're doing a `--force-reauth`
we now wait to see the node key change, which is a more reliable indicator
that a successful authentication has occurred.

Updates tailscale/corp#31476
Updates tailscale/tailscale#17108

Signed-off-by: Alex Chan <alexc@tailscale.com>
2025-09-17 12:33:39 +01:00

1763 lines
42 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package integration
//go:generate go run gen_deps.go
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/miekg/dns"
"go4.org/mem"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/clientupdate"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/util/must"
"tailscale.com/util/set"
)
func TestMain(m *testing.M) {
// Have to disable UPnP which hits the network, otherwise it fails due to HTTP proxy.
os.Setenv("TS_DISABLE_UPNP", "true")
flag.Parse()
v := m.Run()
if v != 0 {
os.Exit(v)
}
if err := MainError.Load(); err != nil {
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
// Tests that tailscaled starts up in TUN mode, and also without data races:
// https://github.com/tailscale/tailscale/issues/7894
func TestTUNMode(t *testing.T) {
tstest.Shard(t)
if os.Getuid() != 0 {
t.Skip("skipping when not root")
}
tstest.Parallel(t)
env := NewTestEnv(t)
env.tunMode = true
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestOneNodeUpNoAuth(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
}
func TestOneNodeExpiredKey(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
n1.AwaitRunning()
nodes := env.Control.AllNodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d nodes", len(nodes))
}
nodeKey := nodes[0].Key
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
}
cancel()
env.Control.SetExpireAllNodes(true)
n1.AwaitNeedsLogin()
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
}
cancel()
env.Control.SetExpireAllNodes(false)
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestControlKnobs(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
defer d1.MustCleanShutdown(t)
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
cmd := n1.Tailscale("debug", "control-knobs")
cmd.Stdout = nil // in case --verbose-tailscale was set
cmd.Stderr = nil // in case --verbose-tailscale was set
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err)
}
t.Logf("control-knobs output:\n%s", out)
var m map[string]any
if err := json.Unmarshal(out, &m); err != nil {
t.Fatal(err)
}
if got, want := m["DisableUPnP"], true; got != want {
t.Errorf("control-knobs DisableUPnP = %v; want %v", got, want)
}
}
func TestCollectPanic(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/15865")
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n := NewTestNode(t, env)
cmd := exec.Command(env.daemon, "--cleanup")
cmd.Env = append(os.Environ(),
"TS_PLEASE_PANIC=1",
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
)
got, _ := cmd.CombinedOutput() // we expect it to fail, ignore err
t.Logf("initial run: %s", got)
// Now we run it again, and on start, it will upload the logs to logcatcher.
cmd = exec.Command(env.daemon, "--cleanup")
cmd.Env = append(os.Environ(), "TS_LOG_TARGET="+n.env.LogCatcherServer.URL)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("cleanup failed: %v: %q", err, out)
}
if err := tstest.WaitFor(20*time.Second, func() error {
const sub = `panic`
if !n.env.LogCatcher.logsContains(mem.S(sub)) {
return fmt.Errorf("log catcher didn't see %#q; got %s", sub, n.env.LogCatcher.logsString())
}
return nil
}); err != nil {
t.Fatal(err)
}
}
func TestControlTimeLogLine(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
env.LogCatcher.StoreRawJSON()
n := NewTestNode(t, env)
n.StartDaemon()
n.AwaitResponding()
n.MustUp()
n.AwaitRunning()
if err := tstest.WaitFor(20*time.Second, func() error {
const sub = `"controltime":"2020-08-03T00:00:00.000000001Z"`
if !n.env.LogCatcher.logsContains(mem.S(sub)) {
return fmt.Errorf("log catcher didn't see %#q; got %s", sub, n.env.LogCatcher.logsString())
}
return nil
}); err != nil {
t.Fatal(err)
}
}
// test Issue 2321: Start with UpdatePrefs should save prefs to disk
func TestStateSavedOnStart(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
p1 := n1.diskPrefs()
t.Logf("Prefs1: %v", p1.Pretty())
// Bring it down, to prevent an EditPrefs call in the
// subsequent "up", as we want to test the bug when
// cmd/tailscale implements "up" via LocalBackend.Start.
n1.MustDown()
// And change the hostname to something:
if err := n1.Tailscale("up", "--login-server="+n1.env.ControlURL(), "--hostname=foo").Run(); err != nil {
t.Fatalf("up: %v", err)
}
p2 := n1.diskPrefs()
if pretty := p1.Pretty(); pretty == p2.Pretty() {
t.Errorf("Prefs didn't change on disk after 'up', still: %s", pretty)
}
if p2.Hostname != "foo" {
t.Errorf("Prefs.Hostname = %q; want foo", p2.Hostname)
}
d1.MustCleanShutdown(t)
}
func TestOneNodeUpAuth(t *testing.T) {
for _, tt := range []struct {
name string
args []string
//
// What auth key should we use for control?
authKey string
//
// Is tailscaled already logged in before we run this `up` command?
alreadyLoggedIn bool
//
// Do we need to log in again with a new /auth/ URL?
needsNewLogin bool
}{
{
name: "up",
args: []string{"up"},
needsNewLogin: true,
},
{
name: "up-with-force-reauth",
args: []string{"up", "--force-reauth"},
needsNewLogin: true,
},
{
name: "up-with-auth-key",
args: []string{"up", "--auth-key=opensesame"},
authKey: "opensesame",
needsNewLogin: false,
},
{
name: "up-with-force-reauth-and-auth-key",
args: []string{"up", "--force-reauth", "--auth-key=opensesame"},
authKey: "opensesame",
needsNewLogin: false,
},
{
name: "up-after-login",
args: []string{"up"},
alreadyLoggedIn: true,
needsNewLogin: false,
},
{
name: "up-with-force-reauth-after-login",
args: []string{"up", "--force-reauth"},
alreadyLoggedIn: true,
needsNewLogin: true,
},
{
name: "up-with-auth-key-after-login",
args: []string{"up", "--auth-key=opensesame"},
authKey: "opensesame",
alreadyLoggedIn: true,
needsNewLogin: false,
},
{
name: "up-with-force-reauth-and-auth-key-after-login",
args: []string{"up", "--force-reauth", "--auth-key=opensesame"},
authKey: "opensesame",
alreadyLoggedIn: true,
needsNewLogin: false,
},
} {
for _, useSeamlessKeyRenewal := range []bool{true, false} {
tt := tt // subtests are run in parallel, rebind tt
t.Run(fmt.Sprintf("%s-seamless-%t", tt.name, useSeamlessKeyRenewal), func(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
// TODO(alexc): Get this test case working reliably.
//
// This test case works correctly when you run it in a real CLI, but is
// quite flaky in this test suite -- the test times out waiting for the
// `tailscale up` command to complete, because IPN notifications seem
// to get lost. I suspect it's a bug somewhere in the testcontrol server,
// possibly a deadlock? But I spent a day investigating and was unable
// to fix it.
//
// These tests still represent a big increase in coverage for `tailscale up`,
// so I'm going to leave this as an exercise for a future reader.
if t.Name() == "TestOneNodeUpAuth/up-with-force-reauth-after-login-seamless-false" {
t.Skip()
}
env := NewTestEnv(t, ConfigureControl(
func(control *testcontrol.Server) {
if tt.authKey != "" {
control.RequireAuthKey = tt.authKey
} else {
control.RequireAuth = true
}
control.AllNodesSameUser = true
if useSeamlessKeyRenewal {
control.DefaultNodeCapabilities = &tailcfg.NodeCapMap{
tailcfg.NodeAttrSeamlessKeyRenewal: []tailcfg.RawMessage{},
}
}
},
))
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
defer d1.MustCleanShutdown(t)
cmdArgs := append(tt.args, "--login-server="+env.ControlURL())
// This handler looks for /auth/ URLs in the stdout from "tailscale up",
// and if it sees them, completes the auth process.
//
// It counts how many auth URLs it's seen.
var authCountAtomic atomic.Int32
authURLHandler := &authURLParserWriter{fn: func(urlStr string) error {
t.Logf("saw auth URL %q", urlStr)
if env.Control.CompleteAuth(urlStr) {
if authCountAtomic.Add(1) > 1 {
err := errors.New("completed multiple auth URLs")
t.Error(err)
return err
}
t.Logf("completed login to %s", urlStr)
return nil
} else {
err := fmt.Errorf("Failed to complete initial login to %q", urlStr)
t.Fatal(err)
return err
}
}}
// If we should be logged in at the start of the test case, go ahead
// and run the login command.
//
// Otherwise, just wait for tailscaled to be listening.
if tt.alreadyLoggedIn {
t.Logf("Running initial login: %s", strings.Join(cmdArgs, " "))
cmd := n1.Tailscale(cmdArgs...)
cmd.Stdout = authURLHandler
cmd.Stderr = cmd.Stdout
if err := cmd.Run(); err != nil {
t.Fatalf("up: %v", err)
}
authCountAtomic.Store(0)
n1.AwaitRunning()
} else {
n1.AwaitListening()
}
st := n1.MustStatus()
t.Logf("Status: %s", st.BackendState)
t.Logf("Running command: %s", strings.Join(cmdArgs, " "))
cmd := n1.Tailscale(cmdArgs...)
cmd.Stdout = authURLHandler
cmd.Stderr = cmd.Stdout
if err := cmd.Run(); err != nil {
t.Fatalf("up: %v", err)
}
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
var expectedAuthUrls int32
if tt.needsNewLogin {
expectedAuthUrls = 1
}
if n := authCountAtomic.Load(); n != expectedAuthUrls {
t.Errorf("Auth URLs completed = %d; want %d", n, expectedAuthUrls)
}
})
}
}
}
func TestConfigFileAuthKey(t *testing.T) {
tstest.SkipOnUnshardedCI(t)
tstest.Shard(t)
t.Parallel()
const authKey = "opensesame"
env := NewTestEnv(t, ConfigureControl(func(control *testcontrol.Server) {
control.RequireAuthKey = authKey
}))
n1 := NewTestNode(t, env)
n1.configFile = filepath.Join(n1.dir, "config.json")
authKeyFile := filepath.Join(n1.dir, "my-auth-key")
must.Do(os.WriteFile(authKeyFile, fmt.Appendf(nil, "%s\n", authKey), 0666))
must.Do(os.WriteFile(n1.configFile, must.Get(json.Marshal(ipn.ConfigVAlpha{
Version: "alpha0",
AuthKey: ptr.To("file:" + authKeyFile),
ServerURL: ptr.To(n1.env.ControlServer.URL),
})), 0644))
d1 := n1.StartDaemon()
n1.AwaitListening()
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
func TestTwoNodes(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
// Create two nodes:
n1 := NewTestNode(t, env)
n1SocksAddrCh := n1.socks5AddrChan()
d1 := n1.StartDaemon()
n2 := NewTestNode(t, env)
n2SocksAddrCh := n2.socks5AddrChan()
d2 := n2.StartDaemon()
// Drop some logs to disk on test failure.
//
// TODO(bradfitz): make all nodes for all tests do this? give each node a
// unique integer within the test? But for now only do this test because
// this is what we often saw flaking.
t.Cleanup(func() {
if !t.Failed() {
return
}
n1.mu.Lock()
n2.mu.Lock()
defer n1.mu.Unlock()
defer n2.mu.Unlock()
rxNoDates := regexp.MustCompile(`(?m)^\d{4}.\d{2}.\d{2}.\d{2}:\d{2}:\d{2}`)
cleanLog := func(n *TestNode) []byte {
b := n.tailscaledParser.allBuf.Bytes()
b = rxNoDates.ReplaceAll(b, nil)
return b
}
t.Logf("writing tailscaled logs to n1.log and n2.log")
os.WriteFile("n1.log", cleanLog(n1), 0666)
os.WriteFile("n2.log", cleanLog(n2), 0666)
})
n1Socks := n1.AwaitSocksAddr(n1SocksAddrCh)
n2Socks := n1.AwaitSocksAddr(n2SocksAddrCh)
t.Logf("node1 SOCKS5 addr: %v", n1Socks)
t.Logf("node2 SOCKS5 addr: %v", n2Socks)
n1.AwaitListening()
t.Logf("n1 is listening")
n2.AwaitListening()
t.Logf("n2 is listening")
n1.MustUp()
t.Logf("n1 is up")
n2.MustUp()
t.Logf("n2 is up")
n1.AwaitRunning()
t.Logf("n1 is running")
n2.AwaitRunning()
t.Logf("n2 is running")
if err := tstest.WaitFor(2*time.Second, func() error {
st := n1.MustStatus()
if len(st.Peer) == 0 {
return errors.New("no peers")
}
if len(st.Peer) > 1 {
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
}
peer := st.Peer[st.Peers()[0]]
if peer.ID == st.Self.ID {
return errors.New("peer is self")
}
if len(st.TailscaleIPs) == 0 {
return errors.New("no Tailscale IPs")
}
return nil
}); err != nil {
t.Error(err)
}
d1.MustCleanShutdown(t)
d2.MustCleanShutdown(t)
}
// tests two nodes where the first gets a incremental MapResponse (with only
// PeersRemoved set) saying that the second node disappeared.
func TestIncrementalMapUpdatePeersRemoved(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
// Create one node:
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning()
all := env.Control.AllNodes()
if len(all) != 1 {
t.Fatalf("expected 1 node, got %d nodes", len(all))
}
tnode1 := all[0]
n2 := NewTestNode(t, env)
d2 := n2.StartDaemon()
n2.AwaitListening()
n2.MustUp()
n2.AwaitRunning()
all = env.Control.AllNodes()
if len(all) != 2 {
t.Fatalf("expected 2 node, got %d nodes", len(all))
}
var tnode2 *tailcfg.Node
for _, n := range all {
if n.ID != tnode1.ID {
tnode2 = n
break
}
}
if tnode2 == nil {
t.Fatalf("failed to find second node ID (two dups?)")
}
t.Logf("node1=%v, node2=%v", tnode1.ID, tnode2.ID)
if err := tstest.WaitFor(2*time.Second, func() error {
st := n1.MustStatus()
if len(st.Peer) == 0 {
return errors.New("no peers")
}
if len(st.Peer) > 1 {
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
}
peer := st.Peer[st.Peers()[0]]
if peer.ID == st.Self.ID {
return errors.New("peer is self")
}
return nil
}); err != nil {
t.Fatal(err)
}
t.Logf("node1 saw node2")
// Now tell node1 that node2 is removed.
if !env.Control.AddRawMapResponse(tnode1.Key, &tailcfg.MapResponse{
PeersRemoved: []tailcfg.NodeID{tnode2.ID},
}) {
t.Fatalf("failed to add map response")
}
// And see that node1 saw that.
if err := tstest.WaitFor(2*time.Second, func() error {
st := n1.MustStatus()
if len(st.Peer) == 0 {
return nil
}
return fmt.Errorf("got %d peers; want 0", len(st.Peer))
}); err != nil {
t.Fatal(err)
}
t.Logf("node1 saw node2 disappear")
d1.MustCleanShutdown(t)
d2.MustCleanShutdown(t)
}
func TestNodeAddressIPFields(t *testing.T) {
tstest.Shard(t)
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/7008")
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning()
testNodes := env.Control.AllNodes()
if len(testNodes) != 1 {
t.Errorf("Expected %d nodes, got %d", 1, len(testNodes))
}
node := testNodes[0]
if len(node.Addresses) == 0 {
t.Errorf("Empty Addresses field in node")
}
if len(node.AllowedIPs) == 0 {
t.Errorf("Empty AllowedIPs field in node")
}
d1.MustCleanShutdown(t)
}
func TestAddPingRequest(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
n1.StartDaemon()
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning()
gotPing := make(chan bool, 1)
waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPing <- true
}))
defer waitPing.Close()
nodes := env.Control.AllNodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d nodes", len(nodes))
}
nodeKey := nodes[0].Key
// Check that we get at least one ping reply after 10 tries.
for try := 1; try <= 10; try++ {
t.Logf("ping %v ...", try)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
}
cancel()
pr := &tailcfg.PingRequest{URL: fmt.Sprintf("%s/ping-%d", waitPing.URL, try), Log: true}
if !env.Control.AddPingRequest(nodeKey, pr) {
t.Logf("failed to AddPingRequest")
continue
}
// Wait for PingRequest to come back
pingTimeout := time.NewTimer(2 * time.Second)
defer pingTimeout.Stop()
select {
case <-gotPing:
t.Logf("got ping; success")
return
case <-pingTimeout.C:
// Try again.
}
}
t.Error("all ping attempts failed")
}
func TestC2NPingRequest(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
gotPing := make(chan bool, 1)
env.Control.HandleC2N = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("unexpected ping method %q", r.Method)
}
got, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("ping body read error: %v", err)
}
const want = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nabc"
if string(got) != want {
t.Errorf("body error\n got: %q\nwant: %q", got, want)
}
gotPing <- true
})
n1 := NewTestNode(t, env)
n1.StartDaemon()
n1.AwaitListening()
n1.MustUp()
n1.AwaitRunning()
nodes := env.Control.AllNodes()
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d nodes", len(nodes))
}
nodeKey := nodes[0].Key
// Check that we get at least one ping reply after 10 tries.
for try := 1; try <= 10; try++ {
t.Logf("ping %v ...", try)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
t.Fatal(err)
}
cancel()
pr := &tailcfg.PingRequest{
URL: fmt.Sprintf("https://unused/some-c2n-path/ping-%d", try),
Log: true,
Types: "c2n",
Payload: []byte("POST /echo HTTP/1.0\r\nContent-Length: 3\r\n\r\nabc"),
}
if !env.Control.AddPingRequest(nodeKey, pr) {
t.Logf("failed to AddPingRequest")
continue
}
// Wait for PingRequest to come back
pingTimeout := time.NewTimer(2 * time.Second)
defer pingTimeout.Stop()
select {
case <-gotPing:
t.Logf("got ping; success")
return
case <-pingTimeout.C:
// Try again.
}
}
t.Error("all ping attempts failed")
}
// Issue 2434: when "down" (WantRunning false), tailscaled shouldn't
// be connected to control.
func TestNoControlConnWhenDown(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
// Come up the first time.
n1.MustUp()
ip1 := n1.AwaitIP4()
n1.AwaitRunning()
// Then bring it down and stop the daemon.
n1.MustDown()
d1.MustCleanShutdown(t)
env.LogCatcher.Reset()
d2 := n1.StartDaemon()
n1.AwaitResponding()
n1.AwaitBackendState("Stopped")
ip2 := n1.AwaitIP4()
if ip1 != ip2 {
t.Errorf("IPs different: %q vs %q", ip1, ip2)
}
// The real test: verify our daemon doesn't have an HTTP request open.
if n := env.Control.InServeMap(); n != 0 {
t.Errorf("in serve map = %d; want 0", n)
}
d2.MustCleanShutdown(t)
}
// Issue 2137: make sure Windows tailscaled works with the CLI alone,
// without the GUI to kick off a Start.
func TestOneNodeUpWindowsStyle(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n1 := NewTestNode(t, env)
n1.upFlagGOOS = "windows"
d1 := n1.StartDaemonAsIPNGOOS("windows")
n1.AwaitResponding()
n1.MustUp("--unattended")
t.Logf("Got IP: %v", n1.AwaitIP4())
n1.AwaitRunning()
d1.MustCleanShutdown(t)
}
// TestClientSideJailing tests that when one node is jailed for another, the
// jailed node cannot initiate connections to the other node however the other
// node can initiate connections to the jailed node.
func TestClientSideJailing(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
registerNode := func() (*TestNode, key.NodePublic) {
n := NewTestNode(t, env)
n.StartDaemon()
n.AwaitListening()
n.MustUp()
n.AwaitRunning()
k := n.MustStatus().Self.PublicKey
return n, k
}
n1, k1 := registerNode()
n2, k2 := registerNode()
ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
port := uint16(ln.Addr().(*net.TCPAddr).Port)
lc1 := &local.Client{
Socket: n1.sockFile,
UseSocketOnly: true,
}
lc2 := &local.Client{
Socket: n2.sockFile,
UseSocketOnly: true,
}
ip1 := n1.AwaitIP4()
ip2 := n2.AwaitIP4()
tests := []struct {
name string
n1JailedForN2 bool
n2JailedForN1 bool
}{
{
name: "not_jailed",
n1JailedForN2: false,
n2JailedForN1: false,
},
{
name: "uni_jailed",
n1JailedForN2: true,
n2JailedForN1: false,
},
{
name: "bi_jailed", // useless config?
n1JailedForN2: true,
n2JailedForN1: true,
},
}
testDial := func(t *testing.T, lc *local.Client, ip netip.Addr, port uint16, shouldFail bool) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c, err := lc.DialTCP(ctx, ip.String(), port)
failed := err != nil
if failed != shouldFail {
t.Errorf("failed = %v; want %v", failed, shouldFail)
}
if c != nil {
c.Close()
}
}
b1, err := lc1.WatchIPNBus(context.Background(), 0)
if err != nil {
t.Fatal(err)
}
b2, err := lc2.WatchIPNBus(context.Background(), 0)
if err != nil {
t.Fatal(err)
}
waitPeerIsJailed := func(t *testing.T, b *tailscale.IPNBusWatcher, jailed bool) {
t.Helper()
for {
n, err := b.Next()
if err != nil {
t.Fatal(err)
}
if n.NetMap == nil {
continue
}
if len(n.NetMap.Peers) == 0 {
continue
}
if j := n.NetMap.Peers[0].IsJailed(); j == jailed {
break
}
}
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
env.Control.SetJailed(k1, k2, tc.n2JailedForN1)
env.Control.SetJailed(k2, k1, tc.n1JailedForN2)
// Wait for the jailed status to propagate.
waitPeerIsJailed(t, b1, tc.n2JailedForN1)
waitPeerIsJailed(t, b2, tc.n1JailedForN2)
testDial(t, lc1, ip2, port, tc.n1JailedForN2)
testDial(t, lc2, ip1, port, tc.n2JailedForN1)
})
}
}
// TestNATPing creates two nodes, n1 and n2, sets up masquerades for both and
// tries to do bi-directional pings between them.
func TestNATPing(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/12169")
tstest.Shard(t)
tstest.Parallel(t)
for _, v6 := range []bool{false, true} {
env := NewTestEnv(t)
registerNode := func() (*TestNode, key.NodePublic) {
n := NewTestNode(t, env)
n.StartDaemon()
n.AwaitListening()
n.MustUp()
n.AwaitRunning()
k := n.MustStatus().Self.PublicKey
return n, k
}
n1, k1 := registerNode()
n2, k2 := registerNode()
var n1IP, n2IP netip.Addr
if v6 {
n1IP = n1.AwaitIP6()
n2IP = n2.AwaitIP6()
} else {
n1IP = n1.AwaitIP4()
n2IP = n2.AwaitIP4()
}
n1ExternalIP := netip.MustParseAddr("100.64.1.1")
n2ExternalIP := netip.MustParseAddr("100.64.2.1")
if v6 {
n1ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1a")
n2ExternalIP = netip.MustParseAddr("fd7a:115c:a1e0::1b")
}
tests := []struct {
name string
pairs []testcontrol.MasqueradePair
n1SeesN2IP netip.Addr
n2SeesN1IP netip.Addr
}{
{
name: "no_nat",
n1SeesN2IP: n2IP,
n2SeesN1IP: n1IP,
},
{
name: "n1_has_external_ip",
pairs: []testcontrol.MasqueradePair{
{
Node: k1,
Peer: k2,
NodeMasqueradesAs: n1ExternalIP,
},
},
n1SeesN2IP: n2IP,
n2SeesN1IP: n1ExternalIP,
},
{
name: "n2_has_external_ip",
pairs: []testcontrol.MasqueradePair{
{
Node: k2,
Peer: k1,
NodeMasqueradesAs: n2ExternalIP,
},
},
n1SeesN2IP: n2ExternalIP,
n2SeesN1IP: n1IP,
},
{
name: "both_have_external_ips",
pairs: []testcontrol.MasqueradePair{
{
Node: k1,
Peer: k2,
NodeMasqueradesAs: n1ExternalIP,
},
{
Node: k2,
Peer: k1,
NodeMasqueradesAs: n2ExternalIP,
},
},
n1SeesN2IP: n2ExternalIP,
n2SeesN1IP: n1ExternalIP,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("v6=%t/%v", v6, tc.name), func(t *testing.T) {
env.Control.SetMasqueradeAddresses(tc.pairs)
ipIdx := 0
if v6 {
ipIdx = 1
}
s1 := n1.MustStatus()
n2AsN1Peer := s1.Peer[k2]
if got := n2AsN1Peer.TailscaleIPs[ipIdx]; got != tc.n1SeesN2IP {
t.Fatalf("n1 sees n2 as %v; want %v", got, tc.n1SeesN2IP)
}
s2 := n2.MustStatus()
n1AsN2Peer := s2.Peer[k1]
if got := n1AsN2Peer.TailscaleIPs[ipIdx]; got != tc.n2SeesN1IP {
t.Fatalf("n2 sees n1 as %v; want %v", got, tc.n2SeesN1IP)
}
if err := n1.Tailscale("ping", tc.n1SeesN2IP.String()).Run(); err != nil {
t.Fatal(err)
}
if err := n1.Tailscale("ping", "-peerapi", tc.n1SeesN2IP.String()).Run(); err != nil {
t.Fatal(err)
}
if err := n2.Tailscale("ping", tc.n2SeesN1IP.String()).Run(); err != nil {
t.Fatal(err)
}
if err := n2.Tailscale("ping", "-peerapi", tc.n2SeesN1IP.String()).Run(); err != nil {
t.Fatal(err)
}
})
}
}
}
func TestLogoutRemovesAllPeers(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
// Spin up some nodes.
nodes := make([]*TestNode, 2)
for i := range nodes {
nodes[i] = NewTestNode(t, env)
nodes[i].StartDaemon()
nodes[i].AwaitResponding()
nodes[i].MustUp()
nodes[i].AwaitIP4()
nodes[i].AwaitRunning()
}
expectedPeers := len(nodes) - 1
// Make every node ping every other node.
// This makes sure magicsock is fully populated.
for i := range nodes {
for j := range nodes {
if i <= j {
continue
}
if err := tstest.WaitFor(20*time.Second, func() error {
return nodes[i].Ping(nodes[j])
}); err != nil {
t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP4(), nodes[j].AwaitIP4(), err)
}
}
}
// wantNode0PeerCount waits until node[0] status includes exactly want peers.
wantNode0PeerCount := func(want int) {
if err := tstest.WaitFor(20*time.Second, func() error {
s := nodes[0].MustStatus()
if peers := s.Peers(); len(peers) != want {
return fmt.Errorf("want %d peer(s) in status, got %v", want, peers)
}
return nil
}); err != nil {
t.Fatal(err)
}
}
wantNode0PeerCount(expectedPeers) // all other nodes are peers
nodes[0].MustLogOut()
wantNode0PeerCount(0) // node[0] is logged out, so it should not have any peers
nodes[0].MustUp() // This will create a new node
expectedPeers++
nodes[0].AwaitIP4()
wantNode0PeerCount(expectedPeers) // all existing peers and the new node
}
func TestAutoUpdateDefaults(t *testing.T) {
if !clientupdate.CanAutoUpdate() {
t.Skip("auto-updates not supported on this platform")
}
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
checkDefault := func(n *TestNode, want bool) error {
enabled, ok := n.diskPrefs().AutoUpdate.Apply.Get()
if !ok {
return fmt.Errorf("auto-update for node is unset, should be set as %v", want)
}
if enabled != want {
return fmt.Errorf("auto-update for node is %v, should be set as %v", enabled, want)
}
return nil
}
sendAndCheckDefault := func(t *testing.T, n *TestNode, send, want bool) {
t.Helper()
if !env.Control.AddRawMapResponse(n.MustStatus().Self.PublicKey, &tailcfg.MapResponse{
DefaultAutoUpdate: opt.NewBool(send),
}) {
t.Fatal("failed to send MapResponse to node")
}
if err := tstest.WaitFor(2*time.Second, func() error {
return checkDefault(n, want)
}); err != nil {
t.Fatal(err)
}
}
tests := []struct {
desc string
run func(t *testing.T, n *TestNode)
}{
{
desc: "tailnet-default-false",
run: func(t *testing.T, n *TestNode) {
// First received default "false".
sendAndCheckDefault(t, n, false, false)
// Should not be changed even if sent "true" later.
sendAndCheckDefault(t, n, true, false)
// But can be changed explicitly by the user.
if out, err := n.TailscaleForOutput("set", "--auto-update").CombinedOutput(); err != nil {
t.Fatalf("failed to enable auto-update on node: %v\noutput: %s", err, out)
}
sendAndCheckDefault(t, n, false, true)
},
},
{
desc: "tailnet-default-true",
run: func(t *testing.T, n *TestNode) {
// First received default "true".
sendAndCheckDefault(t, n, true, true)
// Should not be changed even if sent "false" later.
sendAndCheckDefault(t, n, false, true)
// But can be changed explicitly by the user.
if out, err := n.TailscaleForOutput("set", "--auto-update=false").CombinedOutput(); err != nil {
t.Fatalf("failed to disable auto-update on node: %v\noutput: %s", err, out)
}
sendAndCheckDefault(t, n, true, false)
},
},
{
desc: "user-sets-first",
run: func(t *testing.T, n *TestNode) {
// User sets auto-update first, before receiving defaults.
if out, err := n.TailscaleForOutput("set", "--auto-update=false").CombinedOutput(); err != nil {
t.Fatalf("failed to disable auto-update on node: %v\noutput: %s", err, out)
}
// Defaults sent from control should be ignored.
sendAndCheckDefault(t, n, true, false)
sendAndCheckDefault(t, n, false, false)
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
n := NewTestNode(t, env)
d := n.StartDaemon()
defer d.MustCleanShutdown(t)
n.AwaitResponding()
n.MustUp()
n.AwaitRunning()
tt.run(t, n)
})
}
}
// TestDNSOverTCPIntervalResolver tests that the quad-100 resolver successfully
// serves TCP queries. It exercises the host's TCP stack, a TUN device, and
// gVisor/netstack.
// https://github.com/tailscale/corp/issues/22511
func TestDNSOverTCPIntervalResolver(t *testing.T) {
tstest.Shard(t)
if os.Getuid() != 0 {
t.Skip("skipping when not root")
}
env := NewTestEnv(t)
env.tunMode = true
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
n1.AwaitRunning()
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
cases := []struct {
network string
serviceAddr netip.Addr
}{
{
"tcp4",
tsaddr.TailscaleServiceIP(),
},
{
"tcp6",
tsaddr.TailscaleServiceIPv6(),
},
}
for _, c := range cases {
err := tstest.WaitFor(time.Second*5, func() error {
m := new(dns.Msg)
m.SetQuestion(dnsSymbolicFQDN, dns.TypeA)
conn, err := net.DialTimeout(c.network, net.JoinHostPort(c.serviceAddr.String(), "53"), time.Second*1)
if err != nil {
return err
}
defer conn.Close()
dnsConn := &dns.Conn{
Conn: conn,
}
dnsClient := &dns.Client{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, _, err := dnsClient.ExchangeWithConnContext(ctx, m, dnsConn)
if err != nil {
return err
}
if len(resp.Answer) != 1 {
return fmt.Errorf("unexpected DNS resp: %s", resp)
}
var gotAddr net.IP
answer, ok := resp.Answer[0].(*dns.A)
if !ok {
return fmt.Errorf("unexpected answer type: %s", resp.Answer[0])
}
gotAddr = answer.A
if !bytes.Equal(gotAddr, tsaddr.TailscaleServiceIP().AsSlice()) {
return fmt.Errorf("got (%s) != want (%s)", gotAddr, tsaddr.TailscaleServiceIP())
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
d1.MustCleanShutdown(t)
}
// TestNetstackTCPLoopback tests netstack loopback of a TCP stream, in both
// directions.
func TestNetstackTCPLoopback(t *testing.T) {
tstest.Shard(t)
if os.Getuid() != 0 {
t.Skip("skipping when not root")
}
env := NewTestEnv(t)
env.tunMode = true
loopbackPort := 5201
env.loopbackPort = &loopbackPort
loopbackPortStr := strconv.Itoa(loopbackPort)
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
n1.AwaitIP4()
n1.AwaitRunning()
cases := []struct {
lisAddr string
network string
dialAddr string
}{
{
lisAddr: net.JoinHostPort("127.0.0.1", loopbackPortStr),
network: "tcp4",
dialAddr: net.JoinHostPort(tsaddr.TailscaleServiceIPString, loopbackPortStr),
},
{
lisAddr: net.JoinHostPort("::1", loopbackPortStr),
network: "tcp6",
dialAddr: net.JoinHostPort(tsaddr.TailscaleServiceIPv6String, loopbackPortStr),
},
}
writeBufSize := 128 << 10 // 128KiB, exercise GSO if enabled
writeBufIterations := 100 // allow TCP send window to open up
wantTotal := writeBufSize * writeBufIterations
for _, c := range cases {
lis, err := net.Listen(c.network, c.lisAddr)
if err != nil {
t.Fatal(err)
}
defer lis.Close()
writeFn := func(conn net.Conn) error {
for i := 0; i < writeBufIterations; i++ {
toWrite := make([]byte, writeBufSize)
var wrote int
for {
n, err := conn.Write(toWrite)
if err != nil {
return err
}
wrote += n
if wrote == len(toWrite) {
break
}
}
}
return nil
}
readFn := func(conn net.Conn) error {
var read int
for {
b := make([]byte, writeBufSize)
n, err := conn.Read(b)
if err != nil {
return err
}
read += n
if read == wantTotal {
return nil
}
}
}
lisStepCh := make(chan error)
go func() {
conn, err := lis.Accept()
if err != nil {
lisStepCh <- err
return
}
lisStepCh <- readFn(conn)
lisStepCh <- writeFn(conn)
}()
var conn net.Conn
err = tstest.WaitFor(time.Second*5, func() error {
conn, err = net.DialTimeout(c.network, c.dialAddr, time.Second*1)
if err != nil {
return err
}
return nil
})
if err != nil {
t.Fatal(err)
}
defer conn.Close()
dialerStepCh := make(chan error)
go func() {
dialerStepCh <- writeFn(conn)
dialerStepCh <- readFn(conn)
}()
var (
dialerSteps int
lisSteps int
)
for {
select {
case lisErr := <-lisStepCh:
if lisErr != nil {
t.Fatal(err)
}
lisSteps++
if dialerSteps == 2 && lisSteps == 2 {
return
}
case dialerErr := <-dialerStepCh:
if dialerErr != nil {
t.Fatal(err)
}
dialerSteps++
if dialerSteps == 2 && lisSteps == 2 {
return
}
}
}
}
d1.MustCleanShutdown(t)
}
// TestNetstackUDPLoopback tests netstack loopback of UDP packets, in both
// directions.
func TestNetstackUDPLoopback(t *testing.T) {
tstest.Shard(t)
if os.Getuid() != 0 {
t.Skip("skipping when not root")
}
env := NewTestEnv(t)
env.tunMode = true
loopbackPort := 5201
env.loopbackPort = &loopbackPort
n1 := NewTestNode(t, env)
d1 := n1.StartDaemon()
n1.AwaitResponding()
n1.MustUp()
ip4 := n1.AwaitIP4()
ip6 := n1.AwaitIP6()
n1.AwaitRunning()
cases := []struct {
pingerLAddr *net.UDPAddr
pongerLAddr *net.UDPAddr
network string
dialAddr *net.UDPAddr
}{
{
pingerLAddr: &net.UDPAddr{IP: ip4.AsSlice(), Port: loopbackPort + 1},
pongerLAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: loopbackPort},
network: "udp4",
dialAddr: &net.UDPAddr{IP: tsaddr.TailscaleServiceIP().AsSlice(), Port: loopbackPort},
},
{
pingerLAddr: &net.UDPAddr{IP: ip6.AsSlice(), Port: loopbackPort + 1},
pongerLAddr: &net.UDPAddr{IP: net.ParseIP("::1"), Port: loopbackPort},
network: "udp6",
dialAddr: &net.UDPAddr{IP: tsaddr.TailscaleServiceIPv6().AsSlice(), Port: loopbackPort},
},
}
writeBufSize := int(tstun.DefaultTUNMTU()) - 40 - 8 // mtu - ipv6 header - udp header
wantPongs := 100
for _, c := range cases {
pongerConn, err := net.ListenUDP(c.network, c.pongerLAddr)
if err != nil {
t.Fatal(err)
}
defer pongerConn.Close()
var pingerConn *net.UDPConn
err = tstest.WaitFor(time.Second*5, func() error {
pingerConn, err = net.DialUDP(c.network, c.pingerLAddr, c.dialAddr)
return err
})
if err != nil {
t.Fatal(err)
}
defer pingerConn.Close()
pingerFn := func(conn *net.UDPConn) error {
b := make([]byte, writeBufSize)
n, err := conn.Write(b)
if err != nil {
return err
}
if n != len(b) {
return fmt.Errorf("bad write size: %d", n)
}
err = conn.SetReadDeadline(time.Now().Add(time.Millisecond * 500))
if err != nil {
return err
}
n, err = conn.Read(b)
if err != nil {
return err
}
if n != len(b) {
return fmt.Errorf("bad read size: %d", n)
}
return nil
}
pongerFn := func(conn *net.UDPConn) error {
for {
b := make([]byte, writeBufSize)
n, from, err := conn.ReadFromUDP(b)
if err != nil {
return err
}
if n != len(b) {
return fmt.Errorf("bad read size: %d", n)
}
n, err = conn.WriteToUDP(b, from)
if err != nil {
return err
}
if n != len(b) {
return fmt.Errorf("bad write size: %d", n)
}
}
}
pongerErrCh := make(chan error, 1)
go func() {
pongerErrCh <- pongerFn(pongerConn)
}()
err = tstest.WaitFor(time.Second*5, func() error {
err = pingerFn(pingerConn)
if err != nil {
return err
}
return nil
})
if err != nil {
t.Fatal(err)
}
var pongsRX int
for {
pingerErrCh := make(chan error)
go func() {
pingerErrCh <- pingerFn(pingerConn)
}()
select {
case err := <-pongerErrCh:
t.Fatal(err)
case err := <-pingerErrCh:
if err != nil {
t.Fatal(err)
}
}
pongsRX++
if pongsRX == wantPongs {
break
}
}
}
d1.MustCleanShutdown(t)
}
func TestEncryptStateMigration(t *testing.T) {
if !hostinfo.New().TPM.Present() {
t.Skip("TPM not available")
}
if runtime.GOOS != "linux" && runtime.GOOS != "windows" {
t.Skip("--encrypt-state for tailscaled state not supported on this platform")
}
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t)
n := NewTestNode(t, env)
runNode := func(t *testing.T, wantStateKeys []string) {
t.Helper()
// Run the node.
d := n.StartDaemon()
n.AwaitResponding()
n.MustUp()
n.AwaitRunning()
// Check the contents of the state file.
buf, err := os.ReadFile(n.stateFile)
if err != nil {
t.Fatalf("reading %q: %v", n.stateFile, err)
}
t.Logf("state file content:\n%s", buf)
var content map[string]any
if err := json.Unmarshal(buf, &content); err != nil {
t.Fatalf("parsing %q: %v", n.stateFile, err)
}
for _, k := range wantStateKeys {
if _, ok := content[k]; !ok {
t.Errorf("state file is missing key %q", k)
}
}
// Stop the node.
d.MustCleanShutdown(t)
}
wantPlaintextStateKeys := []string{"_machinekey", "_current-profile", "_profiles"}
wantEncryptedStateKeys := []string{"key", "nonce", "data"}
t.Run("regular-state", func(t *testing.T) {
n.encryptState = false
runNode(t, wantPlaintextStateKeys)
})
t.Run("migrate-to-encrypted", func(t *testing.T) {
n.encryptState = true
runNode(t, wantEncryptedStateKeys)
})
t.Run("migrate-to-plaintext", func(t *testing.T) {
n.encryptState = false
runNode(t, wantPlaintextStateKeys)
})
}
// TestPeerRelayPing creates three nodes with one acting as a peer relay.
// The test succeeds when "tailscale ping" flows through the peer
// relay between all 3 nodes.
func TestPeerRelayPing(t *testing.T) {
tstest.Shard(t)
tstest.Parallel(t)
env := NewTestEnv(t, ConfigureControl(func(server *testcontrol.Server) {
server.PeerRelayGrants = true
}))
env.neverDirectUDP = true
env.relayServerUseLoopback = true
n1 := NewTestNode(t, env)
n2 := NewTestNode(t, env)
peerRelay := NewTestNode(t, env)
allNodes := []*TestNode{n1, n2, peerRelay}
wantPeerRelayServers := make(set.Set[string])
for _, n := range allNodes {
n.StartDaemon()
n.AwaitResponding()
n.MustUp()
wantPeerRelayServers.Add(n.AwaitIP4().String())
n.AwaitRunning()
}
if err := peerRelay.Tailscale("set", "--relay-server-port=0").Run(); err != nil {
t.Fatal(err)
}
errCh := make(chan error)
for _, a := range allNodes {
go func() {
err := tstest.WaitFor(time.Second*5, func() error {
out, err := a.Tailscale("debug", "peer-relay-servers").CombinedOutput()
if err != nil {
return fmt.Errorf("debug peer-relay-servers failed: %v", err)
}
servers := make([]string, 0)
err = json.Unmarshal(out, &servers)
if err != nil {
return fmt.Errorf("failed to unmarshal debug peer-relay-servers: %v", err)
}
gotPeerRelayServers := make(set.Set[string])
for _, server := range servers {
gotPeerRelayServers.Add(server)
}
if !gotPeerRelayServers.Equal(wantPeerRelayServers) {
return fmt.Errorf("got peer relay servers: %v want: %v", gotPeerRelayServers, wantPeerRelayServers)
}
return nil
})
errCh <- err
}()
}
for range allNodes {
err := <-errCh
if err != nil {
t.Fatal(err)
}
}
pingPairs := make([][2]*TestNode, 0)
for _, a := range allNodes {
for _, z := range allNodes {
if a == z {
continue
}
pingPairs = append(pingPairs, [2]*TestNode{a, z})
}
}
for _, pair := range pingPairs {
go func() {
a := pair[0]
z := pair[1]
err := tstest.WaitFor(time.Second*10, func() error {
remoteKey := z.MustStatus().Self.PublicKey
if err := a.Tailscale("ping", "--until-direct=false", "--c=1", "--timeout=1s", z.AwaitIP4().String()).Run(); err != nil {
return err
}
remotePeer, ok := a.MustStatus().Peer[remoteKey]
if !ok {
return fmt.Errorf("%v->%v remote peer not found", a.MustStatus().Self.ID, z.MustStatus().Self.ID)
}
if len(remotePeer.PeerRelay) == 0 {
return fmt.Errorf("%v->%v not using peer relay, curAddr=%v relay=%v", a.MustStatus().Self.ID, z.MustStatus().Self.ID, remotePeer.CurAddr, remotePeer.Relay)
}
t.Logf("%v->%v using peer relay addr: %v", a.MustStatus().Self.ID, z.MustStatus().Self.ID, remotePeer.PeerRelay)
return nil
})
errCh <- err
}()
}
for range pingPairs {
err := <-errCh
if err != nil {
t.Fatal(err)
}
}
}