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 --- 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 +++++++++ 4 files changed, 538 insertions(+) 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 (limited to 'cmd') 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 +} -- cgit v1.2.3-54-g00ecf From c74f8f04fd368ce74e35cc081e32133f7502d89e Mon Sep 17 00:00:00 2001 From: "Jason T. Greene" Date: Sat, 29 Jan 2022 03:10:28 -0600 Subject: Introduce podman machine init --root=t|f and podman machine set --root=t|f Switch default to rootless for mac and windows Signed-off-by: Jason T. Greene --- cmd/podman/machine/init.go | 5 +- cmd/podman/machine/set.go | 56 ++++++++++++ docs/source/markdown/podman-machine-init.1.md | 9 ++ docs/source/markdown/podman-machine-set.1.md | 59 +++++++++++++ docs/source/markdown/podman-machine.1.md | 1 + pkg/machine/config.go | 9 +- pkg/machine/connection.go | 25 ++++++ pkg/machine/qemu/config.go | 2 + pkg/machine/qemu/machine.go | 122 ++++++++++++++++++++------ pkg/machine/wsl/machine.go | 70 ++++++++++++--- 10 files changed, 318 insertions(+), 40 deletions(-) create mode 100644 cmd/podman/machine/set.go create mode 100644 docs/source/markdown/podman-machine-set.1.md (limited to 'cmd') diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 0834aa381..ab13d8651 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -26,7 +26,7 @@ var ( var ( initOpts = machine.InitOptions{} - defaultMachineName = "podman-machine-default" + defaultMachineName = machine.DefaultMachineName now bool ) @@ -99,6 +99,9 @@ func init() { IgnitionPathFlagName := "ignition-path" flags.StringVar(&initOpts.IgnitionPath, IgnitionPathFlagName, "", "Path to ignition file") _ = initCmd.RegisterFlagCompletionFunc(IgnitionPathFlagName, completion.AutocompleteDefault) + + rootfulFlagName := "rootful" + flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container exectution") } // TODO should we allow for a users to append to the qemu cmdline? diff --git a/cmd/podman/machine/set.go b/cmd/podman/machine/set.go new file mode 100644 index 000000000..c978206f0 --- /dev/null +++ b/cmd/podman/machine/set.go @@ -0,0 +1,56 @@ +// +build amd64 arm64 + +package machine + +import ( + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v4/cmd/podman/registry" + "github.com/containers/podman/v4/pkg/machine" + "github.com/spf13/cobra" +) + +var ( + setCmd = &cobra.Command{ + Use: "set [options] [NAME]", + Short: "Sets a virtual machine setting", + Long: "Sets an updatable virtual machine setting", + RunE: setMachine, + Args: cobra.MaximumNArgs(1), + Example: `podman machine set --root=false`, + ValidArgsFunction: completion.AutocompleteNone, + } +) + +var ( + setOpts = machine.SetOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: setCmd, + Parent: machineCmd, + }) + flags := setCmd.Flags() + + rootfulFlagName := "rootful" + flags.BoolVar(&setOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution") +} + +func setMachine(cmd *cobra.Command, args []string) error { + var ( + vm machine.VM + err error + ) + + vmName := defaultMachineName + if len(args) > 0 && len(args[0]) > 0 { + vmName = args[0] + } + provider := getSystemDefaultProvider() + vm, err = provider.LoadVMByName(vmName) + if err != nil { + return err + } + + return vm.Set(vmName, setOpts) +} diff --git a/docs/source/markdown/podman-machine-init.1.md b/docs/source/markdown/podman-machine-init.1.md index b515e8763..36db5b1cd 100644 --- a/docs/source/markdown/podman-machine-init.1.md +++ b/docs/source/markdown/podman-machine-init.1.md @@ -55,6 +55,14 @@ Memory (in MB). Start the virtual machine immediately after it has been initialized. +#### **--rootful**=*true|false* + +Whether this machine should prefer rootful (`true`) or rootless (`false`) +container execution. This option will also determine the remote connection default +if there is no existing remote connection configurations. + +API forwarding, if available, will follow this setting. + #### **--timezone** Set the timezone for the machine and containers. Valid values are `local` or @@ -84,6 +92,7 @@ Print usage statement. ``` $ podman machine init $ podman machine init myvm +$ podman machine init --rootful $ podman machine init --disk-size 50 $ podman machine init --memory=1024 myvm $ podman machine init -v /Users:/mnt/Users diff --git a/docs/source/markdown/podman-machine-set.1.md b/docs/source/markdown/podman-machine-set.1.md new file mode 100644 index 000000000..e69779564 --- /dev/null +++ b/docs/source/markdown/podman-machine-set.1.md @@ -0,0 +1,59 @@ +% podman-machine-set(1) + +## NAME +podman\-machine\-set - Sets a virtual machine setting + +## SYNOPSIS +**podman machine set** [*options*] [*name*] + +## DESCRIPTION + +Sets an updatable virtual machine setting. + +Options mirror values passed to `podman machine init`. Only a limited +subset can be changed after machine initialization. + +## OPTIONS + +#### **--rootful**=*true|false* + +Whether this machine should prefer rootful (`true`) or rootless (`false`) +container execution. This option will also update the current podman +remote connection default if it is currently pointing at the specified +machine name (or `podman-machine-default` if no name is specified). + +API forwarding, if available, will follow this setting. + +#### **--help** + +Print usage statement. + +## EXAMPLES + +To switch the default VM `podman-machine-default` from rootless to rootful: + +``` +$ podman machine set --rootful +``` + +or more explicitly: + +``` +$ podman machine set --rootful=true +``` + +To switch the default VM `podman-machine-default` from rootful to rootless: +``` +$ podman machine set --rootful=false +``` + +To switch the VM `myvm` from rootless to rootful: +``` +$ podman machine set --rootful myvm +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)** + +## HISTORY +February 2022, Originally compiled by Jason Greene diff --git a/docs/source/markdown/podman-machine.1.md b/docs/source/markdown/podman-machine.1.md index 8d9e77ea5..3bdfd0be9 100644 --- a/docs/source/markdown/podman-machine.1.md +++ b/docs/source/markdown/podman-machine.1.md @@ -16,6 +16,7 @@ podman\-machine - Manage Podman's virtual machine | init | [podman-machine-init(1)](podman-machine-init.1.md) | Initialize a new virtual machine | | list | [podman-machine-list(1)](podman-machine-list.1.md) | List virtual machines | | rm | [podman-machine-rm(1)](podman-machine-rm.1.md) | Remove a virtual machine | +| set | [podman-machine-set(1)](podman-machine-set.1.md) | Sets a virtual machine setting | | ssh | [podman-machine-ssh(1)](podman-machine-ssh.1.md) | SSH into a virtual machine | | start | [podman-machine-start(1)](podman-machine-start.1.md) | Start a virtual machine | | stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine | diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 97237f5e5..efb1eda15 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -27,6 +27,7 @@ type InitOptions struct { URI url.URL Username string ReExec bool + Rootful bool } type QemuMachineStatus = string @@ -35,7 +36,8 @@ const ( // Running indicates the qemu vm is running Running QemuMachineStatus = "running" // Stopped indicates the vm has stopped - Stopped QemuMachineStatus = "stopped" + Stopped QemuMachineStatus = "stopped" + DefaultMachineName string = "podman-machine-default" ) type Provider interface { @@ -89,6 +91,10 @@ type ListResponse struct { IdentityPath string } +type SetOptions struct { + Rootful bool +} + type SSHOptions struct { Username string Args []string @@ -107,6 +113,7 @@ type RemoveOptions struct { type VM interface { Init(opts InitOptions) (bool, error) Remove(name string, opts RemoveOptions) (string, func() error, error) + Set(name string, opts SetOptions) error SSH(name string, opts SSHOptions) error Start(name string, opts StartOptions) error Stop(name string, opts StopOptions) error diff --git a/pkg/machine/connection.go b/pkg/machine/connection.go index d28ffcef1..841b2afa6 100644 --- a/pkg/machine/connection.go +++ b/pkg/machine/connection.go @@ -39,6 +39,31 @@ func AddConnection(uri fmt.Stringer, name, identity string, isDefault bool) erro return cfg.Write() } +func AnyConnectionDefault(name ...string) (bool, error) { + cfg, err := config.ReadCustomConfig() + if err != nil { + return false, err + } + for _, n := range name { + if n == cfg.Engine.ActiveService { + return true, nil + } + } + + return false, nil +} + +func ChangeDefault(name string) error { + cfg, err := config.ReadCustomConfig() + if err != nil { + return err + } + + cfg.Engine.ActiveService = name + + return cfg.Write() +} + func RemoveConnection(name string) error { cfg, err := config.ReadCustomConfig() if err != nil { diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index e76509bb1..c619b7dd4 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -33,6 +33,8 @@ type MachineVM struct { QMPMonitor Monitor // RemoteUsername of the vm user RemoteUsername string + // Whether this machine should run in a rootful or rootless manner + Rootful bool } type Mount struct { diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 81a6b4935..9beec2173 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -5,14 +5,15 @@ package qemu import ( "bufio" - "encoding/base64" "context" + "encoding/base64" "encoding/json" "fmt" "io/fs" "io/ioutil" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -166,14 +167,8 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { key string ) sshDir := filepath.Join(homedir.Get(), ".ssh") - // GetConfDir creates the directory so no need to check for - // its existence - vmConfigDir, err := machine.GetConfDir(vmtype) - if err != nil { - return false, err - } - jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" v.IdentityPath = filepath.Join(sshDir, v.Name) + v.Rootful = opts.Rootful switch opts.ImagePath { case "testing", "next", "stable", "": @@ -256,29 +251,33 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { // This kind of stinks but no other way around this r/n if len(opts.IgnitionPath) < 1 { uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername) - if err := machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { - return false, err + uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") + identity := filepath.Join(sshDir, v.Name) + + uris := []url.URL{uri, uriRoot} + names := []string{v.Name, v.Name + "-root"} + + // The first connection defined when connections is empty will become the default + // regardless of IsDefault, so order according to rootful + if opts.Rootful { + uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] } - uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") - if err := machine.AddConnection(&uriRoot, v.Name+"-root", filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { - return false, err + for i := 0; i < 2; i++ { + if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil { + return false, err + } } } else { fmt.Println("An ignition path was provided. No SSH connection was added to Podman") } // Write the JSON file - b, err := json.MarshalIndent(v, "", " ") - if err != nil { - return false, err - } - if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil { - return false, err - } + v.writeConfig() // User has provided ignition file so keygen // will be skipped. if len(opts.IgnitionPath) < 1 { + var err error key, err = machine.CreateSSHKeys(v.IdentityPath) if err != nil { return false, err @@ -325,6 +324,30 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { return err == nil, err } +func (v *MachineVM) Set(name string, opts machine.SetOptions) error { + if v.Rootful == opts.Rootful { + return nil + } + + changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") + if err != nil { + return err + } + + if changeCon { + newDefault := v.Name + if opts.Rootful { + newDefault += "-root" + } + if err := machine.ChangeDefault(newDefault); err != nil { + return err + } + } + + v.Rootful = opts.Rootful + return v.writeConfig() +} + // Start executes the qemu command line and forks it func (v *MachineVM) Start(name string, _ machine.StartOptions) error { var ( @@ -457,7 +480,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { } } - printAPIForwardInstructions(forwardState, forwardSock) + waitAPIAndPrintInfo(forwardState, forwardSock, v.Rootful, v.Name) return nil } @@ -929,9 +952,17 @@ func (v *MachineVM) setupAPIForwarding(cmd []string) ([]string, string, apiForwa return cmd, "", noForwarding } + destSock := "/run/user/1000/podman/podman.sock" + forwardUser := "core" + + if v.Rootful { + destSock = "/run/podman/podman.sock" + forwardUser = "root" + } + 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-dest", destSock}...) + cmd = append(cmd, []string{"-forward-user", forwardUser}...) cmd = append(cmd, []string{"-forward-identity", v.IdentityPath}...) link := filepath.Join(filepath.Dir(filepath.Dir(socket)), "podman.sock") @@ -1031,19 +1062,32 @@ func waitAndPingAPI(sock string) { } } -func printAPIForwardInstructions(forwardState apiForwardingState, forwardSock string) { +func waitAPIAndPrintInfo(forwardState apiForwardingState, forwardSock string, rootFul bool, name string) { if forwardState != noForwarding { waitAndPingAPI(forwardSock) + if !rootFul { + fmt.Printf("\nThis machine is currently configured in rootless mode. If your containers\n") + fmt.Printf("require root permissions (e.g. ports < 1024), or if you run into compatibility\n") + fmt.Printf("issues with non-podman clients, you can switch using the following command: \n") + + suffix := "" + if name != machine.DefaultMachineName { + suffix = " " + name + } + fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) + } + 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") + fmt.Printf("Docker 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") + fmt.Printf("\nThe system helper service is not installed; the default Docker API socket\n") + fmt.Printf("address can't be used by podman. ") if helper := findClaimHelper(); len(helper) > 0 { - fmt.Printf("If you would like to install it run the following command:\n") + fmt.Printf("If you would like to install it run the\nfollowing command:\n") fmt.Printf("\n\tsudo %s install\n\n", helper) } case machineLocal: @@ -1053,9 +1097,31 @@ func printAPIForwardInstructions(forwardState apiForwardingState, forwardSock st default: stillString = "" } - fmt.Printf("You can %sconnect Docker API clients by setting DOCKER HOST using the\n", 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) } } } + +func (v *MachineVM) writeConfig() error { + // GetConfDir creates the directory so no need to check for + // its existence + vmConfigDir, err := machine.GetConfDir(vmtype) + if err != nil { + return err + } + + jsonFile := filepath.Join(vmConfigDir, v.Name) + ".json" + // Write the JSON file + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil { + return err + } + + return nil +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 3edf3ddf6..6fe79863f 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package wsl @@ -8,6 +9,7 @@ import ( "fmt" "io" "io/ioutil" + "net/url" "os" "os/exec" "path/filepath" @@ -35,9 +37,6 @@ const ( ErrorSuccessRebootRequired = 3010 ) -// Usermode networking avoids potential nftables compatibility issues between the distro -// and the WSL Kernel. Additionally it avoids fw rule conflicts between distros, since -// all instances run under the same Kernel at runtime const containersConf = `[containers] [engine] @@ -162,6 +161,8 @@ type MachineVM struct { Port int // RemoteUsername of the vm user RemoteUsername string + // Whether this machine should run in a rootful or rootless manner + Rootful bool } type ExitCodeError struct { @@ -227,12 +228,13 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { homeDir := homedir.Get() sshDir := filepath.Join(homeDir, ".ssh") v.IdentityPath = filepath.Join(sshDir, v.Name) + v.Rootful = opts.Rootful if err := downloadDistro(v, opts); err != nil { return false, err } - if err := writeJSON(v); err != nil { + if err := v.writeConfig(); err != nil { return false, err } @@ -282,7 +284,7 @@ func downloadDistro(v *MachineVM, opts machine.InitOptions) error { return machine.DownloadImage(dd) } -func writeJSON(v *MachineVM) error { +func (v *MachineVM) writeConfig() error { vmConfigDir, err := machine.GetConfDir(vmtype) if err != nil { return err @@ -302,14 +304,26 @@ func writeJSON(v *MachineVM) error { } func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) error { + uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername) uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") - if err := machine.AddConnection(&uriRoot, v.Name+"-root", filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { - return err + identity := filepath.Join(sshDir, v.Name) + + uris := []url.URL{uri, uriRoot} + names := []string{v.Name, v.Name + "-root"} + + // The first connection defined when connections is empty will become the default + // regardless of IsDefault, so order according to rootful + if opts.Rootful { + uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0] + } + + for i := 0; i < 2; i++ { + if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil { + return err + } } - user := opts.Username - uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", withUser("/run/[USER]/1000/podman/podman.sock", user), strconv.Itoa(v.Port), v.RemoteUsername) - return machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault) + return nil } func provisionWSLDist(v *MachineVM) (string, error) { @@ -704,6 +718,30 @@ func pipeCmdPassThrough(name string, input string, arg ...string) error { return cmd.Run() } +func (v *MachineVM) Set(name string, opts machine.SetOptions) error { + if v.Rootful == opts.Rootful { + return nil + } + + changeCon, err := machine.AnyConnectionDefault(v.Name, v.Name+"-root") + if err != nil { + return err + } + + if changeCon { + newDefault := v.Name + if opts.Rootful { + newDefault += "-root" + } + if err := machine.ChangeDefault(newDefault); err != nil { + return err + } + } + + v.Rootful = opts.Rootful + return v.writeConfig() +} + func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if v.isRunning() { return errors.Errorf("%q is already running", name) @@ -716,6 +754,18 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return errors.Wrap(err, "WSL bootstrap script failed") } + if !v.Rootful { + fmt.Printf("\nThis machine is currently configured in rootless mode. If your containers\n") + fmt.Printf("require root permissions (e.g. ports < 1024), or if you run into compatibility\n") + fmt.Printf("issues with non-podman clients, you can switch using the following command: \n") + + suffix := "" + if name != machine.DefaultMachineName { + suffix = " " + name + } + fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) + } + globalName, pipeName, err := launchWinProxy(v) if err != nil { fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") -- cgit v1.2.3-54-g00ecf