gokrazy, Makefile: improve appliance build tooling

Several improvements to the gokrazy appliance build and flash workflow:

gokrazy/build.go:
  - Round Pi image size up to a power of 2 (QEMU raspi3b requires it)
  - Use monogok's mkfs.Perm for the /perm ext4 partition (pure Go,
    cross-platform, no e2fsprogs dependency)

gokrazy/mkfs:
  - Accept optional PermFile entries to include in the freshly-created
    ext4 filesystem (used for breakglass authorized_keys)
  - Use progresstracking.Ticker for flush progress reporting

gokrazy/tsapp*/config.json:
  - Point breakglass at /perm/breakglass.authorized_keys (not ec2)
  - Fix Pi SerialConsole to serial0,115200 (not ttyS0)

cmd/tailscale/cli/configure-flash-appliance.go:
  - Add --add-ssh-authorized-keys flag to write an authorized_keys
    file into /perm during flash (for breakglass SSH access)
  - Use progresstracking.CountingWriter + Ticker for write progress

Makefile:
  - tsapp-build-and-flash-pi: auto-include ~/.ssh/id_ed25519.pub
  - tsapp-qemu-pi: use virt machine + UEFI + ramfb + e1000 (working
    network + framebuffer), with DTB watchdog patch and
    gokrazy.log_to_serial for debugging
  - Auto-detect UEFI firmware path across Debian/Homebrew/Fedora

Updates #1866

Change-Id: Ifa97ad34c509a81e1637d9bce12a788037dfe5ec
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-02 09:24:01 -07:00
committed by Brad Fitzpatrick
parent 4c22d22df5
commit 76eece2d15
6 changed files with 108 additions and 17 deletions
+22 -4
View File
@@ -30,7 +30,7 @@ var (
gaf = flag.Bool("gaf", false, "if true, build a gokrazy archive format file instead of a full disk image")
)
// imageSizeBytes is the size of the disk image we ask monogok to
// baseImageSizeBytes is the size of the disk image we ask monogok to
// produce (and that the AWS AMI import expects). It has to be large
// enough to fit gokrazy's standard partition layout (see
// github.com/bradfitz/monogok/disklayout):
@@ -45,7 +45,25 @@ var (
// file larger). The same value is passed to monogok via
// --target_storage_bytes and to mkfs.Perm so the GPT and the ext4
// inside it agree on the disk's size.
const imageSizeBytes = 1258299392
//
// imageSizeBytesFor may round this up; callers should use that helper
// instead of this constant.
const baseImageSizeBytes = 1258299392
// imageSizeBytesFor returns the disk image size to use for app. For Raspberry
// Pi appliances the size is rounded up to the next power of two because
// qemu-system-aarch64's raspi3b machine rejects SD card images whose size
// isn't a power of two.
func imageSizeBytesFor(app string) int64 {
if !strings.HasPrefix(app, "tsapp-pi.") {
return baseImageSizeBytes
}
n := int64(1)
for n < baseImageSizeBytes {
n <<= 1
}
return n
}
var conf gokrazyConfig
@@ -135,7 +153,7 @@ func buildImage() error {
args = append(args,
"overwrite",
"--full", filepath.Join(dir, *app+".img"),
fmt.Sprintf("--target_storage_bytes=%d", imageSizeBytes),
fmt.Sprintf("--target_storage_bytes=%d", imageSizeBytesFor(*app)),
)
}
@@ -156,7 +174,7 @@ func buildImage() error {
return fmt.Errorf("open %s: %w", imgPath, err)
}
defer f.Close()
if err := mkfs.Perm(f, imageSizeBytes); err != nil {
if err := mkfs.Perm(f, imageSizeBytesFor(*app)); err != nil {
return fmt.Errorf("formatting /perm in %s: %v", imgPath, err)
}
log.Printf("Wrote ext4 /perm filesystem to %s.", imgPath)
+22 -3
View File
@@ -38,11 +38,20 @@ const gptSecondaryReservedSectors = 34
const sectorSize = 512
// PermFile is a file to include in the /perm partition.
type PermFile struct {
Path string // path within the filesystem, e.g. "breakglass.authorized_keys"
Content []byte
}
// Perm creates an ext4 filesystem with volume label "PERM" inside the
// gokrazy /perm partition of f. devsizeBytes is the total disk size
// that the gokrazy GPT in f was written for; the partition layout is
// derived from it via [disklayout].
//
// If files is non-empty, the listed files are written into the
// filesystem before flushing to disk.
//
// To avoid issuing ext4.Create's hundreds of small scattered writes
// against slow storage one syscall at a time, the filesystem is first
// built in an in-memory sparse buffer and then only the genuinely
@@ -56,14 +65,14 @@ const sectorSize = 512
//
// f must be open read/write, and on macOS should be the buffered
// /dev/diskN device rather than the raw /dev/rdiskN alias.
func Perm(f *os.File, devsizeBytes int64) error {
func Perm(f *os.File, devsizeBytes int64, files ...PermFile) error {
permStart := int64(disklayout.PermStartLBA(disklayout.DefaultBootPartitionStartLBA)) * sectorSize
permSize := int64(disklayout.PermSize(disklayout.DefaultBootPartitionStartLBA, uint64(devsizeBytes))-gptSecondaryReservedSectors) * sectorSize
fmt.Fprintf(os.Stderr, "Formatting /perm as ext4 (PERM): %s filesystem\n", humanBytes(permSize))
mem := newMemBackend(permSize)
if _, err := ext4.Create(mem, permSize, 0, sectorSize, &ext4.Params{
fsys, err := ext4.Create(mem, permSize, 0, sectorSize, &ext4.Params{
VolumeName: "PERM",
// Force 4 KiB blocks. go-diskfs v1.9.3 otherwise defaults to 1
// KiB blocks regardless of filesystem size, which makes a 128
@@ -79,9 +88,19 @@ func Perm(f *os.File, devsizeBytes int64) error {
Features: []ext4.FeatureOpt{
ext4.WithFeatureReservedGDTBlocksForExpansion(false),
},
}); err != nil {
})
if err != nil {
return fmt.Errorf("ext4.Create: %w", err)
}
for _, pf := range files {
w, err := fsys.OpenFile("/"+pf.Path, os.O_CREATE|os.O_RDWR)
if err != nil {
return fmt.Errorf("create %s in /perm: %w", pf.Path, err)
}
if _, err := w.Write(pf.Content); err != nil {
return fmt.Errorf("write %s in /perm: %w", pf.Path, err)
}
}
return mem.flushTo(f, permStart)
}
+1 -1
View File
@@ -18,7 +18,7 @@
"PackageConfig": {
"github.com/gokrazy/breakglass": {
"CommandLineFlags": [
"-authorized_keys=ec2"
"-authorized_keys=/perm/breakglass.authorized_keys"
]
},
"tailscale.com/cmd/tailscale": {