From 9d01b036c798fec65432f4ec918b2ec3c2e57597 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 14 Jul 2026 17:37:51 +0000 Subject: [PATCH] tstest/natlab/vmtest: test jailed and masqueraded peers end-to-end The tun-layer per-peer data plane (jailed packet filter selection and masquerade NAT rewrites) had no end-to-end coverage: nothing asserted that a peer the control server marks jailed actually has its flows dropped, or that masquerade addresses assigned by control actually carry rewritten traffic in both directions. Add a two-gokrazy-node natlab test driving both knobs from the test control server and probing with HTTP requests between the nodes' webservers after each netmap transition, asserting each response carries the serving node's greeting so masqueraded flows provably reach the intended node. TSMP pings are deliberately not used as probes: tstun answers those before running the packet filter, so they succeed even for jailed peers. Updates #12542 Change-Id: Ia978e8d368f08a5a1117280f12bd50310969d0ec Signed-off-by: Brad Fitzpatrick --- tstest/natlab/vmtest/jailmasq_test.go | 138 ++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tstest/natlab/vmtest/jailmasq_test.go diff --git a/tstest/natlab/vmtest/jailmasq_test.go b/tstest/natlab/vmtest/jailmasq_test.go new file mode 100644 index 000000000..a3ab5433d --- /dev/null +++ b/tstest/natlab/vmtest/jailmasq_test.go @@ -0,0 +1,138 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +package vmtest_test + +import ( + "fmt" + "net/netip" + "strings" + "testing" + "time" + + "tailscale.com/tailcfg" + "tailscale.com/tstest/integration/testcontrol" + "tailscale.com/tstest/natlab/vmtest" + "tailscale.com/tstest/natlab/vnet" + "tailscale.com/types/key" +) + +// TestJailedAndMasqueradedPeers exercises the tun-layer per-peer data +// plane end-to-end with two gokrazy VMs: the control server marks one +// peer jailed (so the other side's jailed packet filter must drop its +// TCP flows) and later assigns masquerade addresses (so per-packet +// NAT must rewrite addresses in both directions). Connectivity is +// probed with HTTP requests between the nodes' webservers, which flow +// through the WireGuard tunnel and the tun-layer filters; TSMP pings +// are no good here because tstun answers them before running the +// packet filter. Each transition arrives as a netmap update and must +// take effect without restarting anything. +func TestJailedAndMasqueradedPeers(t *testing.T) { + env := vmtest.New(t, vmtest.AllOnline()) + addNode := func() *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), + vmtest.WebServer(8080)) + } + n1 := addNode() + n2 := addNode() + env.Start() + + st1 := env.Status(n1) + st2 := env.Status(n2) + k1, k2 := st1.Self.PublicKey, st2.Self.PublicKey + n1IP, n2IP := st1.Self.TailscaleIPs[0], st2.Self.TailscaleIPs[0] + cs := env.ControlServer() + + url := func(ip netip.Addr) string { return fmt.Sprintf("http://%s:8080/", ip) } + + // awaitHTTPOK polls until an HTTP GET from the given node succeeds + // and the body carries the serving node's greeting, proving the + // TCP flow crossed the tunnel to the intended node. + awaitHTTPOK := func(from *vmtest.Node, targetURL, wantGreeting string) { + t.Helper() + deadline := time.Now().Add(2 * time.Minute) + var lastErr error + for time.Now().Before(deadline) { + res, err := env.HTTPGetStatus(from, targetURL) + if err == nil && res.Status == 200 && strings.Contains(res.Body, wantGreeting) { + return + } + if err != nil { + lastErr = err + } else { + lastErr = fmt.Errorf("status=%d body=%q", res.Status, res.Body) + } + } + t.Fatalf("GET %s from %s never succeeded: %v", targetURL, from.Name(), lastErr) + } + + // awaitHTTPFails polls until an HTTP GET from the given node + // fails, for asserting that a jailed peer's flows get dropped + // once the netmap update lands. + awaitHTTPFails := func(from *vmtest.Node, targetURL string) { + t.Helper() + deadline := time.Now().Add(2 * time.Minute) + for time.Now().Before(deadline) { + res, err := env.HTTPGetStatus(from, targetURL) + if err != nil || res.Status != 200 { + return + } + } + t.Fatalf("GET %s from %s still succeeding; want drop by jailed filter", targetURL, from.Name()) + } + + // Warm up the tunnel, then check HTTP in both directions. + if err := env.Ping(n1, n2, tailcfg.PingTSMP, 30*time.Second); err != nil { + t.Fatal(err) + } + awaitHTTPOK(n2, url(n1IP), n1.Name()) + awaitHTTPOK(n1, url(n2IP), n2.Name()) + + // Jail n2 as seen by n1: n1's data plane must start dropping + // n2-initiated flows. + cs.SetJailed(k1, k2, true) + awaitHTTPFails(n2, url(n1IP)) + + // Unjailing must restore connectivity the same way. + cs.SetJailed(k1, k2, false) + awaitHTTPOK(n2, url(n1IP), n1.Name()) + awaitHTTPOK(n1, url(n2IP), n2.Name()) + + // Masquerade both nodes: each side now knows the other only by + // its masquerade address, and the tun layer must NAT between the + // masquerade and native addresses in both directions. + n1Masq := netip.MustParseAddr("100.64.101.1") + n2Masq := netip.MustParseAddr("100.64.102.1") + cs.SetMasqueradeAddresses([]testcontrol.MasqueradePair{ + {Node: k1, Peer: k2, NodeMasqueradesAs: n1Masq}, + {Node: k2, Peer: k1, NodeMasqueradesAs: n2Masq}, + }) + + // Wait for each side's netmap to show the peer at its masquerade + // address, then fetch through it. + awaitPeerIP := func(on *vmtest.Node, peer key.NodePublic, want netip.Addr) { + t.Helper() + deadline := time.Now().Add(time.Minute) + for time.Now().Before(deadline) { + if ps, ok := env.Status(on).Peer[peer]; ok { + for _, ip := range ps.TailscaleIPs { + if ip == want { + return + } + } + } + time.Sleep(500 * time.Millisecond) + } + t.Fatalf("%s never saw peer %s at %v", on.Name(), peer.ShortString(), want) + } + awaitPeerIP(n1, k2, n2Masq) + awaitPeerIP(n2, k1, n1Masq) + + awaitHTTPOK(n2, url(n1Masq), n1.Name()) + awaitHTTPOK(n1, url(n2Masq), n2.Name()) +}