.github, tstest/natlab/vmtest: replace old VM runner job with natlab tests

The "vm" CI job ran a single test (TestRunUbuntu2404 from
tstest/integration/vms) on a privileged self-hosted runner. Its coverage
is nearly all redundant with the modern natlab vmtest suite, which boots
real Ubuntu VMs and already exercises connectivity, kernel TUN, SSH,
Taildrop, ACME, and OS DNS integration on GitHub-hosted runners.

The two things it tested that natlab didn't are added back as natlab
tests so the runner can be decommissioned:

TestUbuntuSystemdUnit runs tailscaled via the stock systemd unit that
Linux packages ship (cmd/tailscaled/tailscaled.service with
tailscaled.defaults as its EnvironmentFile) instead of launching the
binary directly, verifying the unit's directives and its Type=notify
readiness handshake.

TestDNSExtraRecordsSearchDomains verifies that control-plane DNS
ExtraRecords and search domains are resolvable through the guest's OS
resolver (libc to systemd-resolved to quad-100).

Updates #13038

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8d0dfb8b8153289e7ca78f3af03dfece9497bfe8
This commit is contained in:
Brad Fitzpatrick
2026-07-15 15:25:14 -04:00
committed by Brad Fitzpatrick
parent 6bf05cb63e
commit bef2cd8088
5 changed files with 192 additions and 33 deletions
+54 -6
View File
@@ -6,6 +6,7 @@ package vmtest
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"fmt"
"os"
@@ -110,6 +111,10 @@ func (e *Env) generateLinuxUserData(n *Node) string {
ud.WriteString(fmt.Sprintf(" ssh_authorized_keys:\n - %s\n", strings.TrimSpace(string(pubkey))))
}
if n.systemdUnit {
e.writeSystemdUnitFiles(&ud, n)
}
ud.WriteString("runcmd:\n")
// Remove the default route from the debug NIC (enp0s4) so traffic goes through vnet.
@@ -130,12 +135,21 @@ func (e *Env) generateLinuxUserData(n *Node) string {
ud.WriteString(" - [\"sysctl\", \"-w\", \"net.ipv6.conf.all.forwarding=1\"]\n")
}
// Start tailscaled in the background. --statedir provides a VarRoot so
// features like Taildrop (which needs a place to stash incoming files)
// have a directory to work with.
ud.WriteString(" - [\"mkdir\", \"-p\", \"/var/lib/tailscale\"]\n")
fmt.Fprintf(&ud, " - [\"/bin/sh\", \"-c\", \"%s/usr/local/bin/tailscaled --state=mem: --statedir=/var/lib/tailscale &\"]\n", tailscaledEnvPrefix(n))
ud.WriteString(" - [\"sleep\", \"2\"]\n")
// Start tailscaled, either via the stock systemd unit or directly in
// the background. --statedir provides a VarRoot so features like
// Taildrop (which needs a place to stash incoming files) have a
// directory to work with.
if n.systemdUnit {
// The unit's ExecStart runs /usr/sbin/tailscaled.
ud.WriteString(" - [\"cp\", \"/usr/local/bin/tailscaled\", \"/usr/sbin/tailscaled\"]\n")
ud.WriteString(" - [\"systemctl\", \"daemon-reload\"]\n")
// Type=notify makes this block until tailscaled reports readiness.
ud.WriteString(" - [\"systemctl\", \"start\", \"tailscaled.service\"]\n")
} else {
ud.WriteString(" - [\"mkdir\", \"-p\", \"/var/lib/tailscale\"]\n")
fmt.Fprintf(&ud, " - [\"/bin/sh\", \"-c\", \"%s/usr/local/bin/tailscaled --state=mem: --statedir=/var/lib/tailscale &\"]\n", tailscaledEnvPrefix(n))
ud.WriteString(" - [\"sleep\", \"2\"]\n")
}
// Start tta (Tailscale Test Agent).
ud.WriteString(" - [\"/bin/sh\", \"-c\", \"/usr/local/bin/tta &\"]\n")
@@ -225,6 +239,40 @@ func (e *Env) generateFreeBSDUserData(n *Node) string {
return ud.String()
}
// writeSystemdUnitFiles appends a cloud-init write_files section that
// installs the stock tailscaled systemd unit from the source tree, along
// with the packaging's /etc/default/tailscaled EnvironmentFile (plus any
// per-node TailscaledEnv variables). File contents are base64-encoded to
// sidestep YAML quoting.
func (e *Env) writeSystemdUnitFiles(ud *strings.Builder, n *Node) {
modRoot, err := findModRoot()
if err != nil {
e.t.Fatalf("finding module root for tailscaled.service: %v", err)
}
unit, err := os.ReadFile(filepath.Join(modRoot, "cmd/tailscaled/tailscaled.service"))
if err != nil {
e.t.Fatalf("reading tailscaled.service: %v", err)
}
defaults, err := os.ReadFile(filepath.Join(modRoot, "cmd/tailscaled/tailscaled.defaults"))
if err != nil {
e.t.Fatalf("reading tailscaled.defaults: %v", err)
}
var envFile strings.Builder
envFile.Write(defaults)
for _, env := range n.vnetNode.Env() {
fmt.Fprintf(&envFile, "%s=%q\n", env.Key, env.Value)
}
ud.WriteString("write_files:\n")
writeFile := func(path string, content []byte) {
fmt.Fprintf(ud, " - path: %s\n", path)
fmt.Fprintf(ud, " encoding: b64\n")
fmt.Fprintf(ud, " content: %s\n", base64.StdEncoding.EncodeToString(content))
}
writeFile("/etc/systemd/system/tailscaled.service", unit)
writeFile("/etc/default/tailscaled", []byte(envFile.String()))
}
func tailscaledEnvPrefix(n *Node) string {
env := n.vnetNode.Env()
if len(env) == 0 {