summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason T. Greene <jason.greene@redhat.com>2022-01-29 03:10:28 -0600
committerJason T. Greene <jason.greene@redhat.com>2022-02-16 03:49:17 -0600
commit8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe (patch)
tree5b81e5c9fccb7f2e44ce8c46729ec2c717507122
parent98962a28f5d89fa7545688bbfca635744279ff7a (diff)
downloadpodman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.tar.gz
podman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.tar.bz2
podman-8f5ba05ec4e0f58da80137f24fcb4ab27a2998fe.zip
Initial implementation of mac forwarding using a privileged docker sock claim helper
Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
-rw-r--r--Makefile12
-rw-r--r--cmd/podman-mac-helper/install.go244
-rw-r--r--cmd/podman-mac-helper/main.go149
-rw-r--r--cmd/podman-mac-helper/service.go85
-rw-r--r--cmd/podman-mac-helper/uninstall.go60
-rw-r--r--pkg/machine/qemu/claim_darwin.go63
-rw-r--r--pkg/machine/qemu/claim_unsupported.go20
-rw-r--r--pkg/machine/qemu/machine.go163
8 files changed, 787 insertions, 9 deletions
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 = `<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>com.github.containers.podman.helper-{{.User}}</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>{{.Program}}</string>
+ <string>service</string>
+ <string>{{.Target}}</string>
+ </array>
+ <key>inetdCompatibility</key>
+ <dict>
+ <key>Wait</key>
+ <false/>
+ </dict>
+ <key>UserName</key>
+ <string>root</string>
+ <key>Sockets</key>
+ <dict>
+ <key>Listeners</key>
+ <dict>
+ <key>SockFamily</key>
+ <string>Unix</string>
+ <key>SockPathName</key>
+ <string>/private/var/run/podman-helper-{{.User}}.socket</string>
+ <key>SockPathOwner</key>
+ <integer>{{.UID}}</integer>
+ <key>SockPathMode</key>
+ <!-- SockPathMode takes base 10 (384 = 0600) -->
+ <integer>384</integer>
+ <key>SockType</key>
+ <string>stream</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
+`
+
+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)
+ }
+ }
+}