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 <acui@redhat.com>
This commit is contained in:
Ashley Cui
2023-12-01 16:55:54 -05:00
parent 9c80f358fb
commit e3f167f770
6 changed files with 228 additions and 171 deletions

View File

@ -187,7 +187,7 @@ func composeDockerHost() (string, error) {
if info.State != define.Running { if info.State != define.Running {
return "", fmt.Errorf("machine %s is not running but in state %s", item.Name, info.State) 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 { if info.ConnectionInfo.PodmanPipe == nil {
return "", errors.New("pipe of machine is not set") return "", errors.New("pipe of machine is not set")
} }

View File

@ -343,11 +343,14 @@ func (m *HyperVMachine) Inspect() (*machine.InspectInfo, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
machinePipe := machine.ToDist(m.Name)
podmanPipe := &define.VMFile{Path: `\\.\pipe\` + machinePipe}
return &machine.InspectInfo{ return &machine.InspectInfo{
ConfigPath: m.ConfigPath, ConfigPath: m.ConfigPath,
ConnectionInfo: machine.ConnectionConfig{ ConnectionInfo: machine.ConnectionConfig{
PodmanSocket: podmanSocket, PodmanSocket: podmanSocket,
PodmanPipe: podmanPipe,
}, },
Created: m.Created, Created: m.Created,
Image: machine.ImageConfig{ Image: machine.ImageConfig{
@ -592,6 +595,15 @@ func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error {
m.HostUser.Modified = false 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 // Write the config with updated starting status and hostuser modification
if err := m.writeConfig(); err != nil { if err := m.writeConfig(); err != nil {
@ -653,6 +665,10 @@ func (m *HyperVMachine) Stop(name string, opts machine.StopOptions) error {
logrus.Error(err) 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 { if err := vm.Stop(); err != nil {
return fmt.Errorf("stopping virtual machine: %w", err) return fmt.Errorf("stopping virtual machine: %w", err)
} }

View File

@ -3,13 +3,38 @@
package machine package machine
import ( import (
"fmt"
"os" "os"
"os/exec"
"path/filepath"
"strings"
"syscall" "syscall"
"time" "time"
"github.com/containers/podman/v4/pkg/machine/define"
"github.com/sirupsen/logrus" "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) { func GetProcessState(pid int) (active bool, exitCode int) {
const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE
handle, err := syscall.OpenProcess(da, false, uint32(pid)) handle, err := syscall.OpenProcess(da, false, uint32(pid))
@ -45,3 +70,172 @@ func WaitPipeExists(pipeName string, retries int, checkFailure func() error) err
return 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
}

View File

@ -273,14 +273,13 @@ http://docs.microsoft.com/en-us/windows/wsl/install\
` `
const ( const (
gvProxy = "gvproxy.exe" gvProxy = "gvproxy.exe"
winSShProxy = "win-sshproxy.exe" winSShProxy = "win-sshproxy.exe"
winSshProxyTid = "win-sshproxy.tid" pipePrefix = "npipe:////./pipe/"
pipePrefix = "npipe:////./pipe/" globalPipe = "docker_engine"
globalPipe = "docker_engine" userModeDist = "podman-net-usermode"
userModeDist = "podman-net-usermode" rootfulSock = "/run/podman/podman.sock"
rootfulSock = "/run/podman/podman.sock" rootlessSock = "/run/user/1000/podman/podman.sock"
rootlessSock = "/run/user/1000/podman/podman.sock"
) )
type MachineVM struct { 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) fmt.Printf("\n\tpodman machine set --rootful%s\n\n", suffix)
} }
winProxyOpts := machine.WinProxyOpts{
globalName, pipeName, err := launchWinProxy(v) Name: v.Name,
if !opts.NoInfo { IdentityPath: v.IdentityPath,
if err != nil { Port: v.Port,
fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") RemoteUsername: v.RemoteUsername,
fmt.Fprintf(os.Stderr, "\t%s\n", err.Error()) Rootful: v.Rootful,
fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.") VMType: vmtype,
} 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")
}
}
} }
machine.LaunchWinProxy(winProxyOpts, opts.NoInfo)
_, _, err = v.updateTimeStamps(true) _, _, err = v.updateTimeStamps(true)
return err return err
@ -1303,85 +1289,6 @@ func (v *MachineVM) reassignSshPort() error {
return nil 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 { func IsWSLFeatureEnabled() bool {
return wutil.SilentExec("wsl", "--set-default-version", "2") == nil 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) _, _, _ = 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()) 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 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 //nolint:cyclop
func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) {
var files []string var files []string

View File

@ -74,7 +74,7 @@ func (v *MachineVM) startUserModeNetworking() error {
return nil return nil
} }
exe, err := findExecutablePeer(gvProxy) exe, err := machine.FindExecutablePeer(gvProxy)
if err != nil { if err != nil {
return fmt.Errorf("could not locate %s, which is necessary for user-mode networking, please reinstall", gvProxy) return fmt.Errorf("could not locate %s, which is necessary for user-mode networking, please reinstall", gvProxy)
} }

View File

@ -67,7 +67,6 @@ const (
TOKEN_QUERY = 0x0008 TOKEN_QUERY = 0x0008
SE_PRIVILEGE_ENABLED = 0x00000002 SE_PRIVILEGE_ENABLED = 0x00000002
SE_ERR_ACCESSDENIED = 0x05 SE_ERR_ACCESSDENIED = 0x05
WM_QUIT = 0x12
) )
func winVersionAtLeast(major uint, minor uint, build uint) bool { func winVersionAtLeast(major uint, minor uint, build uint) bool {
@ -337,9 +336,3 @@ func buildCommandArgs(elevate bool) string {
} }
return strings.Join(args, " ") 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)
}