From 2d0b5ebb5bdc06cf34488a75318c0ba66e3f2edf Mon Sep 17 00:00:00 2001
From: "Jason T. Greene" <jason.greene@redhat.com>
Date: Tue, 18 Jan 2022 14:39:48 -0600
Subject: [PATCH] Implement API forwarding for podman machine on Windows

Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
---
 Makefile                        |  22 ++++-
 cmd/podman/machine/start.go     |   2 +
 contrib/msi/podman.wxs          |   4 +
 pkg/machine/wsl/machine.go      | 163 +++++++++++++++++++++++++++++++-
 pkg/machine/wsl/util_windows.go |  19 ++++
 5 files changed, 206 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile
index 6482378cd5..face2b4994 100644
--- a/Makefile
+++ b/Makefile
@@ -186,6 +186,13 @@ ifdef HOMEBREW_PREFIX
 endif
 endif
 
+# win-sshproxy is checked out manually to keep from pulling in gvisor and it's transitive
+# dependencies. This is only used for the Windows installer task (podman.msi), which must
+# include this lightweight helper binary.
+#
+GV_GITURL=git://github.com/containers/gvisor-tap-vsock.git
+GV_SHA=e943b1806d94d387c4c38d96719432d50a84bbd0
+
 ###
 ### Primary entry-point targets
 ###
@@ -695,7 +702,7 @@ podman-remote-release-%.zip: test/version/version ## Build podman-remote for %=$
 .PHONY: podman.msi
 podman.msi: test/version/version  ## Build podman-remote, package for installation on Windows
 	$(MAKE) podman-v$(RELEASE_NUMBER).msi
-podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath
+podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath win-sshproxy
 	$(eval DOCFILE := docs/build/remote/windows)
 	find $(DOCFILE) -print | \
 		wixl-heat --var var.ManSourceDir --component-group ManFiles \
@@ -704,6 +711,19 @@ podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs
 	wixl -D VERSION=$(call err_if_empty,RELEASE_VERSION) -D ManSourceDir=$(DOCFILE) \
 		-o $@ contrib/msi/podman.wxs $(DOCFILE)/pages.wsx --arch x64
 
+# Checks out and builds win-sshproxy helper. See comment on GV_GITURL declaration
+.PHONY: win-sshproxy
+win-sshproxy: test/version/version
+	rm -rf tmp-gv; mkdir tmp-gv
+	(cd tmp-gv; \
+         git init; \
+         git remote add origin $(GV_GITURL); \
+         git fetch --depth 1 origin $(GV_SHA); \
+         git checkout FETCH_HEAD; make win-sshproxy)
+	mkdir -p bin/windows/
+	cp tmp-gv/bin/win-sshproxy.exe bin/windows/
+	rm -rf tmp-gv
+
 .PHONY: package
 package:  ## Build rpm packages
 	## TODO(ssbarnea): make version number predictable, it should not change
diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go
index 0bcf32cd59..16faa25eff 100644
--- a/cmd/podman/machine/start.go
+++ b/cmd/podman/machine/start.go
@@ -1,3 +1,4 @@
+//go:build amd64 || arm64
 // +build amd64 arm64
 
 package machine
@@ -64,5 +65,6 @@ func start(cmd *cobra.Command, args []string) error {
 	if err := vm.Start(vmName, machine.StartOptions{}); err != nil {
 		return err
 	}
+	fmt.Printf("Machine %q started successfully\n", vmName)
 	return nil
 }
diff --git a/contrib/msi/podman.wxs b/contrib/msi/podman.wxs
index c2826fc19d..c4ba623c05 100644
--- a/contrib/msi/podman.wxs
+++ b/contrib/msi/podman.wxs
@@ -29,6 +29,9 @@
             <Component Id="WinPathExecutable" Guid="00F5B731-D4A6-4B69-87B0-EA4EBAB89F95" Win64="Yes">
               <File Id="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Name="winpath.exe" Source="bin/windows/winpath.exe" KeyPath="yes"/>
             </Component>
+            <Component Id="WinSshProxyExecutable" Guid="0DA730AB-2F97-40E8-A8FC-356E88EAA4D2" Win64="Yes">
+              <File Id="4A2AD125-34E7-4BD8-BE28-B2A9A5EDBEB5" Name="win-sshproxy.exe" Source="bin/windows/win-sshproxy.exe" KeyPath="yes"/>
+            </Component>
           </Directory>
         </Directory>
       </Directory>
@@ -41,6 +44,7 @@
       <ComponentRef Id="INSTALLDIR_Component"/>
       <ComponentRef Id="MainExecutable"/>
       <ComponentRef Id="WinPathExecutable"/>
+      <ComponentRef Id="WinSshProxyExecutable"/>
       <ComponentGroupRef Id="ManFiles"/>
     </Feature>
 
diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go
index 6cab855d31..c7d857954a 100644
--- a/pkg/machine/wsl/machine.go
+++ b/pkg/machine/wsl/machine.go
@@ -1,4 +1,3 @@
-//go:build windows
 // +build windows
 
 package wsl
@@ -143,6 +142,11 @@ http://docs.microsoft.com/en-us/windows/wsl/install\
 
 `
 
+const (
+	winSShProxy    = "win-sshproxy.exe"
+	winSshProxyTid = "win-sshproxy.tid"
+)
+
 type Provider struct{}
 
 type MachineVM struct {
@@ -705,8 +709,6 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
 		return errors.Errorf("%q is already running", name)
 	}
 
-	fmt.Println("Starting machine...")
-
 	dist := toDist(name)
 
 	err := runCmdPassThrough("wsl", "-d", dist, "/root/bootstrap")
@@ -714,9 +716,107 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error {
 		return errors.Wrap(err, "WSL bootstrap script failed")
 	}
 
+	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.")
+		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")
+		}
+	}
+
 	return markStart(name)
 }
 
+func launchWinProxy(v *MachineVM) (bool, string, error) {
+	globalName := true
+	pipeName := "docker_engine"
+	if !pipeAvailable(pipeName) {
+		pipeName = toDist(v.Name)
+		globalName = false
+		if !pipeAvailable(pipeName) {
+			return globalName, "", errors.Errorf("could not start api proxy since expected pipe is not available: %s", pipeName)
+		}
+	}
+	fullPipeName := "npipe:////./pipe/" + pipeName
+
+	exe, err := os.Executable()
+	if err != nil {
+		return globalName, "", err
+	}
+
+	exe, err = filepath.EvalSymlinks(exe)
+	if err != nil {
+		return globalName, "", err
+	}
+
+	command := filepath.Join(filepath.Dir(exe), winSShProxy)
+	stateDir, err := getWinProxyStateDir(v)
+	if err != nil {
+		return globalName, "", err
+	}
+
+	dest := fmt.Sprintf("ssh://root@localhost:%d/run/podman/podman.sock", v.Port)
+	cmd := exec.Command(command, v.Name, stateDir, fullPipeName, dest, v.IdentityPath)
+	if err := cmd.Start(); err != nil {
+		return globalName, "", err
+	}
+
+	return globalName, fullPipeName, waitPipeExists(pipeName, 30, func() error {
+		active, exitCode := getProcessState(cmd.Process.Pid)
+		if !active {
+			return errors.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 pipeAvailable(pipeName string) bool {
+	_, err := os.Stat(`\\.\pipe\` + pipeName)
+	return os.IsNotExist(err)
+}
+
+func waitPipeExists(pipeName string, retries int, checkFailure func() error) error {
+	var err error
+	for i := 0; i < retries; i++ {
+		_, err = os.Stat(`\\.\pipe\` + pipeName)
+		if err == nil {
+			break
+		}
+		if fail := checkFailure(); fail != nil {
+			return fail
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	return err
+}
+
 func isWSLInstalled() bool {
 	cmd := exec.Command("wsl", "--status")
 	out, err := cmd.StdoutPipe()
@@ -817,6 +917,10 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
 		return errors.Errorf("%q is not running", v.Name)
 	}
 
+	if err := stopWinProxy(v); err != nil {
+		fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error())
+	}
+
 	cmd := exec.Command("wsl", "-d", dist, "sh")
 	cmd.Stdin = strings.NewReader(waitTerm)
 	if err = cmd.Start(); err != nil {
@@ -840,6 +944,59 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
 	return 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 := ioutil.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/util_windows.go b/pkg/machine/wsl/util_windows.go
index 95e4c98949..b5c28e015c 100644
--- a/pkg/machine/wsl/util_windows.go
+++ b/pkg/machine/wsl/util_windows.go
@@ -67,6 +67,7 @@ const (
 	TOKEN_QUERY                     = 0x0008
 	SE_PRIVILEGE_ENABLED            = 0x00000002
 	SE_ERR_ACCESSDENIED             = 0x05
+	WM_QUIT                         = 0x12
 )
 
 func winVersionAtLeast(major uint, minor uint, build uint) bool {
@@ -279,6 +280,18 @@ func obtainShutdownPrivilege() error {
 	return nil
 }
 
+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))
+	if err != nil {
+		return false, int(syscall.ERROR_PROC_NOT_FOUND)
+	}
+
+	var code uint32
+	syscall.GetExitCodeProcess(handle, &code)
+	return code == 259, int(code)
+}
+
 func addRunOnceRegistryEntry(command string) error {
 	k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE)
 	if err != nil {
@@ -336,3 +349,9 @@ 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)
+}