WIP: rebase for 2026-05-18 #7
@@ -42,4 +42,4 @@ jobs:
|
|||||||
make -C gokrazy natlab
|
make -C gokrazy natlab
|
||||||
- name: Run natlab integration tests
|
- name: Run natlab integration tests
|
||||||
run: |
|
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"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -116,10 +115,6 @@ func findKernelPath(goMod string) (string, error) {
|
|||||||
|
|
||||||
type addNodeFunc func(c *vnet.Config) *vnet.Node // returns nil to omit test
|
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 {
|
func easy(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
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))
|
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 {
|
func easyAF(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
return c.AddNode(c.AddNetwork(
|
||||||
@@ -196,22 +140,6 @@ func sameLAN(c *vnet.Config) *vnet.Node {
|
|||||||
return c.AddNode(nw)
|
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 {
|
func one2one(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
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))
|
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 {
|
func hard(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
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))
|
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 {
|
func hardPMP(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
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)
|
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 {
|
func (nt *natTest) runTailscaleConnectivityTest(addNode ...addNodeFunc) pingRoute {
|
||||||
ctx, cancel := testContext(nt.tb)
|
ctx, cancel := testContext(nt.tb)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -708,60 +554,6 @@ func up(ctx context.Context, c *vnet.NodeAgentClient) error {
|
|||||||
return nil
|
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 {
|
type nodeType struct {
|
||||||
name string
|
name string
|
||||||
fn addNodeFunc
|
fn addNodeFunc
|
||||||
@@ -778,30 +570,6 @@ var types = []nodeType{
|
|||||||
{"cgnat", cgnatNoTailnet},
|
{"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 {
|
func cgnatNoTailnet(c *vnet.Config) *vnet.Node {
|
||||||
n := c.NumNodes() + 1
|
n := c.NumNodes() + 1
|
||||||
return c.AddNode(c.AddNetwork(
|
return c.AddNode(c.AddNetwork(
|
||||||
@@ -811,98 +579,6 @@ func cgnatNoTailnet(c *vnet.Config) *vnet.Node {
|
|||||||
vnet.DontJoinTailnet)
|
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)")
|
var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPMP, hardPMP, one2one, sameLAN)")
|
||||||
|
|
||||||
func TestPair(t *testing.T) {
|
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")
|
sysLogAddr := net.JoinHostPort(vnet.FakeSyslogIPv4().String(), "995")
|
||||||
if n.vnetNode.IsV6Only() {
|
if n.vnetNode.IsV6Only() {
|
||||||
|
fmt.Fprintf(&envBuf, " tta.nameserver=%s", vnet.FakeDNSIPv6())
|
||||||
sysLogAddr = net.JoinHostPort(vnet.FakeSyslogIPv6().String(), "995")
|
sysLogAddr = net.JoinHostPort(vnet.FakeSyslogIPv6().String(), "995")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,13 +446,14 @@ func (e *Env) AddNode(name string, opts ...any) *Node {
|
|||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// LanIP returns the LAN IPv4 address of this node on the given network.
|
// Name returns the name of the Node.
|
||||||
// This is only valid after Env.Start() has been called.
|
|
||||||
// Name returns the node's name as set in [Env.AddNode].
|
|
||||||
func (n *Node) Name() string {
|
func (n *Node) Name() string {
|
||||||
return n.name
|
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 {
|
func (n *Node) LanIP(net *vnet.Network) netip.Addr {
|
||||||
return n.vnetNode.LanIP(net)
|
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)
|
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