Add experimental Void guest workflow and vsock agent

Make iterating on a Firecracker-friendly Void guest practical without replacing the Debian default image path.

Add local Void rootfs build/register/verify plumbing, a language-agnostic dev package baseline, and guest SSH/work-disk hardening so new images use the runtime bundle key, keep a normal root bash environment, and repair stale nested /root layouts on restart.

Replace the guest PING/PONG responder with an HTTP /healthz agent over vsock, rename the runtime bundle and config surface from ping helper to agent while still accepting the legacy keys, and route the post-SSH reminder through the new vm.health path.

Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, bash -n customize.sh make-rootfs-void.sh, and git diff --check.
This commit is contained in:
Thales Maciel 2026-03-19 14:51:25 -03:00
parent c8d9a122f9
commit 3ed78fdcfc
No known key found for this signature in database
GPG key ID: 33112E6833C34679
42 changed files with 2222 additions and 388 deletions

View file

@ -13,11 +13,13 @@ import (
"banger/internal/api"
"banger/internal/firecracker"
"banger/internal/guest"
"banger/internal/guestconfig"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/system"
"banger/internal/vmdns"
"banger/internal/vsockagent"
)
var (
@ -582,11 +584,11 @@ func (d *Daemon) GetVMStats(ctx context.Context, idOrName string) (model.VMRecor
return vm, vm.Stats, nil
}
func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPingResult, err error) {
func (d *Daemon) HealthVM(ctx context.Context, idOrName string) (result api.VMHealthResult, err error) {
_, err = d.withVMLockByRef(ctx, idOrName, func(vm model.VMRecord) (model.VMRecord, error) {
result.Name = vm.Name
if vm.State != model.VMStateRunning || !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
result.Alive = false
result.Healthy = false
return vm, nil
}
if strings.TrimSpace(vm.Runtime.VSockPath) == "" {
@ -600,15 +602,23 @@ func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPing
}
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := firecracker.PingVSock(pingCtx, d.logger, vm.Runtime.VSockPath); err != nil {
if err := vsockagent.Health(pingCtx, d.logger, vm.Runtime.VSockPath); err != nil {
return model.VMRecord{}, err
}
result.Alive = true
result.Healthy = true
return vm, nil
})
return result, err
}
func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPingResult, err error) {
health, err := d.HealthVM(ctx, idOrName)
if err != nil {
return api.VMPingResult{}, err
}
return api.VMPingResult{Name: health.Name, Alive: health.Healthy}, nil
}
func (d *Daemon) getVMStatsLocked(ctx context.Context, vm model.VMRecord) (model.VMRecord, error) {
stats, err := d.collectStats(ctx, vm)
if err == nil {
@ -814,6 +824,84 @@ func (d *Daemon) ensureWorkDisk(ctx context.Context, vm *model.VMRecord, image m
return nil
}
func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
if err != nil {
return fmt.Errorf("derive authorized ssh key: %w", err)
}
workMount, cleanupWork, err := system.MountTempDir(ctx, d.runner, vm.Runtime.WorkDiskPath, false)
if err != nil {
return err
}
defer cleanupWork()
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
return err
}
sshDir := filepath.Join(workMount, ".ssh")
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
return err
}
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
return err
}
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
existing, err := d.runner.RunSudo(ctx, "cat", authorizedKeysPath)
if err != nil {
existing = nil
}
merged := mergeAuthorizedKey(existing, publicKey)
tmpFile, err := os.CreateTemp("", "banger-authorized-keys-*")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
if _, err := tmpFile.Write(merged); err != nil {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err := tmpFile.Close(); err != nil {
_ = os.Remove(tmpPath)
return err
}
defer os.Remove(tmpPath)
if _, err := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
return err
}
return nil
}
func mergeAuthorizedKey(existing, managed []byte) []byte {
managedLine := strings.TrimSpace(string(managed))
if managedLine == "" {
return append([]byte(nil), existing...)
}
lines := strings.Split(strings.ReplaceAll(string(existing), "\r\n", "\n"), "\n")
out := make([]string, 0, len(lines)+1)
found := false
for _, line := range lines {
line = strings.TrimRight(line, "\r")
trimmed := strings.TrimSpace(line)
if trimmed == "" {
continue
}
if trimmed == managedLine {
found = true
}
out = append(out, line)
}
if !found {
out = append(out, managedLine)
}
return []byte(strings.Join(out, "\n") + "\n")
}
func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) error {
nestedHome := filepath.Join(workMount, "root")
if !exists(nestedHome) {