Merge pull request #13075 from n1hility/mac-forward-helper

Mac API forwarding using a privileged docker socket claim helper
This commit is contained in:
OpenShift Merge Robot
2022-02-16 13:39:42 -05:00
committed by GitHub
17 changed files with 1106 additions and 40 deletions

View File

@ -376,13 +376,23 @@ podman-winpath: .gopathok $(SOURCES) go.mod go.sum
./cmd/winpath ./cmd/winpath
.PHONY: podman-remote-darwin .PHONY: podman-remote-darwin
podman-remote-darwin: ## Build podman-remote for macOS podman-remote-darwin: podman-mac-helper ## Build podman-remote for macOS
$(MAKE) \ $(MAKE) \
CGO_ENABLED=$(DARWIN_GCO) \ CGO_ENABLED=$(DARWIN_GCO) \
GOOS=darwin \ GOOS=darwin \
GOARCH=$(GOARCH) \ GOARCH=$(GOARCH) \
bin/darwin/podman 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 bin/rootlessport: .gopathok $(SOURCES) go.mod go.sum
CGO_ENABLED=$(CGO_ENABLED) \ CGO_ENABLED=$(CGO_ENABLED) \
$(GO) build \ $(GO) build \

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -26,7 +26,7 @@ var (
var ( var (
initOpts = machine.InitOptions{} initOpts = machine.InitOptions{}
defaultMachineName = "podman-machine-default" defaultMachineName = machine.DefaultMachineName
now bool now bool
) )
@ -99,6 +99,9 @@ func init() {
IgnitionPathFlagName := "ignition-path" IgnitionPathFlagName := "ignition-path"
flags.StringVar(&initOpts.IgnitionPath, IgnitionPathFlagName, "", "Path to ignition file") flags.StringVar(&initOpts.IgnitionPath, IgnitionPathFlagName, "", "Path to ignition file")
_ = initCmd.RegisterFlagCompletionFunc(IgnitionPathFlagName, completion.AutocompleteDefault) _ = 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? // TODO should we allow for a users to append to the qemu cmdline?

56
cmd/podman/machine/set.go Normal file
View File

@ -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)
}

View File

@ -55,6 +55,14 @@ Memory (in MB).
Start the virtual machine immediately after it has been initialized. 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** #### **--timezone**
Set the timezone for the machine and containers. Valid values are `local` or 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
$ podman machine init myvm $ podman machine init myvm
$ podman machine init --rootful
$ podman machine init --disk-size 50 $ podman machine init --disk-size 50
$ podman machine init --memory=1024 myvm $ podman machine init --memory=1024 myvm
$ podman machine init -v /Users:/mnt/Users $ podman machine init -v /Users:/mnt/Users

View File

@ -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 <jason.greene@redhat.com>

View File

@ -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 | | 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 | | 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 | | 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 | | 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 | | 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 | | stop | [podman-machine-stop(1)](podman-machine-stop.1.md) | Stop a virtual machine |

View File

@ -27,6 +27,7 @@ type InitOptions struct {
URI url.URL URI url.URL
Username string Username string
ReExec bool ReExec bool
Rootful bool
} }
type QemuMachineStatus = string type QemuMachineStatus = string
@ -35,7 +36,8 @@ const (
// Running indicates the qemu vm is running // Running indicates the qemu vm is running
Running QemuMachineStatus = "running" Running QemuMachineStatus = "running"
// Stopped indicates the vm has stopped // Stopped indicates the vm has stopped
Stopped QemuMachineStatus = "stopped" Stopped QemuMachineStatus = "stopped"
DefaultMachineName string = "podman-machine-default"
) )
type Provider interface { type Provider interface {
@ -89,6 +91,10 @@ type ListResponse struct {
IdentityPath string IdentityPath string
} }
type SetOptions struct {
Rootful bool
}
type SSHOptions struct { type SSHOptions struct {
Username string Username string
Args []string Args []string
@ -107,6 +113,7 @@ type RemoveOptions struct {
type VM interface { type VM interface {
Init(opts InitOptions) (bool, error) Init(opts InitOptions) (bool, error)
Remove(name string, opts RemoveOptions) (string, func() error, error) Remove(name string, opts RemoveOptions) (string, func() error, error)
Set(name string, opts SetOptions) error
SSH(name string, opts SSHOptions) error SSH(name string, opts SSHOptions) error
Start(name string, opts StartOptions) error Start(name string, opts StartOptions) error
Stop(name string, opts StopOptions) error Stop(name string, opts StopOptions) error

View File

@ -39,6 +39,31 @@ func AddConnection(uri fmt.Stringer, name, identity string, isDefault bool) erro
return cfg.Write() 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 { func RemoveConnection(name string) error {
cfg, err := config.ReadCustomConfig() cfg, err := config.ReadCustomConfig()
if err != nil { if err != nil {

View File

@ -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")
}

View File

@ -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 ""
}

View File

@ -33,6 +33,8 @@ type MachineVM struct {
QMPMonitor Monitor QMPMonitor Monitor
// RemoteUsername of the vm user // RemoteUsername of the vm user
RemoteUsername string RemoteUsername string
// Whether this machine should run in a rootful or rootless manner
Rootful bool
} }
type Mount struct { type Mount struct {

View File

@ -5,11 +5,15 @@ package qemu
import ( import (
"bufio" "bufio"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -39,8 +43,21 @@ func GetQemuProvider() machine.Provider {
} }
const ( const (
VolumeTypeVirtfs = "virtfs" VolumeTypeVirtfs = "virtfs"
MountType9p = "9p" 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 // NewMachine initializes an instance of a virtual machine based on the qemu
@ -150,14 +167,8 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
key string key string
) )
sshDir := filepath.Join(homedir.Get(), ".ssh") 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.IdentityPath = filepath.Join(sshDir, v.Name)
v.Rootful = opts.Rootful
switch opts.ImagePath { switch opts.ImagePath {
case "testing", "next", "stable", "": case "testing", "next", "stable", "":
@ -240,29 +251,33 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
// This kind of stinks but no other way around this r/n // This kind of stinks but no other way around this r/n
if len(opts.IgnitionPath) < 1 { if len(opts.IgnitionPath) < 1 {
uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/user/1000/podman/podman.sock", strconv.Itoa(v.Port), v.RemoteUsername) 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 { uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root")
return false, 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]
} }
uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(v.Port), "root") for i := 0; i < 2; i++ {
if err := machine.AddConnection(&uriRoot, v.Name+"-root", filepath.Join(sshDir, v.Name), opts.IsDefault); err != nil { if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil {
return false, err return false, err
}
} }
} else { } else {
fmt.Println("An ignition path was provided. No SSH connection was added to Podman") fmt.Println("An ignition path was provided. No SSH connection was added to Podman")
} }
// Write the JSON file // Write the JSON file
b, err := json.MarshalIndent(v, "", " ") v.writeConfig()
if err != nil {
return false, err
}
if err := ioutil.WriteFile(jsonFile, b, 0644); err != nil {
return false, err
}
// User has provided ignition file so keygen // User has provided ignition file so keygen
// will be skipped. // will be skipped.
if len(opts.IgnitionPath) < 1 { if len(opts.IgnitionPath) < 1 {
var err error
key, err = machine.CreateSSHKeys(v.IdentityPath) key, err = machine.CreateSSHKeys(v.IdentityPath)
if err != nil { if err != nil {
return false, err return false, err
@ -309,6 +324,30 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
return err == nil, err 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 // Start executes the qemu command line and forks it
func (v *MachineVM) Start(name string, _ machine.StartOptions) error { func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
var ( var (
@ -318,7 +357,8 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
wait time.Duration = time.Millisecond * 500 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) return errors.Errorf("unable to start host networking: %q", err)
} }
@ -439,6 +479,9 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return fmt.Errorf("unknown mount type: %s", mount.Type) return fmt.Errorf("unknown mount type: %s", mount.Type)
} }
} }
waitAPIAndPrintInfo(forwardState, forwardSock, v.Rootful, v.Name)
return nil return nil
} }
@ -869,19 +912,19 @@ func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) {
// startHostNetworking runs a binary on the host system that allows users // startHostNetworking runs a binary on the host system that allows users
// to setup port forwarding to the podman virtual machine // 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() cfg, err := config.Default()
if err != nil { if err != nil {
return err return "", noForwarding, err
} }
binary, err := cfg.FindHelperBinary(machine.ForwarderBinaryName, false) binary, err := cfg.FindHelperBinary(machine.ForwarderBinaryName, false)
if err != nil { if err != nil {
return err return "", noForwarding, err
} }
qemuSocket, pidFile, err := v.getSocketandPid() qemuSocket, pidFile, err := v.getSocketandPid()
if err != nil { if err != nil {
return err return "", noForwarding, err
} }
attr := new(os.ProcAttr) attr := new(os.ProcAttr)
// Pass on stdin, stdout, stderr // Pass on stdin, stdout, stderr
@ -891,12 +934,82 @@ func (v *MachineVM) startHostNetworking() error {
cmd = append(cmd, []string{"-listen-qemu", fmt.Sprintf("unix://%s", qemuSocket), "-pid-file", pidFile}...) cmd = append(cmd, []string{"-listen-qemu", fmt.Sprintf("unix://%s", qemuSocket), "-pid-file", pidFile}...)
// Add the ssh port // Add the ssh port
cmd = append(cmd, []string{"-ssh-port", fmt.Sprintf("%d", v.Port)}...) cmd = append(cmd, []string{"-ssh-port", fmt.Sprintf("%d", v.Port)}...)
cmd, forwardSock, state := v.setupAPIForwarding(cmd)
if logrus.GetLevel() == logrus.DebugLevel { if logrus.GetLevel() == logrus.DebugLevel {
cmd = append(cmd, "--debug") cmd = append(cmd, "--debug")
fmt.Println(cmd) fmt.Println(cmd)
} }
_, err = os.StartProcess(cmd[0], cmd, attr) _, 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
}
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", 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")
// 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) { func (v *MachineVM) getSocketandPid() (string, string, error) {
@ -912,3 +1025,103 @@ func (v *MachineVM) getSocketandPid() (string, string, error) {
qemuSocket := filepath.Join(socketDir, fmt.Sprintf("qemu_%s.sock", v.Name)) qemuSocket := filepath.Join(socketDir, fmt.Sprintf("qemu_%s.sock", v.Name))
return qemuSocket, pidFile, nil 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 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("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\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\nfollowing 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)
}
}
}
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
}

View File

@ -1,3 +1,4 @@
//go:build windows
// +build windows // +build windows
package wsl package wsl
@ -8,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -35,9 +37,6 @@ const (
ErrorSuccessRebootRequired = 3010 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] const containersConf = `[containers]
[engine] [engine]
@ -162,6 +161,8 @@ type MachineVM struct {
Port int Port int
// RemoteUsername of the vm user // RemoteUsername of the vm user
RemoteUsername string RemoteUsername string
// Whether this machine should run in a rootful or rootless manner
Rootful bool
} }
type ExitCodeError struct { type ExitCodeError struct {
@ -227,12 +228,13 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
homeDir := homedir.Get() homeDir := homedir.Get()
sshDir := filepath.Join(homeDir, ".ssh") sshDir := filepath.Join(homeDir, ".ssh")
v.IdentityPath = filepath.Join(sshDir, v.Name) v.IdentityPath = filepath.Join(sshDir, v.Name)
v.Rootful = opts.Rootful
if err := downloadDistro(v, opts); err != nil { if err := downloadDistro(v, opts); err != nil {
return false, err return false, err
} }
if err := writeJSON(v); err != nil { if err := v.writeConfig(); err != nil {
return false, err return false, err
} }
@ -282,7 +284,7 @@ func downloadDistro(v *MachineVM, opts machine.InitOptions) error {
return machine.DownloadImage(dd) return machine.DownloadImage(dd)
} }
func writeJSON(v *MachineVM) error { func (v *MachineVM) writeConfig() error {
vmConfigDir, err := machine.GetConfDir(vmtype) vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil { if err != nil {
return err return err
@ -302,14 +304,26 @@ func writeJSON(v *MachineVM) error {
} }
func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) 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") 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 { identity := filepath.Join(sshDir, v.Name)
return err
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]
} }
user := opts.Username for i := 0; i < 2; i++ {
uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", withUser("/run/[USER]/1000/podman/podman.sock", user), strconv.Itoa(v.Port), v.RemoteUsername) if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil {
return machine.AddConnection(&uri, v.Name, filepath.Join(sshDir, v.Name), opts.IsDefault) return err
}
}
return nil
} }
func provisionWSLDist(v *MachineVM) (string, error) { func provisionWSLDist(v *MachineVM) (string, error) {
@ -335,6 +349,16 @@ func provisionWSLDist(v *MachineVM) (string, error) {
return "", errors.Wrap(err, "package upgrade on guest OS failed") return "", errors.Wrap(err, "package upgrade on guest OS failed")
} }
fmt.Println("Enabling Copr")
if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "install", "-y", "'dnf-command(copr)'"); err != nil {
return "", errors.Wrap(err, "enabling copr failed")
}
fmt.Println("Enabling podman4 repo")
if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "-y", "copr", "enable", "rhcontainerbot/podman4"); err != nil {
return "", errors.Wrap(err, "enabling copr failed")
}
if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "install", if err = runCmdPassThrough("wsl", "-d", dist, "dnf", "install",
"podman", "podman-docker", "openssh-server", "procps-ng", "-y"); err != nil { "podman", "podman-docker", "openssh-server", "procps-ng", "-y"); err != nil {
return "", errors.Wrap(err, "package installation on guest OS failed") return "", errors.Wrap(err, "package installation on guest OS failed")
@ -704,6 +728,30 @@ func pipeCmdPassThrough(name string, input string, arg ...string) error {
return cmd.Run() 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 { func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
if v.isRunning() { if v.isRunning() {
return errors.Errorf("%q is already running", name) return errors.Errorf("%q is already running", name)
@ -716,6 +764,18 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return errors.Wrap(err, "WSL bootstrap script failed") 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) globalName, pipeName, err := launchWinProxy(v)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.")