Ctrl K
ComunidadGuías…

utils/reexec_linux.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build linux

package utils

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"unicode/utf8"

	"go.keploy.io/server/v3/pkg/platform/engine"
	keployLog "go.keploy.io/server/v3/utils/log"
	"go.uber.org/zap"
)

// execProcess is syscall.Exec, which ReexecWithSudo replaces this process
// with sudo through; a test swaps in one that looks at what the process leaves
// behind at that moment.
var execProcess = syscall.Exec

// sudoVersionOf is SudoVersion; a test swaps in what a given sudo prints.
var sudoVersionOf = SudoVersion

// ReexecWithSudo re-executes the current keploy command with sudo.
// This is used when a Docker/Docker Compose command is detected and keploy is not running as root.
// Uses syscall.Exec to replace the current process entirely - no parent process remains.
// The re-executed keploy gets this process's environment through sudo's
// --preserve-env list (sudoReexecArgs), and a sudo that would not keep it is
// refused before anything runs (sudoReexecRefusal).
// This function never returns on success. On a failure it says why and returns
// the error, for the caller to end the run with exit code 1 through its own
// return: an os.Exit here skipped every deferred call of the run, the removal
// of its temp log among them.
func ReexecWithSudo(logger *zap.Logger) error {
	// Find sudo binary
	sudoPath, err := exec.LookPath("sudo")
	if err != nil {
		logger.Error("sudo not found in PATH, cannot re-execute with elevated privileges", zap.Error(err))
		fmt.Println("Error: sudo is required to run Docker commands. Please install sudo or run as root.")
		return fmt.Errorf("find sudo: %w", err)
	}

	// Find keploy binary - use the current executable
	keployPath, err := os.Executable()
	if err != nil {
		logger.Error("failed to get current executable path", zap.Error(err))
		return fmt.Errorf("find this executable: %w", err)
	}

	if gap, ok := SudoPreserveEnvListGap(sudoVersionOf(context.Background())); ok {
		err := sudoReexecRefusal(gap)
		logger.Error("cannot re-execute with sudo", zap.Error(err))
		return err
	}

	args, unkept := sudoReexecArgs(os.Environ(), os.Getenv("PATH"), keployPath, os.Args[1:])
	if len(unkept) > 0 {
		logger.Warn("these environment variables do not reach the re-executed keploy: sudo's --preserve-env list cannot name a variable whose name has a comma or is not UTF-8", zap.Strings("variables", unkept))
	}

	logger.Info("Re-executing with sudo for elevated privileges as Linux Docker/cloud replay path needs root-level eBPF privileges.")
	logger.Debug("Re-exec command", zap.Strings("args", args))

	// The run goes on in the process the exec starts, which creates a log of
	// its own; this one's, a temp file, would be left with no run to remove
	// it. Its name goes now; the file itself, open close-on-exec as Go opens
	// every file, goes at the exec, or with this process if the exec fails.
	if err := keployLog.RemoveLogFile(LogFile); err != nil {
		logger.Debug("failed to remove this process's log before re-executing", zap.Error(err))
	}
	// Use syscall.Exec to replace the current process
	// This means no parent process remains - clean handoff
	err = execProcess(sudoPath, args, os.Environ())
	if err != nil {
		// syscall.Exec only returns on error
		logger.Error("failed to re-execute with sudo", zap.Error(err))
		fmt.Printf("Error: failed to re-execute with sudo: %v\n", err)
		return fmt.Errorf("re-execute with sudo: %w", err)
	}
	return nil
}

// sudoReexecArgs is the command ReexecWithSudo execs: from this process's
// environment, its PATH, its executable and its arguments,
//
//	sudo --preserve-env=NAME,… env PATH=<PATH> <keploy> <arguments>
//
// sudo resets the environment of its command. The re-executed keploy, and the
// app and compose it runs, need the user's: a compose file's ${VAR}, the
// app's settings, keploy's own. `sudo -E` asked for all of it, and sudo-rs
// (Ubuntu 26.04's sudo) ignores -E ("preserving the entire environment
// is not supported"). Both sudo and sudo-rs keep each variable a
// --preserve-env list names wherever the sudoers rule lets the user set
// variables (SETENV, which a rule for ALL implies), and refuse the whole
// command loudly where it does not. So the list names every variable of the
// environment, and their values stay in the environment, off the command line
// that every user on the host can read.
//
// Left out of the list, so that both sudos give what `sudo -E` gave:
//   - PATH: `env` sets it after sudo, where a secure_path would replace it;
//   - USER, LOGNAME, USERNAME and every SUDO_* variable: sudo sets them, to
//     root and to the user who ran sudo;
//   - a name with a comma, which the list cannot carry, and a name that is not
//     UTF-8, for which sudo-rs refuses the whole command line: returned in
//     unkept, for the caller to say so;
//   - a name met before, and an entry with no name.
func sudoReexecArgs(environ []string, path, keploy string, args []string) (argv, unkept []string) {
	var names []string
	seen := map[string]bool{}
	for _, kv := range environ {
		name, _, _ := strings.Cut(kv, "=")
		if name == "" || seen[name] {
			continue
		}
		seen[name] = true
		switch {
		case name == "PATH", name == "USER", name == "LOGNAME", name == "USERNAME", strings.HasPrefix(name, "SUDO_"):
		case strings.Contains(name, ","), !utf8.ValidString(name):
			unkept = append(unkept, name)
		default:
			names = append(names, name)
		}
	}
	argv = []string{"sudo"}
	if len(names) > 0 {
		argv = append(argv, "--preserve-env="+strings.Join(names, ","))
	}
	argv = append(argv, "env", "PATH="+path, keploy)
	return append(argv, args...), unkept
}

// sudoReexecRefusal is why ReexecWithSudo does not re-execute keploy through
// the sudo gap names, which would not keep the user's environment
// (SudoPreserveEnvListGap), and what the user can do instead.
func sudoReexecRefusal(gap SudoListGap) error {
	if gap.Ignored {
		return fmt.Errorf("keploy re-runs itself as root through sudo for this command, and names every variable of your environment for sudo to keep (--preserve-env=NAME,…). %s ignores that list (sudo-rs implements it from %s on), so the run would go on without your environment. Upgrade sudo-rs to %s or later, or run keploy as root yourself, with the variables the run needs: sudo env VAR=value … keploy …",
			gap, gap.SinceString(), gap.SinceString())
	}
	return fmt.Errorf("keploy re-runs itself as root through sudo for this command, and names every variable of your environment for sudo to keep (--preserve-env=NAME,…). %s does not accept that list (sudo accepts it from %s on). Upgrade sudo to %s or later, or run keploy as root yourself: sudo -E keploy … keeps your environment on this sudo",
		gap, gap.SinceString(), gap.SinceString())
}

// ShouldReexecWithSudo checks if keploy should re-execute itself with sudo.
// Returns true if:
// 1. A Docker/Docker Compose command is detected in the -c/--command flag, OR
// 2. The "cloud replay" subcommand is being used (it generates a docker-compose internally)
// AND Keploy is NOT currently running as root.
func ShouldReexecWithSudo() bool {
	// Already running as root - no need to re-exec
	if os.Geteuid() == 0 {
		return false
	}

	// Check if this is a "cloud replay" command, which always needs root
	// because it generates and runs a docker-compose file internally.
	if isCloudReplayCmd(os.Args) {
		return true
	}

	// An explicit --cmd-type wins over sniffing the command string, and has
	// to be read straight out of os.Args: this runs before cobra parses
	// anything and long before any config file is read. Without it,
	// `--cmd-type docker-compose -c "make up"` would take the native path
	// here, start unprivileged, and then fail deep inside the docker branch
	// trying to write /proc/sys/kernel/perf_event_paranoid (#4399).
	// --from-container runs the app as a container, so it needs root for the
	// same reason every other docker kind does. It is checked before --cmd-type
	// because it does not require one: the flag alone settles the kind.
	if ExtractFromContainerFromArgs(os.Args) != "" {
		return true
	}

	// Extract the command from arguments
	cmd := ExtractCommandFromArgs(os.Args)

	// A container engine this build cannot drive is refused by ValidateFlags
	// with a message saying so. Asking for a sudo password first, only to be
	// refused after it, would be the worse experience.
	if cmd != "" && !engine.Supported(engine.Detect(cmd)) {
		return false
	}

	if explicit := ExtractCmdTypeFromArgs(os.Args); explicit != "" {
		switch kind := CmdType(explicit); kind {
		case Native, DockerRun, DockerStart, DockerCompose, FromContainer:
			return needsRoot(kind)
		}
		// Anything else is rejected later by ValidateFlags with a proper
		// message; fall through to sniffing rather than guessing here.
	}

	if cmd == "" {
		return false
	}

	// Check if it's a Docker command
	return needsRoot(FindDockerCmd(cmd))
}

// needsRoot reports whether a command of this kind re-executes keploy under
// sudo: the container kinds, except docker-start, which ValidateFlags refuses
// (it cannot work) and which must not ask for a password first.
func needsRoot(kind CmdType) bool {
	return IsDockerCmd(kind) && kind != DockerStart
}

// isCloudReplayCmd checks if the args represent the "keploy cloud replay" subcommand.
func isCloudReplayCmd(args []string) bool {
	// It is tolerant of flags that may appear before or between subcommands,
	// e.g. `keploy --debug cloud replay` or `keploy cloud --debug replay`.
	if len(args) < 3 {
		return false
	}

	// Skip args[0] (program name) and search for "cloud" followed by "replay".
	for i := 1; i < len(args); i++ {
		if args[i] != "cloud" {
			continue
		}

		// Look ahead for "replay", allowing flags and their separate values between
		// "cloud" and "replay" (e.g. `cloud --config /tmp/cfg replay`).
		for j := i + 1; j < len(args); j++ {
			token := args[j]
			if token == "replay" {
				return true
			}

			// End-of-options marker; next token must be the subcommand.
			if token == "--" {
				if j+1 < len(args) && args[j+1] == "replay" {
					return true
				}
				break
			}

			// If this is not a flag, this isn't "cloud replay".
			if !strings.HasPrefix(token, "-") {
				break
			}

			// Flag with inline value (`--config=/tmp/cfg`) is fully consumed.
			if strings.Contains(token, "=") {
				continue
			}

			// Heuristically treat the next non-flag token as a flag value and skip it.
			// If the next token is "replay", keep it as the subcommand candidate.
			if j+1 < len(args) && args[j+1] != "replay" && !strings.HasPrefix(args[j+1], "-") {
				j++
			}
		}
	}

	return false
}