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:
parent
c8d9a122f9
commit
3ed78fdcfc
42 changed files with 2222 additions and 388 deletions
|
|
@ -2,6 +2,10 @@ package daemon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
|
@ -253,7 +257,7 @@ func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
||||
func TestHealthVMReturnsHealthyForRunningGuest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -296,16 +300,24 @@ func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
|||
serverDone <- err
|
||||
return
|
||||
}
|
||||
n, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
reqBuf := make([]byte, 0, 512)
|
||||
reqBuf = append(reqBuf, buf[:0]...)
|
||||
for {
|
||||
n, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
reqBuf = append(reqBuf, buf[:n]...)
|
||||
if strings.Contains(string(reqBuf), "\r\n\r\n") {
|
||||
break
|
||||
}
|
||||
}
|
||||
if got := string(reqBuf); !strings.Contains(got, "GET /healthz HTTP/1.1\r\n") {
|
||||
serverDone <- fmt.Errorf("unexpected health payload %q", got)
|
||||
return
|
||||
}
|
||||
if got := string(buf[:n]); got != "PING\n" {
|
||||
serverDone <- fmt.Errorf("unexpected ping payload %q", got)
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("PONG\n"))
|
||||
_, err = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"))
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
|
|
@ -326,12 +338,12 @@ func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
|||
},
|
||||
}
|
||||
d := &Daemon{store: db, runner: runner}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
result, err := d.HealthVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("PingVM: %v", err)
|
||||
t.Fatalf("HealthVM: %v", err)
|
||||
}
|
||||
if !result.Alive || result.Name != vm.Name {
|
||||
t.Fatalf("PingVM result = %+v, want alive %s", result, vm.Name)
|
||||
if !result.Healthy || result.Name != vm.Name {
|
||||
t.Fatalf("HealthVM result = %+v, want healthy %s", result, vm.Name)
|
||||
}
|
||||
runner.assertExhausted()
|
||||
if err := <-serverDone; err != nil {
|
||||
|
|
@ -339,7 +351,65 @@ func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPingVMReturnsFalseForStoppedVM(t *testing.T) {
|
||||
func TestPingVMAliasReturnsAliveForHealthyVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
apiSock := filepath.Join(t.TempDir(), "fc.sock")
|
||||
fake := startFakeFirecrackerProcess(t, apiSock)
|
||||
t.Cleanup(func() {
|
||||
_ = fake.Process.Kill()
|
||||
_ = fake.Wait()
|
||||
})
|
||||
vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
|
||||
listener, err := net.Listen("unix", vsockSock)
|
||||
if err != nil {
|
||||
t.Fatalf("listen vsock: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = listener.Close()
|
||||
_ = os.Remove(vsockSock)
|
||||
})
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 512)
|
||||
_, _ = conn.Read(buf)
|
||||
_, _ = conn.Write([]byte("OK 1\n"))
|
||||
_, _ = conn.Read(buf)
|
||||
_, _ = conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 15\r\n\r\n{\"status\":\"ok\"}"))
|
||||
}()
|
||||
vm := testVM("healthy-ping", "image-healthy", "172.16.0.42")
|
||||
vm.State = model.VMStateRunning
|
||||
vm.Runtime.State = model.VMStateRunning
|
||||
vm.Runtime.PID = fake.Process.Pid
|
||||
vm.Runtime.APISockPath = apiSock
|
||||
vm.Runtime.VSockPath = vsockSock
|
||||
vm.Runtime.VSockCID = 10042
|
||||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
runner := &scriptedRunner{
|
||||
t: t,
|
||||
steps: []runnerStep{
|
||||
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
|
||||
sudoStep("", nil, "chmod", "600", vsockSock),
|
||||
},
|
||||
}
|
||||
d := &Daemon{store: db, runner: runner}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("PingVM: %v", err)
|
||||
}
|
||||
if !result.Alive {
|
||||
t.Fatalf("PingVM result = %+v, want alive", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthVMReturnsFalseForStoppedVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -348,12 +418,12 @@ func TestPingVMReturnsFalseForStoppedVM(t *testing.T) {
|
|||
upsertDaemonVM(t, ctx, db, vm)
|
||||
|
||||
d := &Daemon{store: db}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
result, err := d.HealthVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("PingVM: %v", err)
|
||||
t.Fatalf("HealthVM: %v", err)
|
||||
}
|
||||
if result.Alive {
|
||||
t.Fatalf("PingVM result = %+v, want not alive", result)
|
||||
if result.Healthy {
|
||||
t.Fatalf("HealthVM result = %+v, want not healthy", result)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -406,6 +476,64 @@ func TestFlattenNestedWorkHomeCopiesEntriesIndividually(t *testing.T) {
|
|||
runner.assertExhausted()
|
||||
}
|
||||
|
||||
func TestEnsureAuthorizedKeyOnWorkDiskRepairsNestedRootLayout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
workDiskDir := t.TempDir()
|
||||
nestedHome := filepath.Join(workDiskDir, "root")
|
||||
if err := os.MkdirAll(filepath.Join(nestedHome, ".ssh"), 0o700); err != nil {
|
||||
t.Fatalf("MkdirAll(.ssh): %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(nestedHome, ".bashrc"), []byte("export TEST_PROMPT=1\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(.bashrc): %v", err)
|
||||
}
|
||||
legacyKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEgacykey legacy@test\n"
|
||||
if err := os.WriteFile(filepath.Join(nestedHome, ".ssh", "authorized_keys"), []byte(legacyKey), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(authorized_keys): %v", err)
|
||||
}
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
})
|
||||
sshKeyPath := filepath.Join(t.TempDir(), "id_rsa")
|
||||
if err := os.WriteFile(sshKeyPath, privateKeyPEM, 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(private key): %v", err)
|
||||
}
|
||||
|
||||
d := &Daemon{
|
||||
runner: &filesystemRunner{t: t},
|
||||
config: model.DaemonConfig{SSHKeyPath: sshKeyPath},
|
||||
}
|
||||
vm := testVM("seed-repair", "image-seed-repair", "172.16.0.61")
|
||||
vm.Runtime.WorkDiskPath = workDiskDir
|
||||
|
||||
if err := d.ensureAuthorizedKeyOnWorkDisk(context.Background(), &vm); err != nil {
|
||||
t.Fatalf("ensureAuthorizedKeyOnWorkDisk: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workDiskDir, "root")); !os.IsNotExist(err) {
|
||||
t.Fatalf("nested root still exists: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workDiskDir, ".bashrc")); err != nil {
|
||||
t.Fatalf(".bashrc missing at top level: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(workDiskDir, ".ssh", "authorized_keys"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(authorized_keys): %v", err)
|
||||
}
|
||||
content := string(data)
|
||||
if !strings.Contains(content, strings.TrimSpace(legacyKey)) {
|
||||
t.Fatalf("authorized_keys missing legacy key: %q", content)
|
||||
}
|
||||
if !strings.Contains(content, "ssh-rsa ") {
|
||||
t.Fatalf("authorized_keys missing managed key: %q", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateVMRejectsNonPositiveCPUAndMemory(t *testing.T) {
|
||||
d := &Daemon{}
|
||||
if _, err := d.CreateVM(context.Background(), api.VMCreateParams{VCPUCount: ptr(0)}); err == nil || !strings.Contains(err.Error(), "vcpu must be a positive integer") {
|
||||
|
|
@ -824,6 +952,29 @@ func testImage(name string) model.Image {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMergeAuthorizedKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
managed := []byte("ssh-ed25519 AAAATESTKEY banger\n")
|
||||
existing := []byte("ssh-ed25519 AAAAOTHER other\n")
|
||||
merged := mergeAuthorizedKey(existing, managed)
|
||||
got := string(merged)
|
||||
if !strings.Contains(got, "ssh-ed25519 AAAAOTHER other") {
|
||||
t.Fatalf("merged keys dropped existing entry: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "ssh-ed25519 AAAATESTKEY banger") {
|
||||
t.Fatalf("merged keys missing managed entry: %q", got)
|
||||
}
|
||||
if strings.Count(got, "ssh-ed25519 AAAATESTKEY banger") != 1 {
|
||||
t.Fatalf("managed key duplicated in %q", got)
|
||||
}
|
||||
|
||||
merged = mergeAuthorizedKey(merged, managed)
|
||||
if strings.Count(string(merged), "ssh-ed25519 AAAATESTKEY banger") != 1 {
|
||||
t.Fatalf("managed key duplicated after second merge: %q", string(merged))
|
||||
}
|
||||
}
|
||||
|
||||
func startFakeFirecrackerProcess(t *testing.T, apiSock string) *exec.Cmd {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -878,6 +1029,117 @@ type processKillingRunner struct {
|
|||
proc *exec.Cmd
|
||||
}
|
||||
|
||||
type filesystemRunner struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (r *filesystemRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
r.t.Helper()
|
||||
return nil, fmt.Errorf("unexpected Run call: %s %v", name, args)
|
||||
}
|
||||
|
||||
func (r *filesystemRunner) RunSudo(ctx context.Context, args ...string) ([]byte, error) {
|
||||
r.t.Helper()
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("missing sudo command")
|
||||
}
|
||||
switch args[0] {
|
||||
case "mount":
|
||||
if len(args) != 3 {
|
||||
return nil, fmt.Errorf("unexpected mount args: %v", args)
|
||||
}
|
||||
source, mountDir := args[1], args[2]
|
||||
if err := os.Remove(mountDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.Symlink(source, mountDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
case "umount":
|
||||
return nil, nil
|
||||
case "chmod":
|
||||
if len(args) != 3 {
|
||||
return nil, fmt.Errorf("unexpected chmod args: %v", args)
|
||||
}
|
||||
mode, err := strconv.ParseUint(args[1], 8, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, os.Chmod(args[2], os.FileMode(mode))
|
||||
case "cp":
|
||||
if len(args) != 4 || args[1] != "-a" {
|
||||
return nil, fmt.Errorf("unexpected cp args: %v", args)
|
||||
}
|
||||
return nil, copyIntoDir(args[2], args[3])
|
||||
case "rm":
|
||||
if len(args) != 3 || args[1] != "-rf" {
|
||||
return nil, fmt.Errorf("unexpected rm args: %v", args)
|
||||
}
|
||||
return nil, os.RemoveAll(args[2])
|
||||
case "mkdir":
|
||||
if len(args) != 3 || args[1] != "-p" {
|
||||
return nil, fmt.Errorf("unexpected mkdir args: %v", args)
|
||||
}
|
||||
return nil, os.MkdirAll(args[2], 0o755)
|
||||
case "cat":
|
||||
if len(args) != 2 {
|
||||
return nil, fmt.Errorf("unexpected cat args: %v", args)
|
||||
}
|
||||
return os.ReadFile(args[1])
|
||||
case "install":
|
||||
if len(args) != 5 || args[1] != "-m" {
|
||||
return nil, fmt.Errorf("unexpected install args: %v", args)
|
||||
}
|
||||
mode, err := strconv.ParseUint(args[2], 8, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(args[3])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(args[4]), 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, os.WriteFile(args[4], data, os.FileMode(mode))
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected sudo command: %v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func copyIntoDir(sourcePath, targetDir string) error {
|
||||
targetDir = strings.TrimSuffix(targetDir, "/")
|
||||
info, err := os.Stat(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPath := filepath.Join(targetDir, filepath.Base(sourcePath))
|
||||
if info.IsDir() {
|
||||
if err := os.MkdirAll(destPath, info.Mode().Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
entries, err := os.ReadDir(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if err := copyIntoDir(filepath.Join(sourcePath, entry.Name()), destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return os.Chmod(destPath, info.Mode().Perm())
|
||||
}
|
||||
data, err := os.ReadFile(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destPath, data, info.Mode().Perm())
|
||||
}
|
||||
|
||||
func (r *processKillingRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
return r.scriptedRunner.Run(ctx, name, args...)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue