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
.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 \

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 (
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?

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.
#### **--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

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

View File

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

View File

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

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
// RemoteUsername of the vm user
RemoteUsername string
// Whether this machine should run in a rootful or rootless manner
Rootful bool
}
type Mount struct {

View File

@ -5,11 +5,15 @@ package qemu
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@ -39,8 +43,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
@ -150,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", "":
@ -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
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
@ -309,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 (
@ -318,7 +357,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 +479,9 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
return fmt.Errorf("unknown mount type: %s", mount.Type)
}
}
waitAPIAndPrintInfo(forwardState, forwardSock, v.Rootful, v.Name)
return nil
}
@ -869,19 +912,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 +934,82 @@ 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
}
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) {
@ -912,3 +1025,103 @@ 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 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
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]
}
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)
for i := 0; i < 2; i++ {
if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil {
return err
}
}
return nil
}
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")
}
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",
"podman", "podman-docker", "openssh-server", "procps-ng", "-y"); err != nil {
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()
}
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 +764,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.")