Move the supported systemd path to two services: an owner-user bangerd for orchestration and a narrow root helper for bridge/tap, NAT/resolver, dm/loop, and Firecracker ownership. This removes repeated sudo from daily vm and image flows without leaving the general daemon running as root. Add install metadata, system install/status/restart/uninstall commands, and a system-owned runtime layout. Keep user SSH/config material in the owner home, lock file_sync to the owner home, and move daemon known_hosts handling out of the old root-owned control path. Route privileged lifecycle steps through typed privilegedOps calls, harden the two systemd units, and rewrite smoke plus docs around the supported service model. Verified with make build, make test, make lint, and make smoke on the supported systemd host path.
215 lines
6.2 KiB
Go
215 lines
6.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/installmeta"
|
|
)
|
|
|
|
func TestEnsureDaemonRequiresSystemInstallWhenMetadataMissing(t *testing.T) {
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config"))
|
|
t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state"))
|
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache"))
|
|
t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run"))
|
|
|
|
restoreLoad := loadInstallMetadata
|
|
restoreUID := currentUID
|
|
t.Cleanup(func() {
|
|
loadInstallMetadata = restoreLoad
|
|
currentUID = restoreUID
|
|
})
|
|
|
|
loadInstallMetadata = func() (installmeta.Metadata, error) {
|
|
return installmeta.Metadata{}, os.ErrNotExist
|
|
}
|
|
currentUID = os.Getuid
|
|
|
|
d := defaultDeps()
|
|
d.daemonPing = func(context.Context, string) (api.PingResult, error) {
|
|
return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: no such file")
|
|
}
|
|
|
|
_, _, err := d.ensureDaemon(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), "sudo banger system install") {
|
|
t.Fatalf("ensureDaemon error = %v, want install guidance", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDaemonSuggestsRestartWhenInstalledButUnavailable(t *testing.T) {
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(t.TempDir(), "config"))
|
|
t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "state"))
|
|
t.Setenv("XDG_CACHE_HOME", filepath.Join(t.TempDir(), "cache"))
|
|
t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "run"))
|
|
|
|
restoreLoad := loadInstallMetadata
|
|
restoreUID := currentUID
|
|
t.Cleanup(func() {
|
|
loadInstallMetadata = restoreLoad
|
|
currentUID = restoreUID
|
|
})
|
|
|
|
loadInstallMetadata = func() (installmeta.Metadata, error) {
|
|
return installmeta.Metadata{
|
|
OwnerUser: "tester",
|
|
OwnerUID: os.Getuid(),
|
|
OwnerGID: os.Getgid(),
|
|
OwnerHome: t.TempDir(),
|
|
}, nil
|
|
}
|
|
currentUID = os.Getuid
|
|
|
|
d := defaultDeps()
|
|
d.daemonPing = func(context.Context, string) (api.PingResult, error) {
|
|
return api.PingResult{}, errors.New("dial unix /run/banger/bangerd.sock: connection refused")
|
|
}
|
|
|
|
_, _, err := d.ensureDaemon(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), "sudo banger system restart") {
|
|
t.Fatalf("ensureDaemon error = %v, want restart guidance", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureDaemonRejectsNonOwnerUser(t *testing.T) {
|
|
restoreLoad := loadInstallMetadata
|
|
restoreUID := currentUID
|
|
t.Cleanup(func() {
|
|
loadInstallMetadata = restoreLoad
|
|
currentUID = restoreUID
|
|
})
|
|
|
|
loadInstallMetadata = func() (installmeta.Metadata, error) {
|
|
return installmeta.Metadata{
|
|
OwnerUser: "alice",
|
|
OwnerUID: os.Getuid() + 1,
|
|
OwnerGID: os.Getgid(),
|
|
OwnerHome: t.TempDir(),
|
|
}, nil
|
|
}
|
|
currentUID = os.Getuid
|
|
|
|
d := defaultDeps()
|
|
d.daemonPing = func(context.Context, string) (api.PingResult, error) {
|
|
t.Fatal("daemonPing should not be called for a non-owner user")
|
|
return api.PingResult{}, nil
|
|
}
|
|
|
|
_, _, err := d.ensureDaemon(context.Background())
|
|
if err == nil || !strings.Contains(err.Error(), "installed for alice") {
|
|
t.Fatalf("ensureDaemon error = %v, want owner mismatch guidance", err)
|
|
}
|
|
}
|
|
|
|
func TestSystemSubcommandFlagsAreScoped(t *testing.T) {
|
|
root := NewBangerCommand()
|
|
|
|
systemCmd, _, err := root.Find([]string{"system"})
|
|
if err != nil {
|
|
t.Fatalf("find system: %v", err)
|
|
}
|
|
installCmd, _, err := systemCmd.Find([]string{"install"})
|
|
if err != nil {
|
|
t.Fatalf("find system install: %v", err)
|
|
}
|
|
uninstallCmd, _, err := systemCmd.Find([]string{"uninstall"})
|
|
if err != nil {
|
|
t.Fatalf("find system uninstall: %v", err)
|
|
}
|
|
if installCmd.Flags().Lookup("owner") == nil {
|
|
t.Fatal("system install is missing --owner")
|
|
}
|
|
if uninstallCmd.Flags().Lookup("purge") == nil {
|
|
t.Fatal("system uninstall is missing --purge")
|
|
}
|
|
}
|
|
|
|
func TestRenderSystemdUnitIncludesHardeningDirectives(t *testing.T) {
|
|
unit := renderSystemdUnit(installmeta.Metadata{
|
|
OwnerUser: "alice",
|
|
OwnerUID: 1000,
|
|
OwnerGID: 1000,
|
|
OwnerHome: "/home/alice/dev home",
|
|
})
|
|
|
|
for _, want := range []string{
|
|
"ExecStart=/usr/local/bin/bangerd --system",
|
|
"User=alice",
|
|
"Wants=network-online.target bangerd-root.service",
|
|
"After=bangerd-root.service",
|
|
"Requires=bangerd-root.service",
|
|
"UMask=0077",
|
|
"Environment=TMPDIR=/run/banger",
|
|
"NoNewPrivileges=yes",
|
|
"PrivateMounts=yes",
|
|
"ProtectSystem=strict",
|
|
"ProtectHome=read-only",
|
|
"ProtectControlGroups=yes",
|
|
"ProtectKernelLogs=yes",
|
|
"ProtectKernelModules=yes",
|
|
"ProtectClock=yes",
|
|
"ProtectHostname=yes",
|
|
"RestrictSUIDSGID=yes",
|
|
"LockPersonality=yes",
|
|
"SystemCallArchitectures=native",
|
|
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK",
|
|
"StateDirectory=banger",
|
|
"StateDirectoryMode=0700",
|
|
"CacheDirectory=banger",
|
|
"CacheDirectoryMode=0700",
|
|
"RuntimeDirectory=banger",
|
|
"RuntimeDirectoryMode=0700",
|
|
`ReadOnlyPaths="/home/alice/dev home"`,
|
|
} {
|
|
if !strings.Contains(unit, want) {
|
|
t.Fatalf("unit = %q, want %q", unit, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderRootHelperSystemdUnitIncludesRequiredCapabilities(t *testing.T) {
|
|
unit := renderRootHelperSystemdUnit()
|
|
|
|
for _, want := range []string{
|
|
"ExecStart=/usr/local/bin/bangerd --root-helper",
|
|
"Environment=TMPDIR=/run/banger-root",
|
|
"NoNewPrivileges=yes",
|
|
"PrivateTmp=yes",
|
|
"PrivateMounts=yes",
|
|
"ProtectSystem=strict",
|
|
"ProtectHome=yes",
|
|
"RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK",
|
|
"CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_ADMIN",
|
|
"ReadWritePaths=/var/lib/banger",
|
|
"RuntimeDirectory=banger-root",
|
|
"RuntimeDirectoryMode=0711",
|
|
} {
|
|
if !strings.Contains(unit, want) {
|
|
t.Fatalf("unit = %q, want %q", unit, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderSystemdUnitsIncludeOptionalCoverageEnv(t *testing.T) {
|
|
t.Setenv(systemCoverDirEnv, "/var/lib/banger")
|
|
t.Setenv(rootCoverDirEnv, "/var/lib/banger")
|
|
|
|
userUnit := renderSystemdUnit(installmeta.Metadata{
|
|
OwnerUser: "alice",
|
|
OwnerUID: 1000,
|
|
OwnerGID: 1000,
|
|
OwnerHome: "/home/alice",
|
|
})
|
|
if !strings.Contains(userUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) {
|
|
t.Fatalf("user unit = %q, want GOCOVERDIR env", userUnit)
|
|
}
|
|
|
|
rootUnit := renderRootHelperSystemdUnit()
|
|
if !strings.Contains(rootUnit, `Environment=GOCOVERDIR="/var/lib/banger"`) {
|
|
t.Fatalf("root unit = %q, want GOCOVERDIR env", rootUnit)
|
|
}
|
|
}
|