// Package priv is the one place that knows how this daemon gets root: it already is root, // or it has passwordless sudo, or passwordless doas (Alpine, some BSD-minded setups), or // it has nothing and the privileged features stay off. package priv import ( "context" "os" "os/exec" "strings" "sync" "time" ) var ( mu sync.Mutex mode string // "root" | "sudo" | "doas" | "" prefix []string probed time.Time reprobe = 30 * time.Second ) // detect settles how root is reached. Finding a way is remembered for good; finding none // is only believed for a while, because the probe can fail for reasons that pass — sudo // taking longer than the timeout on a machine that is still booting left a daemon // convinced it had no root until its next restart. func detect() { mu.Lock() defer mu.Unlock() if mode != "" || (!probed.IsZero() && time.Since(probed) < reprobe) { return } probed = time.Now() if os.Geteuid() == 0 { mode = "root" return } for _, tool := range []string{"sudo", "doas"} { if _, err := exec.LookPath(tool); err != nil { continue } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) err := exec.CommandContext(ctx, tool, "-n", "true").Run() cancel() if err == nil { mode, prefix = tool, []string{tool, "-n"} return } } } // Mode reports how root is reached: "root", "sudo", "doas", or "" when it can't be. func Mode() string { detect(); mu.Lock(); defer mu.Unlock(); return mode } // Available reports whether commands can be run as root without a prompt. func Available() bool { return Mode() != "" } // Wrap returns args prefixed so they run as root. Without privilege it returns args // unchanged; callers gate on Available() where that matters. func Wrap(args ...string) []string { detect() mu.Lock() defer mu.Unlock() return append(append([]string{}, prefix...), args...) } // AsUser returns args prefixed so they run as another user with that user's HOME. func AsUser(user string, args ...string) []string { switch Mode() { case "sudo": return append([]string{"sudo", "-n", "-u", user, "-H"}, args...) case "doas": return append([]string{"doas", "-n", "-u", user}, args...) case "root": if user == "root" { return args } // su exists everywhere (util-linux, shadow, busybox), needs no password from root // and sets HOME for the target user. return []string{"su", "-s", "/bin/sh", user, "-c", Quote(args)} } return args } // Quote joins args into one POSIX shell command line. func Quote(args []string) string { q := make([]string, len(args)) for i, a := range args { q[i] = "'" + strings.ReplaceAll(a, "'", `'\''`) + "'" } return strings.Join(q, " ") }