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

146 lines
4.4 KiB
Go

//go:build linux
package dockeragent
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)
var (
rootlessRuntimeRoot = "/run/user"
effectiveUID = os.Geteuid
)
func collectorOwnsRootlessEndpoint(endpoint string) bool {
const unixPrefix = "unix://"
if !strings.HasPrefix(endpoint, unixPrefix) {
return false
}
path := strings.TrimPrefix(endpoint, unixPrefix)
if !filepath.IsAbs(path) || filepath.Clean(path) != path {
return false
}
uid := effectiveUID()
root := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(uid))
dockerPath := filepath.Join(root, "docker.sock")
podmanPath := filepath.Join(root, "podman", "podman.sock")
if path != dockerPath && path != podmanPath {
return false
}
info, err := os.Lstat(path)
if err != nil || info.Mode()&os.ModeSocket == 0 || info.Mode()&os.ModeSymlink != 0 {
return false
}
if info.Mode().Perm()&0o600 != 0o600 {
return false
}
stat, ok := info.Sys().(*syscall.Stat_t)
return ok && stat.Uid == uint32(uid)
}
// collectorRootlessRuntimeCandidates returns one exact, live runtime endpoint
// owned by the collector. A typed-helper collector must never probe a rootful,
// remote, cross-user, or ambiguous daemon endpoint. Explicit environment pins
// resolve automatic discovery, but conflicting live pins still fail closed.
func collectorRootlessRuntimeCandidates(preference RuntimeKind) ([]runtimeCandidate, error) {
uidRoot := filepath.Join(rootlessRuntimeRoot, strconv.Itoa(effectiveUID()))
dockerURI := "unix://" + filepath.Join(uidRoot, "docker.sock")
podmanURI := "unix://" + filepath.Join(uidRoot, "podman", "podman.sock")
configured := make([]runtimeCandidate, 0, 3)
addConfigured := func(label, value, expected string) error {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
if value != expected {
return fmt.Errorf("%s must pin the exact collector-owned endpoint %q", label, expected)
}
configured = append(configured, runtimeCandidate{host: value, label: label})
return nil
}
var configErr error
switch preference {
case RuntimeDocker:
configErr = addConfigured("DOCKER_HOST", os.Getenv("DOCKER_HOST"), dockerURI)
case RuntimePodman:
configErr = errors.Join(
addConfigured("PODMAN_HOST", os.Getenv("PODMAN_HOST"), podmanURI),
addConfigured("CONTAINER_HOST", os.Getenv("CONTAINER_HOST"), podmanURI),
)
default:
configErr = errors.Join(
addConfigured("DOCKER_HOST", os.Getenv("DOCKER_HOST"), dockerURI),
addConfigured("PODMAN_HOST", os.Getenv("PODMAN_HOST"), podmanURI),
addConfigured("CONTAINER_HOST", os.Getenv("CONTAINER_HOST"), podmanURI),
)
}
if configErr != nil {
return nil, configErr
}
candidates := configured
if len(candidates) == 0 {
switch preference {
case RuntimeDocker:
candidates = append(candidates, runtimeCandidate{
host: "unix://" + filepath.Join(uidRoot, "docker.sock"),
label: "collector rootless docker socket",
})
case RuntimePodman:
candidates = append(candidates, runtimeCandidate{
host: "unix://" + filepath.Join(uidRoot, "podman", "podman.sock"),
label: "collector rootless podman socket",
})
default:
candidates = append(candidates,
runtimeCandidate{
host: "unix://" + filepath.Join(uidRoot, "docker.sock"),
label: "collector rootless docker socket",
},
runtimeCandidate{
host: "unix://" + filepath.Join(uidRoot, "podman", "podman.sock"),
label: "collector rootless podman socket",
},
)
}
}
configuredEndpoints := make(map[string]struct{}, len(configured))
for _, candidate := range configured {
configuredEndpoints[candidate.host] = struct{}{}
}
if len(configuredEndpoints) > 1 {
return nil, fmt.Errorf("conflicting collector rootless runtime pins are configured")
}
live := make([]runtimeCandidate, 0, len(candidates))
seen := make(map[string]struct{}, len(candidates))
for _, candidate := range candidates {
if _, exists := seen[candidate.host]; exists {
continue
}
seen[candidate.host] = struct{}{}
if collectorOwnsRootlessEndpoint(candidate.host) {
live = append(live, candidate)
}
}
switch len(live) {
case 0:
return nil, fmt.Errorf("no live collector-owned rootless %s endpoint is available", preference)
case 1:
return live, nil
default:
endpoints := make([]string, 0, len(live))
for _, candidate := range live {
endpoints = append(endpoints, candidate.host)
}
return nil, fmt.Errorf("ambiguous collector-owned rootless runtime endpoints: %s", strings.Join(endpoints, ", "))
}
}