From bb47ea2c6b7a59907a70776271c2262907e2e4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20Lensb=C3=B8l?= Date: Wed, 13 May 2026 16:44:53 -0400 Subject: [PATCH] tstest/natlab/vmtest: start migrating old natlab tests to vmtest (#19727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of having two entry points for running natlab tests, start converting the connectivity tests to use the vmtest framework. Grid and pair tests have yet to be moved over. Updates #13038 Signed-off-by: Claus Lensbøl --- .github/workflows/natlab-basic.yml | 2 +- tstest/integration/nat/nat_test.go | 324 ---------------------- tstest/natlab/vmtest/connectivity.go | 30 ++ tstest/natlab/vmtest/connectivity_test.go | 257 +++++++++++++++++ tstest/natlab/vmtest/qemu.go | 1 + tstest/natlab/vmtest/vmtest.go | 12 +- 6 files changed, 298 insertions(+), 328 deletions(-) create mode 100644 tstest/natlab/vmtest/connectivity.go create mode 100644 tstest/natlab/vmtest/connectivity_test.go diff --git a/.github/workflows/natlab-basic.yml b/.github/workflows/natlab-basic.yml index 584da6e69..1a19acfb8 100644 --- a/.github/workflows/natlab-basic.yml +++ b/.github/workflows/natlab-basic.yml @@ -42,4 +42,4 @@ jobs: make -C gokrazy natlab - name: Run natlab integration tests run: | - ./tool/go test -v -run=^TestEasyEasy$ -timeout=3m -count=1 ./tstest/integration/nat --run-vm-tests + ./tool/go test -v -run=^TestEasyEasy$ -timeout=3m -count=1 ./tstest/natlab/vmtest --run-vm-tests diff --git a/tstest/integration/nat/nat_test.go b/tstest/integration/nat/nat_test.go index 8eca5742f..e3e53374c 100644 --- a/tstest/integration/nat/nat_test.go +++ b/tstest/integration/nat/nat_test.go @@ -19,7 +19,6 @@ import ( "os/exec" "path/filepath" "runtime" - "strconv" "strings" "sync" "testing" @@ -116,10 +115,6 @@ func findKernelPath(goMod string) (string, error) { type addNodeFunc func(c *vnet.Config) *vnet.Node // returns nil to omit test -func v6cidr(n int) string { - return fmt.Sprintf("2000:%d::1/64", n) -} - func easy(c *vnet.Config) *vnet.Node { n := c.NumNodes() + 1 return c.AddNode(c.AddNetwork( @@ -127,57 +122,6 @@ func easy(c *vnet.Config) *vnet.Node { fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT)) } -func easyAnd6(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), - v6cidr(n), - vnet.EasyNAT)) -} - -// 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(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - nw := c.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 c.AddNode( - vnet.TailscaledEnv{ - Key: "TS_USE_CACHED_NETMAP", - Value: "true", - }, - vnet.RotateDisco, vnet.PreICMPPing, nw) -} - -func v6AndBlackholedIPv4(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - nw := c.AddNetwork( - fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP - fmt.Sprintf("192.168.%d.1/24", n), - v6cidr(n), - vnet.EasyNAT) - nw.SetBlackholedIPv4(true) - return c.AddNode(nw) -} - -func just6(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(c.AddNetwork(v6cidr(n))) // public IPv6 prefix -} - -// easy + host firewall -func easyFW(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode(vnet.HostFirewall, 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( @@ -196,22 +140,6 @@ func sameLAN(c *vnet.Config) *vnet.Node { return c.AddNode(nw) } -func sameLANNoDropCGNAT(c *vnet.Config) *vnet.Node { - nw := c.FirstNetwork() - if nw == nil { - return nil - } - if !nw.CanTakeMoreNodes() { - return nil - } - return c.AddNode( - nw, - tailcfg.NodeCapMap{ - tailcfg.NodeAttrDisableLinuxCGNATDropRule: nil, - }, - ) -} - func one2one(c *vnet.Config) *vnet.Node { n := c.NumNodes() + 1 return c.AddNode(c.AddNetwork( @@ -226,46 +154,6 @@ func easyPMP(c *vnet.Config) *vnet.Node { fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP)) } -// easy + port mapping + host firewall + BPF -func easyPMPFWPlusBPF(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode( - 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", - }, - 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)) -} - -// easy + port mapping + host firewall - BPF -func easyPMPFWNoBPF(c *vnet.Config) *vnet.Node { - n := c.NumNodes() + 1 - return c.AddNode( - vnet.HostFirewall, - vnet.TailscaledEnv{ - Key: "TS_ENABLE_RAW_DISCO", - Value: "false", - }, - 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( @@ -273,22 +161,6 @@ func hard(c *vnet.Config) *vnet.Node { fmt.Sprintf("10.0.%d.1/24", n), vnet.HardNAT)) } -func hardNoDERPOrEndoints(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), - vnet.TailscaledEnv{ - Key: "TS_DEBUG_STRIP_ENDPOINTS", - Value: "1", - }, - vnet.TailscaledEnv{ - Key: "TS_DEBUG_STRIP_HOME_DERP", - Value: "1", - }, - ) -} - func hardPMP(c *vnet.Config) *vnet.Node { n := c.NumNodes() + 1 return c.AddNode(c.AddNetwork( @@ -492,32 +364,6 @@ func testContext(tb testing.TB) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 60*time.Second) } -func (nt *natTest) runHostConnectivityTest(addNode ...addNodeFunc) bool { - ctx, cancel := testContext(nt.tb) - defer cancel() - nodes, clients, cleanup := nt.setupTest(ctx, addNode...) - defer cleanup() - - if len(nodes) != 2 { - nt.tb.Logf("ping can only be done among exactly two nodes") - return false - } - var fromClient, toClient *vnet.NodeAgentClient - for i, n := range nodes { - if n.ShouldJoinTailnet() && fromClient == nil { - fromClient = clients[i] - } else { - toClient = clients[i] - } - } - got, err := sendHostNetworkPing(ctx, nt.tb, fromClient, toClient) - if err != nil { - nt.tb.Fatalf("ping host: %v", err) - } - nt.tb.Logf("ping success: %v", got) - return got -} - func (nt *natTest) runTailscaleConnectivityTest(addNode ...addNodeFunc) pingRoute { ctx, cancel := testContext(nt.tb) defer cancel() @@ -708,60 +554,6 @@ func up(ctx context.Context, c *vnet.NodeAgentClient) error { return nil } -func getClientIP(ctx context.Context, c *vnet.NodeAgentClient) (netip.Addr, error) { - getIPReq, err := http.NewRequestWithContext(ctx, "GET", "http://unused/ip", nil) - if err != nil { - return netip.Addr{}, err - } - res, err := c.HTTPClient.Do(getIPReq) - if err != nil { - return netip.Addr{}, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return netip.Addr{}, fmt.Errorf("client returned http status %q", res.Status) - } - ipBytes, err := io.ReadAll(res.Body) - if err != nil { - return netip.Addr{}, err - } - addrPort, err := netip.ParseAddrPort(string(ipBytes)) - if err != nil { - return netip.Addr{}, err - } - return addrPort.Addr(), nil -} - -// sendHostNetworkPing pings toClient from fromClient, and returns whether -// toClient responded to the ping. -func sendHostNetworkPing(ctx context.Context, tb testing.TB, fromClient, toClient *vnet.NodeAgentClient) (bool, error) { - toIP, err := getClientIP(ctx, toClient) - if err != nil { - return false, fmt.Errorf("get ip: %w", err) - } - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://unused/ping?host=%s", toIP.String()), nil) - if err != nil { - return false, err - } - res, err := fromClient.HTTPClient.Do(req) - if err != nil { - return false, err - } - defer res.Body.Close() - got, err := io.ReadAll(res.Body) - if err != nil { - tb.Logf("error while reading http body: %v", err) - } else { - tb.Logf("got response from ping: %q", got) - } - ec, err := strconv.Atoi(res.Header.Get("Exec-Exit-Code")) - if err != nil { - return false, fmt.Errorf("parse exit code: %w", err) - } - tb.Logf("got ec: %v", ec) - return ec == 0, nil -} - type nodeType struct { name string fn addNodeFunc @@ -778,30 +570,6 @@ var types = []nodeType{ {"cgnat", cgnatNoTailnet}, } -// want sets the expected ping route for the test. -func (nt *natTest) want(r pingRoute) { - if nt.gotRoute != r { - nt.tb.Errorf("ping route = %v; want %v", nt.gotRoute, r) - } -} - -func TestEasyEasy(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easy, easy) - nt.want(routeDirect) -} - -// TestTwoEasyNoControlDiscoRotate tests a situation where two nodes have been -// online and connected through control, but then loose 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) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easyNoControlDiscoRotate, easyNoControlDiscoRotate) - nt.want(routeDirect) -} - func cgnatNoTailnet(c *vnet.Config) *vnet.Node { n := c.NumNodes() + 1 return c.AddNode(c.AddNetwork( @@ -811,98 +579,6 @@ func cgnatNoTailnet(c *vnet.Config) *vnet.Node { vnet.DontJoinTailnet) } -func TestNonTailscaleCGNATEndpoint(t *testing.T) { - nt := newNatTest(t) - if !nt.runHostConnectivityTest(cgnatNoTailnet, sameLANNoDropCGNAT) { - t.Fatalf("could not ping") - } -} - -// Issue tailscale/corp#26438: use learned DERP route as send path of last -// resort -// -// See (*magicsock.Conn).fallbackDERPRegionForPeer and its comment for -// background. -// -// This sets up a test with two nodes that must use DERP to communicate but the -// target of the ping (the second node) additionally is not getting DERP or -// Endpoint updates from the control plane. (Or rather, it's getting them but is -// configured to scrub them right when they come off the network before being -// processed) This then tests whether node2, upon receiving a packet, will be -// able to reply to node1 since it knows neither node1's endpoints nor its home -// DERP. The only reply route it can use is that fact that it just received a -// packet over a particular DERP from that peer. -func TestFallbackDERPRegionForPeer(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(hard, hardNoDERPOrEndoints) - nt.want(routeDERP) -} - -func TestSingleJustIPv6(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(just6) -} - -var knownBroken = flag.Bool("known-broken", false, "run known-broken tests") - -// TestSingleDualStackButBrokenIPv4 tests a dual-stack node with broken -// (blackholed) IPv4. -// -// See https://github.com/tailscale/tailscale/issues/13346 -func TestSingleDualBrokenIPv4(t *testing.T) { - if !*knownBroken { - t.Skip("skipping known-broken test; set --known-broken to run; see https://github.com/tailscale/tailscale/issues/13346") - } - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(v6AndBlackholedIPv4) -} - -func TestJustIPv6(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(just6, just6) - nt.want(routeDirect) -} - -func TestEasy4AndJust6(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easyAnd6, just6) - nt.want(routeDirect) -} - -func TestSameLAN(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easy, sameLAN) - nt.want(routeLocal) -} - -// TestBPFDisco tests https://github.com/tailscale/tailscale/issues/3824 ... -// * server behind a Hard NAT -// * client behind a NAT with UPnP support -// * client machine has a stateful host firewall (e.g. ufw) -func TestBPFDisco(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easyPMPFWPlusBPF, hard) - nt.want(routeDirect) -} - -func TestHostFWNoBPF(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easyPMPFWNoBPF, hard) - nt.want(routeDERP) -} - -func TestHostFWPair(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easyFW, easyFW) - nt.want(routeDirect) -} - -func TestOneHostFW(t *testing.T) { - nt := newNatTest(t) - nt.runTailscaleConnectivityTest(easy, easyFW) - nt.want(routeDirect) -} - var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPMP, hardPMP, one2one, sameLAN)") func TestPair(t *testing.T) { diff --git a/tstest/natlab/vmtest/connectivity.go b/tstest/natlab/vmtest/connectivity.go new file mode 100644 index 000000000..60d6e0e36 --- /dev/null +++ b/tstest/natlab/vmtest/connectivity.go @@ -0,0 +1,30 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package vmtest + +import ( + "fmt" + "time" +) + +// AddNodeFunc is used to describe a func passed to [RunConnectivityTest]. +type AddNodeFunc func(*Env) *Node + +// RunConnectivityTest 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) + + discoPingStep := env.AddStep( + fmt.Sprintf("[%s] Ping a → b Disco (want %s)", name, pingRoute)) + env.Start() + + discoPingStep.Begin() + if err := env.PingExpect(env.nodes[0], env.nodes[1], pingRoute, 30*time.Second); err != nil { + discoPingStep.End(err) + env.t.Error(err) + } + discoPingStep.End(nil) +} diff --git a/tstest/natlab/vmtest/connectivity_test.go b/tstest/natlab/vmtest/connectivity_test.go new file mode 100644 index 000000000..f9be6589a --- /dev/null +++ b/tstest/natlab/vmtest/connectivity_test.go @@ -0,0 +1,257 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package vmtest_test + +import ( + "flag" + "fmt" + "testing" + + "tailscale.com/tailcfg" + "tailscale.com/tstest/natlab/vmtest" + "tailscale.com/tstest/natlab/vnet" +) + +var knownBroken = flag.Bool("known-broken", false, "run known-broken tests") + +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( + 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( + fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP + fmt.Sprintf("192.168.%d.1/24", n), + v6cidr(n), + vnet.EasyNAT), + 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), + vnet.HostFirewall, + env.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), + 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( + 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), + vnet.HostFirewall, + vnet.TailscaledEnv{Key: "TS_ENABLE_RAW_DISCO", Value: "false"}, + env.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( + 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( + 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"}, + vnet.TailscaledEnv{Key: "TS_DEBUG_STRIP_HOME_DERP", Value: "1"}, + 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 + vmtest.OS(vmtest.Gokrazy)) +} + +func v6AndBlackholedIPv4(env *vmtest.Env) *vmtest.Node { + n := env.NumNodes() + nw := env.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)) +} + +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) +} + +func TestJustIPv6(t *testing.T) { + env := vmtest.New(t) + env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, just6, just6) +} + +func TestEasy4AndJust6(t *testing.T) { + env := vmtest.New(t) + env.RunConnectivityTest(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( + 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)) + } + sameLAN := func(env *vmtest.Env) *vmtest.Node { + n := env.NumNodes() + return env.AddNode(fmt.Sprintf("node-%d", n), sharedNW, vmtest.OS(vmtest.Gokrazy)) + } + env.RunConnectivityTest(t.Name(), vmtest.PingRouteLocal, makeEasy, sameLAN) +} + +// TestBPFDisco tests https://github.com/tailscale/tailscale/issues/3824 ... +// * server behind a Hard NAT +// * client behind a NAT with UPnP support +// * 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) +} + +func TestHostFWNoBPF(t *testing.T) { + env := vmtest.New(t) + env.RunConnectivityTest(t.Name(), vmtest.PingRouteDERP, easyPMPFWNoBPF, hard) +} + +func TestHostFWPair(t *testing.T) { + env := vmtest.New(t) + env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easyFW, easyFW) +} + +func TestOneHostFW(t *testing.T) { + env := vmtest.New(t) + env.RunConnectivityTest(t.Name(), vmtest.PingRouteDirect, easy, easyFW) +} + +// Issue tailscale/corp#26438: use learned DERP route as send path of last +// resort +// +// See (*magicsock.Conn).fallbackDERPRegionForPeer and its comment for +// background. +// +// This sets up a test with two nodes that must use DERP to communicate but the +// target of the ping (the second node) additionally is not getting DERP or +// Endpoint updates from the control plane. (Or rather, it's getting them but is +// configured to scrub them right when they come off the network before being +// processed) This then tests whether node2, upon receiving a packet, will be +// able to reply to node1 since it knows neither node1's endpoints nor its home +// DERP. The only reply route it can use is that fact that it just received a +// 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) +} + +// TestSingleJustIPv6 tests that a node can connect to control with just IPv6. +// Since there is no connectivity testing needed, the test just asserts the +// node coming up which will be asserted by env.Start(). +func TestSingleJustIPv6(t *testing.T) { + env := vmtest.New(t) + just6(env) + env.Start() +} + +// TestSingleDualBrokenIPv4 tests a dual-stack node with broken +// (blackholed) IPv4. +// +// See https://github.com/tailscale/tailscale/issues/13346 +func TestSingleDualBrokenIPv4(t *testing.T) { + if !*knownBroken { + t.Skip("skipping known-broken test; set --known-broken to run; see https://github.com/tailscale/tailscale/issues/13346") + } + env := vmtest.New(t) + v6AndBlackholedIPv4(env) + env.Start() +} + +func TestNonTailscaleCGNATEndpoint(t *testing.T) { + env := vmtest.New(t) + + cgnatNW := env.AddNetwork("100.65.1.1/16", "2.1.1.1", vnet.EasyNAT) + n0 := env.AddNode("node-0", + cgnatNW, + vmtest.DontJoinTailnet(), + vmtest.OS(vmtest.Gokrazy)) + n1 := env.AddNode("node-1", + cgnatNW, + tailcfg.NodeCapMap{tailcfg.NodeAttrDisableLinuxCGNATDropRule: nil}, + vmtest.OS(vmtest.Gokrazy)) + + env.Start() + env.LANPing(n1, n0.LanIP(cgnatNW)) +} diff --git a/tstest/natlab/vmtest/qemu.go b/tstest/natlab/vmtest/qemu.go index fbf31adb3..73b265078 100644 --- a/tstest/natlab/vmtest/qemu.go +++ b/tstest/natlab/vmtest/qemu.go @@ -112,6 +112,7 @@ func (e *Env) startGokrazyQEMU(n *Node) error { } sysLogAddr := net.JoinHostPort(vnet.FakeSyslogIPv4().String(), "995") if n.vnetNode.IsV6Only() { + fmt.Fprintf(&envBuf, " tta.nameserver=%s", vnet.FakeDNSIPv6()) sysLogAddr = net.JoinHostPort(vnet.FakeSyslogIPv6().String(), "995") } diff --git a/tstest/natlab/vmtest/vmtest.go b/tstest/natlab/vmtest/vmtest.go index 206ffde78..ac9689f47 100644 --- a/tstest/natlab/vmtest/vmtest.go +++ b/tstest/natlab/vmtest/vmtest.go @@ -446,13 +446,14 @@ func (e *Env) AddNode(name string, opts ...any) *Node { return n } -// LanIP returns the LAN IPv4 address of this node on the given network. -// This is only valid after Env.Start() has been called. -// Name returns the node's name as set in [Env.AddNode]. +// Name returns the name of the Node. func (n *Node) Name() string { return n.name } +// LanIP returns the LAN IPv4 address of this node on the given network. +// This is only valid after Env.Start() has been called. +// Name returns the node's name as set in [Env.AddNode]. func (n *Node) LanIP(net *vnet.Network) netip.Addr { return n.vnetNode.LanIP(net) } @@ -1818,3 +1819,8 @@ 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) +}