From 2d0b5ebb5bdc06cf34488a75318c0ba66e3f2edf Mon Sep 17 00:00:00 2001 From: "Jason T. Greene" Date: Tue, 18 Jan 2022 14:39:48 -0600 Subject: Implement API forwarding for podman machine on Windows Signed-off-by: Jason T. Greene --- 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 6482378cd..face2b499 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 0bcf32cd5..16faa25ef 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 c2826fc19..c4ba623c0 100644 --- a/contrib/msi/podman.wxs +++ b/contrib/msi/podman.wxs @@ -29,6 +29,9 @@ + + + @@ -41,6 +44,7 @@ + diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 6cab855d3..c7d857954 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 95e4c9894..b5c28e015 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) +} -- cgit v1.2.3-54-g00ecf