vmtest: add VM-based integration test framework

Add tstest/natlab/vmtest, a high-level framework for running multi-VM
integration tests with mixed OS types (gokrazy + Ubuntu/Debian cloud
images) connected via natlab's vnet virtual network.

The vmtest package provides:
  - Env type that orchestrates vnet, QEMU processes, and agent connections
  - OS image support (Gokrazy, Ubuntu2404, Debian12) with download/cache
  - QEMU launch per OS type (microvm for gokrazy, q35+KVM for cloud)
  - Cloud-init seed ISO generation with network-config for multi-NIC
  - Cross-compilation of test binaries for cloud VMs
  - Debug SSH NIC on cloud VMs for interactive debugging
  - Test helpers: ApproveRoutes, HTTPGet, TailscalePing, DumpStatus,
    WaitForPeerRoute, SSHExec

TTA enhancements (cmd/tta):
  - Parameterize /up (accept-routes, advertise-routes, snat-subnet-routes)
  - Add /set, /start-webserver, /http-get endpoints
  - /http-get uses local.Client.UserDial for Tailscale-routed requests
  - Fix /ping for non-gokrazy systems

TestSubnetRouter exercises a 3-VM subnet router scenario:
  client (gokrazy) → subnet-router (Ubuntu, dual-NIC) → backend (gokrazy)
  Verifies HTTP access to the backend webserver through the Tailscale
  subnet route. Passes in ~30 seconds.

Updates tailscale/tailscale#13038

Change-Id: I165b64af241d37f5f5870e796a52502fc56146fa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-08 18:09:05 +00:00
committed by Brad Fitzpatrick
parent d948b78b23
commit ec0b23a21f
12 changed files with 1382 additions and 11 deletions
+35 -1
View File
@@ -294,6 +294,24 @@ func stringifyTEI(tei stack.TransportEndpointID) string {
return fmt.Sprintf("%s -> %s", remoteHostPort, localHostPort)
}
// vipNameOf returns the VIP name for the given IP, or "" if it's not a VIP.
func vipNameOf(ip netip.Addr) string {
for _, v := range vips {
if v.Match(ip) {
return v.name
}
}
return ""
}
// nodeNameOf returns the node's name for the given IP on this network, or "" if unknown.
func (n *network) nodeNameOf(ip netip.Addr) string {
if node, ok := n.nodeByIP(ip); ok {
return node.String()
}
return ""
}
func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
reqDetails := r.ID()
@@ -305,7 +323,17 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
return
}
log.Printf("vnet-AcceptTCP: %v", stringifyTEI(reqDetails))
// Annotate the log with node/VIP names for readability.
srcHP := net.JoinHostPort(clientRemoteIP.String(), strconv.Itoa(int(reqDetails.RemotePort)))
srcStr := srcHP
if name := n.nodeNameOf(clientRemoteIP); name != "" {
srcStr = fmt.Sprintf("%s (%s)", srcHP, name)
}
dstStr := net.JoinHostPort(destIP.String(), strconv.Itoa(int(destPort)))
if name := vipNameOf(destIP); name != "" {
dstStr = fmt.Sprintf("%s (%s)", dstStr, name)
}
log.Printf("vnet-AcceptTCP: %s -> %s", srcStr, dstStr)
var wq waiter.Queue
ep, err := r.CreateEndpoint(&wq)
@@ -1466,6 +1494,12 @@ func (n *network) HandleEthernetPacketForRouter(ep EthernetPacket) {
return
}
if toForward {
// Traffic to destinations we don't handle (e.g. VMs trying to reach
// the real internet for NTP, package updates, etc). Expected; drop silently.
return
}
n.logf("router got unknown packet: %v", packet)
}