From 8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe Mon Sep 17 00:00:00 2001 From: "Jason T. Greene" Date: Sat, 29 Jan 2022 03:10:28 -0600 Subject: Initial implementation of mac forwarding using a privileged docker sock claim helper Signed-off-by: Jason T. Greene --- Makefile | 12 +- cmd/podman-mac-helper/install.go | 244 ++++++++++++++++++++++++++++++++++ cmd/podman-mac-helper/main.go | 149 +++++++++++++++++++++ cmd/podman-mac-helper/service.go | 85 ++++++++++++ cmd/podman-mac-helper/uninstall.go | 60 +++++++++ pkg/machine/qemu/claim_darwin.go | 63 +++++++++ pkg/machine/qemu/claim_unsupported.go | 20 +++ pkg/machine/qemu/machine.go | 163 +++++++++++++++++++++-- 8 files changed, 787 insertions(+), 9 deletions(-) create mode 100644 cmd/podman-mac-helper/install.go create mode 100644 cmd/podman-mac-helper/main.go create mode 100644 cmd/podman-mac-helper/service.go create mode 100644 cmd/podman-mac-helper/uninstall.go create mode 100644 pkg/machine/qemu/claim_darwin.go create mode 100644 pkg/machine/qemu/claim_unsupported.go diff --git a/Makefile b/Makefile index 9e9b3676e..0a5389ce9 100644 --- a/Makefile +++ b/Makefile @@ -376,13 +376,23 @@ podman-winpath: .gopathok $(SOURCES) go.mod go.sum ./cmd/winpath .PHONY: podman-remote-darwin -podman-remote-darwin: ## Build podman-remote for macOS +podman-remote-darwin: podman-mac-helper ## Build podman-remote for macOS $(MAKE) \ CGO_ENABLED=$(DARWIN_GCO) \ GOOS=darwin \ GOARCH=$(GOARCH) \ bin/darwin/podman +.PHONY: podman-mac-helper +podman-mac-helper: ## Build podman-mac-helper for macOS + CGO_ENABLED=0 \ + GOOS=darwin \ + GOARCH=$(GOARCH) \ + $(GO) build \ + $(BUILDFLAGS) \ + -o bin/darwin/podman-mac-helper \ + ./cmd/podman-mac-helper + bin/rootlessport: .gopathok $(SOURCES) go.mod go.sum CGO_ENABLED=$(CGO_ENABLED) \ $(GO) build \ diff --git a/cmd/podman-mac-helper/install.go b/cmd/podman-mac-helper/install.go new file mode 100644 index 000000000..7f623ecb6 --- /dev/null +++ b/cmd/podman-mac-helper/install.go @@ -0,0 +1,244 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "text/template" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + rwx_rx_rx = 0755 + rw_r_r = 0644 +) + +const launchConfig = ` + + + + Label + com.github.containers.podman.helper-{{.User}} + ProgramArguments + + {{.Program}} + service + {{.Target}} + + inetdCompatibility + + Wait + + + UserName + root + Sockets + + Listeners + + SockFamily + Unix + SockPathName + /private/var/run/podman-helper-{{.User}}.socket + SockPathOwner + {{.UID}} + SockPathMode + + 384 + SockType + stream + + + + +` + +type launchParams struct { + Program string + User string + UID string + Target string +} + +var installCmd = &cobra.Command{ + Use: "install", + Short: "installs the podman helper agent", + Long: "installs the podman helper agent, which manages the /var/run/docker.sock link", + PreRun: silentUsage, + RunE: install, +} + +func init() { + addPrefixFlag(installCmd) + rootCmd.AddCommand(installCmd) +} + +func install(cmd *cobra.Command, args []string) error { + userName, uid, homeDir, err := getUser() + if err != nil { + return err + } + + labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName) + fileName := filepath.Join("/Library", "LaunchDaemons", labelName) + + if _, err := os.Stat(fileName); err == nil || !os.IsNotExist(err) { + return errors.New("helper is already installed, uninstall first") + } + + prog, err := installExecutable(userName) + if err != nil { + return err + } + + target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock") + var buf bytes.Buffer + t := template.Must(template.New("launchdConfig").Parse(launchConfig)) + err = t.Execute(&buf, launchParams{prog, userName, uid, target}) + if err != nil { + return err + } + + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, rw_r_r) + if err != nil { + return errors.Wrap(err, "error creating helper plist file") + } + defer file.Close() + _, err = buf.WriteTo(file) + if err != nil { + return err + } + + if err = runDetectErr("launchctl", "load", fileName); err != nil { + return errors.Wrap(err, "launchctl failed loading service") + } + + return nil +} + +func restrictRecursive(targetDir string, until string) error { + for targetDir != until && len(targetDir) > 1 { + info, err := os.Lstat(targetDir) + if err != nil { + return err + } + if info.Mode()&fs.ModeSymlink != 0 { + return errors.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir) + } + if err = os.Chown(targetDir, 0, 0); err != nil { + return errors.Wrap(err, "could not update ownership of helper path") + } + if err = os.Chmod(targetDir, rwx_rx_rx|fs.ModeSticky); err != nil { + return errors.Wrap(err, "could not update permissions of helper path") + } + targetDir = filepath.Dir(targetDir) + } + + return nil +} + +func verifyRootDeep(path string) error { + path = filepath.Clean(path) + current := "/" + segs := strings.Split(path, "/") + depth := 0 + for i := 1; i < len(segs); i++ { + seg := segs[i] + current = filepath.Join(current, seg) + info, err := os.Lstat(current) + if err != nil { + return err + } + + stat := info.Sys().(*syscall.Stat_t) + if stat.Uid != 0 { + return errors.Errorf("installation target path must be solely owned by root: %s is not", current) + } + + if info.Mode()&fs.ModeSymlink != 0 { + target, err := os.Readlink(current) + if err != nil { + return err + } + + targetParts := strings.Split(target, "/") + segs = append(targetParts, segs[i+1:]...) + + if depth++; depth > 1000 { + return errors.New("reached max recursion depth, link structure is cyclical or too complex") + } + + if !filepath.IsAbs(target) { + current = filepath.Dir(current) + i = -1 // Start at 0 + } else { + current = "/" + i = 0 // Skip empty first segment + } + } + } + + return nil +} + +func installExecutable(user string) (string, error) { + // Since the installed executable runs as root, as a precaution verify root ownership of + // the entire installation path, and utilize sticky + read only perms for the helper path + // suffix. The goal is to help users harden against privilege escalation from loose + // filesystem permissions. + // + // Since userpsace package management tools, such as brew, delegate management of system + // paths to standard unix users, the daemon executable is copied into a separate more + // restricted area of the filesystem. + if err := verifyRootDeep(installPrefix); err != nil { + return "", err + } + + targetDir := filepath.Join(installPrefix, "podman", "helper", user) + if err := os.MkdirAll(targetDir, rwx_rx_rx); err != nil { + return "", errors.Wrap(err, "could not create helper directory structure") + } + + // Correct any incorrect perms on previously existing directories and verify no symlinks + if err := restrictRecursive(targetDir, installPrefix); err != nil { + return "", err + } + + exec, err := os.Executable() + if err != nil { + return "", err + } + install := filepath.Join(targetDir, filepath.Base(exec)) + + return install, copyFile(install, exec, rwx_rx_rx) +} + +func copyFile(dest string, source string, perms fs.FileMode) error { + in, err := os.Open(source) + if err != nil { + return err + } + + defer in.Close() + out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms) + if err != nil { + return err + } + + defer out.Close() + if _, err := io.Copy(out, in); err != nil { + return err + } + + return nil +} diff --git a/cmd/podman-mac-helper/main.go b/cmd/podman-mac-helper/main.go new file mode 100644 index 000000000..8d995519f --- /dev/null +++ b/cmd/podman-mac-helper/main.go @@ -0,0 +1,149 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + defaultPrefix = "/usr/local" + dockerSock = "/var/run/docker.sock" +) + +var installPrefix string + +var rootCmd = &cobra.Command{ + Use: "podman-mac-helper", + Short: "A system helper to manage docker.sock", + Long: `podman-mac-helper is a system helper service and tool for managing docker.sock `, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + SilenceErrors: true, +} + +// Note, this code is security sensitive since it runs under privilege. +// Limit actions to what is strictly necessary, and take appropriate +// safeguards +// +// After installation the service call is ran under launchd in a nowait +// inetd style fashion, so stdin, stdout, and stderr are all pointing to +// an accepted connection +// +// This service is installed once per user and will redirect +// /var/run/docker to the fixed user-assigned unix socket location. +// +// Control communication is restricted to each user specific service via +// unix file permissions + +func main() { + if os.Geteuid() != 0 { + fmt.Printf("This command must be ran as root via sudo or osascript\n") + os.Exit(1) + } + + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + } +} + +func getUserInfo(name string) (string, string, string, error) { + // We exec id instead of using user.Lookup to remain compat + // with CGO disabled. + cmd := exec.Command("/usr/bin/id", "-P", name) + output, err := cmd.StdoutPipe() + if err != nil { + return "", "", "", err + } + + if err := cmd.Start(); err != nil { + return "", "", "", err + } + + entry := readCapped(output) + elements := strings.Split(entry, ":") + if len(elements) < 9 || elements[0] != name { + return "", "", "", errors.New("Could not lookup user") + } + + return elements[0], elements[2], elements[8], nil +} + +func getUser() (string, string, string, error) { + name, found := os.LookupEnv("SUDO_USER") + if !found { + name, found = os.LookupEnv("USER") + if !found { + return "", "", "", errors.New("could not determine user") + } + } + + _, uid, home, err := getUserInfo(name) + if err != nil { + return "", "", "", fmt.Errorf("could not lookup user: %s", name) + } + id, err := strconv.Atoi(uid) + if err != nil { + return "", "", "", fmt.Errorf("invalid uid for user: %s", name) + } + if id == 0 { + return "", "", "", fmt.Errorf("unexpected root user") + } + + return name, uid, home, nil +} + +// Used for commands that don't return a proper exit code +func runDetectErr(name string, args ...string) error { + cmd := exec.Command(name, args...) + errReader, err := cmd.StderrPipe() + if err != nil { + return err + } + + err = cmd.Start() + if err == nil { + errString := readCapped(errReader) + if len(errString) > 0 { + re := regexp.MustCompile(`\r?\n`) + err = errors.New(re.ReplaceAllString(errString, ": ")) + } + } + + if werr := cmd.Wait(); werr != nil { + err = werr + } + + return err +} + +func readCapped(reader io.Reader) string { + // Cap output + buffer := make([]byte, 2048) + n, _ := io.ReadFull(reader, buffer) + _, _ = io.Copy(ioutil.Discard, reader) + if n > 0 { + return string(buffer[:n]) + } + + return "" +} + +func addPrefixFlag(cmd *cobra.Command) { + cmd.Flags().StringVar(&installPrefix, "prefix", defaultPrefix, "Sets the install location prefix") +} + +func silentUsage(cmd *cobra.Command, args []string) { + cmd.SilenceUsage = true + cmd.SilenceErrors = true +} diff --git a/cmd/podman-mac-helper/service.go b/cmd/podman-mac-helper/service.go new file mode 100644 index 000000000..65cd89f34 --- /dev/null +++ b/cmd/podman-mac-helper/service.go @@ -0,0 +1,85 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "io" + "io/fs" + "os" + "time" + + "github.com/spf13/cobra" +) + +const ( + trigger = "GO\n" + fail = "NO" + success = "OK" +) + +var serviceCmd = &cobra.Command{ + Use: "service", + Short: "services requests", + Long: "services requests", + PreRun: silentUsage, + Run: serviceRun, + Hidden: true, +} + +func init() { + rootCmd.AddCommand(serviceCmd) +} + +func serviceRun(cmd *cobra.Command, args []string) { + info, err := os.Stdin.Stat() + if err != nil || info.Mode()&fs.ModeSocket == 0 { + fmt.Fprintln(os.Stderr, "This is an internal command that is not intended for standard terminal usage") + os.Exit(1) + } + + os.Exit(service()) +} + +func service() int { + defer os.Stdout.Close() + defer os.Stdin.Close() + defer os.Stderr.Close() + if len(os.Args) < 3 { + fmt.Print(fail) + return 1 + } + target := os.Args[2] + + request := make(chan bool) + go func() { + buf := make([]byte, 3) + _, err := io.ReadFull(os.Stdin, buf) + request <- err == nil && string(buf) == trigger + }() + + valid := false + select { + case valid = <-request: + case <-time.After(5 * time.Second): + } + + if !valid { + fmt.Println(fail) + return 2 + } + + err := os.Remove(dockerSock) + if err == nil || os.IsNotExist(err) { + err = os.Symlink(target, dockerSock) + } + + if err != nil { + fmt.Print(fail) + return 3 + } + + fmt.Print(success) + return 0 +} diff --git a/cmd/podman-mac-helper/uninstall.go b/cmd/podman-mac-helper/uninstall.go new file mode 100644 index 000000000..f72d0efd1 --- /dev/null +++ b/cmd/podman-mac-helper/uninstall.go @@ -0,0 +1,60 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "uninstalls the podman helper agent", + Long: "uninstalls the podman helper agent, which manages the /var/run/docker.sock link", + PreRun: silentUsage, + RunE: uninstall, +} + +func init() { + addPrefixFlag(uninstallCmd) + rootCmd.AddCommand(uninstallCmd) +} + +func uninstall(cmd *cobra.Command, args []string) error { + userName, _, _, err := getUser() + if err != nil { + return err + } + + labelName := fmt.Sprintf("com.github.containers.podman.helper-%s", userName) + fileName := filepath.Join("/Library", "LaunchDaemons", labelName+".plist") + + if err = runDetectErr("launchctl", "unload", fileName); err != nil { + // Try removing the service by label in case the service is half uninstalled + if rerr := runDetectErr("launchctl", "remove", labelName); rerr != nil { + // Exit code 3 = no service to remove + if exitErr, ok := rerr.(*exec.ExitError); !ok || exitErr.ExitCode() != 3 { + fmt.Fprintf(os.Stderr, "Warning: service unloading failed: %s\n", err.Error()) + fmt.Fprintf(os.Stderr, "Warning: remove also failed: %s\n", rerr.Error()) + } + } + } + + if err := os.Remove(fileName); err != nil { + if !os.IsNotExist(err) { + return errors.Errorf("could not remove plist file: %s", fileName) + } + } + + helperPath := filepath.Join(installPrefix, "podman", "helper", userName) + if err := os.RemoveAll(helperPath); err != nil { + return errors.Errorf("could not remove helper binary path: %s", helperPath) + } + return nil +} diff --git a/pkg/machine/qemu/claim_darwin.go b/pkg/machine/qemu/claim_darwin.go new file mode 100644 index 000000000..66aed9ad8 --- /dev/null +++ b/pkg/machine/qemu/claim_darwin.go @@ -0,0 +1,63 @@ +package qemu + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "os/user" + "path/filepath" + "time" +) + +func dockerClaimSupported() bool { + return true +} + +func dockerClaimHelperInstalled() bool { + u, err := user.Current() + if err != nil { + return false + } + + labelName := fmt.Sprintf("com.github.containers.podman.helper-%s", u.Username) + fileName := filepath.Join("/Library", "LaunchDaemons", labelName+".plist") + info, err := os.Stat(fileName) + return err == nil && info.Mode().IsRegular() +} + +func claimDockerSock() bool { + u, err := user.Current() + if err != nil { + return false + } + + helperSock := fmt.Sprintf("/var/run/podman-helper-%s.socket", u.Username) + con, err := net.DialTimeout("unix", helperSock, time.Second*5) + if err != nil { + return false + } + _ = con.SetWriteDeadline(time.Now().Add(time.Second * 5)) + _, err = fmt.Fprintln(con, "GO") + if err != nil { + return false + } + _ = con.SetReadDeadline(time.Now().Add(time.Second * 5)) + read, err := ioutil.ReadAll(con) + + return err == nil && string(read) == "OK" +} + +func findClaimHelper() string { + exe, err := os.Executable() + if err != nil { + return "" + } + + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + return "" + } + + return filepath.Join(filepath.Dir(exe), "podman-mac-helper") +} diff --git a/pkg/machine/qemu/claim_unsupported.go b/pkg/machine/qemu/claim_unsupported.go new file mode 100644 index 000000000..e0b3dd3d3 --- /dev/null +++ b/pkg/machine/qemu/claim_unsupported.go @@ -0,0 +1,20 @@ +//go:build !darwin && !windows +// +build !darwin,!windows + +package qemu + +func dockerClaimHelperInstalled() bool { + return false +} + +func claimDockerSock() bool { + return false +} + +func dockerClaimSupported() bool { + return false +} + +func findClaimHelper() string { + return "" +} diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 240442e49..81a6b4935 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -6,10 +6,13 @@ package qemu import ( "bufio" "encoding/base64" + "context" "encoding/json" "fmt" + "io/fs" "io/ioutil" "net" + "net/http" "os" "os/exec" "path/filepath" @@ -39,8 +42,21 @@ func GetQemuProvider() machine.Provider { } const ( - VolumeTypeVirtfs = "virtfs" - MountType9p = "9p" + VolumeTypeVirtfs = "virtfs" + MountType9p = "9p" + dockerSock = "/var/run/docker.sock" + dockerConnectTimeout = 5 * time.Second + apiUpTimeout = 20 * time.Second +) + +type apiForwardingState int + +const ( + noForwarding apiForwardingState = iota + claimUnsupported + notInstalled + machineLocal + dockerGlobal ) // NewMachine initializes an instance of a virtual machine based on the qemu @@ -318,7 +334,8 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { wait time.Duration = time.Millisecond * 500 ) - if err := v.startHostNetworking(); err != nil { + forwardSock, forwardState, err := v.startHostNetworking() + if err != nil { return errors.Errorf("unable to start host networking: %q", err) } @@ -439,6 +456,9 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return fmt.Errorf("unknown mount type: %s", mount.Type) } } + + printAPIForwardInstructions(forwardState, forwardSock) + return nil } @@ -869,19 +889,19 @@ func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { // startHostNetworking runs a binary on the host system that allows users // to setup port forwarding to the podman virtual machine -func (v *MachineVM) startHostNetworking() error { +func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { cfg, err := config.Default() if err != nil { - return err + return "", noForwarding, err } binary, err := cfg.FindHelperBinary(machine.ForwarderBinaryName, false) if err != nil { - return err + return "", noForwarding, err } qemuSocket, pidFile, err := v.getSocketandPid() if err != nil { - return err + return "", noForwarding, err } attr := new(os.ProcAttr) // Pass on stdin, stdout, stderr @@ -891,12 +911,74 @@ func (v *MachineVM) startHostNetworking() error { cmd = append(cmd, []string{"-listen-qemu", fmt.Sprintf("unix://%s", qemuSocket), "-pid-file", pidFile}...) // Add the ssh port cmd = append(cmd, []string{"-ssh-port", fmt.Sprintf("%d", v.Port)}...) + + cmd, forwardSock, state := v.setupAPIForwarding(cmd) + if logrus.GetLevel() == logrus.DebugLevel { cmd = append(cmd, "--debug") fmt.Println(cmd) } _, err = os.StartProcess(cmd[0], cmd, attr) - return err + return forwardSock, state, err +} + +func (v *MachineVM) setupAPIForwarding(cmd []string) ([]string, string, apiForwardingState) { + socket, err := v.getForwardSocketPath() + + if err != nil { + return cmd, "", noForwarding + } + + cmd = append(cmd, []string{"-forward-sock", socket}...) + cmd = append(cmd, []string{"-forward-dest", "/run/podman/podman.sock"}...) + cmd = append(cmd, []string{"-forward-user", "root"}...) + cmd = append(cmd, []string{"-forward-identity", v.IdentityPath}...) + link := filepath.Join(filepath.Dir(filepath.Dir(socket)), "podman.sock") + + // The linking pattern is /var/run/docker.sock -> user global sock (link) -> machine sock (socket) + // This allows the helper to only have to maintain one constant target to the user, which can be + // repositioned without updating docker.sock. + if !dockerClaimSupported() { + return cmd, socket, claimUnsupported + } + + if !dockerClaimHelperInstalled() { + return cmd, socket, notInstalled + } + + if !alreadyLinked(socket, link) { + if checkSockInUse(link) { + return cmd, socket, machineLocal + } + + _ = os.Remove(link) + if err = os.Symlink(socket, link); err != nil { + logrus.Warnf("could not create user global API forwarding link: %s", err.Error()) + return cmd, socket, machineLocal + } + } + + if !alreadyLinked(link, dockerSock) { + if checkSockInUse(dockerSock) { + return cmd, socket, machineLocal + } + + if !claimDockerSock() { + logrus.Warn("podman helper is installed, but was not able to claim the global docker sock") + return cmd, socket, machineLocal + } + } + + return cmd, dockerSock, dockerGlobal +} + +func (v *MachineVM) getForwardSocketPath() (string, error) { + path, err := machine.GetDataDir(v.Name) + if err != nil { + logrus.Errorf("Error resolving data dir: %s", err.Error()) + return "", nil + } + return filepath.Join(path, "podman.sock"), nil } func (v *MachineVM) getSocketandPid() (string, string, error) { @@ -912,3 +994,68 @@ func (v *MachineVM) getSocketandPid() (string, string, error) { qemuSocket := filepath.Join(socketDir, fmt.Sprintf("qemu_%s.sock", v.Name)) return qemuSocket, pidFile, nil } + +func checkSockInUse(sock string) bool { + if info, err := os.Stat(sock); err == nil && info.Mode()&fs.ModeSocket == fs.ModeSocket { + _, err = net.DialTimeout("unix", dockerSock, dockerConnectTimeout) + return err == nil + } + + return false +} + +func alreadyLinked(target string, link string) bool { + read, err := os.Readlink(link) + return err == nil && read == target +} + +func waitAndPingAPI(sock string) { + client := http.Client{ + Transport: &http.Transport{ + DialContext: func(context.Context, string, string) (net.Conn, error) { + con, err := net.DialTimeout("unix", sock, apiUpTimeout) + if err == nil { + con.SetDeadline(time.Now().Add(apiUpTimeout)) + } + return con, err + }, + }, + } + + resp, err := client.Get("http://host/_ping") + if err == nil { + defer resp.Body.Close() + } + if err != nil || resp.StatusCode != 200 { + logrus.Warn("API socket failed ping test") + } +} + +func printAPIForwardInstructions(forwardState apiForwardingState, forwardSock string) { + if forwardState != noForwarding { + waitAndPingAPI(forwardSock) + fmt.Printf("API forwarding listening on: %s\n", forwardSock) + if forwardState == dockerGlobal { + fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n\n") + } else { + stillString := "still " + switch forwardState { + case notInstalled: + fmt.Printf("\nThe system helper service is not installed; the default Docker API socket address can't be used by podman.\n") + if helper := findClaimHelper(); len(helper) > 0 { + fmt.Printf("If you would like to install it run the following command:\n") + fmt.Printf("\n\tsudo %s install\n\n", helper) + } + case machineLocal: + fmt.Printf("\nAnother process was listening on the default Docker API socket address.\n") + case claimUnsupported: + fallthrough + default: + stillString = "" + } + fmt.Printf("You can %sconnect Docker API clients by setting DOCKER HOST using the\n", stillString) + fmt.Printf("following command in your terminal session:\n") + fmt.Printf("\n\texport DOCKER_HOST='unix://%s'\n\n", forwardSock) + } + } +} -- cgit v1.2.3-54-g00ecf