diff --git a/.github/workflows/natlab-test.yml b/.github/workflows/natlab-test.yml index 4bbd8e1ad..16412a87e 100644 --- a/.github/workflows/natlab-test.yml +++ b/.github/workflows/natlab-test.yml @@ -131,7 +131,7 @@ jobs: # compileBinariesForOS in vmtest.go. - name: Warm Go build cache run: | - ./tool/go test -c -o /dev/null ./tstest/natlab/vmtest ./tstest/integration/nat + ./tool/go test -c -o /dev/null ./tstest/natlab/vmtest for pkg in tta tailscale tailscaled; do GOOS=linux GOARCH=amd64 CGO_ENABLED=0 ./tool/go build -o /dev/null ./cmd/"$pkg" done @@ -148,9 +148,9 @@ jobs: # single-test-per-matrix-job model. They stay runnable locally. run: | set -euo pipefail - exclude='^(TestGrid|TestVnetPerf.*)$' + exclude='^(TestGrid|TestPair|TestVnetPerf.*)$' tmp=$(mktemp) - for pkg_dir in tstest/natlab/vmtest tstest/integration/nat; do + for pkg_dir in tstest/natlab/vmtest; do pkg="./${pkg_dir}/" for f in "${pkg_dir}"/*_test.go; do [ -e "$f" ] || continue diff --git a/tstest/integration/nat/nat_test.go b/tstest/integration/nat/nat_test.go deleted file mode 100644 index 520d1f093..000000000 --- a/tstest/integration/nat/nat_test.go +++ /dev/null @@ -1,699 +0,0 @@ -// Copyright (c) Tailscale Inc & contributors -// SPDX-License-Identifier: BSD-3-Clause - -package nat - -import ( - "bytes" - "cmp" - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "io" - "net" - "net/http" - "net/netip" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "sync" - "testing" - "time" - - "golang.org/x/mod/modfile" - "golang.org/x/sync/errgroup" - "tailscale.com/client/tailscale" - "tailscale.com/ipn/ipnstate" - "tailscale.com/syncs" - "tailscale.com/tailcfg" - "tailscale.com/tstest/natlab/vnet" -) - -var ( - runVMTests = flag.Bool("run-vm-tests", false, "run tests that require a VM") - logTailscaled = flag.Bool("log-tailscaled", false, "log tailscaled output") - pcapFile = flag.String("pcap", "", "write pcap to file") -) - -type natTest struct { - tb testing.TB - base string // base image - tempDir string // for qcow2 images - vnet *vnet.Server - kernel string // linux kernel path - - gotRoute pingRoute -} - -func newNatTest(tb testing.TB) *natTest { - root, err := os.Getwd() - if err != nil { - tb.Fatal(err) - } - modRoot := filepath.Join(root, "../../..") - - nt := &natTest{ - tb: tb, - tempDir: tb.TempDir(), - base: filepath.Join(modRoot, "gokrazy/natlabapp.qcow2"), - } - - if !*runVMTests { - tb.Skip("skipping heavy test; set --run-vm-tests to run") - } - - if _, err := os.Stat(nt.base); err != nil { - if !os.IsNotExist(err) { - tb.Fatal(err) - } - tb.Logf("building VM image...") - cmd := exec.Command("make", "natlab") - cmd.Dir = filepath.Join(modRoot, "gokrazy") - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - if err := cmd.Run(); err != nil { - tb.Fatalf("Error running 'make natlab' in gokrazy directory: %v", err) - } - if _, err := os.Stat(nt.base); err != nil { - tb.Skipf("still can't find VM image: %v", err) - } - } - - nt.kernel, err = findKernelPath(filepath.Join(modRoot, "go.mod")) - if err != nil { - tb.Skipf("skipping test; kernel not found: %v", err) - } - tb.Logf("found kernel: %v", nt.kernel) - - return nt -} - -func findKernelPath(goMod string) (string, error) { - b, err := os.ReadFile(goMod) - if err != nil { - return "", err - } - mf, err := modfile.Parse("go.mod", b, nil) - if err != nil { - return "", err - } - goModB, err := exec.Command("go", "env", "GOMODCACHE").CombinedOutput() - if err != nil { - return "", err - } - for _, r := range mf.Require { - if r.Mod.Path == "github.com/gokrazy/kernel.amd64" { - return strings.TrimSpace(string(goModB)) + "/" + r.Mod.String() + "/vmlinuz", nil - } - } - return "", fmt.Errorf("failed to find kernel in %v", goMod) -} - -type addNodeFunc func(c *vnet.Config) *vnet.Node // returns nil to omit test - -func easy(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT)) -} - -func easyAF(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyAFNAT)) -} - -func sameLAN(c *vnet.Config) *vnet.Node { - nw := c.FirstNetwork() - if nw == nil { - return nil - } - if !nw.CanTakeMoreNodes() { - return nil - } - return c.AddNode(nw) -} - -func one2one(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("172.16.%d.1/24", n), vnet.One2OneNAT)) -} - -func easyPMP(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP)) -} - -func hard(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("10.0.%d.1/24", n), vnet.HardNAT)) -} - -func hardPMP(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("10.7.%d.1/24", n), vnet.HardNAT, vnet.NATPMP)) -} - -func (nt *natTest) setupTest(ctx context.Context, addNode ...addNodeFunc) (nodes []*vnet.Node, clients []*vnet.NodeAgentClient, cleanup func()) { - if len(addNode) < 1 || len(addNode) > 2 { - nt.tb.Fatalf("runTest: invalid number of nodes %v; want 1 or 2", len(addNode)) - } - t := nt.tb - - var c vnet.Config - c.SetPCAPFile(*pcapFile) - for _, fn := range addNode { - node := fn(&c) - if node == nil { - t.Skip("skipping test; not applicable combination") - } - nodes = append(nodes, node) - if *logTailscaled { - node.SetVerboseSyslog(true) - } - } - - var err error - nt.vnet, err = vnet.New(&c) - if err != nil { - t.Fatalf("newServer: %v", err) - } - nt.tb.Cleanup(func() { - nt.vnet.Close() - }) - - var wg sync.WaitGroup // waiting for srv.Accept goroutine - defer wg.Wait() - - sockAddr := filepath.Join(nt.tempDir, "qemu.sock") - srv, err := net.Listen("unix", sockAddr) - if err != nil { - t.Fatalf("Listen: %v", err) - } - defer srv.Close() - - wg.Go(func() { - for { - c, err := srv.Accept() - if err != nil { - return - } - go nt.vnet.ServeUnixConn(c.(*net.UnixConn), vnet.ProtocolQEMU) - } - }) - - haveKVM := false - if runtime.GOOS == "linux" { - if f, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0); err == nil { - f.Close() - haveKVM = true - } - } - - qmpSocks := make([]string, len(nodes)) - for i, node := range nodes { - disk := fmt.Sprintf("%s/node-%d.qcow2", nt.tempDir, i) - out, err := exec.Command("qemu-img", "create", - "-f", "qcow2", - "-F", "qcow2", - "-b", nt.base, - disk).CombinedOutput() - if err != nil { - t.Fatalf("qemu-img create: %v, %s", err, out) - } - - var envBuf bytes.Buffer - for _, e := range node.Env() { - fmt.Fprintf(&envBuf, " tailscaled.env=%s=%s", e.Key, e.Value) - } - sysLogAddr := net.JoinHostPort(vnet.FakeSyslogIPv4().String(), "995") - if node.IsV6Only() { - fmt.Fprintf(&envBuf, " tta.nameserver=%s", vnet.FakeDNSIPv6()) - sysLogAddr = net.JoinHostPort(vnet.FakeSyslogIPv6().String(), "995") - } - envStr := envBuf.String() - - qmpSocks[i] = fmt.Sprintf("%s/qmp-node-%d.sock", nt.tempDir, i) - qemuArgs := []string{ - "-M", "microvm,isa-serial=off", - "-m", "384M", - "-nodefaults", "-no-user-config", "-nographic", - "-kernel", nt.kernel, - "-append", "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-76baa2d60001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb gokrazy.remote_syslog.target=" + sysLogAddr + " tailscale-tta=1" + envStr, - "-drive", "id=blk0,file=" + disk + ",format=qcow2", - "-device", "virtio-blk-device,drive=blk0", - "-netdev", "stream,id=net0,addr.type=unix,addr.path=" + sockAddr, - "-device", "virtio-serial-device", - "-device", "virtio-rng-device", - "-device", "virtio-net-device,netdev=net0,mac=" + node.MAC().String(), - "-chardev", "stdio,id=virtiocon0,mux=on", - "-device", "virtconsole,chardev=virtiocon0", - "-mon", "chardev=virtiocon0,mode=readline", - "-qmp", "unix:" + qmpSocks[i] + ",server=on,wait=off", - } - if haveKVM { - qemuArgs = append(qemuArgs, "-enable-kvm", "-cpu", "host") - } - cmd := exec.Command("qemu-system-x86_64", qemuArgs...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Start(); err != nil { - t.Fatalf("qemu: %v", err) - } - nt.tb.Cleanup(func() { - cmd.Process.Kill() - cmd.Wait() - }) - } - - for i, node := range nodes { - if err := nt.vnet.AwaitFirstPacket(ctx, node.MAC()); err != nil { - t.Logf("node %v: no boot progress (no packets received): %v", node, err) - t.Logf("node %v: QMP status: %s", node, qmpQueryStatus(qmpSocks[i])) - t.FailNow() - } - t.Logf("node %v: boot detected (first packet received)", node) - } - - for _, n := range nodes { - client := nt.vnet.NodeAgentClient(n) - n.SetClient(client) - clients = append(clients, client) - } - - var eg errgroup.Group - for i, c := range clients { - eg.Go(func() error { - node := nodes[i] - t.Logf("%v calling Status...", node) - st, err := c.Status(ctx) - if err != nil { - return fmt.Errorf("%v status: %w", node, err) - } - t.Logf("%v status: %v", node, st.BackendState) - - if node.HostFirewall() { - if err := c.EnableHostFirewall(ctx); err != nil { - return fmt.Errorf("%v firewall: %w", node, err) - } - t.Logf("%v firewalled", node) - } - - if node.ShouldJoinTailnet() { - if err := up(ctx, c); err != nil { - return fmt.Errorf("%v up: %w", node, err) - } - t.Logf("%v up!", node) - - st, err = c.Status(ctx) - if err != nil { - return fmt.Errorf("%v status: %w", node, err) - } - - if capMap := node.WantCapMap(); capMap != nil { - nt.tb.Logf("using capmap for %s: %+v", node.String(), capMap) - nt.vnet.ControlServer().SetNodeCapMap(st.Self.PublicKey, capMap) - } - - if st.BackendState != "Running" { - return fmt.Errorf("%v state = %q", node, st.BackendState) - } - - t.Logf("%v AllowedIPs: %v", node, st.Self.Addrs) - t.Logf("%v up with %v", node, st.Self.TailscaleIPs) - } else { - t.Logf("%v skipping joining tailnet", node) - } - return nil - }) - } - if err := eg.Wait(); err != nil { - t.Fatalf("initial setup: %v", err) - } - - return nodes, clients, nt.vnet.Close -} - -type hasDeadline interface { - Deadline() (deadline time.Time, ok bool) -} - -// testContext returns a context derived from the test's deadline (from -timeout), -// leaving a small margin for cleanup. Falls back to 60s if no deadline is set. -func testContext(tb testing.TB) (context.Context, context.CancelFunc) { - if t, ok := tb.(hasDeadline); ok { - if dl, ok := t.Deadline(); ok { - const margin = 5 * time.Second - return context.WithDeadline(context.Background(), dl.Add(-margin)) - } - } - return context.WithTimeout(context.Background(), 60*time.Second) -} - -func (nt *natTest) runTailscaleConnectivityTest(addNode ...addNodeFunc) pingRoute { - ctx, cancel := testContext(nt.tb) - defer cancel() - - nodes, clients, cleanup := nt.setupTest(ctx, addNode...) - defer cleanup() - t := nt.tb - - if len(nodes) < 2 { - return "" - } - for _, n := range nodes { - if !n.ShouldJoinTailnet() { - t.Logf("%v did not join tailnet", n) - return "" - } - } - - sts := make([]*ipnstate.Status, len(nodes)) - var eg errgroup.Group - for i, c := range clients { - eg.Go(func() error { - node := nodes[i] - st, err := c.Status(ctx) - if err != nil { - return fmt.Errorf("%v: %w", node, err) - } - sts[i] = st - return nil - }) - } - if err := eg.Wait(); err != nil { - t.Fatalf("get node statuses: %v", err) - } - - preICMPPing := false - for _, node := range nodes { - node.Network().PostConnectedToControl() - if err := node.PostConnectedToControl(ctx); err != nil { - t.Fatalf("post control error: %s", err) - } - if node.PreICMPPing() { - preICMPPing = true - } - } - - // Should we send traffic across the nodes before starting disco? - // For nodes that rotated disco keys after control going away. - if preICMPPing { - _, err := ping(ctx, t, clients[0], sts[1].Self.TailscaleIPs[0], tailcfg.PingICMP) - if err != nil { - t.Fatalf("ICMP ping failure: %v", err) - } - } - - pingRes, err := ping(ctx, t, clients[0], sts[1].Self.TailscaleIPs[0], tailcfg.PingDisco) - if err != nil { - t.Logf("ping failure: %v", err) - } - nt.gotRoute = classifyPing(pingRes) - t.Logf("ping route: %v", nt.gotRoute) - - return nt.gotRoute -} - -func classifyPing(pr *ipnstate.PingResult) pingRoute { - if pr == nil { - return routeNil - } - if pr.Endpoint != "" { - ap, err := netip.ParseAddrPort(pr.Endpoint) - if err == nil { - if ap.Addr().IsPrivate() { - return routeLocal - } - return routeDirect - } - } - return routeDERP // presumably -} - -type pingRoute string - -const ( - routeDERP pingRoute = "derp" - routeLocal pingRoute = "local" - routeDirect pingRoute = "direct" - routeNil pingRoute = "nil" // *ipnstate.PingResult is nil -) - -func ping(ctx context.Context, t testing.TB, c *vnet.NodeAgentClient, target netip.Addr, pType tailcfg.PingType) (*ipnstate.PingResult, error) { - var lastRes *ipnstate.PingResult - for n := range 10 { - t.Logf("ping attempt %d to %v ...", n+1, target) - pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second) - pr, err := c.PingWithOpts(pingCtx, target, pType, tailscale.PingOpts{}) - cancel() - if err != nil { - t.Logf("ping attempt %d error: %v", n+1, err) - if ctx.Err() != nil { - break - } - continue - } - if pr.Err != "" { - return nil, errors.New(pr.Err) - } - t.Logf("ping attempt %d: derp=%d endpoint=%v latency=%v", n+1, pr.DERPRegionID, pr.Endpoint, pr.LatencySeconds) - if pr.DERPRegionID == 0 { - return pr, nil - } - lastRes = pr - select { - case <-ctx.Done(): - return lastRes, nil - case <-time.After(time.Second): - } - } - if lastRes != nil { - return lastRes, nil - } - return nil, fmt.Errorf("no ping response (ctx: %v)", ctx.Err()) -} - -// qmpQueryStatus connects to a QEMU QMP socket and returns the VM status -// (e.g. "running", "paused", "prelaunch") or an error string. -func qmpQueryStatus(sockPath string) string { - conn, err := net.DialTimeout("unix", sockPath, 2*time.Second) - if err != nil { - return fmt.Sprintf("dial error: %v", err) - } - defer conn.Close() - conn.SetDeadline(time.Now().Add(5 * time.Second)) - dec := json.NewDecoder(conn) - - // Read QMP greeting. - var greeting json.RawMessage - if err := dec.Decode(&greeting); err != nil { - return fmt.Sprintf("greeting error: %v", err) - } - - // Enter command mode. - if _, err := conn.Write([]byte(`{"execute":"qmp_capabilities"}` + "\n")); err != nil { - return fmt.Sprintf("write caps: %v", err) - } - var capsResp json.RawMessage - if err := dec.Decode(&capsResp); err != nil { - return fmt.Sprintf("caps response: %v", err) - } - - // Query status. - if _, err := conn.Write([]byte(`{"execute":"query-status"}` + "\n")); err != nil { - return fmt.Sprintf("write query-status: %v", err) - } - var statusResp struct { - Return struct { - Running bool `json:"running"` - Status string `json:"status"` - } `json:"return"` - Error *struct { - Class string `json:"class"` - Desc string `json:"desc"` - } `json:"error"` - } - if err := dec.Decode(&statusResp); err != nil { - return fmt.Sprintf("status response: %v", err) - } - if statusResp.Error != nil { - return fmt.Sprintf("qmp error: %s: %s", statusResp.Error.Class, statusResp.Error.Desc) - } - return fmt.Sprintf("status=%s running=%v", statusResp.Return.Status, statusResp.Return.Running) -} - -func up(ctx context.Context, c *vnet.NodeAgentClient) error { - req, err := http.NewRequestWithContext(ctx, "GET", "http://unused/up", nil) - if err != nil { - return err - } - res, err := c.HTTPClient.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - all, _ := io.ReadAll(res.Body) - if res.StatusCode != 200 { - return fmt.Errorf("unexpected status code %v: %s", res.Status, all) - } - return nil -} - -type nodeType struct { - name string - fn addNodeFunc -} - -var types = []nodeType{ - {"easy", easy}, - {"easyAF", easyAF}, - {"hard", hard}, - {"easyPMP", easyPMP}, - {"hardPMP", hardPMP}, - {"one2one", one2one}, - {"sameLAN", sameLAN}, - {"cgnat", cgnatNoTailnet}, -} - -func cgnatNoTailnet(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork( - fmt.Sprintf("100.65.%d.1/16", n), - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - vnet.EasyNAT), - vnet.DontJoinTailnet) -} - -var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPMP, hardPMP, one2one, sameLAN)") - -func TestPair(t *testing.T) { - t1, t2, ok := strings.Cut(*pair, ",") - if !ok { - t.Skipf("skipping test without --pair=type1,type2 set") - } - find := func(name string) addNodeFunc { - for _, nt := range types { - if nt.name == name { - return nt.fn - } - } - t.Fatalf("unknown type %q", name) - return nil - } - - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(find(t1), find(t2)) -} - -var runGrid = flag.Bool("run-grid", false, "run grid test") - -func TestGrid(t *testing.T) { - if !*runGrid { - t.Skip("skipping grid test; set --run-grid to run") - } - t.Parallel() - - sem := syncs.NewSemaphore(2) - var ( - mu sync.Mutex - res = make(map[string]pingRoute) - ) - for _, a := range types { - for _, b := range types { - key := a.name + "-" + b.name - keyBack := b.name + "-" + a.name - t.Run(key, func(t *testing.T) { - t.Parallel() - - sem.Acquire() - defer sem.Release() - - filename := key + ".cache" - contents, _ := os.ReadFile(filename) - if len(contents) == 0 { - filename2 := keyBack + ".cache" - contents, _ = os.ReadFile(filename2) - } - route := pingRoute(strings.TrimSpace(string(contents))) - - if route == "" { - nt := newNatTest(t) - route = nt.runTailscaleConnectivityTest(a.fn, b.fn) - if err := os.WriteFile(filename, []byte(string(route)), 0666); err != nil { - t.Fatalf("writeFile: %v", err) - } - } - - mu.Lock() - defer mu.Unlock() - res[key] = route - t.Logf("results: %v", res) - }) - } - } - - t.Cleanup(func() { - mu.Lock() - defer mu.Unlock() - var hb bytes.Buffer - pf := func(format string, args ...any) { - fmt.Fprintf(&hb, format, args...) - } - rewrite := func(s string) string { - return strings.ReplaceAll(s, "PMP", "+pm") - } - pf("") - pf("") - for _, a := range types { - pf("", rewrite(a.name)) - } - pf("\n") - - for _, a := range types { - if a.name == "sameLAN" { - continue - } - pf("", rewrite(a.name)) - for _, b := range types { - key := a.name + "-" + b.name - key2 := b.name + "-" + a.name - v := cmp.Or(res[key], res[key2], "-") - if v == "derp" { - pf("", v) - } else if v == "local" { - pf("", v) - } else { - pf("", v) - } - } - pf("\n") - } - pf("
%s
%s
%s
%s
%s
") - pf("easy: Endpoint-Independent Mapping, Address and Port-Dependent Filtering (e.g. Linux, Google Wifi, Unifi, eero)
") - pf("easyAF: Endpoint-Independent Mapping, Address-Dependent Filtering (James says telephony things or Zyxel type things)
") - pf("hard: Address and Port-Dependent Mapping, Address and Port-Dependent Filtering (FreeBSD, OPNSense, pfSense)
") - pf("one2one: One-to-One NAT (e.g. an EC2 instance with a public IPv4)
") - pf("x+pm: x, with port mapping (NAT-PMP, PCP, UPnP, etc)
") - pf("sameLAN: a second node in the same LAN as the first
") - pf("") - - if err := os.WriteFile("grid.html", hb.Bytes(), 0666); err != nil { - t.Fatalf("writeFile: %v", err) - } - }) -} diff --git a/tstest/natlab/vmtest/connectivity.go b/tstest/natlab/vmtest/connectivity.go index 60d6e0e36..04ac3e511 100644 --- a/tstest/natlab/vmtest/connectivity.go +++ b/tstest/natlab/vmtest/connectivity.go @@ -8,23 +8,50 @@ import ( "time" ) -// AddNodeFunc is used to describe a func passed to [RunConnectivityTest]. +// AddNodeFunc is used to describe a func passed to [RunConnectivityTestExpect]. type AddNodeFunc func(*Env) *Node -// RunConnectivityTest adds the specified nodes to the network and then +// RunConnectivityTestExpect adds the specified nodes to the network and then // verifies that a Disco ping from n1 to n2 completes within 30 seconds. -func (env *Env) RunConnectivityTest(name string, pingRoute PingRoute, n1, n2 AddNodeFunc) { - n1(env) - n2(env) +func (e *Env) RunConnectivityTestExpect(name string, pingRoute PingRoute, n1, n2 AddNodeFunc) { + node1 := n1(e) + node2 := n2(e) - discoPingStep := env.AddStep( + discoPingStep := e.AddStep( fmt.Sprintf("[%s] Ping a → b Disco (want %s)", name, pingRoute)) - env.Start() + e.Start() discoPingStep.Begin() - if err := env.PingExpect(env.nodes[0], env.nodes[1], pingRoute, 30*time.Second); err != nil { + if err := e.PingExpect(node1, node2, pingRoute, 30*time.Second); err != nil { discoPingStep.End(err) - env.t.Error(err) + e.t.Error(err) + return } discoPingStep.End(nil) } + +// RunConnectivityTest adds the specified nodes to the network and then +// verifies that a Disco ping from n1 to n2 completes within 30 seconds. +func (e *Env) RunConnectivityTest(name string, n1, n2 AddNodeFunc) PingRoute { + e.t.Helper() + node1 := n1(e) + node2 := n2(e) + if node1 == nil || node2 == nil { + e.t.Skip("skipping test; not applicable combination") + } + + discoPingStep := e.AddStep( + fmt.Sprintf("[%s] Ping a → b Disco", name)) + e.Start() + + discoPingStep.Begin() + pRes, err := e.PingSettle(node1, node2, 10*time.Second) + if err != nil { + discoPingStep.End(err) + e.t.Error(err) + return PingRouteNil + } + pRoute := classifyPing(pRes) + discoPingStep.End(nil) + return pRoute +} diff --git a/tstest/natlab/vmtest/connectivity_test.go b/tstest/natlab/vmtest/connectivity_test.go index 8085655cf..e777e9808 100644 --- a/tstest/natlab/vmtest/connectivity_test.go +++ b/tstest/natlab/vmtest/connectivity_test.go @@ -4,9 +4,16 @@ package vmtest_test import ( + "bytes" + "cmp" + "flag" "fmt" + "os" + "strings" + "sync" "testing" + "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest/natlab/vmtest" @@ -17,19 +24,27 @@ func v6cidr(n int) string { return fmt.Sprintf("2000:%d::1/64", n) } -func easy(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), - env.AddNetwork( +func easy(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT), vmtest.OS(vmtest.Gokrazy)) } -func easyAnd6(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), - env.AddNetwork( +func easyAF(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyAFNAT)) +} + +func easyAnd6(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), v6cidr(n), @@ -37,74 +52,73 @@ func easyAnd6(env *vmtest.Env) *vmtest.Node { vmtest.OS(vmtest.Gokrazy)) } -// easyNoControlDiscoRotate sets up a node with easy NAT, cuts traffic to -// control after connecting, and then rotates the disco key to simulate a newly -// started node (from a disco perspective). -func easyNoControlDiscoRotate(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - nw := env.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("192.168.%d.1/24", n), - vnet.EasyNAT) - nw.SetPostConnectControlBlackhole(true) - return env.AddNode(fmt.Sprintf("node-%d", n), - vnet.TailscaledEnv{Key: "TS_USE_CACHED_NETMAP", Value: "true"}, - vnet.RotateDisco, vnet.PreICMPPing, - nw, - vmtest.OS(vmtest.Gokrazy)) -} - // easyFW is easy + host firewall. -func easyFW(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), +func easyFW(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), vnet.HostFirewall, - env.AddNetwork( + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT), vmtest.OS(vmtest.Gokrazy)) } // easyPMPFWPlusBPF is easy + port mapping + host firewall + BPF. -func easyPMPFWPlusBPF(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), +func easyPMPFWPlusBPF(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), vnet.HostFirewall, vnet.TailscaledEnv{Key: "TS_ENABLE_RAW_DISCO", Value: "true"}, vnet.TailscaledEnv{Key: "TS_DEBUG_RAW_DISCO", Value: "1"}, vnet.TailscaledEnv{Key: "TS_DEBUG_DISCO", Value: "1"}, vnet.TailscaledEnv{Key: "TS_LOG_VERBOSITY", Value: "2"}, - env.AddNetwork( + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP), vmtest.OS(vmtest.Gokrazy)) } // easyPMPFWNoBPF is easy + port mapping + host firewall - BPF. -func easyPMPFWNoBPF(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), +func easyPMPFWNoBPF(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), vnet.HostFirewall, vnet.TailscaledEnv{Key: "TS_ENABLE_RAW_DISCO", Value: "false"}, - env.AddNetwork( + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP), vmtest.OS(vmtest.Gokrazy)) } -func hard(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), - env.AddNetwork( +func hard(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("10.0.%d.1/24", n), vnet.HardNAT), vmtest.OS(vmtest.Gokrazy)) } -func hardNoDERPOrEndpoints(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), - env.AddNetwork( +func easyPMP(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP)) +} + +func hardPMP(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + fmt.Sprintf("10.7.%d.1/24", n), vnet.HardNAT, vnet.NATPMP)) +} + +func hardNoDERPOrEndpoints(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("10.0.%d.1/24", n), vnet.HardNAT), vnet.TailscaledEnv{Key: "TS_DEBUG_STRIP_ENDPOINTS", Value: "1"}, @@ -112,64 +126,83 @@ func hardNoDERPOrEndpoints(env *vmtest.Env) *vmtest.Node { vmtest.OS(vmtest.Gokrazy)) } -func just6(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), - env.AddNetwork(v6cidr(n)), // public IPv6 prefix +func one2one(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + fmt.Sprintf("172.16.%d.1/24", n), vnet.One2OneNAT)) +} + +func sameLAN(e *vmtest.Env) *vmtest.Node { + nw := e.FirstNetwork() + if nw == nil { + return nil + } + if !nw.CanTakeMoreNodes() { + return nil + } + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), nw) +} + +func cgnat(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork( + fmt.Sprintf("100.65.%d.1/16", n), + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + vnet.EasyNAT)) +} + +func just6(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), + e.AddNetwork(v6cidr(n)), // public IPv6 prefix vmtest.OS(vmtest.Gokrazy)) } -func v6AndBlackholedIPv4(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - nw := env.AddNetwork( +func v6AndBlackholedIPv4(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + nw := e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), fmt.Sprintf("192.168.%d.1/24", n), v6cidr(n), vnet.EasyNAT) nw.SetBlackholedIPv4(true) - return env.AddNode(fmt.Sprintf("node-%d", n), nw, vmtest.OS(vmtest.Gokrazy)) + return e.AddNode(fmt.Sprintf("node-%d", n), nw, vmtest.OS(vmtest.Gokrazy)) } func TestEasyEasy(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easy, easy) -} - -// TestTwoEasyNoControlDiscoRotate tests a situation where two nodes have been -// online and connected through control, but then lose control access and also -// rotate keys. It is not a perfect proxy for a cached node, as the node will -// still have a mapState and not use the backup method of inserting keys into -// the engine directly. -func TestTwoEasyNoControlDiscoRotate(t *testing.T) { - env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easyNoControlDiscoRotate, easyNoControlDiscoRotate) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, easy, easy) } func TestJustIPv6(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, just6, just6) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, just6, just6) } func TestEasy4AndJust6(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easyAnd6, just6) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, easyAnd6, just6) } func TestSameLAN(t *testing.T) { env := vmtest.New(t) var sharedNW *vnet.Network - makeEasy := func(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - sharedNW = env.AddNetwork( + makeEasy := func(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + sharedNW = e.AddNetwork( fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT) - return env.AddNode(fmt.Sprintf("node-%d", n), sharedNW, vmtest.OS(vmtest.Gokrazy)) + return e.AddNode(fmt.Sprintf("node-%d", n), sharedNW, vmtest.OS(vmtest.Gokrazy)) } - sameLAN := func(env *vmtest.Env) *vmtest.Node { - n := env.NumNodes() - return env.AddNode(fmt.Sprintf("node-%d", n), sharedNW, vmtest.OS(vmtest.Gokrazy)) + sameLAN := func(e *vmtest.Env) *vmtest.Node { + n := e.NumNodes() + return e.AddNode(fmt.Sprintf("node-%d", n), sharedNW, vmtest.OS(vmtest.Gokrazy)) } - env.RunConnectivityTest(t.Name(), vmtest.PingRouteLocal, makeEasy, sameLAN) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteLocal, makeEasy, sameLAN) } // TestBPFDisco tests https://github.com/tailscale/tailscale/issues/3824 ... @@ -178,22 +211,22 @@ func TestSameLAN(t *testing.T) { // * client machine has a stateful host firewall (e.g. ufw) func TestBPFDisco(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easyPMPFWPlusBPF, hard) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, easyPMPFWPlusBPF, hard) } func TestHostFWNoBPF(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDERP, easyPMPFWNoBPF, hard) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDERP, easyPMPFWNoBPF, hard) } func TestHostFWPair(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easyFW, easyFW) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, easyFW, easyFW) } func TestOneHostFW(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easy, easyFW) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDirect, easy, easyFW) } // Issue tailscale/corp#26438: use learned DERP route as send path of last @@ -212,7 +245,7 @@ func TestOneHostFW(t *testing.T) { // packet over a particular DERP from that peer. func TestFallbackDERPRegionForPeer(t *testing.T) { env := vmtest.New(t) - env.RunConnectivityTest(t.Name(), vmtest.PingRouteDERP, hard, hardNoDERPOrEndpoints) + env.RunConnectivityTestExpect(t.Name(), vmtest.PingRouteDERP, hard, hardNoDERPOrEndpoints) } // TestSingleJustIPv6 tests that a node can connect to control with just IPv6. @@ -248,3 +281,140 @@ func TestNonTailscaleCGNATEndpoint(t *testing.T) { env.Start() env.LANPing(n1, n0.LanIP(cgnatNW)) } + +type nodeType struct { + name string + fn vmtest.AddNodeFunc +} + +var types = []nodeType{ + {"easy", easy}, + {"easyAF", easyAF}, + {"hard", hard}, + {"easyPMP", easyPMP}, + {"hardPMP", hardPMP}, + {"one2one", one2one}, + {"sameLAN", sameLAN}, + {"cgnat", cgnat}, +} + +var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPMP, hardPMP, one2one, sameLAN)") + +func TestPair(t *testing.T) { + t1, t2, ok := strings.Cut(*pair, ",") + if !ok { + t.Skipf("skipping test without --pair=type1,type2 set") + } + find := func(name string) vmtest.AddNodeFunc { + for _, nt := range types { + if nt.name == name { + return nt.fn + } + } + t.Fatalf("unknown type %q", name) + return nil + } + + env := vmtest.New(t) + route := env.RunConnectivityTest(t.Name(), find(t1), find(t2)) + t.Logf("pair: got ping route: %s", route) +} + +var runGrid = flag.Bool("run-grid", false, "run grid test") + +func TestGrid(t *testing.T) { + if !*runGrid { + t.Skip("skipping grid test; set --run-grid to run") + } + t.Parallel() + + sem := syncs.NewSemaphore(2) + var ( + mu sync.Mutex + res = make(map[string]vmtest.PingRoute) + ) + for _, a := range types { + for _, b := range types { + key := a.name + "-" + b.name + keyBack := b.name + "-" + a.name + t.Run(key, func(t *testing.T) { + t.Parallel() + + sem.Acquire() + defer sem.Release() + + filename := key + ".cache" + contents, _ := os.ReadFile(filename) + if len(contents) == 0 { + filename2 := keyBack + ".cache" + contents, _ = os.ReadFile(filename2) + } + route := vmtest.PingRoute(strings.TrimSpace(string(contents))) + + if route == "" { + env := vmtest.New(t) + route = env.RunConnectivityTest( + fmt.Sprintf("%s<->%s", a.name, b.name), a.fn, b.fn) + if err := os.WriteFile(filename, []byte(string(route)), 0o666); err != nil { + t.Fatalf("writeFile: %v", err) + } + } + + mu.Lock() + defer mu.Unlock() + res[key] = route + t.Logf("results: %v", res) + }) + } + } + + t.Cleanup(func() { + mu.Lock() + defer mu.Unlock() + var hb bytes.Buffer + pf := func(format string, args ...any) { + fmt.Fprintf(&hb, format, args...) + } + rewrite := func(s string) string { + return strings.ReplaceAll(s, "PMP", "+pm") + } + pf("") + pf("") + for _, a := range types { + pf("", rewrite(a.name)) + } + pf("\n") + + for _, a := range types { + if a.name == "sameLAN" { + continue + } + pf("", rewrite(a.name)) + for _, b := range types { + key := a.name + "-" + b.name + key2 := b.name + "-" + a.name + v := cmp.Or(res[key], res[key2], "-") + if v == "derp" { + pf("", v) + } else if v == "local" { + pf("", v) + } else { + pf("", v) + } + } + pf("\n") + } + pf("
%s
%s
%s
%s
%s
") + pf("easy: Endpoint-Independent Mapping, Address and Port-Dependent Filtering (e.g. Linux, Google Wifi, Unifi, eero)
") + pf("easyAF: Endpoint-Independent Mapping, Address-Dependent Filtering (James says telephony things or Zyxel type things)
") + pf("hard: Address and Port-Dependent Mapping, Address and Port-Dependent Filtering (FreeBSD, OPNSense, pfSense)
") + pf("one2one: One-to-One NAT (e.g. an EC2 instance with a public IPv4)
") + pf("x+pm: x, with port mapping (NAT-PMP, PCP, UPnP, etc)
") + pf("sameLAN: a second node in the same LAN as the first
") + pf("") + + if err := os.WriteFile("grid.html", hb.Bytes(), 0o666); err != nil { + t.Fatalf("writeFile: %v", err) + } + }) +} diff --git a/tstest/natlab/vmtest/vmtest.go b/tstest/natlab/vmtest/vmtest.go index 69a46ca71..f1bf5b6af 100644 --- a/tstest/natlab/vmtest/vmtest.go +++ b/tstest/natlab/vmtest/vmtest.go @@ -437,6 +437,12 @@ func (e *Env) AddNetwork(opts ...any) *vnet.Network { return e.cfg.AddNetwork(opts...) } +// FirstNetwork returns the first existing network. If no network exists, it +// returns nil. +func (e *Env) FirstNetwork() *vnet.Network { + return e.cfg.FirstNetwork() +} + // RegisterFile registers a file with the vnet fileserver. // It is served at http://files.tailscale/. func (e *Env) RegisterFile(path string, data []byte) { @@ -564,16 +570,7 @@ func (n *Node) LanIP(net *vnet.Network) netip.Addr { return n.vnetNode.LanIP(net) } -// DropControlTraffic sets up a blackhole for control traffic for just this -// node on all the networks belonging to the node. -func (n *Node) DropControlTraffic() { - for _, network := range n.nets { - network.BlackholeControlForAddr(n.LanIP(network)) - } -} - // NodeOption types for configuring nodes. - type nodeOptOS OSImage type nodeOptNoTailscale struct{} type nodeOptTailscaleSSH struct{} @@ -2327,11 +2324,11 @@ func (e *Env) PingExpect(from, to *Node, wantRoute PingRoute, timeout time.Durat pr, err := from.agent.PingWithOpts(pingCtx, targetIP, tailcfg.PingDisco, local.PingOpts{}) pingCancel() if err == nil && pr.Err == "" { - if got := classifyPing(pr); got == wantRoute { - e.t.Logf("Saw ping type %q", got) + got := classifyPing(pr) + e.t.Logf("Saw ping type %q", got) + if got == wantRoute { return nil } else { - e.t.Logf("Saw ping type %q", got) lastRoute = got } } @@ -2343,7 +2340,68 @@ func (e *Env) PingExpect(from, to *Node, wantRoute PingRoute, timeout time.Durat return fmt.Errorf("ping route = %q, want %q (after %v)", lastRoute, wantRoute, timeout) } -// NumNodes returns the current number of nodes configured in the env. -func (env *Env) NumNodes() int { - return len(env.nodes) +// PingSettle retries disco pings every 1 second between nodes from -> to. The +// intention is to have the route settle into the desired state at ctx timeout, +// making the last returned type the settled state of the connection. If the +// connection is direct before the timeout, the method returns early. +// If no ping has been completed, nil will be returned. +func (e *Env) PingSettle(from, to *Node, timeout time.Duration) (*ipnstate.PingResult, error) { + e.t.Helper() + ctx, cancel := context.WithTimeout(e.t.Context(), timeout) + defer cancel() + toSt, err := to.agent.Status(ctx) + if err != nil { + return nil, fmt.Errorf("ping: can't get %s status: %w", to.name, err) + } + if len(toSt.Self.TailscaleIPs) == 0 { + return nil, fmt.Errorf("ping: %s has no Tailscale IPs", to.name) + } + targetIP := toSt.Self.TailscaleIPs[0] + var lastRes *ipnstate.PingResult + n := 0 + for ctx.Err() == nil { + n++ + e.t.Logf("ping: attempt %d to %v ...", n, targetIP) + pingCtx, pingCancel := context.WithTimeout(ctx, 3*time.Second) + pr, err := from.agent.PingWithOpts(pingCtx, targetIP, tailcfg.PingDisco, local.PingOpts{}) + pingCancel() + if err != nil { + e.t.Logf("ping: attempt %d error: %v", n, err) + if ctx.Err() != nil { + break + } + continue + } + if pr.Err != "" { + return nil, errors.New(pr.Err) + } + e.t.Logf("ping: attempt %d: derp=%d endpoint=%v latency=%v", n, pr.DERPRegionID, pr.Endpoint, pr.LatencySeconds) + // When DERP on the result is 0, we have settled onto a direct path. + if pr.DERPRegionID == 0 { + return pr, nil + } + lastRes = pr + select { + case <-ctx.Done(): + return lastRes, nil + case <-time.After(time.Second): + } + } + if lastRes != nil { + return lastRes, nil + } + return nil, fmt.Errorf("ping: ping no response (ctx: %v)", ctx.Err()) +} + +// NumNodes returns the current number of nodes configured in the env. +func (e *Env) NumNodes() int { + return len(e.nodes) +} + +// DropControlTraffic sets up a blackhole for control traffic for just this +// node on all the networks belonging to the node. +func (e *Env) DropControlTraffic(n *Node) { + for _, network := range n.nets { + network.BlackholeControlForAddr(n.LanIP(network)) + } } diff --git a/tstest/natlab/vmtest/vmtest_test.go b/tstest/natlab/vmtest/vmtest_test.go index 16a80169c..d4dd0236c 100644 --- a/tstest/natlab/vmtest/vmtest_test.go +++ b/tstest/natlab/vmtest/vmtest_test.go @@ -1165,8 +1165,8 @@ func TestCachedNetmapAfterRestart(t *testing.T) { cutControlStep.Begin() // Both nodes lose connection to control - a.DropControlTraffic() - b.DropControlTraffic() + env.DropControlTraffic(a) + env.DropControlTraffic(b) env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) { panic(fmt.Sprintf("got connection from %v", nk)) }) @@ -1261,7 +1261,7 @@ func TestDirectConnectionWithCachedNetmapOnOneNode(t *testing.T) { checkInitialMetrics.End(nil) cutControlStep.Begin() - a.DropControlTraffic() + env.DropControlTraffic(a) env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) { if env.ControlServer().Node(nk).Name == a.Name() { panic(fmt.Sprintf("got connection from %v", a.Name())) @@ -1347,8 +1347,8 @@ func TestDirectConnectionWithCachedNetmapOnTwoNodes(t *testing.T) { checkInitialMetrics.End(nil) cutControlStep.Begin() - a.DropControlTraffic() - b.DropControlTraffic() + env.DropControlTraffic(a) + env.DropControlTraffic(b) env.ControlServer().SetOnMapRequest(func(nk key.NodePublic) { nodeName := env.ControlServer().Node(nk).Name if nodeName == a.Name() || nodeName == b.Name() { diff --git a/tstest/natlab/vnet/conf.go b/tstest/natlab/vnet/conf.go index 191de9e18..c56ad8ed7 100644 --- a/tstest/natlab/vnet/conf.go +++ b/tstest/natlab/vnet/conf.go @@ -5,7 +5,6 @@ package vnet import ( "cmp" - "context" "fmt" "iter" "net/netip" @@ -98,11 +97,11 @@ func nodeLANIP6(n int) netip.Addr { // AddNode creates a new node in the world. // // The opts may be of the following types: -// - *Network: zero, one, or more networks to add this node to +// - [*Network]: zero, one, or more networks to add this node to // - TODO: more // // On an error or unknown opt type, AddNode returns a -// node with a carried error that gets returned later. +// [Node] with a carried error that gets returned later. func (c *Config) AddNode(opts ...any) *Node { num := len(c.nodes) + 1 n := &Node{ @@ -123,10 +122,6 @@ func (c *Config) AddNode(opts ...any) *Node { switch o { case HostFirewall: n.hostFW = true - case RotateDisco: - n.rotateDisco = true - case PreICMPPing: - n.preICMPPing = true case DontJoinTailnet: n.dontJoinTailnet = true case VerboseSyslog: @@ -154,8 +149,6 @@ type NodeOption string const ( HostFirewall NodeOption = "HostFirewall" - RotateDisco NodeOption = "RotateDisco" - PreICMPPing NodeOption = "PreICMPPing" DontJoinTailnet NodeOption = "DontJoinTailnet" VerboseSyslog NodeOption = "VerboseSyslog" ) @@ -172,8 +165,8 @@ type TailscaledEnv struct { // - string IP address, for the network's WAN IP (if any) // - string netip.Prefix, for the network's LAN IP (defaults to 192.168.0.0/24) // if IPv4, or its WAN IPv6 + CIDR (e.g. "2000:52::1/64") -// - NAT, the type of NAT to use -// - NetworkService, a service to add to the network +// - [NAT], the type of NAT to use +// - [NetworkService], a service to add to the network // // On an error or unknown opt type, AddNetwork returns a // network with a carried error that gets returned later. @@ -224,8 +217,6 @@ type Node struct { env []TailscaledEnv hostFW bool - rotateDisco bool - preICMPPing bool verboseSyslog bool dontJoinTailnet bool capMap tailcfg.NodeCapMap @@ -292,29 +283,6 @@ func (n *Node) SetClient(c *NodeAgentClient) { n.client = c } -// PostConnectedToControl should be called after the clients have connected to -// control to modify the client behaviour after getting the network maps. -// Currently, the only implemented behavior is rotating disco keys. -func (n *Node) PostConnectedToControl(ctx context.Context) error { - if n.rotateDisco { - if err := n.client.DebugAction(ctx, "rotate-disco-key"); err != nil { - return err - } - } - return nil -} - -// PreICMPPing reports whether node should send an ICMP Ping sent before -// the disco ping. This is important for the nodes having rotated their -// disco keys while control is down. Disco pings deliberately does not -// trigger a TSMPDiscoKeyAdvertisement, making the need for other traffic (here -// simlulated as an ICMP ping) needed first. Any traffic could trigger this key -// exchange, the ICMP Ping is used as a handy existing way of sending some -// non-disco traffic. -func (n *Node) PreICMPPing() bool { - return n.preICMPPing -} - // ShouldJoinTailnet reports whether node should join the test tailnet. Machines in // the virtual universe that aren't on the tailnet are useful for testing that // Tailscale does not break connectivity to resources outside the tailnet. @@ -384,7 +352,6 @@ type Network struct { lanIP4 netip.Prefix nodes []*Node breakWAN4 bool // whether to break WAN IPv4 connectivity - postConnectBlackholeControl bool // whether to break control connectivity after nodes have connected network *network svcs set.Set[NetworkService] @@ -417,12 +384,6 @@ func (n *Network) SetBlackholedIPv4(v bool) { n.breakWAN4 = v } -// SetPostConnectControlBlackhole sets whether the network should blackhole all -// traffic to the control server after the clients have connected. -func (n *Network) SetPostConnectControlBlackhole(v bool) { - n.postConnectBlackholeControl = v -} - func (n *Network) CanV4() bool { return n.lanIP4.IsValid() || n.wanIP4.IsValid() } @@ -438,13 +399,6 @@ func (n *Network) CanTakeMoreNodes() bool { return len(n.nodes) < 150 } -// PostConnectedToControl should be called after the clients have connected to -// the control server to modify network behaviors. Currently the only -// implemented behavior is to conditionally blackhole traffic to control. -func (n *Network) PostConnectedToControl() { - n.network.SetControlBlackholed(n.postConnectBlackholeControl) -} - // BlackholeControlForAddr sets weither the network should drop all control // traffic for the specified addr starting immediately. func (n *Network) BlackholeControlForAddr(addr netip.Addr) { @@ -460,7 +414,7 @@ const ( UPnP NetworkService = "UPnP" ) -// AddService adds a network service (such as port mapping protocols) to a +// AddService adds a [NetworkService] (such as port mapping protocols) to a // network. func (n *Network) AddService(s NetworkService) { if n.svcs == nil { diff --git a/tstest/natlab/vnet/vnet.go b/tstest/natlab/vnet/vnet.go index 580dd9a31..4a974d817 100644 --- a/tstest/natlab/vnet/vnet.go +++ b/tstest/natlab/vnet/vnet.go @@ -638,6 +638,7 @@ func (m MAC) HWAddr() net.HardwareAddr { return net.HardwareAddr(m[:]) } +// String returns the mac address as "xx:xx:xx:xx:xx:xx". func (m MAC) String() string { return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", m[0], m[1], m[2], m[3], m[4], m[5]) }