WIP: rebase for 2026-05-18 #7

Draft
codinget wants to merge 234 commits from rebase/2026-05-18 into webnet
6 changed files with 298 additions and 328 deletions
Showing only changes of commit bb47ea2c6b - Show all commits
+1 -1
View File
@@ -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
-324
View File
@@ -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) {
+30
View File
@@ -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)
}
+257
View File
@@ -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))
}
+1
View File
@@ -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")
}
+9 -3
View File
@@ -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)
}