From e3f167f7709ff8490b77efcb2cc204282c74ec84 Mon Sep 17 00:00:00 2001 From: Ashley Cui Date: Fri, 1 Dec 2023 16:55:54 -0500 Subject: [PATCH] Add API forwarding support for HyperV Provides Docker API client access, allowing compose to work by default for HyperV. Basically the HyperV equiv of the work done here by #12916. [NO NEW TESTS NEEDED] Signed-off-by: Ashley Cui --- cmd/podman/compose.go | 2 +- pkg/machine/hyperv/machine.go | 16 +++ pkg/machine/machine_windows.go | 194 ++++++++++++++++++++++++++++++++ pkg/machine/wsl/machine.go | 178 +++-------------------------- pkg/machine/wsl/usermodenet.go | 2 +- pkg/machine/wsl/util_windows.go | 7 -- 6 files changed, 228 insertions(+), 171 deletions(-) diff --git a/cmd/podman/compose.go b/cmd/podman/compose.go index f11f28b3c3..80cde2289d 100644 --- a/cmd/podman/compose.go +++ b/cmd/podman/compose.go @@ -187,7 +187,7 @@ func composeDockerHost() (string, error) { if info.State != define.Running { return "", fmt.Errorf("machine %s is not running but in state %s", item.Name, info.State) } - if machineProvider.VMType() == define.WSLVirt { + if machineProvider.VMType() == define.WSLVirt || machineProvider.VMType() == define.HyperVVirt { if info.ConnectionInfo.PodmanPipe == nil { return "", errors.New("pipe of machine is not set") } diff --git a/pkg/machine/hyperv/machine.go b/pkg/machine/hyperv/machine.go index ea8d522144..bdce9ee36e 100644 --- a/pkg/machine/hyperv/machine.go +++ b/pkg/machine/hyperv/machine.go @@ -343,11 +343,14 @@ func (m *HyperVMachine) Inspect() (*machine.InspectInfo, error) { if err != nil { return nil, err } + machinePipe := machine.ToDist(m.Name) + podmanPipe := &define.VMFile{Path: `\\.\pipe\` + machinePipe} return &machine.InspectInfo{ ConfigPath: m.ConfigPath, ConnectionInfo: machine.ConnectionConfig{ PodmanSocket: podmanSocket, + PodmanPipe: podmanPipe, }, Created: m.Created, Image: machine.ImageConfig{ @@ -592,6 +595,15 @@ func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error { m.HostUser.Modified = false } } + winProxyOpts := machine.WinProxyOpts{ + Name: m.Name, + IdentityPath: m.IdentityPath, + Port: m.Port, + RemoteUsername: m.RemoteUsername, + Rootful: m.Rootful, + VMType: vmtype, + } + machine.LaunchWinProxy(winProxyOpts, opts.NoInfo) // Write the config with updated starting status and hostuser modification if err := m.writeConfig(); err != nil { @@ -653,6 +665,10 @@ func (m *HyperVMachine) Stop(name string, opts machine.StopOptions) error { logrus.Error(err) } + if err := machine.StopWinProxy(m.Name, vmtype); err != nil { + fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) + } + if err := vm.Stop(); err != nil { return fmt.Errorf("stopping virtual machine: %w", err) } diff --git a/pkg/machine/machine_windows.go b/pkg/machine/machine_windows.go index 993aeefd41..87c9ae8807 100644 --- a/pkg/machine/machine_windows.go +++ b/pkg/machine/machine_windows.go @@ -3,13 +3,38 @@ package machine import ( + "fmt" "os" + "os/exec" + "path/filepath" + "strings" "syscall" "time" + "github.com/containers/podman/v4/pkg/machine/define" "github.com/sirupsen/logrus" ) +const ( + pipePrefix = "npipe:////./pipe/" + globalPipe = "docker_engine" + winSShProxy = "win-sshproxy.exe" + winSshProxyTid = "win-sshproxy.tid" + rootfulSock = "/run/podman/podman.sock" + rootlessSock = "/run/user/1000/podman/podman.sock" +) + +const WM_QUIT = 0x12 //nolint + +type WinProxyOpts struct { + Name string + IdentityPath string + Port int + RemoteUsername string + Rootful bool + VMType define.VMType +} + func GetProcessState(pid int) (active bool, exitCode int) { const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE handle, err := syscall.OpenProcess(da, false, uint32(pid)) @@ -45,3 +70,172 @@ func WaitPipeExists(pipeName string, retries int, checkFailure func() error) err return err } + +func LaunchWinProxy(opts WinProxyOpts, noInfo bool) { + globalName, pipeName, err := launchWinProxy(opts) + if !noInfo { + if err != nil { + fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") + fmt.Fprintf(os.Stderr, "\t%s\n", err.Error()) + fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.") + } else { + fmt.Printf("API forwarding listening on: %s\n", pipeName) + if globalName { + fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n") + } else { + fmt.Printf("\nAnother process was listening on the default Docker API pipe address.\n") + fmt.Printf("You can still connect Docker API clients by setting DOCKER HOST using the\n") + fmt.Printf("following powershell command in your terminal session:\n") + fmt.Printf("\n\t$Env:DOCKER_HOST = '%s'\n", pipeName) + fmt.Printf("\nOr in a classic CMD prompt:\n") + fmt.Printf("\n\tset DOCKER_HOST=%s\n", pipeName) + fmt.Printf("\nAlternatively, terminate the other process and restart podman machine.\n") + } + } + } +} + +func launchWinProxy(opts WinProxyOpts) (bool, string, error) { + machinePipe := ToDist(opts.Name) + if !PipeNameAvailable(machinePipe) { + return false, "", fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe) + } + + globalName := false + if PipeNameAvailable(globalPipe) { + globalName = true + } + + command, err := FindExecutablePeer(winSShProxy) + if err != nil { + return globalName, "", err + } + + stateDir, err := GetWinProxyStateDir(opts.Name, opts.VMType) + if err != nil { + return globalName, "", err + } + + destSock := rootlessSock + forwardUser := opts.RemoteUsername + + if opts.Rootful { + destSock = rootfulSock + forwardUser = "root" + } + + dest := fmt.Sprintf("ssh://%s@localhost:%d%s", forwardUser, opts.Port, destSock) + args := []string{opts.Name, stateDir, pipePrefix + machinePipe, dest, opts.IdentityPath} + waitPipe := machinePipe + if globalName { + args = append(args, pipePrefix+globalPipe, dest, opts.IdentityPath) + waitPipe = globalPipe + } + + cmd := exec.Command(command, args...) + if err := cmd.Start(); err != nil { + return globalName, "", err + } + + return globalName, pipePrefix + waitPipe, WaitPipeExists(waitPipe, 80, func() error { + active, exitCode := GetProcessState(cmd.Process.Pid) + if !active { + return fmt.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode) + } + + return nil + }) +} + +func StopWinProxy(name string, vmtype define.VMType) error { + pid, tid, tidFile, err := readWinProxyTid(name, vmtype) + if err != nil { + return err + } + + proc, err := os.FindProcess(int(pid)) + if err != nil { + return nil + } + sendQuit(tid) + _ = waitTimeout(proc, 20*time.Second) + _ = os.Remove(tidFile) + + return nil +} + +func readWinProxyTid(name string, vmtype define.VMType) (uint32, uint32, string, error) { + stateDir, err := GetWinProxyStateDir(name, vmtype) + if err != nil { + return 0, 0, "", err + } + + tidFile := filepath.Join(stateDir, winSshProxyTid) + contents, err := os.ReadFile(tidFile) + if err != nil { + return 0, 0, "", err + } + + var pid, tid uint32 + fmt.Sscanf(string(contents), "%d:%d", &pid, &tid) + return pid, tid, tidFile, nil +} + +func waitTimeout(proc *os.Process, timeout time.Duration) bool { + done := make(chan bool) + go func() { + proc.Wait() + done <- true + }() + ret := false + select { + case <-time.After(timeout): + proc.Kill() + <-done + case <-done: + ret = true + break + } + + return ret +} + +func sendQuit(tid uint32) { + user32 := syscall.NewLazyDLL("user32.dll") + postMessage := user32.NewProc("PostThreadMessageW") + postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) +} + +func FindExecutablePeer(name string) (string, error) { + exe, err := os.Executable() + if err != nil { + return "", err + } + + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + return "", err + } + + return filepath.Join(filepath.Dir(exe), name), nil +} + +func GetWinProxyStateDir(name string, vmtype define.VMType) (string, error) { + dir, err := GetDataDir(vmtype) + if err != nil { + return "", err + } + stateDir := filepath.Join(dir, name) + if err = os.MkdirAll(stateDir, 0755); err != nil { + return "", err + } + + return stateDir, nil +} + +func ToDist(name string) string { + if !strings.HasPrefix(name, "podman") { + name = "podman-" + name + } + return name +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index ee0301070e..154bc0af52 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -273,14 +273,13 @@ http://docs.microsoft.com/en-us/windows/wsl/install\ ` const ( - gvProxy = "gvproxy.exe" - winSShProxy = "win-sshproxy.exe" - winSshProxyTid = "win-sshproxy.tid" - pipePrefix = "npipe:////./pipe/" - globalPipe = "docker_engine" - userModeDist = "podman-net-usermode" - rootfulSock = "/run/podman/podman.sock" - rootlessSock = "/run/user/1000/podman/podman.sock" + gvProxy = "gvproxy.exe" + winSShProxy = "win-sshproxy.exe" + pipePrefix = "npipe:////./pipe/" + globalPipe = "docker_engine" + userModeDist = "podman-net-usermode" + rootfulSock = "/run/podman/podman.sock" + rootlessSock = "/run/user/1000/podman/podman.sock" ) type MachineVM struct { @@ -1208,28 +1207,15 @@ func (v *MachineVM) Start(name string, opts machine.StartOptions) error { } fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix) } - - globalName, pipeName, err := launchWinProxy(v) - if !opts.NoInfo { - if err != nil { - fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") - fmt.Fprintf(os.Stderr, "\t%s\n", err.Error()) - fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.") - } else { - fmt.Printf("API forwarding listening on: %s\n", pipeName) - if globalName { - fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n") - } else { - fmt.Printf("\nAnother process was listening on the default Docker API pipe address.\n") - fmt.Printf("You can still connect Docker API clients by setting DOCKER HOST using the\n") - fmt.Printf("following powershell command in your terminal session:\n") - fmt.Printf("\n\t$Env:DOCKER_HOST = '%s'\n", pipeName) - fmt.Printf("\nOr in a classic CMD prompt:\n") - fmt.Printf("\n\tset DOCKER_HOST=%s\n", pipeName) - fmt.Printf("\nAlternatively, terminate the other process and restart podman machine.\n") - } - } + winProxyOpts := machine.WinProxyOpts{ + Name: v.Name, + IdentityPath: v.IdentityPath, + Port: v.Port, + RemoteUsername: v.RemoteUsername, + Rootful: v.Rootful, + VMType: vmtype, } + machine.LaunchWinProxy(winProxyOpts, opts.NoInfo) _, _, err = v.updateTimeStamps(true) return err @@ -1303,85 +1289,6 @@ func (v *MachineVM) reassignSshPort() error { return nil } -func findExecutablePeer(name string) (string, error) { - exe, err := os.Executable() - if err != nil { - return "", err - } - - exe, err = filepath.EvalSymlinks(exe) - if err != nil { - return "", err - } - - return filepath.Join(filepath.Dir(exe), name), nil -} - -func launchWinProxy(v *MachineVM) (bool, string, error) { - machinePipe := toDist(v.Name) - if !machine.PipeNameAvailable(machinePipe) { - return false, "", fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe) - } - - globalName := false - if machine.PipeNameAvailable(globalPipe) { - globalName = true - } - - command, err := findExecutablePeer(winSShProxy) - if err != nil { - return globalName, "", err - } - - stateDir, err := getWinProxyStateDir(v) - if err != nil { - return globalName, "", err - } - - destSock := rootlessSock - forwardUser := v.RemoteUsername - - if v.Rootful { - destSock = rootfulSock - forwardUser = "root" - } - - dest := fmt.Sprintf("ssh://%s@localhost:%d%s", forwardUser, v.Port, destSock) - args := []string{v.Name, stateDir, pipePrefix + machinePipe, dest, v.IdentityPath} - waitPipe := machinePipe - if globalName { - args = append(args, pipePrefix+globalPipe, dest, v.IdentityPath) - waitPipe = globalPipe - } - - cmd := exec.Command(command, args...) - if err := cmd.Start(); err != nil { - return globalName, "", err - } - - return globalName, pipePrefix + waitPipe, machine.WaitPipeExists(waitPipe, 80, func() error { - active, exitCode := machine.GetProcessState(cmd.Process.Pid) - if !active { - return fmt.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode) - } - - return nil - }) -} - -func getWinProxyStateDir(v *MachineVM) (string, error) { - dir, err := machine.GetDataDir(vmtype) - if err != nil { - return "", err - } - stateDir := filepath.Join(dir, v.Name) - if err = os.MkdirAll(stateDir, 0755); err != nil { - return "", err - } - - return stateDir, nil -} - func IsWSLFeatureEnabled() bool { return wutil.SilentExec("wsl", "--set-default-version", "2") == nil } @@ -1487,7 +1394,7 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { _, _, _ = v.updateTimeStamps(true) - if err := stopWinProxy(v); err != nil { + if err := machine.StopWinProxy(v.Name, vmtype); err != nil { fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) } @@ -1527,59 +1434,6 @@ func (v *MachineVM) State(bypass bool) (define.Status, error) { return define.Stopped, nil } -func stopWinProxy(v *MachineVM) error { - pid, tid, tidFile, err := readWinProxyTid(v) - if err != nil { - return err - } - - proc, err := os.FindProcess(int(pid)) - if err != nil { - return nil - } - sendQuit(tid) - _ = waitTimeout(proc, 20*time.Second) - _ = os.Remove(tidFile) - - return nil -} - -func waitTimeout(proc *os.Process, timeout time.Duration) bool { - done := make(chan bool) - go func() { - proc.Wait() - done <- true - }() - ret := false - select { - case <-time.After(timeout): - proc.Kill() - <-done - case <-done: - ret = true - break - } - - return ret -} - -func readWinProxyTid(v *MachineVM) (uint32, uint32, string, error) { - stateDir, err := getWinProxyStateDir(v) - if err != nil { - return 0, 0, "", err - } - - tidFile := filepath.Join(stateDir, winSshProxyTid) - contents, err := os.ReadFile(tidFile) - if err != nil { - return 0, 0, "", err - } - - var pid, tid uint32 - fmt.Sscanf(string(contents), "%d:%d", &pid, &tid) - return pid, tid, tidFile, nil -} - //nolint:cyclop func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { var files []string diff --git a/pkg/machine/wsl/usermodenet.go b/pkg/machine/wsl/usermodenet.go index e88da990c7..3bbef936d1 100644 --- a/pkg/machine/wsl/usermodenet.go +++ b/pkg/machine/wsl/usermodenet.go @@ -74,7 +74,7 @@ func (v *MachineVM) startUserModeNetworking() error { return nil } - exe, err := findExecutablePeer(gvProxy) + exe, err := machine.FindExecutablePeer(gvProxy) if err != nil { return fmt.Errorf("could not locate %s, which is necessary for user-mode networking, please reinstall", gvProxy) } diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go index 75a943cff4..3046dca855 100644 --- a/pkg/machine/wsl/util_windows.go +++ b/pkg/machine/wsl/util_windows.go @@ -67,7 +67,6 @@ const ( TOKEN_QUERY = 0x0008 SE_PRIVILEGE_ENABLED = 0x00000002 SE_ERR_ACCESSDENIED = 0x05 - WM_QUIT = 0x12 ) func winVersionAtLeast(major uint, minor uint, build uint) bool { @@ -337,9 +336,3 @@ func buildCommandArgs(elevate bool) string { } return strings.Join(args, " ") } - -func sendQuit(tid uint32) { - user32 := syscall.NewLazyDLL("user32.dll") - postMessage := user32.NewProc("PostThreadMessageW") - postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) -}