tstest/natlab/vmtest, cmd/tta: add TestTaildrop

Add a vmtest that brings up two Ubuntu nodes, each behind its own
EasyNAT, joined to the tailnet. The sender pushes a small file via
"tailscale file cp" and the receiver fetches it via "tailscale file
get --wait", asserting that the filename and contents round-trip
unchanged.

To make Taildrop work in vmtest, three small pieces were needed:

The Linux/FreeBSD cloud-init now starts tailscaled with --statedir as
well as --state=mem:, so the daemon has a VarRoot to host Taildrop's
incoming-files directory. State itself remains in-memory (so nothing
persists across reboots); only the var-root scratch space is on disk.

vmtest.New grows a variadic EnvOption parameter and a SameTailnetUser
helper. When the option is passed, Start sets AllNodesSameUser=true
on the embedded testcontrol.Server. Cross-node Taildrop requires the
sender and receiver to share a Tailnet user (or have an explicit
PeerCapabilityFileSharingTarget granted between them, which we don't
plumb here), so TestTaildrop opts in. Existing tests don't.

cmd/tta gains /taildrop-send and /taildrop-recv handlers that wrap
"tailscale file cp" and "tailscale file get --wait", plus
Env.SendTaildropFile and Env.RecvTaildropFile helpers in vmtest that
drive them.

Updates #13038

Change-Id: I8f5f70f88106e6e2ee07780dd46fe00f8efcfdf1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-28 17:26:10 +00:00
committed by Brad Fitzpatrick
parent 4b8e0ede6d
commit ec7b11d986
4 changed files with 222 additions and 5 deletions
+53
View File
@@ -4,6 +4,7 @@
package vmtest_test
import (
"bytes"
"fmt"
"net/netip"
"strings"
@@ -363,6 +364,58 @@ func TestSubnetRouterAndExitNode(t *testing.T) {
}
}
// TestTaildrop verifies that one Ubuntu node can send a file to another
// Ubuntu node via Taildrop, and the receiver gets the same content.
//
// Topology: two Ubuntu nodes, each behind its own EasyNAT, both joined to the
// tailnet. The sender runs `tailscale file cp` to push to the receiver's
// Tailscale IP; the receiver then runs `tailscale file get --wait` to fetch
// it.
func TestTaildrop(t *testing.T) {
env := vmtest.New(t, vmtest.SameTailnetUser())
senderNet := env.AddNetwork("1.0.0.1", "192.168.1.1/24", vnet.EasyNAT)
receiverNet := env.AddNetwork("2.0.0.1", "192.168.2.1/24", vnet.EasyNAT)
sender := env.AddNode("sender", senderNet,
vmtest.OS(vmtest.Ubuntu2404))
receiver := env.AddNode("receiver", receiverNet,
vmtest.OS(vmtest.Ubuntu2404))
// Declare test-specific steps for the web UI.
sendStep := env.AddStep("Taildrop send (sender -> receiver)")
recvStep := env.AddStep("Taildrop receive (on receiver)")
verifyStep := env.AddStep("Verify received name and contents")
env.Start()
const filename = "hello.txt"
want := []byte("hello world this is a Taildrop test\n")
sendStep.Begin()
env.SendTaildropFile(sender, receiver, filename, want)
sendStep.End(nil)
recvStep.Begin()
gotName, gotContent := env.RecvTaildropFile(t.Context(), receiver)
recvStep.End(nil)
verifyStep.Begin()
if gotName != filename {
err := fmt.Errorf("received name = %q; want %q", gotName, filename)
verifyStep.End(err)
t.Error(err)
return
}
if !bytes.Equal(gotContent, want) {
err := fmt.Errorf("received content = %q; want %q", gotContent, want)
verifyStep.End(err)
t.Error(err)
return
}
verifyStep.End(nil)
}
// TestExitNode verifies that switching the client's exit node setting between
// off, exit1, and exit2 correctly routes the client's internet traffic.
//