mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-06 12:46:20 +02:00
Cache a pre-booted macOS VM snapshot on disk so subsequent test runs restore from the snapshot instead of cold-booting. The snapshot is keyed by the Tart base image digest and a code version constant (macOSSnapshotCodeVersion); bumping either invalidates the cache. Snapshot preparation (one-time): - Boot the Tart base image with a NAT NIC (--nat-nic flag) - Wait for SSH, compile and install cmd/tta as a LaunchDaemon - TTA polls the host via AF_VSOCK for an IP assignment; during prep the host replies "wait" - Disconnect NIC, save VM state via SIGINT Test fast path (cached, ~7s to agent connected): - APFS clone the snapshot, write test-specific config.json - Launch Host.app with --disconnected-nic --attach-network --assign-ip - VZ restores from SaveFile.vzvmsave (~5s with 4GB RAM) - TTA's vsock poll gets the IP config, sets static IP via ifconfig (bypasses DHCP entirely), switches driver addr to the IP directly (bypasses DNS), and resets the dial context so the reverse-dial reconnects immediately - TTA agent connects to test driver within ~2s of IP assignment Key optimizations: - 4GB RAM instead of 8GB: halves SaveFile.vzvmsave (1.4GB vs 2.4GB), halves restore time (5.5s vs 11s) - AF_VSOCK IP assignment: bypasses macOS DHCP (~5-7s saved) - Direct IP dial: bypasses DNS resolution for test-driver.tailscale - Dial context reset: cancels stale in-flight dials from snapshot - Kill instead of SIGINT for test VM cleanup (no state save needed) - Parallel VM launches Also: - Add TestDriverIPv4/TestDriverPort constants to vnet - Add --nat-nic and --assign-ip flags to Host.app - Fix SIGINT handler: retain DispatchSource globally, use dispatchMain() - Add vsock listener (port 51011) to Host.app for IP config protocol - Add disconnectNetwork() to VMController for clean snapshot state - Fix Makefile: set -o pipefail so xcodebuild failures aren't swallowed Updates #13038 Change-Id: Icbab73b57af7df3ae96136fb49cda2536310f31b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os/exec"
|
|
"strconv"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/tstest/natlab/vnet"
|
|
)
|
|
|
|
const (
|
|
afVSOCK = 40 // AF_VSOCK on macOS
|
|
vmaddrCIDHost = 2 // VMADDR_CID_HOST
|
|
vsockPort = 51011 // port for IP assignment protocol
|
|
)
|
|
|
|
// sockaddrVM is the Go equivalent of struct sockaddr_vm from <sys/vsock.h>.
|
|
type sockaddrVM struct {
|
|
Len uint8
|
|
Family uint8
|
|
Reserved1 uint16
|
|
Port uint32
|
|
CID uint32
|
|
}
|
|
|
|
type netConfig struct {
|
|
IP string `json:"ip"`
|
|
Mask string `json:"mask"`
|
|
GW string `json:"gw"`
|
|
}
|
|
|
|
// startIPAssignLoop starts a background goroutine that polls the host
|
|
// via the virtio socket for an IP assignment. When the host responds
|
|
// with a JSON config (rather than "wait"), TTA sets the IP statically
|
|
// using ifconfig and stops polling.
|
|
func startIPAssignLoop() {
|
|
go ipAssignLoop()
|
|
}
|
|
|
|
func ipAssignLoop() {
|
|
log.Printf("ipassign: starting vsock poll loop")
|
|
var lastErr string
|
|
for attempt := 0; ; attempt++ {
|
|
resp, err := askHostForIP()
|
|
if err != nil {
|
|
if e := err.Error(); e != lastErr {
|
|
log.Printf("ipassign: attempt %d: %v", attempt, err)
|
|
lastErr = e
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
continue
|
|
}
|
|
if resp == "wait" {
|
|
time.Sleep(500 * time.Millisecond)
|
|
continue
|
|
}
|
|
var nc netConfig
|
|
if err := json.Unmarshal([]byte(resp), &nc); err != nil {
|
|
log.Printf("ipassign: bad config: %v", err)
|
|
time.Sleep(500 * time.Millisecond)
|
|
continue
|
|
}
|
|
if err := setStaticIP(nc); err != nil {
|
|
log.Printf("ipassign: %v", err)
|
|
time.Sleep(500 * time.Millisecond)
|
|
continue
|
|
}
|
|
log.Printf("ipassign: configured en0 with %s/%s gw %s", nc.IP, nc.Mask, nc.GW)
|
|
|
|
// Switch the driver address from the DNS name to the IP directly
|
|
// (avoids DNS resolution delay) and kick the dial-out loop so it
|
|
// retries immediately with the new address.
|
|
ipAddr := net.JoinHostPort(vnet.TestDriverIPv4().String(), strconv.Itoa(vnet.TestDriverPort))
|
|
*driverAddr = ipAddr
|
|
log.Printf("ipassign: switched driver addr to %s", ipAddr)
|
|
resetDialCancels()
|
|
return
|
|
}
|
|
}
|
|
|
|
// askHostForIP connects to the host via AF_VSOCK and reads the response.
|
|
func askHostForIP() (string, error) {
|
|
fd, err := unix.Socket(afVSOCK, unix.SOCK_STREAM, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("socket: %w", err)
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
// Set a short connect+read timeout via SO_RCVTIMEO.
|
|
tv := unix.Timeval{Sec: 1}
|
|
unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
|
|
|
|
addr := sockaddrVM{
|
|
Len: uint8(unsafe.Sizeof(sockaddrVM{})),
|
|
Family: afVSOCK,
|
|
Port: vsockPort,
|
|
CID: vmaddrCIDHost,
|
|
}
|
|
_, _, errno := unix.RawSyscall(unix.SYS_CONNECT, uintptr(fd),
|
|
uintptr(unsafe.Pointer(&addr)), unsafe.Sizeof(addr))
|
|
if errno != 0 {
|
|
return "", fmt.Errorf("connect: %w", errno)
|
|
}
|
|
|
|
var buf [1024]byte
|
|
n, err := unix.Read(fd, buf[:])
|
|
if err != nil {
|
|
return "", fmt.Errorf("read: %w", err)
|
|
}
|
|
return string(buf[:n]), nil
|
|
}
|
|
|
|
// setStaticIP configures en0 with a static IP address and default route.
|
|
func setStaticIP(nc netConfig) error {
|
|
out, err := exec.Command("ifconfig", "en0", nc.IP, "netmask", nc.Mask, "up").CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("ifconfig: %v: %s", err, out)
|
|
}
|
|
out, err = exec.Command("route", "add", "default", nc.GW).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("route add: %v: %s", err, out)
|
|
}
|
|
return nil
|
|
}
|