Pulse/internal/dockeragent/rootless_endpoint_linux_test.go
2026-08-31 22:17:07 +01:00

513 lines
17 KiB
Go

//go:build linux
package dockeragent
import (
"context"
"errors"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
containertypes "github.com/moby/moby/api/types/container"
systemtypes "github.com/moby/moby/api/types/system"
"github.com/moby/moby/client"
"github.com/rcourtman/pulse-go-rewrite/internal/agenthelper"
"github.com/rcourtman/pulse-go-rewrite/internal/hostmetrics"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
"github.com/rs/zerolog"
)
func TestCollectorOwnsRootlessEndpointRequiresOwnedRuntimeSocket(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
})
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
socketPath := filepath.Join(runtimeDir, "docker.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
if !collectorOwnsRootlessEndpoint("unix://" + socketPath) {
t.Fatal("collector-owned rootless Unix socket was rejected")
}
if collectorOwnsRootlessEndpoint("unix:///var/run/docker.sock") {
t.Fatal("rootful system socket was accepted")
}
if collectorOwnsRootlessEndpoint("tcp://127.0.0.1:2375") {
t.Fatal("non-Unix runtime endpoint was accepted")
}
symlink := filepath.Join(runtimeDir, "linked.sock")
if err := os.Symlink(socketPath, symlink); err != nil {
t.Fatal(err)
}
if collectorOwnsRootlessEndpoint("unix://" + symlink) {
t.Fatal("symlinked runtime endpoint was accepted")
}
}
func TestCollectorRootlessRuntimeCandidatesRequireOneExactOwnedSocket(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
})
t.Setenv("DOCKER_HOST", "")
t.Setenv("CONTAINER_HOST", "")
t.Setenv("PODMAN_HOST", "")
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(filepath.Join(runtimeDir, "podman"), 0o700); err != nil {
t.Fatal(err)
}
dockerPath := filepath.Join(runtimeDir, "docker.sock")
dockerListener, err := net.Listen("unix", dockerPath)
if err != nil {
t.Fatal(err)
}
defer dockerListener.Close()
candidates, err := collectorRootlessRuntimeCandidates(RuntimeAuto)
if err != nil {
t.Fatalf("single Docker candidate: %v", err)
}
if len(candidates) != 1 || candidates[0].host != "unix://"+dockerPath {
t.Fatalf("Docker candidates = %+v", candidates)
}
podmanPath := filepath.Join(runtimeDir, "podman", "podman.sock")
podmanListener, err := net.Listen("unix", podmanPath)
if err != nil {
t.Fatal(err)
}
defer podmanListener.Close()
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil || !strings.Contains(err.Error(), "ambiguous") {
t.Fatalf("auto discovery with Docker and Podman = %v, want ambiguity", err)
}
candidates, err = collectorRootlessRuntimeCandidates(RuntimeDocker)
if err != nil {
t.Fatalf("explicit Docker preference: %v", err)
}
if len(candidates) != 1 || candidates[0].host != "unix://"+dockerPath {
t.Fatalf("Docker preference candidates = %+v", candidates)
}
t.Setenv("DOCKER_HOST", "unix://"+dockerPath)
candidates, err = collectorRootlessRuntimeCandidates(RuntimeAuto)
if err != nil {
t.Fatalf("explicit endpoint pin: %v", err)
}
if len(candidates) != 1 || candidates[0].label != "DOCKER_HOST" {
t.Fatalf("explicit candidates = %+v", candidates)
}
}
func TestCollectorRootlessRuntimeCandidatesRejectCrossUserAndConflictingPins(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
})
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(filepath.Join(runtimeDir, "podman"), 0o700); err != nil {
t.Fatal(err)
}
dockerPath := filepath.Join(runtimeDir, "docker.sock")
dockerListener, err := net.Listen("unix", dockerPath)
if err != nil {
t.Fatal(err)
}
defer dockerListener.Close()
podmanPath := filepath.Join(runtimeDir, "podman", "podman.sock")
podmanListener, err := net.Listen("unix", podmanPath)
if err != nil {
t.Fatal(err)
}
defer podmanListener.Close()
t.Setenv("DOCKER_HOST", "unix://"+dockerPath)
t.Setenv("PODMAN_HOST", "unix://"+podmanPath)
t.Setenv("CONTAINER_HOST", "")
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil || !strings.Contains(err.Error(), "conflicting") {
t.Fatalf("conflicting live pins = %v, want conflict rejection", err)
}
t.Setenv("DOCKER_HOST", "unix://"+filepath.Join(runtimeDir, "custom.sock"))
t.Setenv("PODMAN_HOST", "")
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil || !strings.Contains(err.Error(), "exact collector-owned endpoint") {
t.Fatalf("custom same-UID pin = %v, want exact-path rejection", err)
}
t.Setenv("DOCKER_HOST", "")
t.Setenv("PODMAN_HOST", "")
effectiveUID = func() int { return os.Geteuid() + 1 }
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil || !strings.Contains(err.Error(), "no live collector-owned") {
t.Fatalf("cross-user sockets = %v, want fail-closed rejection", err)
}
}
func TestCollectorRootlessRuntimeAmbiguityIsRejectedBeforeDaemonProbe(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
originalNewClient := newDockerClientFn
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
newDockerClientFn = originalNewClient
})
t.Setenv("DOCKER_HOST", "")
t.Setenv("CONTAINER_HOST", "")
t.Setenv("PODMAN_HOST", "")
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(filepath.Join(runtimeDir, "podman"), 0o700); err != nil {
t.Fatal(err)
}
listeners := make([]net.Listener, 0, 2)
for _, path := range []string{
filepath.Join(runtimeDir, "docker.sock"),
filepath.Join(runtimeDir, "podman", "podman.sock"),
} {
listener, err := net.Listen("unix", path)
if err != nil {
t.Fatal(err)
}
listeners = append(listeners, listener)
}
defer func() {
for _, listener := range listeners {
_ = listener.Close()
}
}()
probes := 0
newDockerClientFn = func(...client.Opt) (dockerClient, error) {
probes++
return nil, nil
}
if _, _, _, err := connectCollectorOwnedRootlessRuntime(RuntimeAuto, nil); err == nil || !strings.Contains(err.Error(), "ambiguous") {
t.Fatalf("ambiguous connection = %v", err)
}
if probes != 0 {
t.Fatalf("ambiguous endpoints reached daemon client construction %d times", probes)
}
}
func TestCollectorRootlessRuntimeRejectsSymlinkAndUnreadableSocket(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
})
t.Setenv("DOCKER_HOST", "")
t.Setenv("CONTAINER_HOST", "")
t.Setenv("PODMAN_HOST", "")
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
realPath := filepath.Join(runtimeDir, "real.sock")
listener, err := net.Listen("unix", realPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
standardPath := filepath.Join(runtimeDir, "docker.sock")
if err := os.Symlink(realPath, standardPath); err != nil {
t.Fatal(err)
}
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil {
t.Fatal("symlinked rootless runtime endpoint was accepted")
}
if err := os.Remove(standardPath); err != nil {
t.Fatal(err)
}
if err := os.Rename(realPath, standardPath); err != nil {
t.Fatal(err)
}
if err := os.Chmod(standardPath, 0); err != nil {
t.Fatal(err)
}
if _, err := collectorRootlessRuntimeCandidates(RuntimeAuto); err == nil {
t.Fatal("collector-unreadable rootless runtime endpoint was accepted")
}
}
func TestCollectorRootlessRuntimeRejectsOwnedSocketBackedByRootfulDaemon(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
originalNewClient := newDockerClientFn
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
newDockerClientFn = originalNewClient
})
t.Setenv("DOCKER_HOST", "")
t.Setenv("CONTAINER_HOST", "")
t.Setenv("PODMAN_HOST", "")
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
socketPath := filepath.Join(runtimeDir, "docker.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
closed := false
newDockerClientFn = func(...client.Opt) (dockerClient, error) {
return &fakeDockerClient{
daemonHost: "unix://" + socketPath,
infoFunc: func(context.Context) (systemtypes.Info, error) {
return systemtypes.Info{SecurityOptions: []string{"name=seccomp,profile=default"}}, nil
},
closeFn: func() error { closed = true; return nil },
}, nil
}
if _, _, _, err := connectCollectorOwnedRootlessRuntime(RuntimeDocker, nil); err == nil || !strings.Contains(err.Error(), "did not attest rootless mode") {
t.Fatalf("owned-path rootful daemon = %v", err)
}
if !closed {
t.Fatal("owned-path rootful daemon client was not closed")
}
newDockerClientFn = func(...client.Opt) (dockerClient, error) {
return &fakeDockerClient{
daemonHost: "unix://" + socketPath,
infoFunc: func(context.Context) (systemtypes.Info, error) {
return systemtypes.Info{SecurityOptions: []string{"name=rootless", "name=seccomp,profile=default"}}, nil
},
}, nil
}
cli, _, runtimeKind, err := connectCollectorOwnedRootlessRuntime(RuntimeDocker, nil)
if err != nil {
t.Fatalf("rootless daemon: %v", err)
}
if cli == nil || runtimeKind != RuntimeDocker {
t.Fatalf("rootless daemon connection = cli:%T runtime:%s", cli, runtimeKind)
}
}
func TestTypedHelperInventoryPromotesToAttestedRootlessRuntime(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
originalConnect := connectCollectorRuntimeFn
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
connectCollectorRuntimeFn = originalConnect
})
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
socketPath := filepath.Join(runtimeDir, "docker.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
rootlessClient := &fakeDockerClient{daemonHost: "unix://" + socketPath}
connectCollectorRuntimeFn = func(RuntimeKind, *zerolog.Logger) (dockerClient, systemtypes.Info, RuntimeKind, error) {
return rootlessClient, systemtypes.Info{
ID: "rootless-daemon", ServerVersion: "28.0.0", SecurityOptions: []string{"name=rootless"},
}, RuntimeDocker, nil
}
helper := &helperInventoryStub{}
agent := &Agent{
cfg: Config{
HelperInventory: helper, DisableUpdateChecks: true,
},
helperInventory: helper,
directDisableChecks: false,
directServices: true,
directTasks: true,
runtimePref: RuntimeAuto,
runtime: RuntimeDocker,
registryChecker: newRegistryCheckerWithConfig(zerolog.Nop(), false),
logger: zerolog.Nop(),
}
if !agent.maybePromoteCollectorRootlessRuntime() {
t.Fatal("attested rootless runtime was not promoted")
}
if agent.helperInventory != nil || agent.docker == nil || agent.daemonID != "rootless-daemon" {
t.Fatalf("promoted state = helper:%T docker:%T daemon:%q", agent.helperInventory, agent.docker, agent.daemonID)
}
if agent.cfg.DisableUpdateChecks || !agent.cfg.IncludeServices || !agent.cfg.IncludeTasks || !agent.registryChecker.Enabled() {
t.Fatalf("direct configuration was not restored: %+v", agent.cfg)
}
if agent.ContainerActionsAvailable() {
t.Fatal("safe collector promotion exposed container action authority")
}
}
func TestTypedHelperInventoryTransitionsThroughRootlessPodman(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
originalConnect := connectCollectorRuntimeFn
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
connectCollectorRuntimeFn = originalConnect
})
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()), "podman")
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
socketPath := filepath.Join(runtimeDir, "podman.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
connectCollectorRuntimeFn = func(RuntimeKind, *zerolog.Logger) (dockerClient, systemtypes.Info, RuntimeKind, error) {
return &fakeDockerClient{daemonHost: "unix://" + socketPath}, systemtypes.Info{
ID: "rootless-podman", ServerVersion: "5.5.0 podman", SecurityOptions: []string{"name=rootless"},
}, RuntimePodman, nil
}
helper := &helperInventoryStub{result: agenthelper.ContainerInventoryResult{
Runtimes: []agenthelper.ContainerRuntimeSnapshot{{Runtime: "podman", Available: true}},
}}
agent := &Agent{
cfg: Config{HelperInventory: helper, DisableUpdateChecks: true},
helperInventory: helper,
directDisableChecks: false,
directServices: true,
directTasks: true,
runtimePref: RuntimePodman,
runtime: RuntimePodman,
registryChecker: newRegistryCheckerWithConfig(zerolog.Nop(), false),
logger: zerolog.Nop(),
}
if !agent.maybePromoteCollectorRootlessRuntime() {
t.Fatal("attested rootless Podman runtime was not promoted")
}
if agent.runtime != RuntimePodman || agent.cfg.IncludeServices || agent.cfg.IncludeTasks || agent.helperInventory != nil {
t.Fatalf("promoted Podman state = runtime:%s services:%t tasks:%t helper:%T", agent.runtime, agent.cfg.IncludeServices, agent.cfg.IncludeTasks, agent.helperInventory)
}
agent.runtimeGoneStreak = runtimeReconnectFailureThreshold
if !agent.maybeFallbackToHelperInventory(context.Background()) {
t.Fatal("rootless Podman loss did not fall back to typed helper summary")
}
if agent.helperInventory != helper || agent.docker != nil || agent.runtime != RuntimePodman {
t.Fatalf("Podman fallback state = helper:%T docker:%T runtime:%s", agent.helperInventory, agent.docker, agent.runtime)
}
}
func TestCycleBoundaryChangeStopsDirectCallsAndFallsBackToHelper(t *testing.T) {
originalRoot := rootlessRuntimeRoot
originalUID := effectiveUID
originalConnect := connectCollectorRuntimeFn
originalMetrics := hostmetricsCollectWithDiskFilters
t.Cleanup(func() {
rootlessRuntimeRoot = originalRoot
effectiveUID = originalUID
connectCollectorRuntimeFn = originalConnect
hostmetricsCollectWithDiskFilters = originalMetrics
})
rootlessRuntimeRoot = t.TempDir()
effectiveUID = os.Geteuid
runtimeDir := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(os.Geteuid()))
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
t.Fatal(err)
}
socketPath := filepath.Join(runtimeDir, "docker.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatal(err)
}
defer listener.Close()
postInfoCalls := 0
direct := &fakeDockerClient{
daemonHost: "unix://" + socketPath,
infoFunc: func(context.Context) (systemtypes.Info, error) {
return systemtypes.Info{SecurityOptions: []string{"name=seccomp,profile=default"}}, nil
},
containerListFunc: func(context.Context, dockerContainerListOptions) ([]containertypes.Summary, error) {
postInfoCalls++
return nil, nil
},
}
connectCollectorRuntimeFn = func(RuntimeKind, *zerolog.Logger) (dockerClient, systemtypes.Info, RuntimeKind, error) {
return nil, systemtypes.Info{}, RuntimeAuto, errors.New("replacement failed rootless admission")
}
hostmetricsCollectWithDiskFilters = func(context.Context, []string, []string) (hostmetrics.Snapshot, error) {
return hostmetrics.Snapshot{}, nil
}
helper := &helperInventoryStub{result: agenthelper.ContainerInventoryResult{
Runtimes: []agenthelper.ContainerRuntimeSnapshot{{Runtime: "docker", Available: true}},
}}
agent := &Agent{
cfg: Config{HelperInventory: helper, IncludeContainers: true},
docker: newSwappableDockerClient(direct),
directServices: true,
directTasks: true,
runtime: RuntimeDocker,
runtimePref: RuntimeDocker,
runtimeGoneStreak: runtimeReconnectFailureThreshold - 1,
registryChecker: newRegistryCheckerWithConfig(zerolog.Nop(), false),
allowedStates: map[string]struct{}{},
logger: zerolog.Nop(),
reportStreamID: "boundary-change",
}
report, err := agent.collectOnceWithReport(context.Background())
if err != nil {
t.Fatalf("boundary fallback collection: %v", err)
}
if postInfoCalls != 0 {
t.Fatalf("changed runtime boundary received %d post-Info daemon calls", postInfoCalls)
}
if agent.helperInventory != helper || agent.docker != nil || report.Host.CollectionMode != agentsdocker.CollectionModeTypedHelperSummary {
t.Fatalf("boundary fallback state = helper:%T docker:%T mode:%q", agent.helperInventory, agent.docker, report.Host.CollectionMode)
}
}