cmd/tailscale/cli: add 'tailscale configure flash-appliance'

Adds a CLI subcommand that downloads a signed Tailscale appliance
image (Gokrazy archive format, GAF) from pkgs.tailscale.com,
constructs a fresh GPT-partitioned disk from it (mbr.img + a
synthesized partition table + boot.img + root.img), formats /perm
as ext4 in pure Go via go-diskfs, and ejects the disk so a user
running on a regular workstation can flash an SD card or homelab
VM disk in one command without installing e2fsprogs.

On macOS the target disk is auto-discovered via diskutil, skipping
the boot disk and anything bigger than 256 GB out of paranoia. On
Linux the user passes --disk=/dev/sdX explicitly. Windows is not
supported yet and the command returns an error.

The GPT layout matches monogok's full-disk layout via the new
public github.com/bradfitz/monogok/disklayout package; a drift-
guard test inside monogok asserts the two implementations stay
byte-identical so OTA updates against monogok-built images keep
working.

Behind a ts_omit_flashappliance build tag (on by default).

Updates #1866

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ic1a8cd185e7039edccb7702ab4104544fcb58d29
This commit is contained in:
Brad Fitzpatrick
2026-07-01 08:09:50 -07:00
committed by Brad Fitzpatrick
parent 64422f274d
commit d0fcb668d5
26 changed files with 2015 additions and 101 deletions
+9 -6
View File
@@ -361,7 +361,7 @@ func (up *Updater) updateSynology() error {
if err != nil {
return err
}
latest, err := latestPackages(up.Track)
latest, err := LatestPackages(up.Track)
if err != nil {
return err
}
@@ -906,7 +906,7 @@ func (up *Updater) updateGokrazy() error {
if err != nil {
return err
}
latest, err := latestPackages(up.Track)
latest, err := LatestPackages(up.Track)
if err != nil {
return err
}
@@ -1305,7 +1305,7 @@ func LatestTailscaleVersion(track string) (string, error) {
track = CurrentTrack
}
latest, err := latestPackages(track)
latest, err := LatestPackages(track)
if err != nil {
return "", err
}
@@ -1331,7 +1331,8 @@ func LatestTailscaleVersion(track string) (string, error) {
return ver, nil
}
type trackPackages struct {
// TrackPackages is the JSON shape served at <pkgs>/<track>/?mode=json.
type TrackPackages struct {
Version string
Tarballs map[string]string
TarballsVersion string
@@ -1349,14 +1350,16 @@ type trackPackages struct {
var tailscaleHTTPEndpoint = "https://pkgs.tailscale.com"
func latestPackages(track string) (*trackPackages, error) {
// LatestPackages fetches the package manifest served at
// <pkgs>/<track>/?mode=json for the current runtime.GOOS.
func LatestPackages(track string) (*TrackPackages, error) {
url := fmt.Sprintf("%s/%s/?mode=json&os=%s", tailscaleHTTPEndpoint, track, runtime.GOOS)
res, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("fetching latest tailscale version: %w", err)
}
defer res.Body.Close()
var latest trackPackages
var latest TrackPackages
if err := json.NewDecoder(res.Body).Decode(&latest); err != nil {
return nil, fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}