WIP: rebase for 2026-05-18 #7
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user