From 487219d809732e69e052e50ee403a4c35c1a9dc5 Mon Sep 17 00:00:00 2001
From: "Jason T. Greene" <jason.greene@redhat.com>
Date: Wed, 7 Feb 2024 14:49:47 -0600
Subject: [PATCH] Complete WSL implementation, refactor a few areas

Also addresses a number of issues:
- StopHostNetworking isn't plumbed, win-sshproxy leaks on hyperv
- Wait api and print output doesn't work properly on Windows
- API forwarding doesn't work on WSL
- Terminal corruption with after start/stop on Windows
- Gvproxy is forcefully killed vs gracefully quit
- Switching rootful/rootless does not update /var/run/docker.sock on the guest
- File already closed error on init
- HyperV backend is publishing Unix sockets when it should be named pipes
- User-mode networking doesn't always work
- Stop state outside of lock boundaries
- WSL blocks parallel machined (should be supported)

[NO NEW TESTS NEEDED]

Signed-off-by: Jason T. Greene <jason.greene@redhat.com>
---
 cmd/podman/machine/set.go                     |   1 -
 go.mod                                        |   1 +
 go.sum                                        |   2 +
 pkg/machine/applehv/stubber.go                |  10 +-
 pkg/machine/compression/decompress.go         |   4 +-
 pkg/machine/config.go                         |  46 +++-
 pkg/machine/e2e/machine_test.go               |   3 +-
 pkg/machine/gvproxy.go                        |  47 ----
 pkg/machine/gvproxy_unix.go                   |  41 ++++
 pkg/machine/gvproxy_windows.go                |  42 ++++
 pkg/machine/hyperv/stubber.go                 |  29 +--
 pkg/machine/machine_common.go                 |  42 ++--
 pkg/machine/machine_unix.go                   |  11 +
 pkg/machine/machine_windows.go                |  41 ++--
 pkg/machine/qemu/stubber.go                   |  12 +-
 pkg/machine/shim/host.go                      |  39 +++-
 pkg/machine/shim/networking.go                |  97 ++-------
 pkg/machine/shim/networking_unix.go           |  84 ++++++++
 pkg/machine/shim/networking_windows.go        |  24 +++
 pkg/machine/ssh.go                            |  21 +-
 pkg/machine/ssh_unix.go                       |  16 ++
 pkg/machine/ssh_windows.go                    |  42 ++++
 pkg/machine/vmconfigs/config.go               |   4 +-
 pkg/machine/wsl/declares.go                   |   2 +
 pkg/machine/wsl/fedora.go                     |   3 +-
 pkg/machine/wsl/machine.go                    |  35 +--
 pkg/machine/wsl/stubber.go                    |  96 ++++-----
 pkg/machine/wsl/usermodenet.go                |   6 +-
 vendor/github.com/containers/winquit/LICENSE  | 202 ++++++++++++++++++
 .../winquit/pkg/winquit/channels_windows.go   |  50 +++++
 .../containers/winquit/pkg/winquit/client.go  |  31 +++
 .../winquit/pkg/winquit/client_unsupported.go |  17 ++
 .../winquit/pkg/winquit/client_windows.go     |  47 ++++
 .../containers/winquit/pkg/winquit/doc.go     | 135 ++++++++++++
 .../containers/winquit/pkg/winquit/server.go  |  45 ++++
 .../winquit/pkg/winquit/server_unsupported.go |  18 ++
 .../winquit/pkg/winquit/server_windows.go     | 147 +++++++++++++
 .../winquit/pkg/winquit/win32/common.go       |  17 ++
 .../pkg/winquit/win32/common_unsupported.go   |   4 +
 .../winquit/pkg/winquit/win32/msg.go          |  87 ++++++++
 .../winquit/pkg/winquit/win32/proc.go         |  59 +++++
 .../winquit/pkg/winquit/win32/pss.go          | 160 ++++++++++++++
 .../winquit/pkg/winquit/win32/win.go          | 162 ++++++++++++++
 vendor/modules.txt                            |   4 +
 44 files changed, 1710 insertions(+), 276 deletions(-)
 create mode 100644 pkg/machine/gvproxy_unix.go
 create mode 100644 pkg/machine/gvproxy_windows.go
 create mode 100644 pkg/machine/shim/networking_unix.go
 create mode 100644 pkg/machine/shim/networking_windows.go
 create mode 100644 pkg/machine/ssh_unix.go
 create mode 100644 pkg/machine/ssh_windows.go
 create mode 100644 vendor/github.com/containers/winquit/LICENSE
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/client.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/client_windows.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/doc.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/server.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/server_windows.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/common.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go
 create mode 100644 vendor/github.com/containers/winquit/pkg/winquit/win32/win.go

diff --git a/cmd/podman/machine/set.go b/cmd/podman/machine/set.go
index 1c975622df..bf6a21c66d 100644
--- a/cmd/podman/machine/set.go
+++ b/cmd/podman/machine/set.go
@@ -128,7 +128,6 @@ func setMachine(cmd *cobra.Command, args []string) error {
 		setOpts.DiskSize = &newDiskSizeGB
 	}
 	if cmd.Flags().Changed("user-mode-networking") {
-		// TODO This needs help
 		setOpts.UserModeNetworking = &setFlags.UserModeNetworking
 	}
 	if cmd.Flags().Changed("usb") {
diff --git a/go.mod b/go.mod
index 9f3c806d0b..d28ad90e4e 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@ require (
 	github.com/containers/ocicrypt v1.1.9
 	github.com/containers/psgo v1.9.0
 	github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565
+	github.com/containers/winquit v1.1.0
 	github.com/coreos/go-systemd/v22 v22.5.1-0.20231103132048-7d375ecc2b09
 	github.com/coreos/stream-metadata-go v0.4.4
 	github.com/crc-org/crc/v2 v2.32.0
diff --git a/go.sum b/go.sum
index f85ef3759c..04cbfc6835 100644
--- a/go.sum
+++ b/go.sum
@@ -93,6 +93,8 @@ github.com/containers/psgo v1.9.0 h1:eJ74jzSaCHnWt26OlKZROSyUyRcGDf+gYBdXnxrMW4g
 github.com/containers/psgo v1.9.0/go.mod h1:0YoluUm43Mz2UnBIh1P+6V6NWcbpTL5uRtXyOcH0B5A=
 github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565 h1:Gcirfx2DNoayB/+ypLgl5+ABzIPPDAoncs1qgZHHQHE=
 github.com/containers/storage v1.52.1-0.20240202181245-1419a5980565/go.mod h1:2E/QBqWVcJXwumP7nVUrampwRNL4XKjHL/aQya7ZdhI=
+github.com/containers/winquit v1.1.0 h1:jArun04BNDQvt2W0Y78kh9TazN2EIEMG5Im6/JY7+pE=
+github.com/containers/winquit v1.1.0/go.mod h1:PsPeZlnbkmGGIToMPHF1zhWjBUkd8aHjMOr/vFcPxw8=
 github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
 github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
diff --git a/pkg/machine/applehv/stubber.go b/pkg/machine/applehv/stubber.go
index 78095dcfab..3ebaf1eaa4 100644
--- a/pkg/machine/applehv/stubber.go
+++ b/pkg/machine/applehv/stubber.go
@@ -40,6 +40,14 @@ type AppleHVStubber struct {
 }
 
 func (a AppleHVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool {
+return true
+}
+
+func (a AppleHVStubber) UseProviderNetworkSetup() bool {
+	return false
+}
+
+func (a AppleHVStubber) RequireExclusiveActive() bool {
 	return true
 }
 
@@ -319,7 +327,7 @@ func (a AppleHVStubber) PrepareIgnition(_ *vmconfigs.MachineConfig, _ *ignition.
 	return nil, nil
 }
 
-func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
+func (a AppleHVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
 	return nil
 }
 
diff --git a/pkg/machine/compression/decompress.go b/pkg/machine/compression/decompress.go
index 14a6d526de..5ab0221b3a 100644
--- a/pkg/machine/compression/decompress.go
+++ b/pkg/machine/compression/decompress.go
@@ -32,8 +32,8 @@ func Decompress(localPath *define.VMFile, uncompressedPath string) error {
 		return err
 	}
 	defer func() {
-		if err := uncompressedFileWriter.Close(); err != nil {
-			logrus.Errorf("unable to to close decompressed file %s: %q", uncompressedPath, err)
+		if err := uncompressedFileWriter.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
+			logrus.Warnf("unable to close decompressed file %s: %q", uncompressedPath, err)
 		}
 	}()
 	sourceFile, err := localPath.Read()
diff --git a/pkg/machine/config.go b/pkg/machine/config.go
index 7730d5c525..3548b6ee2e 100644
--- a/pkg/machine/config.go
+++ b/pkg/machine/config.go
@@ -333,11 +333,55 @@ func NewVirtualization(artifact define.Artifact, compression compression.ImageCo
 	}
 }
 
+func dialSocket(socket string, timeout time.Duration) (net.Conn, error) {
+	scheme := "unix"
+	if strings.Contains(socket, "://") {
+		url, err := url.Parse(socket)
+		if err != nil {
+			return nil, err
+		}
+		scheme = url.Scheme
+		socket = url.Path
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	var dial func() (net.Conn, error)
+	switch scheme {
+	default:
+		fallthrough
+	case "unix":
+		dial = func() (net.Conn, error) {
+			var dialer net.Dialer
+			return dialer.DialContext(ctx, "unix", socket)
+		}
+	case "npipe":
+		dial = func() (net.Conn, error) {
+			return DialNamedPipe(ctx, socket)
+		}
+	}
+
+	backoff := 500 * time.Millisecond
+	for {
+		conn, err := dial()
+		if !errors.Is(err, os.ErrNotExist) {
+			return conn, err
+		}
+
+		select {
+		case <-time.After(backoff):
+			backoff *= 2
+		case <-ctx.Done():
+			return nil, ctx.Err()
+		}
+	}
+}
+
 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)
+				con, err := dialSocket(sock, apiUpTimeout)
 				if err != nil {
 					return nil, err
 				}
diff --git a/pkg/machine/e2e/machine_test.go b/pkg/machine/e2e/machine_test.go
index 918176fa50..866b23b878 100644
--- a/pkg/machine/e2e/machine_test.go
+++ b/pkg/machine/e2e/machine_test.go
@@ -2,7 +2,6 @@ package e2e_test
 
 import (
 	"fmt"
-	"github.com/containers/podman/v4/pkg/machine/wsl"
 	"io"
 	url2 "net/url"
 	"os"
@@ -13,6 +12,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/containers/podman/v5/pkg/machine/wsl"
+
 	"github.com/containers/podman/v5/pkg/machine"
 	"github.com/containers/podman/v5/pkg/machine/compression"
 	"github.com/containers/podman/v5/pkg/machine/define"
diff --git a/pkg/machine/gvproxy.go b/pkg/machine/gvproxy.go
index 7f1ae58af4..6a008d22d4 100644
--- a/pkg/machine/gvproxy.go
+++ b/pkg/machine/gvproxy.go
@@ -1,16 +1,12 @@
 package machine
 
 import (
-	"errors"
 	"fmt"
-	"runtime"
 	"strconv"
-	"syscall"
 	"time"
 
 	"github.com/containers/podman/v5/pkg/machine/define"
 	psutil "github.com/shirou/gopsutil/v3/process"
-	"github.com/sirupsen/logrus"
 )
 
 const (
@@ -39,49 +35,6 @@ func backoffForProcess(p *psutil.Process) error {
 	return fmt.Errorf("process %d has not ended", p.Pid)
 }
 
-// waitOnProcess takes a pid and sends a sigterm to it. it then waits for the
-// process to not exist.  if the sigterm does not end the process after an interval,
-// then sigkill is sent.  it also waits for the process to exit after the sigkill too.
-func waitOnProcess(processID int) error {
-	logrus.Infof("Going to stop gvproxy (PID %d)", processID)
-
-	p, err := psutil.NewProcess(int32(processID))
-	if err != nil {
-		return fmt.Errorf("looking up PID %d: %w", processID, err)
-	}
-
-	// Try to kill the pid with sigterm
-	if runtime.GOOS != "windows" { // FIXME: temporary work around because signals are lame in windows
-		if err := p.SendSignal(syscall.SIGTERM); err != nil {
-			if errors.Is(err, syscall.ESRCH) {
-				return nil
-			}
-			return fmt.Errorf("sending SIGTERM to grproxy: %w", err)
-		}
-
-		if err := backoffForProcess(p); err == nil {
-			return nil
-		}
-	}
-
-	running, err := p.IsRunning()
-	if err != nil {
-		return fmt.Errorf("checking if gvproxy is running: %w", err)
-	}
-	if !running {
-		return nil
-	}
-
-	if err := p.Kill(); err != nil {
-		if errors.Is(err, syscall.ESRCH) {
-			logrus.Debugf("Gvproxy already dead, exiting cleanly")
-			return nil
-		}
-		return err
-	}
-	return backoffForProcess(p)
-}
-
 // CleanupGVProxy reads the --pid-file for gvproxy attempts to stop it
 func CleanupGVProxy(f define.VMFile) error {
 	gvPid, err := f.Read()
diff --git a/pkg/machine/gvproxy_unix.go b/pkg/machine/gvproxy_unix.go
new file mode 100644
index 0000000000..431e34740f
--- /dev/null
+++ b/pkg/machine/gvproxy_unix.go
@@ -0,0 +1,41 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
+
+package machine
+
+import (
+	"errors"
+	"fmt"
+	"syscall"
+
+	psutil "github.com/shirou/gopsutil/v3/process"
+	"github.com/sirupsen/logrus"
+)
+
+// / waitOnProcess takes a pid and sends a sigterm to it. it then waits for the
+// process to not exist.  if the sigterm does not end the process after an interval,
+// then sigkill is sent.  it also waits for the process to exit after the sigkill too.
+func waitOnProcess(processID int) error {
+	logrus.Infof("Going to stop gvproxy (PID %d)", processID)
+
+	p, err := psutil.NewProcess(int32(processID))
+	if err != nil {
+		return fmt.Errorf("looking up PID %d: %w", processID, err)
+	}
+
+	running, err := p.IsRunning()
+	if err != nil {
+		return fmt.Errorf("checking if gvproxy is running: %w", err)
+	}
+	if !running {
+		return nil
+	}
+
+	if err := p.Kill(); err != nil {
+		if errors.Is(err, syscall.ESRCH) {
+			logrus.Debugf("Gvproxy already dead, exiting cleanly")
+			return nil
+		}
+		return err
+	}
+	return backoffForProcess(p)
+}
diff --git a/pkg/machine/gvproxy_windows.go b/pkg/machine/gvproxy_windows.go
new file mode 100644
index 0000000000..4311aac257
--- /dev/null
+++ b/pkg/machine/gvproxy_windows.go
@@ -0,0 +1,42 @@
+package machine
+
+import (
+	"os"
+	"time"
+
+	"github.com/containers/winquit/pkg/winquit"
+	"github.com/sirupsen/logrus"
+)
+
+func waitOnProcess(processID int) error {
+	logrus.Infof("Going to stop gvproxy (PID %d)", processID)
+
+	p, err := os.FindProcess(processID)
+	if err != nil {
+		return nil
+	}
+
+	// Gracefully quit and force kill after 30 seconds
+	if err := winquit.QuitProcess(processID, 30*time.Second); err != nil {
+		return err
+	}
+
+	logrus.Debugf("completed grace quit || kill of gvproxy (PID %d)", processID)
+
+	// Make sure the process is gone
+	done := make(chan struct{})
+	go func() {
+		_, _ = p.Wait()
+		done <- struct{}{}
+	}()
+
+	select {
+	case <-done:
+		logrus.Debugf("verified gvproxy termination (PID %d)", processID)
+	case <-time.After(10 * time.Second):
+		// Very unlikely but track just in case
+		logrus.Errorf("was not able to kill gvproxy (PID %d)", processID)
+	}
+
+	return nil
+}
diff --git a/pkg/machine/hyperv/stubber.go b/pkg/machine/hyperv/stubber.go
index 1d4fddf9da..080d78c039 100644
--- a/pkg/machine/hyperv/stubber.go
+++ b/pkg/machine/hyperv/stubber.go
@@ -33,6 +33,14 @@ func (h HyperVStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool
 	return true
 }
 
+func (h HyperVStubber) UseProviderNetworkSetup() bool {
+	return false
+}
+
+func (h HyperVStubber) RequireExclusiveActive() bool {
+	return true
+}
+
 func (h HyperVStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, builder *ignition.IgnitionBuilder) error {
 	var (
 		err error
@@ -374,7 +382,7 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, ignBuilder *
 	return &ignOpts, nil
 }
 
-func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
+func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
 	var (
 		err        error
 		executable string
@@ -383,25 +391,6 @@ func (h HyperVStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
 	defer callbackFuncs.CleanIfErr(&err)
 	go callbackFuncs.CleanOnSignal()
 
-	winProxyOpts := machine.WinProxyOpts{
-		Name:           mc.Name,
-		IdentityPath:   mc.SSH.IdentityPath,
-		Port:           mc.SSH.Port,
-		RemoteUsername: mc.SSH.RemoteUsername,
-		Rootful:        mc.HostUser.Rootful,
-		VMType:         h.VMType(),
-	}
-	// TODO Should this process be fatal on error; currenty, no error is
-	// returned but an error can occur in the func itself
-	// TODO we do not currently pass "noinfo" (quiet) into the StartVM
-	// func so this is hard set to false
-	machine.LaunchWinProxy(winProxyOpts, false)
-
-	winProxyCallbackFunc := func() error {
-		return machine.StopWinProxy(mc.Name, h.VMType())
-	}
-	callbackFuncs.Add(winProxyCallbackFunc)
-
 	if len(mc.Mounts) != 0 {
 		var (
 			dirs       *define.MachineDirs
diff --git a/pkg/machine/machine_common.go b/pkg/machine/machine_common.go
index 70116118a9..a8eb8be396 100644
--- a/pkg/machine/machine_common.go
+++ b/pkg/machine/machine_common.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/containers/podman/v5/pkg/machine/connection"
 	"github.com/containers/storage/pkg/ioutils"
@@ -46,19 +47,6 @@ func WaitAPIAndPrintInfo(forwardState APIForwardingState, name, helper, forwardS
 	WaitAndPingAPI(forwardSock)
 
 	if !noInfo {
-		if !rootful {
-			fmtString = `
-This machine is currently configured in rootless mode. If your containers
-require root permissions (e.g. ports < 1024), or if you run into compatibility
-issues with non-podman clients, you can switch using the following command:
-
-        podman machine set --rootful%s
-
-`
-
-			fmt.Printf(fmtString, 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")
@@ -79,7 +67,7 @@ address can't be used by podman. `
         sudo %s install
         podman machine stop%[2]s; podman machine start%[2]s
 
-                `
+`
 					fmt.Printf(fmtString, helper, suffix)
 				}
 			case MachineLocal:
@@ -93,15 +81,35 @@ address can't be used by podman. `
 			fmtString = `You can %sconnect Docker API clients by setting DOCKER_HOST using the
 following command in your terminal session:
 
-        export DOCKER_HOST='unix://%s'
+        %s'
 
 `
-
-			fmt.Printf(fmtString, stillString, forwardSock)
+			prefix := ""
+			if !strings.Contains(forwardSock, "://") {
+				prefix = "unix://"
+			}
+			fmt.Printf(fmtString, stillString, GetEnvSetString("DOCKER_HOST", prefix+forwardSock))
 		}
 	}
 }
 
+func PrintRootlessWarning(name string) {
+	suffix := ""
+	if name != DefaultMachineName {
+		suffix = " " + name
+	}
+
+	fmtString := `
+This machine is currently configured in rootless mode. If your containers
+require root permissions (e.g. ports < 1024), or if you run into compatibility
+issues with non-podman clients, you can switch using the following command:
+
+	podman machine set --rootful%s
+
+`
+	fmt.Printf(fmtString, suffix)
+}
+
 // SetRootful modifies the machine's default connection to be either rootful or
 // rootless
 func SetRootful(rootful bool, name, rootfulName string) error {
diff --git a/pkg/machine/machine_unix.go b/pkg/machine/machine_unix.go
index 66c92ed4e8..10bcf5be8c 100644
--- a/pkg/machine/machine_unix.go
+++ b/pkg/machine/machine_unix.go
@@ -3,7 +3,10 @@
 package machine
 
 import (
+	"context"
 	"errors"
+	"fmt"
+	"net"
 	"strings"
 )
 
@@ -32,3 +35,11 @@ func ParseVolumeFromPath(v string) (source, target, options string, readonly boo
 	}
 	return
 }
+
+func DialNamedPipe(ctx context.Context, path string) (net.Conn, error) {
+	return nil, errors.New("not implemented")
+}
+
+func GetEnvSetString(env string, val string) string {
+	return fmt.Sprintf("export %s='%s'", env, val)
+}
diff --git a/pkg/machine/machine_windows.go b/pkg/machine/machine_windows.go
index 3e5ad1168e..01f5fa4dc8 100644
--- a/pkg/machine/machine_windows.go
+++ b/pkg/machine/machine_windows.go
@@ -3,7 +3,9 @@
 package machine
 
 import (
+	"context"
 	"fmt"
+	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -12,16 +14,17 @@ import (
 	"time"
 
 	"github.com/containers/podman/v5/pkg/machine/define"
+	winio "github.com/Microsoft/go-winio"
 	"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"
+	NamedPipePrefix = "npipe:////./pipe/"
+	GlobalNamedPipe = "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
@@ -71,6 +74,11 @@ func WaitPipeExists(pipeName string, retries int, checkFailure func() error) err
 	return err
 }
 
+func DialNamedPipe(ctx context.Context, path string) (net.Conn, error) {
+	path = strings.Replace(path, "/", "\\", -1)
+	return winio.DialPipeContext(ctx, path)
+}
+
 func LaunchWinProxy(opts WinProxyOpts, noInfo bool) {
 	globalName, pipeName, err := launchWinProxy(opts)
 	if !noInfo {
@@ -102,7 +110,7 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) {
 	}
 
 	globalName := false
-	if PipeNameAvailable(globalPipe) {
+	if PipeNameAvailable(GlobalNamedPipe) {
 		globalName = true
 	}
 
@@ -125,27 +133,20 @@ func launchWinProxy(opts WinProxyOpts) (bool, string, error) {
 	}
 
 	dest := fmt.Sprintf("ssh://%s@localhost:%d%s", forwardUser, opts.Port, destSock)
-	args := []string{opts.Name, stateDir, pipePrefix + machinePipe, dest, opts.IdentityPath}
+	args := []string{opts.Name, stateDir, NamedPipePrefix + machinePipe, dest, opts.IdentityPath}
 	waitPipe := machinePipe
 	if globalName {
-		args = append(args, pipePrefix+globalPipe, dest, opts.IdentityPath)
-		waitPipe = globalPipe
+		args = append(args, NamedPipePrefix+GlobalNamedPipe, dest, opts.IdentityPath)
+		waitPipe = GlobalNamedPipe
 	}
 
 	cmd := exec.Command(command, args...)
 	logrus.Debugf("winssh command: %s %v", command, args)
-	f, err := os.Open("c:\\Users\\baude\\sshproxy.log")
-	if err != nil {
-		return false, "", err
-	}
-	cmd.Stderr = f
-	cmd.Stdout = f
-	defer f.Close()
 	if err := cmd.Start(); err != nil {
 		return globalName, "", err
 	}
 
-	return globalName, pipePrefix + waitPipe, WaitPipeExists(waitPipe, 80, func() error {
+	return globalName, NamedPipePrefix + 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)
@@ -247,3 +248,7 @@ func ToDist(name string) string {
 	}
 	return name
 }
+
+func GetEnvSetString(env string, val string) string {
+	return fmt.Sprintf("$Env:%s=\"%s\"", env, val)
+}
diff --git a/pkg/machine/qemu/stubber.go b/pkg/machine/qemu/stubber.go
index 3fed30c955..28614246bb 100644
--- a/pkg/machine/qemu/stubber.go
+++ b/pkg/machine/qemu/stubber.go
@@ -34,6 +34,14 @@ func (q QEMUStubber) UserModeNetworkEnabled(*vmconfigs.MachineConfig) bool {
 	return true
 }
 
+func (q QEMUStubber) UseProviderNetworkSetup() bool {
+	return false
+}
+
+func (q QEMUStubber) RequireExclusiveActive() bool {
+	return true
+}
+
 func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error {
 	qemuBinary, err := findQEMUBinary()
 	if err != nil {
@@ -73,7 +81,7 @@ func (q *QEMUStubber) setQEMUCommandLine(mc *vmconfigs.MachineConfig) error {
 	return nil
 }
 
-func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, _ *ignition.IgnitionBuilder) error {
+func (q *QEMUStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConfig, builder *ignition.IgnitionBuilder) error {
 	monitor, err := command.NewQMPMonitor(opts.Name, opts.Dirs.RuntimeDir)
 	if err != nil {
 		return err
@@ -327,7 +335,7 @@ func (q *QEMUStubber) MountType() vmconfigs.VolumeMountType {
 	return vmconfigs.NineP
 }
 
-func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
+func (q *QEMUStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
 	return nil
 }
 
diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go
index 807c60741c..9bf7bf0623 100644
--- a/pkg/machine/shim/host.go
+++ b/pkg/machine/shim/host.go
@@ -256,6 +256,11 @@ func VMExists(name string, vmstubbers []vmconfigs.VMProvider) (*vmconfigs.Machin
 
 // CheckExclusiveActiveVM checks if any of the machines are already running
 func CheckExclusiveActiveVM(provider vmconfigs.VMProvider, mc *vmconfigs.MachineConfig) error {
+	// Don't check if provider supports parallel running machines
+	if !provider.RequireExclusiveActive() {
+		return nil
+	}
+
 	// Check if any other machines are running; if so, we error
 	localMachines, err := getMCsOverProviders([]vmconfigs.VMProvider{provider})
 	if err != nil {
@@ -330,7 +335,7 @@ func Stop(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDef
 	}
 
 	// Stop GvProxy and remove PID file
-	if mp.UserModeNetworkEnabled(mc) {
+	if !mp.UseProviderNetworkSetup() {
 		gvproxyPidFile, err := dirs.RuntimeDir.AppendToNewVMFile("gvproxy.pid", nil)
 		if err != nil {
 			return err
@@ -381,7 +386,11 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin
 		}
 	}
 
-	err = mp.PostStartNetworking(mc)
+	if !opts.NoInfo && !mc.HostUser.Rootful {
+		machine.PrintRootlessWarning(mc.Name)
+	}
+
+	err = mp.PostStartNetworking(mc, opts.NoInfo)
 	if err != nil {
 		return err
 	}
@@ -408,15 +417,6 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin
 		return err
 	}
 
-	machine.WaitAPIAndPrintInfo(
-		forwardingState,
-		mc.Name,
-		findClaimHelper(),
-		forwardSocketPath,
-		opts.NoInfo,
-		mc.HostUser.Rootful,
-	)
-
 	// update the podman/docker socket service if the host user has been modified at all (UID or Rootful)
 	if mc.HostUser.Modified {
 		if machine.UpdatePodmanDockerSockService(mc) == nil {
@@ -428,5 +428,22 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, _ *machineDefin
 			}
 		}
 	}
+
+	// Provider is responsible for waiting
+	if mp.UseProviderNetworkSetup() {
+		return nil
+	}
+
+	noInfo := opts.NoInfo
+
+	machine.WaitAPIAndPrintInfo(
+		forwardingState,
+		mc.Name,
+		findClaimHelper(),
+		forwardSocketPath,
+		noInfo,
+		mc.HostUser.Rootful,
+	)
+
 	return nil
 }
diff --git a/pkg/machine/shim/networking.go b/pkg/machine/shim/networking.go
index dfb39008ba..6305458185 100644
--- a/pkg/machine/shim/networking.go
+++ b/pkg/machine/shim/networking.go
@@ -2,9 +2,7 @@ package shim
 
 import (
 	"fmt"
-	"io/fs"
 	"net"
-	"os"
 	"path/filepath"
 	"strings"
 	"time"
@@ -23,7 +21,7 @@ const (
 	dockerConnectTimeout = 5 * time.Second
 )
 
-func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider, dirs *define.MachineDirs, hostSocket *define.VMFile) error {
+func startHostForwarder(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider, dirs *define.MachineDirs, hostSocks []string) error {
 	forwardUser := mc.SSH.RemoteUsername
 
 	// TODO should this go up the stack higher or
@@ -57,10 +55,13 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMP
 
 	cmd.SSHPort = mc.SSH.Port
 
-	cmd.AddForwardSock(hostSocket.GetPath())
-	cmd.AddForwardDest(guestSock)
-	cmd.AddForwardUser(forwardUser)
-	cmd.AddForwardIdentity(mc.SSH.IdentityPath)
+	// Windows providers listen on multiple sockets since they do not involve links
+	for _, hostSock := range hostSocks {
+		cmd.AddForwardSock(hostSock)
+		cmd.AddForwardDest(guestSock)
+		cmd.AddForwardUser(forwardUser)
+		cmd.AddForwardIdentity(mc.SSH.IdentityPath)
+	}
 
 	if logrus.IsLevelEnabled(logrus.DebugLevel) {
 		cmd.Debug = true
@@ -84,92 +85,28 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMP
 }
 
 func startNetworking(mc *vmconfigs.MachineConfig, provider vmconfigs.VMProvider) (string, machine.APIForwardingState, error) {
-	var (
-		forwardingState machine.APIForwardingState
-		forwardSock     string
-	)
+	// Provider has its own networking code path (e.g. WSL)
+	if provider.UseProviderNetworkSetup() {
+		return "", 0, provider.StartNetworking(mc, nil)
+	}
+
 	dirs, err := machine.GetMachineDirs(provider.VMType())
 	if err != nil {
 		return "", 0, err
 	}
-	hostSocket, err := dirs.DataDir.AppendToNewVMFile("podman.sock", nil)
+
+	hostSocks, forwardSock, forwardingState, err := setupMachineSockets(mc.Name, dirs)
 	if err != nil {
 		return "", 0, err
 	}
 
-	linkSocketPath := filepath.Dir(dirs.DataDir.GetPath())
-	linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil)
-	if err != nil {
+	if err := startHostForwarder(mc, provider, dirs, hostSocks); err != nil {
 		return "", 0, err
 	}
 
-	if mc.HostUser.UID != -1 {
-		forwardSock, forwardingState = setupAPIForwarding(hostSocket, linkSocket)
-	}
-
-	if provider.UserModeNetworkEnabled(mc) {
-		if err := startUserModeNetworking(mc, provider, dirs, hostSocket); err != nil {
-			return "", 0, err
-		}
-	}
-
 	return forwardSock, forwardingState, nil
 }
 
-func setupAPIForwarding(hostSocket, linkSocket *define.VMFile) (string, machine.APIForwardingState) {
-	// 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 hostSocket.GetPath(), machine.ClaimUnsupported
-	}
-
-	if !dockerClaimHelperInstalled() {
-		return hostSocket.GetPath(), machine.NotInstalled
-	}
-
-	if !alreadyLinked(hostSocket.GetPath(), linkSocket.GetPath()) {
-		if checkSockInUse(linkSocket.GetPath()) {
-			return hostSocket.GetPath(), machine.MachineLocal
-		}
-
-		_ = linkSocket.Delete()
-
-		if err := os.Symlink(hostSocket.GetPath(), linkSocket.GetPath()); err != nil {
-			logrus.Warnf("could not create user global API forwarding link: %s", err.Error())
-			return hostSocket.GetPath(), machine.MachineLocal
-		}
-	}
-
-	if !alreadyLinked(linkSocket.GetPath(), dockerSock) {
-		if checkSockInUse(dockerSock) {
-			return hostSocket.GetPath(), machine.MachineLocal
-		}
-
-		if !claimDockerSock() {
-			logrus.Warn("podman helper is installed, but was not able to claim the global docker sock")
-			return hostSocket.GetPath(), machine.MachineLocal
-		}
-	}
-
-	return dockerSock, machine.DockerGlobal
-}
-
-func alreadyLinked(target string, link string) bool {
-	read, err := os.Readlink(link)
-	return err == nil && read == target
-}
-
-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
-}
-
 // conductVMReadinessCheck checks to make sure the machine is in the proper state
 // and that SSH is up and running
 func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backoff time.Duration, stateF func() (define.Status, error)) (connected bool, sshError error, err error) {
@@ -191,7 +128,7 @@ func conductVMReadinessCheck(mc *vmconfigs.MachineConfig, maxBackoffs int, backo
 			// CoreOS users have reported the same observation but
 			// the underlying source of the issue remains unknown.
 
-			if sshError = machine.CommonSSH(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil {
+			if sshError = machine.CommonSSHSilent(mc.SSH.RemoteUsername, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"true"}); sshError != nil {
 				logrus.Debugf("SSH readiness check for machine failed: %v", sshError)
 				continue
 			}
diff --git a/pkg/machine/shim/networking_unix.go b/pkg/machine/shim/networking_unix.go
new file mode 100644
index 0000000000..869f99d5ed
--- /dev/null
+++ b/pkg/machine/shim/networking_unix.go
@@ -0,0 +1,84 @@
+//go:build dragonfly || freebsd || linux || netbsd || openbsd || darwin
+
+package shim
+
+import (
+	"io/fs"
+	"net"
+	"os"
+	"path/filepath"
+
+	"github.com/containers/podman/v5/pkg/machine"
+	"github.com/containers/podman/v5/pkg/machine/define"
+	"github.com/sirupsen/logrus"
+)
+
+func setupMachineSockets(name string, dirs *define.MachineDirs) ([]string, string, machine.APIForwardingState, error) {
+	hostSocket, err := dirs.DataDir.AppendToNewVMFile("podman.sock", nil)
+	if err != nil {
+		return nil, "", 0, err
+	}
+
+	linkSocketPath := filepath.Dir(dirs.DataDir.GetPath())
+	linkSocket, err := define.NewMachineFile(filepath.Join(linkSocketPath, "podman.sock"), nil)
+	if err != nil {
+		return nil, "", 0, err
+	}
+
+	forwardSock, state := setupForwardingLinks(hostSocket, linkSocket)
+	return []string{hostSocket.GetPath()}, forwardSock, state, nil
+}
+
+func setupForwardingLinks(hostSocket, linkSocket *define.VMFile) (string, machine.APIForwardingState) {
+	// 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 hostSocket.GetPath(), machine.ClaimUnsupported
+	}
+
+	if !dockerClaimHelperInstalled() {
+		return hostSocket.GetPath(), machine.NotInstalled
+	}
+
+	if !alreadyLinked(hostSocket.GetPath(), linkSocket.GetPath()) {
+		if checkSockInUse(linkSocket.GetPath()) {
+			return hostSocket.GetPath(), machine.MachineLocal
+		}
+
+		_ = linkSocket.Delete()
+
+		if err := os.Symlink(hostSocket.GetPath(), linkSocket.GetPath()); err != nil {
+			logrus.Warnf("could not create user global API forwarding link: %s", err.Error())
+			return hostSocket.GetPath(), machine.MachineLocal
+		}
+	}
+
+	if !alreadyLinked(linkSocket.GetPath(), dockerSock) {
+		if checkSockInUse(dockerSock) {
+			return hostSocket.GetPath(), machine.MachineLocal
+		}
+
+		if !claimDockerSock() {
+			logrus.Warn("podman helper is installed, but was not able to claim the global docker sock")
+			return hostSocket.GetPath(), machine.MachineLocal
+		}
+	}
+
+	return dockerSock, machine.DockerGlobal
+}
+
+func alreadyLinked(target string, link string) bool {
+	read, err := os.Readlink(link)
+	return err == nil && read == target
+}
+
+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
+}
diff --git a/pkg/machine/shim/networking_windows.go b/pkg/machine/shim/networking_windows.go
new file mode 100644
index 0000000000..d13d4a0036
--- /dev/null
+++ b/pkg/machine/shim/networking_windows.go
@@ -0,0 +1,24 @@
+package shim
+
+import (
+	"fmt"
+
+	"github.com/containers/podman/v5/pkg/machine"
+	"github.com/containers/podman/v5/pkg/machine/define"
+)
+
+func setupMachineSockets(name string, dirs *define.MachineDirs) ([]string, string, machine.APIForwardingState, error) {
+	machinePipe := machine.ToDist(name)
+	if !machine.PipeNameAvailable(machinePipe) {
+		return nil, "", 0, fmt.Errorf("could not start api proxy since expected pipe is not available: %s", machinePipe)
+	}
+	sockets := []string{machine.NamedPipePrefix + machinePipe}
+	state := machine.MachineLocal
+
+	if machine.PipeNameAvailable(machine.GlobalNamedPipe) {
+		sockets = append(sockets, machine.NamedPipePrefix+machine.GlobalNamedPipe)
+		state = machine.DockerGlobal
+	}
+
+	return sockets, sockets[len(sockets)-1], state, nil
+}
diff --git a/pkg/machine/ssh.go b/pkg/machine/ssh.go
index de0d2bfad6..7a66984cf2 100644
--- a/pkg/machine/ssh.go
+++ b/pkg/machine/ssh.go
@@ -2,7 +2,6 @@ package machine
 
 import (
 	"fmt"
-	"os"
 	"os/exec"
 	"strconv"
 
@@ -13,24 +12,38 @@ import (
 // and a port
 // TODO This should probably be taught about an machineconfig to reduce input
 func CommonSSH(username, identityPath, name string, sshPort int, inputArgs []string) error {
+	return commonSSH(username, identityPath, name, sshPort, inputArgs, false)
+}
+
+func CommonSSHSilent(username, identityPath, name string, sshPort int, inputArgs []string) error {
+	return commonSSH(username, identityPath, name, sshPort, inputArgs, true)
+}
+
+func commonSSH(username, identityPath, name string, sshPort int, inputArgs []string, silent bool) error {
 	sshDestination := username + "@localhost"
 	port := strconv.Itoa(sshPort)
+	interactive := true
 
 	args := []string{"-i", identityPath, "-p", port, sshDestination,
 		"-o", "IdentitiesOnly=yes",
 		"-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="}
 	if len(inputArgs) > 0 {
+		interactive = false
 		args = append(args, inputArgs...)
 	} else {
+		// ensure we have a tty
+		args = append(args, "-t")
 		fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name)
 	}
 
 	cmd := exec.Command("ssh", args...)
 	logrus.Debugf("Executing: ssh %v\n", args)
 
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Stdin = os.Stdin
+	if !silent {
+		if err := setupIOPassthrough(cmd, interactive); err != nil {
+			return err
+		}
+	}
 
 	return cmd.Run()
 }
diff --git a/pkg/machine/ssh_unix.go b/pkg/machine/ssh_unix.go
new file mode 100644
index 0000000000..17e5acd06f
--- /dev/null
+++ b/pkg/machine/ssh_unix.go
@@ -0,0 +1,16 @@
+//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
+
+package machine
+
+import (
+	"os"
+	"os/exec"
+)
+
+func setupIOPassthrough(cmd *exec.Cmd, interactive bool) error {
+	cmd.Stdin = os.Stdin
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	return nil
+}
diff --git a/pkg/machine/ssh_windows.go b/pkg/machine/ssh_windows.go
new file mode 100644
index 0000000000..3440dc1687
--- /dev/null
+++ b/pkg/machine/ssh_windows.go
@@ -0,0 +1,42 @@
+package machine
+
+import (
+	"io"
+	"os"
+	"os/exec"
+
+	"github.com/sirupsen/logrus"
+)
+
+func setupIOPassthrough(cmd *exec.Cmd, interactive bool) error {
+	cmd.Stdin = os.Stdin
+
+	if interactive {
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		return nil
+	}
+
+	// OpenSSh mucks with the associated virtual console when there is no pty,
+	// leaving it in a broken state. Pipe the output to isolate stdout/stderr
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return err
+	}
+
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		return err
+	}
+
+	copier := func(name string, dest string, from io.Reader, to io.Writer) {
+		if _, err := io.Copy(to, from); err != nil {
+			logrus.Warnf("could not copy output from command %s to %s", name, dest)
+		}
+	}
+
+	go copier(cmd.Path, "stdout", stdout, os.Stdout)
+	go copier(cmd.Path, "stderr", stderr, os.Stderr)
+
+	return nil
+}
diff --git a/pkg/machine/vmconfigs/config.go b/pkg/machine/vmconfigs/config.go
index a5449b56cd..8ddaef8137 100644
--- a/pkg/machine/vmconfigs/config.go
+++ b/pkg/machine/vmconfigs/config.go
@@ -119,13 +119,15 @@ type VMProvider interface { //nolint:interfacebloat
 	RemoveAndCleanMachines(dirs *define.MachineDirs) error
 	SetProviderAttrs(mc *MachineConfig, opts define.SetOptions) error
 	StartNetworking(mc *MachineConfig, cmd *gvproxy.GvproxyCommand) error
-	PostStartNetworking(mc *MachineConfig) error
+	PostStartNetworking(mc *MachineConfig, noInfo bool) error
 	StartVM(mc *MachineConfig) (func() error, func() error, error)
 	State(mc *MachineConfig, bypass bool) (define.Status, error)
 	StopVM(mc *MachineConfig, hardStop bool) error
 	StopHostNetworking(mc *MachineConfig, vmType define.VMType) error
 	VMType() define.VMType
 	UserModeNetworkEnabled(mc *MachineConfig) bool
+	UseProviderNetworkSetup() bool
+	RequireExclusiveActive() bool
 }
 
 // HostUser describes the host user
diff --git a/pkg/machine/wsl/declares.go b/pkg/machine/wsl/declares.go
index 1c59d5f421..a8a8179008 100644
--- a/pkg/machine/wsl/declares.go
+++ b/pkg/machine/wsl/declares.go
@@ -1,3 +1,5 @@
+//go:build windows
+
 package wsl
 
 const (
diff --git a/pkg/machine/wsl/fedora.go b/pkg/machine/wsl/fedora.go
index 859d4221fa..4e4fb9a5b3 100644
--- a/pkg/machine/wsl/fedora.go
+++ b/pkg/machine/wsl/fedora.go
@@ -3,7 +3,6 @@ package wsl
 import (
 	"errors"
 	"fmt"
-	"github.com/sirupsen/logrus"
 	"io"
 	"net/http"
 	"net/url"
@@ -13,6 +12,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/sirupsen/logrus"
+
 	"github.com/containers/podman/v5/pkg/machine"
 	"github.com/containers/podman/v5/pkg/machine/define"
 )
diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go
index e92b39c868..01eb0df2de 100644
--- a/pkg/machine/wsl/machine.go
+++ b/pkg/machine/wsl/machine.go
@@ -56,10 +56,11 @@ func getConfigPathExt(name string, extension string) (string, error) {
 // TODO like provisionWSL, i think this needs to be pushed to use common
 // paths and types where possible
 func unprovisionWSL(mc *vmconfigs.MachineConfig) error {
-	if err := terminateDist(mc.Name); err != nil {
+	dist := machine.ToDist(mc.Name)
+	if err := terminateDist(dist); err != nil {
 		logrus.Error(err)
 	}
-	if err := unregisterDist(mc.Name); err != nil {
+	if err := unregisterDist(dist); err != nil {
 		logrus.Error(err)
 	}
 
@@ -87,17 +88,18 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err
 		return "", fmt.Errorf("could not create wsldist directory: %w", err)
 	}
 
+	dist := machine.ToDist(name)
 	fmt.Println(prompt)
-	if err = runCmdPassThrough("wsl", "--import", name, distTarget, imagePath, "--version", "2"); err != nil {
+	if err = runCmdPassThrough("wsl", "--import", dist, distTarget, imagePath, "--version", "2"); err != nil {
 		return "", fmt.Errorf("the WSL import of guest OS failed: %w", err)
 	}
 
 	// Fixes newuidmap
-	if err = wslInvoke(name, "rpm", "--restore", "shadow-utils"); err != nil {
+	if err = wslInvoke(dist, "rpm", "--restore", "shadow-utils"); err != nil {
 		return "", fmt.Errorf("package permissions restore of shadow-utils on guest OS failed: %w", err)
 	}
 
-	return name, nil
+	return dist, nil
 }
 
 func createKeys(mc *vmconfigs.MachineConfig, dist string) error {
@@ -139,21 +141,21 @@ func configureSystem(mc *vmconfigs.MachineConfig, dist string) error {
 		return fmt.Errorf("could not configure SSH port for guest OS: %w", err)
 	}
 
-	if err := wslPipe(withUser(configServices, user), mc.Name, "sh"); err != nil {
+	if err := wslPipe(withUser(configServices, user), dist, "sh"); err != nil {
 		return fmt.Errorf("could not configure systemd settings for guest OS: %w", err)
 	}
 
-	if err := wslPipe(sudoers, mc.Name, "sh", "-c", "cat >> /etc/sudoers"); err != nil {
+	if err := wslPipe(sudoers, dist, "sh", "-c", "cat >> /etc/sudoers"); err != nil {
 		return fmt.Errorf("could not add wheel to sudoers: %w", err)
 	}
 
-	if err := wslPipe(overrideSysusers, mc.Name, "sh", "-c",
+	if err := wslPipe(overrideSysusers, dist, "sh", "-c",
 		"cat > /etc/systemd/system/systemd-sysusers.service.d/override.conf"); err != nil {
 		return fmt.Errorf("could not generate systemd-sysusers override for guest OS: %w", err)
 	}
 
 	lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user)
-	if err := wslPipe(lingerService, mc.Name, "sh", "-c", lingerCmd); err != nil {
+	if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil {
 		return fmt.Errorf("could not generate linger service for guest OS: %w", err)
 	}
 
@@ -714,14 +716,15 @@ func unregisterDist(dist string) error {
 }
 
 func isRunning(name string) (bool, error) {
-	wsl, err := isWSLRunning(name)
+	dist := machine.ToDist(name)
+	wsl, err := isWSLRunning(dist)
 	if err != nil {
 		return false, err
 	}
 
 	sysd := false
 	if wsl {
-		sysd, err = isSystemdRunning(name)
+		sysd, err = isSystemdRunning(dist)
 
 		if err != nil {
 			return false, err
@@ -746,10 +749,11 @@ func getDiskSize(name string) uint64 {
 }
 
 func getCPUs(name string) (uint64, error) {
-	if run, _ := isWSLRunning(name); !run {
+	dist := machine.ToDist(name)
+	if run, _ := isWSLRunning(dist); !run {
 		return 0, nil
 	}
-	cmd := exec.Command("wsl", "-u", "root", "-d", name, "nproc")
+	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "nproc")
 	out, err := cmd.StdoutPipe()
 	if err != nil {
 		return 0, err
@@ -769,10 +773,11 @@ func getCPUs(name string) (uint64, error) {
 }
 
 func getMem(name string) (uint64, error) {
-	if run, _ := isWSLRunning(name); !run {
+	dist := machine.ToDist(name)
+	if run, _ := isWSLRunning(dist); !run {
 		return 0, nil
 	}
-	cmd := exec.Command("wsl", "-u", "root", "-d", name, "cat", "/proc/meminfo")
+	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "cat", "/proc/meminfo")
 	out, err := cmd.StdoutPipe()
 	if err != nil {
 		return 0, err
diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go
index 25347678cf..a3693d9c99 100644
--- a/pkg/machine/wsl/stubber.go
+++ b/pkg/machine/wsl/stubber.go
@@ -120,7 +120,7 @@ func (w WSLStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error,
 	// below if we wanted to hard error on the wsl unregister
 	// of the vm
 	wslRemoveFunc := func() error {
-		if err := runCmdPassThrough("wsl", "--unregister", mc.Name); err != nil {
+		if err := runCmdPassThrough("wsl", "--unregister", machine.ToDist(mc.Name)); err != nil {
 			logrus.Error(err)
 		}
 		return machine.ReleaseMachinePort(mc.SSH.Port)
@@ -178,24 +178,19 @@ func (w WSLStubber) SetProviderAttrs(mc *vmconfigs.MachineConfig, opts define.Se
 		return errors.New("changing disk size not supported for WSL machines")
 	}
 
-	// TODO This needs to be plumbed in for set as well
-	//if opts.UserModeNetworking != nil && *opts.UserModeNetworking != v.UserModeNetworking {
-	//	update := true
-	//
-	//	if v.isRunning() {
-	//		update = false
-	//		setErrors = append(setErrors, fmt.Errorf("user-mode networking can only be changed when the machine is not running"))
-	//	} else {
-	//		dist := toDist(v.Name)
-	//		if err := changeDistUserModeNetworking(dist, v.RemoteUsername, v.ImagePath, *opts.UserModeNetworking); err != nil {
-	//			update = false
-	//			setErrors = append(setErrors, err)
-	//		}
-	//	}
-	//
-	//	if update {
-	//		v.UserModeNetworking = *opts.UserModeNetworking
-	//	}
+	if opts.UserModeNetworking != nil && mc.WSLHypervisor.UserModeNetworking != *opts.UserModeNetworking {
+		if running, _ := isRunning(mc.Name); running {
+			return errors.New("user-mode networking can only be changed when the machine is not running")
+		}
+
+		dist := machine.ToDist(mc.Name)
+		if err := changeDistUserModeNetworking(dist, mc.SSH.RemoteUsername, mc.ImagePath.GetPath(), *opts.UserModeNetworking); err != nil {
+			return fmt.Errorf("failure changing state of user-mode networking setting", err)
+		}
+
+		mc.WSLHypervisor.UserModeNetworking = *opts.UserModeNetworking
+	}
+
 	return nil
 }
 
@@ -211,26 +206,34 @@ func (w WSLStubber) UserModeNetworkEnabled(mc *vmconfigs.MachineConfig) bool {
 	return mc.WSLHypervisor.UserModeNetworking
 }
 
-func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig) error {
-	if mc.WSLHypervisor.UserModeNetworking {
-		winProxyOpts := machine.WinProxyOpts{
-			Name:           mc.Name,
-			IdentityPath:   mc.SSH.IdentityPath,
-			Port:           mc.SSH.Port,
-			RemoteUsername: mc.SSH.RemoteUsername,
-			Rootful:        mc.HostUser.Rootful,
-			VMType:         w.VMType(),
-		}
-		machine.LaunchWinProxy(winProxyOpts, false)
+func (w WSLStubber) UseProviderNetworkSetup() bool {
+	return true
+}
+
+func (w WSLStubber) RequireExclusiveActive() bool {
+	return false
+}
+
+func (w WSLStubber) PostStartNetworking(mc *vmconfigs.MachineConfig, noInfo bool) error {
+	winProxyOpts := machine.WinProxyOpts{
+		Name:           mc.Name,
+		IdentityPath:   mc.SSH.IdentityPath,
+		Port:           mc.SSH.Port,
+		RemoteUsername: mc.SSH.RemoteUsername,
+		Rootful:        mc.HostUser.Rootful,
+		VMType:         w.VMType(),
 	}
+	machine.LaunchWinProxy(winProxyOpts, noInfo)
+
 	return nil
 }
 
 func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() error, error) {
 	useProxy := setupWslProxyEnv()
+	dist := machine.ToDist(mc.Name)
 
 	// TODO Quiet is hard set to false: follow up
-	if err := configureProxy(mc.Name, useProxy, false); err != nil {
+	if err := configureProxy(dist, useProxy, false); err != nil {
 		return nil, nil, err
 	}
 
@@ -243,25 +246,11 @@ func (w WSLStubber) StartVM(mc *vmconfigs.MachineConfig) (func() error, func() e
 	//	}
 	// }
 
-	err := wslInvoke(mc.Name, "/root/bootstrap")
+	err := wslInvoke(dist, "/root/bootstrap")
 	if err != nil {
 		err = fmt.Errorf("the WSL bootstrap script failed: %w", err)
 	}
 
-	// TODO we dont show this for any other provider. perhaps we should ? and if
-	// so, we need to move it up the stack
-	//if !v.Rootful && !opts.NoInfo {
-	//	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)
-	//}
-
 	readyFunc := func() error {
 		return nil
 	}
@@ -284,11 +273,16 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
 	var (
 		err error
 	)
-	// by this time, state has been verified to be running and a request
-	// to stop is fair game
 	mc.Lock()
 	defer mc.Unlock()
 
+	// recheck after lock
+	if running, err := isRunning(mc.Name); !running {
+		return err
+	}
+
+	dist := machine.ToDist(mc.Name)
+
 	// Stop user-mode networking if enabled
 	if err := stopUserModeNetworking(mc); err != nil {
 		fmt.Fprintf(os.Stderr, "Could not cleanly stop user-mode networking: %s\n", err.Error())
@@ -298,13 +292,13 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
 		fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error())
 	}
 
-	cmd := exec.Command("wsl", "-u", "root", "-d", mc.Name, "sh")
+	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "sh")
 	cmd.Stdin = strings.NewReader(waitTerm)
 	if err = cmd.Start(); err != nil {
 		return fmt.Errorf("executing wait command: %w", err)
 	}
 
-	exitCmd := exec.Command("wsl", "-u", "root", "-d", mc.Name, "/usr/local/bin/enterns", "systemctl", "exit", "0")
+	exitCmd := exec.Command("wsl", "-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0")
 	if err = exitCmd.Run(); err != nil {
 		return fmt.Errorf("stopping sysd: %w", err)
 	}
@@ -313,7 +307,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
 		return err
 	}
 
-	return terminateDist(mc.Name)
+	return terminateDist(dist)
 }
 
 func (w WSLStubber) StopHostNetworking(mc *vmconfigs.MachineConfig, vmType define.VMType) error {
diff --git a/pkg/machine/wsl/usermodenet.go b/pkg/machine/wsl/usermodenet.go
index 320bdda418..1c7ef4eeda 100644
--- a/pkg/machine/wsl/usermodenet.go
+++ b/pkg/machine/wsl/usermodenet.go
@@ -99,7 +99,7 @@ func startUserModeNetworking(mc *vmconfigs.MachineConfig) error {
 		}
 	}
 
-	if err := createUserModeResolvConf(mc.Name); err != nil {
+	if err := createUserModeResolvConf(machine.ToDist(mc.Name)); err != nil {
 		return err
 	}
 
@@ -255,7 +255,7 @@ func addUserModeNetEntry(mc *vmconfigs.MachineConfig) error {
 		return err
 	}
 
-	path := filepath.Join(entriesDir, mc.Name)
+	path := filepath.Join(entriesDir, machine.ToDist(mc.Name))
 	file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
 	if err != nil {
 		return fmt.Errorf("could not add user-mode networking registration: %w", err)
@@ -270,7 +270,7 @@ func removeUserModeNetEntry(name string) error {
 		return err
 	}
 
-	path := filepath.Join(entriesDir, name)
+	path := filepath.Join(entriesDir, machine.ToDist(name))
 	return os.Remove(path)
 }
 
diff --git a/vendor/github.com/containers/winquit/LICENSE b/vendor/github.com/containers/winquit/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/vendor/github.com/containers/winquit/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go
new file mode 100644
index 0000000000..2d7e48a4a4
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/channels_windows.go
@@ -0,0 +1,50 @@
+package winquit
+
+import (
+	"os"
+	"syscall"
+)
+
+type baseChannelType interface {
+	getKey() any
+	notifyNonBlocking()
+	notifyBlocking()
+}
+
+type boolChannelType struct {
+	channel chan bool
+}
+
+func (b *boolChannelType) getKey() any {
+	return b.channel
+}
+
+func (b *boolChannelType) notifyNonBlocking() {
+	select {
+	case b.channel <- true:
+	default:
+	}
+}
+
+func (s *boolChannelType) notifyBlocking() {
+	s.channel <- true
+}
+
+type sigChannelType struct {
+	channel chan os.Signal
+}
+
+func (s *sigChannelType) getKey() any {
+	return s.channel
+}
+
+func (s *sigChannelType) notifyNonBlocking() {
+	select {
+	case s.channel <- syscall.SIGTERM:
+	default:
+	}
+}
+
+func (s *sigChannelType) notifyBlocking() {
+	s.channel <- syscall.SIGTERM
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client.go b/vendor/github.com/containers/winquit/pkg/winquit/client.go
new file mode 100644
index 0000000000..95b813c0ba
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/client.go
@@ -0,0 +1,31 @@
+package winquit
+
+import (
+	"time"
+)
+
+// RequestQuit sends a Windows quit notification to the specified process id.
+// Since communication is performed over the Win32 GUI messaging facilities,
+// console applications may not respond, as they require special handling to do
+// so. Additionally incorrectly written or buggy GUI applications may not listen
+// or respond appropriately to the event.
+//
+// All applications, console or GUI, which use the notification mechanisms
+// provided by this package (NotifyOnQuit, SimulateSigTermOnQuit) will react
+// appropriately to the event sent by RequestQuit.
+//
+// Callers must have appropriate security permissions, otherwise an error will
+// be returned. See the notes in the package documentation for more details.
+func RequestQuit(pid int) error {
+	return requestQuit(pid)
+}
+
+// QuitProcess first sends a Windows quit notification to the specified process id,
+// and waits, up the amount of time passed in the waitNicely argument, for it to
+// exit. If the process does not exit in time, it is forcefully terminated.
+//
+// Callers must have appropriate security permissions, otherwise an error will
+// be returned. See the notes in the package documentation for more details.
+func QuitProcess(pid int, waitNicely time.Duration) error {
+	return quitProcess(pid, waitNicely)
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go
new file mode 100644
index 0000000000..8aa5ed06e1
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/client_unsupported.go
@@ -0,0 +1,17 @@
+//go:build !windows
+// +build !windows
+
+package winquit
+
+import (
+	"fmt"
+	"time"
+)
+
+func requestQuit(pid int) error {
+	return fmt.Errorf("not implemented on non-Windows")
+}
+
+func quitProcess(pid int, waitNicely time.Duration) error {
+	return fmt.Errorf("not implemented on non-Windows")
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go
new file mode 100644
index 0000000000..e03919b4cc
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/client_windows.go
@@ -0,0 +1,47 @@
+package winquit
+
+import (
+	"os"
+	"time"
+
+	"github.com/containers/winquit/pkg/winquit/win32"
+	"github.com/sirupsen/logrus"
+)
+
+func requestQuit(pid int) error {
+	threads, err := win32.GetProcThreads(uint32(pid))
+	if err != nil {
+		return err
+	}
+
+	for _, thread := range threads {
+		logrus.Debugf("Closing windows on thread %d", thread)
+		win32.CloseThreadWindows(uint32(thread))
+	}
+
+	return nil
+}
+
+func quitProcess(pid int, waitNicely time.Duration) error {
+	_ = RequestQuit(pid)
+
+	proc, err := os.FindProcess(pid)
+	if err != nil {
+		return nil
+	}
+
+	done := make(chan bool)
+
+	go func() {
+		proc.Wait()
+		done <- true
+	}()
+
+	select {
+	case <-done:
+		return nil
+	case <-time.After(waitNicely):
+	}
+
+	return proc.Kill()
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/doc.go b/vendor/github.com/containers/winquit/pkg/winquit/doc.go
new file mode 100644
index 0000000000..079794cb4e
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/doc.go
@@ -0,0 +1,135 @@
+// Package winquit supports graceful shutdown of Windows applications through
+// the sending and receiving of Windows quit events on Win32 message queues.
+// This allows golang applications to implement behavior comparable to SIGTERM
+// signal handling on UNIX derived systems. Additionally, it supports the
+// graceful shutdown mechanism employed by Windows system tools, such as
+// taskkill. See the "How it works" section for more details.
+//
+// To aid application portability, and provide familiarity, the API follows a
+// similar convention and approach as the os.signal package. Additionally, the
+// SimulateSigTermOnQuit function supports reuse of the same underlying channel,
+// supporting the blending of os.signal and winquit together (a subset of
+// signals provided by os.signal are still relevant and desirable on Windows,
+// for example, break handling in console applications).
+//
+// # Simple server example
+//
+// The following example demonstrates usage of NotifyOnQuit() to wait for a
+// windows quit event before shutting down:
+//
+//	func server() {
+//	    fmt.Println("Starting server")
+//
+//	    // Create a channel, and register it
+//	    done := make(chan bool, 1)
+//	    winquit.NotifyOnQuit(done)
+//
+//	    // Wait until we receive a quit event
+//	    <-done
+//
+//	    fmt.Println("Shutting down")
+//	    // Perform cleanup tasks
+//	}
+//
+// # Blended signal example
+//
+// The following example demonstrates usage of SimulateSigTermOnQuit() in
+// concert with signal.Notify():
+//
+//	func server() {
+//	    fmt.Println("Starting server")
+//
+//	    // Create a channel, and register it
+//	    done := make(chan os.Signal, 1)
+//
+//	    // Wait on console interrupt events
+//	    signal.Notify(done, syscall.SIGINT)
+//
+//	    // Simulate SIGTERM when a quit occurs
+//	    winquit.SimulateSigTermOnQuit(done)
+//
+//	    // Wait until we receive a signal or quit event
+//	    <-done
+//
+//	    fmt.Println("Shutting down")
+//	    // Perform cleanup tasks
+//	}
+//
+// # Client example
+//
+// The following example demonstrates how an application can ask or
+// force other windows programs to quit:
+//
+//	func client() {
+//	     // Ask nicely for program "one" to quit. This request may not
+//	     // be honored if its a console application, or if the program
+//	     // is hung
+//	     if err := winquit.RequestQuit(pidOne); err != nil {
+//	         fmt.Printf("error sending quit request, %s", err.Error())
+//	     }
+//
+//	     // Force program "two" to quit, but give it 20 seconds to
+//	     // perform any cleanup tasks and quit on it's own
+//	     timeout := time.Second * 20
+//	     if err := winquit.QuitProcess(pidTwo, timeout); err != nil {
+//	         fmt.Printf("error killing process, %s", err.Error())
+//	     }
+//	}
+//
+// # How it works
+//
+// Windows GUI applications consist of multiple components (and windows) which
+// intercommunicate with events over per-thread message queues and/or direct
+// event handoff to window procedures for cross-thread communication.
+// Additionally, GUI applications can use the same mechanism to communicate with
+// windows and threads owned by other applications, including common desktop
+// components.
+//
+// winquit utilizes this mechanism by creating a standard win32 message loop
+// thread and registering a non-visible window to relay a quit message (WM_QUIT)
+// in the event of a window close event. WM_CLOSE is sent by Windows in response
+// to certain system events, or by other requesting applications. For example,
+// the system provided taskkill.exe (similar to the kill command on Unix), works
+// by iterating all windows on the system, and sending a WM_CLOSE when the
+// process owner matches the specified pid. Note that, unlike UNIX/X11 style
+// systems, on Windows the graphical APIs are built-in and accessible to all
+// win32 applications, including console based applications. Therefore, the APIs
+// provided by winquit *do not* require compilation as a windowsgui app to
+// effectively use them.
+//
+// winquit also provides APIs to trigger a quit of another process using a
+// WM_CLOSE event, although in a more efficient manner than taskkill.exe. It
+// instead captures a thread snapshot of the target process (effectively a
+// memory read on Windows), and enumerates each thread's associated Windows, and
+// sending the event to each. In addition to supporting a graceful close of any
+// Windows application, which may have multiple message loops, this approach
+// also obviates the need for cumbersome approaches to lock code to the main
+// thread of the application. The message loop used by winquit does not care
+// which thread the golang runtime internally designates. Note that winquit
+// purposefully relays through a thread's windows as opposed to posting directly
+// to each thread's message queue, since the former is more likely to be
+// expected by an application, and it ensures all window procedures have an
+// opportunity to perform cleanup work not associated with the thread's message
+// loop.
+//
+// # Limitations
+//
+// This API is only implemented on Windows platforms. Non-operational stubs
+// are provided for compilation purposes.
+//
+// In addition to requiring appropriate security permissions (typically a user
+// can only send events to other applications ran by the same user), Windows
+// also restricts inter-app messaging operations to programs running in the same
+// user logon session. While logons migrate between RDP and console sessions,
+// non-graphical logins (e.g sshd) typically create a logon per connection. For
+// this reason, tools like taskkill and winquit are normally disallowed from
+// crossing this boundary. Therefore, a user will not be able to gracefully stop
+// applications between ssh/winrm sessions, and in between ssh and graphical
+// logons. However, the typical user use-case of logging into Windows and
+// running multiple applications and terminals will work fine. Additionally,
+// multiple back-grounded processes in the same ssh session will be able to
+// communicate. Finally, it's possible to bypass this limitation by executing
+// code under the system user using the SeTcbPrivilege. The psexec tool does
+// exactly this, and can additionally be used as a workaround to this
+// limitation.
+package winquit
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server.go b/vendor/github.com/containers/winquit/pkg/winquit/server.go
new file mode 100644
index 0000000000..e50ad1a96b
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/server.go
@@ -0,0 +1,45 @@
+package winquit
+
+import (
+	"os"
+)
+
+// NotifyOnQuit relays a Windows quit notification to the boolean done channel.
+// This is a one-shot operation (will only be delivered once), however multiple
+// channels may be registered. Each registered channel is sent one copy of the
+// same one-shot value.
+//
+// This function is a no-op on non-Windows platforms. While the call will
+// succeed, no notifications will be delivered to the passed channel. Each
+// channel will only ever receive a "true" value.
+//
+// It is recommended that registered channels establish a buffer of 1, since
+// values are sent non-blocking. Blocking redelivery may be attempted to reduce
+// the chance of bugs; however, it should not be relied upon.
+//
+// If this function is called after a Windows quit notification has occurred, it
+// will immediately deliver a "true" value.
+func NotifyOnQuit(done chan bool) {
+	notifyOnQuit(done)
+}
+
+// SimulateSigTermOnQuit relays a Windows quit notification following the same
+// semantics as NotifyOnQuit; however, instead of a boolean message value, this
+// function will send a SIGTERM signal to the passed channel.
+//
+// This function allows for the reuse of the same underlying channel used with
+// in a separate os.signal.Notify method call.
+func SimulateSigTermOnQuit(handler chan os.Signal) {
+	simulateSigTermOnQuit(handler)
+}
+
+// Returns the thread id of the message loop thread created by winquit, or "0"
+// if one is not running. The latter indicates a mistake, as this function
+// should only be called after a call to one of the _OnQuit functions.
+//
+// In most cases this method should not be necessary, as RequestQuit and
+// QuitProcess do not require it. It is primarily provided to enable legacy
+// patterns that serialize the thread id for later direct signaling.
+func GetCurrentMessageLoopThreadId() uint32 {
+	return getCurrentMessageLoopThreadId()
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go
new file mode 100644
index 0000000000..e5013c088e
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/server_unsupported.go
@@ -0,0 +1,18 @@
+//go:build !windows
+// +build !windows
+
+package winquit
+
+import (
+	"os"
+)
+
+func notifyOnQuit(done chan bool) {
+}
+
+func simulateSigTermOnQuit(handler chan os.Signal) {
+}
+
+func getCurrentMessageLoopThreadId() uint32 {
+	return 0
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go b/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go
new file mode 100644
index 0000000000..4309319bf4
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/server_windows.go
@@ -0,0 +1,147 @@
+package winquit
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"sync"
+	"syscall"
+
+	"github.com/containers/winquit/pkg/winquit/win32"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/sys/windows"
+)
+
+type receiversType struct {
+	sync.Mutex
+
+	result   bool
+	channels map[any]baseChannelType
+}
+
+var (
+	receivers *receiversType = &receiversType{
+		channels: make(map[any]baseChannelType),
+	}
+
+	loopInit sync.Once
+	loopTid  uint32
+)
+
+func (r *receiversType) add(channel baseChannelType) {
+	r.Lock()
+	defer r.Unlock()
+
+	if _, ok := r.channels[channel.getKey()]; ok {
+		return
+	}
+
+	if r.result {
+		go func() {
+			channel.notifyBlocking()
+		}()
+		return
+	}
+
+	r.channels[channel.getKey()] = channel
+}
+
+func (r *receiversType) notifyAll() {
+	r.Lock()
+	defer r.Unlock()
+	r.result = true
+	for _, channel := range r.channels {
+		channel.notifyNonBlocking()
+		delete(r.channels, channel.getKey())
+	}
+	for _, channel := range r.channels {
+		channel.notifyBlocking()
+		delete(r.channels, channel)
+	}
+}
+
+func initLoop() {
+	loopInit.Do(func() {
+		go messageLoop()
+	})
+}
+
+func notifyOnQuit(done chan bool) {
+	receivers.add(&boolChannelType{done})
+	initLoop()
+}
+
+func simulateSigTermOnQuit(handler chan os.Signal) {
+	receivers.add(&sigChannelType{handler})
+	initLoop()
+}
+
+func getCurrentMessageLoopThreadId() uint32 {
+	return loopTid
+}
+
+func messageLoop() {
+	runtime.LockOSThread()
+	defer runtime.UnlockOSThread()
+
+	loopTid = windows.GetCurrentThreadId()
+	registerDummyWindow()
+
+	logrus.Debug("Entering loop for quit")
+	for {
+		ret, msg, err := win32.GetMessage(0, 0, 0)
+		if err != nil {
+			logrus.Debugf("Error receiving win32 message, %s", err.Error())
+			continue
+		}
+		if ret == 0 {
+			logrus.Debug("Received QUIT notification")
+			receivers.notifyAll()
+
+			return
+		}
+		logrus.Debugf("Unhandled message: %d", msg.Message)
+		win32.TranslateMessage(msg)
+		win32.DispatchMessage(msg)
+	}
+}
+
+func getAppName() (string, error) {
+	exeName, err := os.Executable()
+	if err != nil {
+		return "", err
+	}
+	suffix := filepath.Ext(exeName)
+	return strings.TrimSuffix(filepath.Base(exeName), suffix), nil
+}
+
+func registerDummyWindow() error {
+	var app syscall.Handle
+	var err error
+
+	app, err = win32.GetModuleHandle("")
+	if err != nil {
+		return err
+	}
+
+	appName, err := getAppName()
+	if err != nil {
+		return err
+	}
+
+	className := appName + "-rclass"
+	winName := appName + "-root"
+
+	_, err = win32.RegisterDummyWinClass(className, app)
+	if err != nil {
+		return err
+	}
+
+	_, err = win32.CreateDummyWindow(winName, className, app)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go
new file mode 100644
index 0000000000..dbb13657e4
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/common.go
@@ -0,0 +1,17 @@
+//go:build windows
+// +build windows
+
+package win32
+
+import (
+	"syscall"
+)
+
+const (
+	ERROR_NO_MORE_ITEMS = 259
+)
+
+var (
+	kernel32 = syscall.NewLazyDLL("kernel32.dll")
+	user32   = syscall.NewLazyDLL("user32.dll")
+)
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go
new file mode 100644
index 0000000000..705e6b3129
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/common_unsupported.go
@@ -0,0 +1,4 @@
+//go:build !windows
+// +build !windows
+
+package win32
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go
new file mode 100644
index 0000000000..f6063a5e12
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/msg.go
@@ -0,0 +1,87 @@
+//go:build windows
+// +build windows
+
+package win32
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+type MSG struct {
+	HWnd    uintptr
+	Message uint32
+	WParam  uintptr
+	LParam  uintptr
+	Time    uint32
+	Pt      struct{ X, Y int32 }
+}
+
+const (
+	WM_QUIT    = 0x12
+	WM_DESTROY = 0x02
+	WM_CLOSE   = 0x10
+)
+
+var (
+	postQuitMessage      = user32.NewProc("PostQuitMessage")
+	procGetMessage       = user32.NewProc("GetMessageW")
+	procTranslateMessage = user32.NewProc("TranslateMessage")
+	procDispatchMessage  = user32.NewProc("DispatchMessageW")
+	procSendMessage      = user32.NewProc("SendMessageW")
+)
+
+func TranslateMessage(msg *MSG) bool {
+	ret, _, _ :=
+		procTranslateMessage.Call( //   BOOL TranslateMessage()
+			uintptr(unsafe.Pointer(msg)), // [in] const MSG *lpMsg
+		)
+
+	return ret != 0
+
+}
+
+func DispatchMessage(msg *MSG) uintptr {
+	ret, _, _ :=
+		procDispatchMessage.Call( //         LRESULT DispatchMessage()
+			uintptr(unsafe.Pointer(msg)), //          [in] const MSG *lpMsg
+		)
+
+	return ret
+}
+
+func SendMessage(handle syscall.Handle, message uint, wparm uintptr, lparam uintptr) uintptr {
+	ret, _, _ :=
+		procSendMessage.Call( // LRESULT SendMessage()
+			uintptr(handle),  //         [in] HWND   hWnd
+			uintptr(message), //         [in] UINT   Msg
+			wparm,            //         [in] WPARAM wParam
+			lparam,           //         [in] LPARAM lParam
+		)
+
+	return ret
+}
+
+func PostQuitMessage(code int) {
+	_, _, _ =
+		postQuitMessage.Call( // void PostQuitMessage()
+			uintptr(code), //         [in] int nExitCode
+		)
+}
+
+func GetMessage(handle syscall.Handle, int, max int) (int32, *MSG, error) {
+	var msg MSG
+	ret, _, err :=
+		procGetMessage.Call( //            // BOOL GetMessage()
+			uintptr(unsafe.Pointer(&msg)), //      [out]          LPMSG lpMsg,
+			uintptr(handle),               //      [in, optional] HWND  hWnd,
+			0,                             //      [in]           UINT  wMsgFilterMin,
+			0,                             //      [in]           UINT  wMsgFilterMax
+		)
+
+	if int32(ret) == -1 {
+		return 0, nil, err
+	}
+
+	return int32(ret), &msg, nil
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go
new file mode 100644
index 0000000000..6f7ccfc5a5
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/proc.go
@@ -0,0 +1,59 @@
+//go:build windows
+// +build windows
+
+package win32
+
+import (
+	"fmt"
+	"syscall"
+)
+
+const (
+	MAXIMUM_ALLOWED = 0x02000000
+)
+
+var (
+	procOpenProcess     = kernel32.NewProc("OpenProcess")
+	procCloseHandle     = kernel32.NewProc("CloseHandle")
+	procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
+)
+
+func OpenProcess(pid uint32) (syscall.Handle, error) {
+	ret, _, err :=
+		procOpenProcess.Call( // HANDLE OpenProcess()
+			MAXIMUM_ALLOWED, //         [in] DWORD dwDesiredAccess,
+			0,               //         [in] BOOL  bInheritHandle,
+			uintptr(pid),    //         [in] DWORD dwProcessId
+		)
+
+	if ret == 0 {
+		return 0, err
+	}
+
+	return syscall.Handle(ret), nil
+}
+
+func CloseHandle(handle syscall.Handle) error {
+	ret, _, err :=
+		procCloseHandle.Call( // BOOL CloseHandle()
+			uintptr(handle), //       [in] HANDLE hObject
+		)
+	if ret != 0 {
+		return fmt.Errorf("error closing handle: %w", err)
+	}
+
+	return nil
+}
+
+func GetProcThreads(pid uint32) ([]uint, error) {
+	process, err := OpenProcess(pid)
+	if err != nil {
+		return nil, err
+	}
+
+	defer func() {
+		_ = CloseHandle(process)
+	}()
+
+	return GetProcThreadIds(process)
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go
new file mode 100644
index 0000000000..bd03959a5f
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/pss.go
@@ -0,0 +1,160 @@
+//go:build windows
+// +build windows
+
+package win32
+
+import (
+	"fmt"
+	"syscall"
+	"unsafe"
+)
+
+type PSS_THREAD_ENTRY struct {
+	ExitStatus               uint32
+	TebBaseAddress           uintptr
+	ProcessId                uint32
+	ThreadId                 uint32
+	AffinityMask             uintptr
+	Priority                 int32
+	BasePriority             int32
+	LastSyscallFirstArgument uintptr
+	LastSyscallNumber        uint16
+	CreateTime               uint64
+	ExitTime                 uint64
+	KernelTime               uint64
+	UserTime                 uint64
+	Win32StartAddress        uintptr
+	CaptureTime              uint64
+	Flags                    uint32
+	SuspendCount             uint16
+	SizeOfContextRecord      uint16
+	ContextRecord            uintptr
+}
+
+const (
+	PSS_CAPTURE_THREADS          = 0x00000080
+	PSS_WALK_THREADS             = 3
+	PSS_QUERY_THREAD_INFORMATION = 5
+)
+
+var (
+	procPssCaptureSnapshot  = kernel32.NewProc("PssCaptureSnapshot")
+	procPssFreeSnapshot     = kernel32.NewProc("PssFreeSnapshot")
+	procPssWalkMarkerCreate = kernel32.NewProc("PssWalkMarkerCreate")
+	procPssWalkMarkerFree   = kernel32.NewProc("PssWalkMarkerFree")
+	procPssWalkSnapshot     = kernel32.NewProc("PssWalkSnapshot")
+)
+
+func PssCaptureSnapshot(process syscall.Handle, flags int32, contextFlags int32) (syscall.Handle, error) {
+	var snapshot syscall.Handle
+	ret, _, err :=
+		procPssCaptureSnapshot.Call( //       DWORD PssCaptureSnapshot()
+			uintptr(process),                   //  [in]           HANDLE            ProcessHandle,
+			uintptr(flags),                     //  [in]           PSS_CAPTURE_FLAGS CaptureFlags,
+			uintptr(contextFlags),              //  [in, optional] DWORD             ThreadContextFlags,
+			uintptr(unsafe.Pointer(&snapshot)), //  [out]          HPSS              *SnapshotHandle
+		)
+
+	if ret != 0 {
+		return 0, err
+	}
+
+	return snapshot, nil
+}
+
+func PssFreeSnapshot(process syscall.Handle, snapshot syscall.Handle) error {
+	ret, _, _ :=
+		procPssFreeSnapshot.Call( // DWORD PssFreeSnapshot()
+			uintptr(process),  //          [in] HANDLE ProcessHandle,
+			uintptr(snapshot), //          [in] HPSS   SnapshotHandle
+		)
+	if ret != 0 {
+		return fmt.Errorf("error freeing snapshot: %d", ret)
+	}
+
+	return nil
+}
+
+func PssWalkMarkerCreate() (syscall.Handle, error) {
+	var walkptr uintptr
+
+	ret, _, _ :=
+		procPssWalkMarkerCreate.Call( //       // DWORD PssWalkMarkerCreate()
+			0,                                 //       [in, optional] PSS_ALLOCATOR const *Allocator
+			uintptr(unsafe.Pointer(&walkptr)), //       [out]          HPSSWALK            *WalkMarkerHandle
+		)
+	if ret != 0 {
+		return 0, fmt.Errorf("error creating process walker mark: %d", ret)
+	}
+
+	return syscall.Handle(walkptr), nil
+}
+
+func PssWalkMarkerFree(handle syscall.Handle) error {
+	ret, _, _ :=
+		procPssWalkMarkerFree.Call( // DWORD PssWalkMarkerFree()
+			uintptr(handle), //              [in] HPSSWALK WalkMarkerHandle
+		)
+	if ret != 0 {
+		return fmt.Errorf("error freeing process walker mark: %d", ret)
+	}
+
+	return nil
+}
+
+func PssWalkThreadSnapshot(snapshot syscall.Handle, marker syscall.Handle) (*PSS_THREAD_ENTRY, error) {
+	var thread PSS_THREAD_ENTRY
+	ret, _, err :=
+		procPssWalkSnapshot.Call( //          // DWORD PssWalkSnapshot()
+			uintptr(snapshot),                //       [in]  HPSS                       SnapshotHandle,
+			PSS_WALK_THREADS,                 //       [in]  PSS_WALK_INFORMATION_CLASS InformationClass,
+			uintptr(marker),                  //       [in]  HPSSWALK                   WalkMarkerHandle,
+			uintptr(unsafe.Pointer(&thread)), //       [out] void                       *Buffer,
+			unsafe.Sizeof(thread),            //       [in]  DWORD                      BufferLength
+		)
+
+	if ret == ERROR_NO_MORE_ITEMS {
+		return nil, nil
+	}
+
+	if ret != 0 {
+		return nil, fmt.Errorf("error waling thread snapshot: %d (%w)", ret, err)
+	}
+
+	return &thread, nil
+}
+
+func GetProcThreadIds(process syscall.Handle) ([]uint, error) {
+	snapshot, err := PssCaptureSnapshot(process, PSS_CAPTURE_THREADS, 0)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		_ = PssFreeSnapshot(process, snapshot)
+	}()
+
+	marker, err := PssWalkMarkerCreate()
+	if err != nil {
+		return nil, err
+	}
+
+	defer func() {
+		_ = PssWalkMarkerFree(marker)
+	}()
+
+	var threads []uint
+
+	for {
+		thread, err := PssWalkThreadSnapshot(snapshot, marker)
+		if err != nil {
+			return nil, err
+		}
+		if thread == nil {
+			break
+		}
+
+		threads = append(threads, uint(thread.ThreadId))
+	}
+
+	return threads, nil
+}
diff --git a/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go b/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go
new file mode 100644
index 0000000000..b243b0be8d
--- /dev/null
+++ b/vendor/github.com/containers/winquit/pkg/winquit/win32/win.go
@@ -0,0 +1,162 @@
+//go:build windows
+// +build windows
+
+package win32
+
+import (
+	"fmt"
+	"syscall"
+	"unsafe"
+)
+
+type WNDCLASSEX struct {
+	cbSize        uint32
+	style         uint32
+	lpfnWndProc   uintptr
+	cbClsExtra    int32
+	cbWndExtra    int32
+	hInstance     syscall.Handle
+	hIcon         syscall.Handle
+	hCursor       syscall.Handle
+	hbrBackground syscall.Handle
+	menuName      *uint16
+	className     *uint16
+	hIconSm       syscall.Handle
+}
+
+const (
+	COLOR_WINDOW  = 0x05
+	CW_USEDEFAULT = ^0x7fffffff
+)
+
+var (
+	procEnumThreadWindows = user32.NewProc("EnumThreadWindows")
+	procRegisterClassEx   = user32.NewProc("RegisterClassExW")
+	procCreateWindowEx    = user32.NewProc("CreateWindowExW")
+	procDefWinProc        = user32.NewProc("DefWindowProcW")
+
+	callbackEnumThreadWindows = syscall.NewCallback(wndProcCloseWindow)
+)
+
+func DefWindowProc(hWnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) int32 {
+
+	ret, _, _ :=
+		procDefWinProc.Call( // LRESULT DefWindowProcW()
+			uintptr(hWnd), //          [in] HWND   hWnd,
+			uintptr(msg),  //          [in] UINT   Msg,
+			wParam,        //          [in] WPARAM wParam,
+			lParam,        //          [in] LPARAM lParam
+		)
+	return int32(ret)
+}
+
+func GetModuleHandle(name string) (syscall.Handle, error) {
+	var name16 *uint16
+	var err error
+
+	if len(name) > 0 {
+		if name16, err = syscall.UTF16PtrFromString(name); err != nil {
+			return 0, err
+		}
+	}
+
+	ret, _, err :=
+		procGetModuleHandle.Call( //     HMODULE GetModuleHandleW()
+			uintptr(unsafe.Pointer(name16)), //  [in, optional] LPCWSTR lpModuleName
+		)
+	if ret == 0 {
+		return 0, fmt.Errorf("could not obtain module handle: %w", err)
+	}
+
+	return syscall.Handle(ret), nil
+}
+
+func RegisterClassEx(class *WNDCLASSEX) (uint16, error) {
+
+	ret, _, err :=
+		procRegisterClassEx.Call( //      ATOM RegisterClassExW()
+			uintptr(unsafe.Pointer(class)), // [in] const WNDCLASSEXW *unnamedParam1
+		)
+	if ret == 0 {
+		return 0, fmt.Errorf("could not register window class: %w", err)
+	}
+
+	return uint16(ret), nil
+}
+
+func wndProc(hWnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr {
+	switch msg {
+	case WM_DESTROY:
+		PostQuitMessage(0)
+		return 0
+	default:
+		return uintptr(DefWindowProc(hWnd, msg, wParam, lParam))
+	}
+}
+
+func CloseThreadWindows(threadId uint32) {
+	_, _, _ =
+		procEnumThreadWindows.Call( // // BOOL EnumThreadWindows()
+			uintptr(threadId),         //      [in] DWORD       dwThreadId,
+			callbackEnumThreadWindows, //      [in] WNDENUMPROC lpfn,
+			0,                         //      [in] LPARAM      lParam
+		)
+}
+
+func wndProcCloseWindow(hwnd uintptr, lparam uintptr) uintptr {
+	SendMessage(syscall.Handle(hwnd), WM_CLOSE, 0, 0)
+
+	return 1
+}
+
+func RegisterDummyWinClass(name string, appInstance syscall.Handle) (uint16, error) {
+	var class16 *uint16
+	var err error
+	if class16, err = syscall.UTF16PtrFromString(name); err != nil {
+		return 0, err
+	}
+
+	class := WNDCLASSEX{
+		className:   class16,
+		hInstance:   appInstance,
+		lpfnWndProc: syscall.NewCallback(wndProc),
+	}
+
+	class.cbSize = uint32(unsafe.Sizeof(class))
+
+	return RegisterClassEx(&class)
+}
+
+func CreateDummyWindow(name string, className string, appInstance syscall.Handle) (syscall.Handle, error) {
+	var name16, class16 *uint16
+	var err error
+
+	cwDefault := CW_USEDEFAULT
+
+	if name16, err = syscall.UTF16PtrFromString(name); err != nil {
+		return 0, err
+	}
+	if class16, err = syscall.UTF16PtrFromString(className); err != nil {
+		return 0, err
+	}
+	ret, _, err := procCreateWindowEx.Call( //HWND CreateWindowExW(
+		0,                                //       [in]           DWORD     dwExStyle,
+		uintptr(unsafe.Pointer(class16)), //       [in, optional] LPCWSTR   lpClassName,
+		uintptr(unsafe.Pointer(name16)),  //       [in, optional] LPCWSTR   lpWindowName,
+		0,                                //       [in]           DWORD     dwStyle,
+		uintptr(cwDefault),               //       [in]           int       X,
+		uintptr(cwDefault),               //       [in]           int       Y,
+		0,                                //       [in]           int       nWidth,
+		0,                                //       [in]           int       nHeight,
+		0,                                //       [in, optional] HWND      hWndParent,
+		0,                                //       [in, optional] HMENU     hMenu,
+		uintptr(appInstance),             //       [in, optional] HINSTANCE hInstance,
+		0,                                //       [in, optional] LPVOID    lpParam
+	)
+
+	if ret == 0 {
+		return 0, fmt.Errorf("could not create window: %w", err)
+	}
+
+	return syscall.Handle(ret), nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0e8e37f9d2..094cbf7814 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -401,6 +401,10 @@ github.com/containers/storage/pkg/tarlog
 github.com/containers/storage/pkg/truncindex
 github.com/containers/storage/pkg/unshare
 github.com/containers/storage/types
+# github.com/containers/winquit v1.1.0
+## explicit; go 1.19
+github.com/containers/winquit/pkg/winquit
+github.com/containers/winquit/pkg/winquit/win32
 # github.com/coreos/go-oidc/v3 v3.9.0
 ## explicit; go 1.19
 github.com/coreos/go-oidc/v3/oidc