Merge pull request #18303 from n1hility/user-mode

Add user-mode networking feature to Windows/WSL
This commit is contained in:
OpenShift Merge Robot
2023-04-26 16:01:48 -04:00
committed by GitHub
19 changed files with 743 additions and 175 deletions

View File

@ -199,7 +199,7 @@ endif
# include this lightweight helper binary. # include this lightweight helper binary.
# #
GV_GITURL=https://github.com/containers/gvisor-tap-vsock.git GV_GITURL=https://github.com/containers/gvisor-tap-vsock.git
GV_SHA=aab0ac9367fc5142f5857c36ac2352bcb3c60ab7 GV_SHA=407efb5dcdb0f4445935f7360535800b60447544
### ###
### Primary entry-point targets ### Primary entry-point targets

View File

@ -11,7 +11,7 @@ import (
"time" "time"
"unsafe" "unsafe"
"github.com/containers/podman/v4/pkg/machine/wsl" "github.com/containers/podman/v4/pkg/machine/wsl/wutil"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/eventlog"
) )
@ -49,7 +49,7 @@ func installWslKernel() error {
) )
backoff := 500 * time.Millisecond backoff := 500 * time.Millisecond
for i := 1; i < 6; i++ { for i := 1; i < 6; i++ {
err = wsl.SilentExec("wsl", "--update") err = wutil.SilentExec("wsl", "--update")
if err == nil { if err == nil {
break break
} }
@ -87,7 +87,7 @@ func warn(title string, caption string) int {
func main() { func main() {
args := os.Args args := os.Args
setupLogging(path.Base(args[0])) setupLogging(path.Base(args[0]))
if wsl.IsWSLInstalled() { if wutil.IsWSLInstalled() {
// nothing to do // nothing to do
logrus.Info("WSL Kernel already installed") logrus.Info("WSL Kernel already installed")
return return

View File

@ -27,10 +27,17 @@ var (
} }
initOpts = machine.InitOptions{} initOpts = machine.InitOptions{}
initOptionalFlags = InitOptionalFlags{}
defaultMachineName = machine.DefaultMachineName defaultMachineName = machine.DefaultMachineName
now bool now bool
defaultProvider = GetSystemDefaultProvider()
) )
// Flags which have a meaning when unspecified that differs from the flag default
type InitOptionalFlags struct {
UserModeNetworking bool
}
// maxMachineNameSize is set to thirty to limit huge machine names primarily // maxMachineNameSize is set to thirty to limit huge machine names primarily
// because macOS has a much smaller file size limit. // because macOS has a much smaller file size limit.
const maxMachineNameSize = 30 const maxMachineNameSize = 30
@ -110,6 +117,10 @@ func init() {
rootfulFlagName := "rootful" rootfulFlagName := "rootful"
flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution") flags.BoolVar(&initOpts.Rootful, rootfulFlagName, false, "Whether this machine should prefer rootful container execution")
userModeNetFlagName := "user-mode-networking"
flags.BoolVar(&initOptionalFlags.UserModeNetworking, userModeNetFlagName, false,
"Whether this machine should use user-mode networking, routing traffic through a host user-space process")
} }
func initMachine(cmd *cobra.Command, args []string) error { func initMachine(cmd *cobra.Command, args []string) error {
@ -118,7 +129,7 @@ func initMachine(cmd *cobra.Command, args []string) error {
vm machine.VM vm machine.VM
) )
provider := GetSystemDefaultProvider() provider := defaultProvider
initOpts.Name = defaultMachineName initOpts.Name = defaultMachineName
if len(args) > 0 { if len(args) > 0 {
if len(args[0]) > maxMachineNameSize { if len(args[0]) > maxMachineNameSize {
@ -132,6 +143,12 @@ func initMachine(cmd *cobra.Command, args []string) error {
for idx, vol := range initOpts.Volumes { for idx, vol := range initOpts.Volumes {
initOpts.Volumes[idx] = os.ExpandEnv(vol) initOpts.Volumes[idx] = os.ExpandEnv(vol)
} }
// Process optional flags (flags where unspecified / nil has meaning )
if cmd.Flags().Changed("user-mode-networking") {
initOpts.UserModeNetworking = &initOptionalFlags.UserModeNetworking
}
vm, err = provider.NewMachine(initOpts) vm, err = provider.NewMachine(initOpts)
if err != nil { if err != nil {
return err return err

View File

@ -178,6 +178,7 @@ func toMachineFormat(vms []*machine.ListResponse) ([]*entities.ListReporter, err
response.RemoteUsername = vm.RemoteUsername response.RemoteUsername = vm.RemoteUsername
response.IdentityPath = vm.IdentityPath response.IdentityPath = vm.IdentityPath
response.Starting = vm.Starting response.Starting = vm.Starting
response.UserModeNetworking = vm.UserModeNetworking
machineResponses = append(machineResponses, response) machineResponses = append(machineResponses, response)
} }

View File

@ -36,6 +36,7 @@ type SetFlags struct {
DiskSize uint64 DiskSize uint64
Memory uint64 Memory uint64
Rootful bool Rootful bool
UserModeNetworking bool
} }
func init() { func init() {
@ -72,6 +73,10 @@ func init() {
"Memory in MB", "Memory in MB",
) )
_ = setCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone) _ = setCmd.RegisterFlagCompletionFunc(memoryFlagName, completion.AutocompleteNone)
userModeNetFlagName := "user-mode-networking"
flags.BoolVar(&setFlags.UserModeNetworking, userModeNetFlagName, false, // defaults not-relevant due to use of Changed()
"Whether this machine should use user-mode networking, routing traffic through a host user-space process")
} }
func setMachine(cmd *cobra.Command, args []string) error { func setMachine(cmd *cobra.Command, args []string) error {
@ -102,6 +107,9 @@ func setMachine(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("disk-size") { if cmd.Flags().Changed("disk-size") {
setOpts.DiskSize = &setFlags.DiskSize setOpts.DiskSize = &setFlags.DiskSize
} }
if cmd.Flags().Changed("user-mode-networking") {
setOpts.UserModeNetworking = &setFlags.UserModeNetworking
}
setErrs, lasterr := vm.Set(vmName, setOpts) setErrs, lasterr := vm.Set(vmName, setOpts)
for _, err := range setErrs { for _, err := range setErrs {

View File

@ -19,7 +19,9 @@ podman-kube-play.1.md
podman-login.1.md podman-login.1.md
podman-logout.1.md podman-logout.1.md
podman-logs.1.md podman-logs.1.md
podman-machine-init.1.md
podman-machine-list.1.md podman-machine-list.1.md
podman-machine-set.1.md
podman-manifest-add.1.md podman-manifest-add.1.md
podman-manifest-annotate.1.md podman-manifest-annotate.1.md
podman-manifest-create.1.md podman-manifest-create.1.md

View File

@ -0,0 +1,21 @@
####> This option file is used in:
####> podman machine init, machine set
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--user-mode-networking**
Whether this machine should relay traffic from the guest through a user-space
process running on the host. In some VPN configurations the VPN may drop
traffic from alternate network interfaces, including VM network devices. By
enabling user-mode networking (a setting of `true`), VPNs will observe all
podman machine traffic as coming from the host, bypassing the problem.
When the qemu backend is used (Linux, Mac), user-mode networking is
mandatory and the only allowed value is `true`. In contrast, The Windows/WSL
backend defaults to `false`, and follows the standard WSL network setup.
Changing this setting to `true` on Windows/WSL will inform Podman to replace
the WSL networking setup on start of this machine instance with a user-mode
networking distribution. Since WSL shares the same kernel across
distributions, all other running distributions will reuse this network.
Likewise, when the last machine instance with a `true` setting stops, the
original networking setup will be restored.

View File

@ -76,6 +76,8 @@ Set the timezone for the machine and containers. Valid values are `local` or
a `timezone` such as `America/Chicago`. A value of `local`, which is the default, a `timezone` such as `America/Chicago`. A value of `local`, which is the default,
means to use the timezone of the machine host. means to use the timezone of the machine host.
@@option user-mode-networking
#### **--username** #### **--username**
Username to use for executing commands in remote VM. Default value is `core` Username to use for executing commands in remote VM. Default value is `core`

View File

@ -31,6 +31,7 @@ Print results with a Go template.
| .Resources ... | Resources used by the machine | | .Resources ... | Resources used by the machine |
| .SSHConfig ... | SSH configuration info for communitating with machine | | .SSHConfig ... | SSH configuration info for communitating with machine |
| .State ... | Machine state | | .State ... | Machine state |
| .UserModeNetworking | Whether this machine uses user-mode networking |
#### **--help** #### **--help**

View File

@ -33,7 +33,7 @@ or a Go template.
Valid placeholders for the Go template are listed below: Valid placeholders for the Go template are listed below:
| **Placeholder** | **Description** | | **Placeholder** | **Description** |
| --------------- | ------------------------------- | | ------------------- | ----------------------------------------- |
| .CPUs | Number of CPUs | | .CPUs | Number of CPUs |
| .Created | Time since VM creation | | .Created | Time since VM creation |
| .Default | Is default machine | | .Default | Is default machine |
@ -47,6 +47,7 @@ Valid placeholders for the Go template are listed below:
| .RemoteUsername | VM Username for rootless Podman | | .RemoteUsername | VM Username for rootless Podman |
| .Running | Is machine running | | .Running | Is machine running |
| .Stream | Stream name | | .Stream | Stream name |
| .UserModeNetworking | Whether machine uses user-mode networking |
| .VMType | VM type | | .VMType | VM type |
#### **--help** #### **--help**

View File

@ -40,6 +40,8 @@ container execution. This option will also update the current podman
remote connection default if it is currently pointing at the specified remote connection default if it is currently pointing at the specified
machine name (or `podman-machine-default` if no name is specified). machine name (or `podman-machine-default` if no name is specified).
@@option user-mode-networking
Unlike [**podman system connection default**](podman-system-connection-default.1.md) Unlike [**podman system connection default**](podman-system-connection-default.1.md)
this option will also make the API socket, if available, forward to the rootful/rootless this option will also make the API socket, if available, forward to the rootful/rootless
socket in the VM. socket in the VM.

View File

@ -17,6 +17,7 @@ type ListReporter struct {
Port int Port int
RemoteUsername string RemoteUsername string
IdentityPath string IdentityPath string
UserModeNetworking bool
} }
// MachineInfo contains info on the machine host and version info // MachineInfo contains info on the machine host and version info

View File

@ -31,8 +31,8 @@ type InitOptions struct {
Username string Username string
ReExec bool ReExec bool
Rootful bool Rootful bool
// The numerical userid of the user that called machine UID string // uid of the user that called machine
UID string UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable
} }
type Status = string type Status = string
@ -104,6 +104,7 @@ type ListResponse struct {
Port int Port int
RemoteUsername string RemoteUsername string
IdentityPath string IdentityPath string
UserModeNetworking bool
} }
type SetOptions struct { type SetOptions struct {
@ -111,6 +112,7 @@ type SetOptions struct {
DiskSize *uint64 DiskSize *uint64
Memory *uint64 Memory *uint64
Rootful *bool Rootful *bool
UserModeNetworking *bool
} }
type SSHOptions struct { type SSHOptions struct {
@ -160,6 +162,7 @@ type InspectInfo struct {
Resources ResourceConfig Resources ResourceConfig
SSHConfig SSHConfig SSHConfig SSHConfig
State Status State Status
UserModeNetworking bool
} }
func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL {

View File

@ -364,6 +364,11 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
if err := v.resizeDisk(opts.DiskSize, originalDiskSize>>(10*3)); err != nil { if err := v.resizeDisk(opts.DiskSize, originalDiskSize>>(10*3)); err != nil {
return false, err return false, err
} }
if opts.UserModeNetworking != nil && !*opts.UserModeNetworking {
logrus.Warn("ignoring init option to disable user-mode networking: this mode is not supported by the QEMU backend")
}
// If the user provides an ignition file, we need to // If the user provides an ignition file, we need to
// copy it into the conf dir // copy it into the conf dir
if len(opts.IgnitionPath) > 0 { if len(opts.IgnitionPath) > 0 {
@ -1152,6 +1157,7 @@ func getVMInfos() ([]*machine.ListResponse, error) {
listEntry.IdentityPath = vm.IdentityPath listEntry.IdentityPath = vm.IdentityPath
listEntry.CreatedAt = vm.Created listEntry.CreatedAt = vm.Created
listEntry.Starting = vm.Starting listEntry.Starting = vm.Starting
listEntry.UserModeNetworking = true // always true
if listEntry.CreatedAt.IsZero() { if listEntry.CreatedAt.IsZero() {
listEntry.CreatedAt = time.Now() listEntry.CreatedAt = time.Now()
@ -1618,6 +1624,7 @@ func (v *MachineVM) Inspect() (*machine.InspectInfo, error) {
Resources: v.ResourceConfig, Resources: v.ResourceConfig,
SSHConfig: v.SSHConfig, SSHConfig: v.SSHConfig,
State: state, State: state,
UserModeNetworking: true, // always true
}, nil }, nil
} }

View File

@ -0,0 +1,73 @@
//go:build windows
// +build windows
package wsl
import (
"io/fs"
"math"
"os"
"golang.org/x/sys/windows"
)
type fileLock struct {
file *os.File
}
// Locks a file path, creating or overwriting a file if necessary. This API only
// supports dedicated empty lock files. Locking is not advisory, once a file is
// locked, additional opens will block on read/write.
func lockFile(path string) (*fileLock, error) {
// In the future we may want to switch this to an async open vs the win32 API
// to bring support for timeouts, so we don't export the current underlying
// File object.
file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
if err != nil {
return nil, &fs.PathError{
Op: "lock",
Path: path,
Err: err,
}
}
const max = uint32(math.MaxUint32)
overlapped := new(windows.Overlapped)
lockType := windows.LOCKFILE_EXCLUSIVE_LOCK
// Lock largest possible length (all 64 bits (lo + hi) set)
err = windows.LockFileEx(windows.Handle(file.Fd()), uint32(lockType), 0, max, max, overlapped)
if err != nil {
file.Close()
return nil, &fs.PathError{
Op: "lock",
Path: file.Name(),
Err: err,
}
}
return &fileLock{file: file}, nil
}
func (flock *fileLock) unlock() error {
if flock == nil || flock.file == nil {
return nil
}
defer func() {
flock.file.Close()
flock.file = nil
}()
const max = uint32(math.MaxUint32)
overlapped := new(windows.Overlapped)
err := windows.UnlockFileEx(windows.Handle(flock.file.Fd()), 0, max, max, overlapped)
if err != nil {
return &fs.PathError{
Op: "unlock",
Path: flock.file.Name(),
Err: err,
}
}
return nil
}

View File

@ -20,6 +20,7 @@ import (
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/podman/v4/pkg/machine" "github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/machine/wsl/wutil"
"github.com/containers/podman/v4/utils" "github.com/containers/podman/v4/utils"
"github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/homedir"
"github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/pkg/ioutils"
@ -115,6 +116,15 @@ const wslConf = `[user]
default=[USER] default=[USER]
` `
const wslConfUserNet = `
[network]
generateResolvConf = false
`
const resolvConfUserNet = `
nameserver 192.168.127.1
`
// WSL kernel does not have sg and crypto_user modules // WSL kernel does not have sg and crypto_user modules
const overrideSysusers = `[Service] const overrideSysusers = `[Service]
LoadCredential= LoadCredential=
@ -198,10 +208,12 @@ http://docs.microsoft.com/en-us/windows/wsl/install\
` `
const ( const (
gvProxy = "gvproxy.exe"
winSShProxy = "win-sshproxy.exe" winSShProxy = "win-sshproxy.exe"
winSshProxyTid = "win-sshproxy.tid" winSshProxyTid = "win-sshproxy.tid"
pipePrefix = "npipe:////./pipe/" pipePrefix = "npipe:////./pipe/"
globalPipe = "docker_engine" globalPipe = "docker_engine"
userModeDist = "podman-net-usermode"
) )
type Virtualization struct { type Virtualization struct {
@ -241,6 +253,8 @@ type MachineVM struct {
machine.SSHConfig machine.SSHConfig
// machine version // machine version
Version int Version int
// Whether to use user-mode networking
UserModeNetworking bool
} }
type ExitCodeError struct { type ExitCodeError struct {
@ -276,6 +290,11 @@ func (p *Virtualization) NewMachine(opts machine.InitOptions) (machine.VM, error
vm.Created = time.Now() vm.Created = time.Now()
vm.LastUp = vm.Created vm.LastUp = vm.Created
// Default is false
if opts.UserModeNetworking != nil {
vm.UserModeNetworking = *opts.UserModeNetworking
}
// Add a random port for ssh // Add a random port for ssh
port, err := utils.GetRandomPort() port, err := utils.GetRandomPort()
if err != nil { if err != nil {
@ -395,11 +414,19 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
return false, err return false, err
} }
dist, err := provisionWSLDist(v) const prompt = "Importing operating system into WSL (this may take a few minutes on a new WSL install)..."
dist, err := provisionWSLDist(v.Name, v.ImagePath, prompt)
if err != nil { if err != nil {
return false, err return false, err
} }
if v.UserModeNetworking {
if err = installUserModeDist(dist, v.ImagePath); err != nil {
_ = unregisterDist(dist)
return false, err
}
}
fmt.Println("Configuring system...") fmt.Println("Configuring system...")
if err = configureSystem(v, dist); err != nil { if err = configureSystem(v, dist); err != nil {
return false, err return false, err
@ -496,21 +523,21 @@ func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) err
return nil return nil
} }
func provisionWSLDist(v *MachineVM) (string, error) { func provisionWSLDist(name string, imagePath string, prompt string) (string, error) {
vmDataDir, err := machine.GetDataDir(vmtype) vmDataDir, err := machine.GetDataDir(vmtype)
if err != nil { if err != nil {
return "", err return "", err
} }
distDir := filepath.Join(vmDataDir, "wsldist") distDir := filepath.Join(vmDataDir, "wsldist")
distTarget := filepath.Join(distDir, v.Name) distTarget := filepath.Join(distDir, name)
if err := os.MkdirAll(distDir, 0755); err != nil { if err := os.MkdirAll(distDir, 0755); err != nil {
return "", fmt.Errorf("could not create wsldist directory: %w", err) return "", fmt.Errorf("could not create wsldist directory: %w", err)
} }
dist := toDist(v.Name) dist := toDist(name)
fmt.Println("Importing operating system into WSL (this may take a few minutes on a new WSL install)...") fmt.Println(prompt)
if err = runCmdPassThrough("wsl", "--import", dist, distTarget, v.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) return "", fmt.Errorf("the WSL import of guest OS failed: %w", err)
} }
@ -519,15 +546,6 @@ func provisionWSLDist(v *MachineVM) (string, error) {
return "", fmt.Errorf("package permissions restore of shadow-utils on guest OS failed: %w", err) return "", fmt.Errorf("package permissions restore of shadow-utils on guest OS failed: %w", err)
} }
// Windows 11 (NT Version = 10, Build 22000) generates harmless but scary messages on every
// operation when mount was not present on the initial start. Force a cycle so that it won't
// repeatedly complain.
if winVersionAtLeast(10, 0, 22000) {
if err := terminateDist(dist); err != nil {
logrus.Warnf("could not cycle WSL dist: %s", err.Error())
}
}
return dist, nil return dist, nil
} }
@ -606,11 +624,7 @@ func configureSystem(v *MachineVM, dist string) error {
return fmt.Errorf("could not create podman-machine file for guest OS: %w", err) return fmt.Errorf("could not create podman-machine file for guest OS: %w", err)
} }
if err := wslPipe(withUser(wslConf, user), dist, "sh", "-c", "cat > /etc/wsl.conf"); err != nil { return changeDistUserModeNetworking(dist, user, "", v.UserModeNetworking)
return fmt.Errorf("could not configure wsl config for guest OS: %w", err)
}
return nil
} }
func configureProxy(dist string, useProxy bool, quiet bool) error { func configureProxy(dist string, useProxy bool, quiet bool) error {
@ -694,8 +708,16 @@ func installScripts(dist string) error {
return nil return nil
} }
func writeWslConf(dist string, user string) error {
if err := wslPipe(withUser(wslConf, user), dist, "sh", "-c", "cat > /etc/wsl.conf"); err != nil {
return fmt.Errorf("could not configure wsl config for guest OS: %w", err)
}
return nil
}
func checkAndInstallWSL(opts machine.InitOptions) (bool, error) { func checkAndInstallWSL(opts machine.InitOptions) (bool, error) {
if IsWSLInstalled() { if wutil.IsWSLInstalled() {
return true, nil return true, nil
} }
@ -1015,6 +1037,27 @@ func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) {
setErrors = append(setErrors, errors.New("changing Disk Size not supported for WSL machines")) setErrors = append(setErrors, errors.New("changing Disk Size not supported for WSL machines"))
} }
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"))
}
if update && *opts.UserModeNetworking {
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
}
}
return setErrors, v.writeConfig() return setErrors, v.writeConfig()
} }
@ -1029,6 +1072,11 @@ func (v *MachineVM) Start(name string, opts machine.StartOptions) error {
return err return err
} }
// Startup user-mode networking if enabled
if err := v.startUserModeNetworking(); err != nil {
return err
}
err := wslInvoke(dist, "/root/bootstrap") err := wslInvoke(dist, "/root/bootstrap")
if err != nil { if err != nil {
return fmt.Errorf("the WSL bootstrap script failed: %w", err) return fmt.Errorf("the WSL bootstrap script failed: %w", err)
@ -1072,6 +1120,20 @@ func (v *MachineVM) Start(name string, opts machine.StartOptions) error {
return err return err
} }
func findExecutablePeer(name string) (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return "", err
}
return filepath.Join(filepath.Dir(exe), name), nil
}
func launchWinProxy(v *MachineVM) (bool, string, error) { func launchWinProxy(v *MachineVM) (bool, string, error) {
machinePipe := toDist(v.Name) machinePipe := toDist(v.Name)
if !machine.PipeNameAvailable(machinePipe) { if !machine.PipeNameAvailable(machinePipe) {
@ -1083,17 +1145,11 @@ func launchWinProxy(v *MachineVM) (bool, string, error) {
globalName = true globalName = true
} }
exe, err := os.Executable() command, err := findExecutablePeer(winSShProxy)
if err != nil { if err != nil {
return globalName, "", err return globalName, "", err
} }
exe, err = filepath.EvalSymlinks(exe)
if err != nil {
return globalName, "", err
}
command := filepath.Join(filepath.Dir(exe), winSShProxy)
stateDir, err := getWinProxyStateDir(v) stateDir, err := getWinProxyStateDir(v)
if err != nil { if err != nil {
return globalName, "", err return globalName, "", err
@ -1143,59 +1199,54 @@ func getWinProxyStateDir(v *MachineVM) (string, error) {
return stateDir, nil return stateDir, nil
} }
func IsWSLInstalled() bool {
cmd := SilentExecCmd("wsl", "--status")
out, err := cmd.StdoutPipe()
cmd.Stderr = nil
if err != nil {
return false
}
if err = cmd.Start(); err != nil {
return false
}
scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
result := true
for scanner.Scan() {
line := scanner.Text()
// Windows 11 does not set an error exit code when a kernel is not avail
if strings.Contains(line, "kernel file is not found") {
result = false
break
}
}
if err := cmd.Wait(); !result || err != nil {
return false
}
return true
}
func IsWSLFeatureEnabled() bool { func IsWSLFeatureEnabled() bool {
return SilentExec("wsl", "--set-default-version", "2") == nil return wutil.SilentExec("wsl", "--set-default-version", "2") == nil
} }
func isWSLRunning(dist string) (bool, error) { func isWSLRunning(dist string) (bool, error) {
cmd := exec.Command("wsl", "-l", "--running", "--quiet") return wslCheckExists(dist, true)
out, err := cmd.StdoutPipe() }
func isWSLExist(dist string) (bool, error) {
return wslCheckExists(dist, false)
}
func wslCheckExists(dist string, running bool) (bool, error) {
all, err := getAllWSLDistros(running)
if err != nil { if err != nil {
return false, err return false, err
} }
if err = cmd.Start(); err != nil {
return false, err _, exists := all[dist]
return exists, nil
} }
func getAllWSLDistros(running bool) (map[string]struct{}, error) {
args := []string{"-l", "--quiet"}
if running {
args = append(args, "--running")
}
cmd := exec.Command("wsl", args...)
out, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
if err = cmd.Start(); err != nil {
return nil, err
}
all := make(map[string]struct{})
scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
result := false
for scanner.Scan() { for scanner.Scan() {
fields := strings.Fields(scanner.Text()) fields := strings.Fields(scanner.Text())
if len(fields) > 0 && dist == fields[0] { if len(fields) > 0 {
result = true all[fields[0]] = struct{}{}
break
} }
} }
_ = cmd.Wait() _ = cmd.Wait()
return result, nil return all, nil
} }
func isSystemdRunning(dist string) (bool, error) { func isSystemdRunning(dist string) (bool, error) {
@ -1243,6 +1294,11 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error {
return fmt.Errorf("%q is not running", v.Name) return fmt.Errorf("%q is not running", v.Name)
} }
// Stop user-mode networking if enabled
if err := v.stopUserModeNetworking(dist); err != nil {
fmt.Fprintf(os.Stderr, "Could not cleanly stop user-mode networking: %s\n", err.Error())
}
_, _, _ = v.updateTimeStamps(true) _, _, _ = v.updateTimeStamps(true)
if err := stopWinProxy(v); err != nil { if err := stopWinProxy(v); err != nil {
@ -1272,6 +1328,11 @@ func terminateDist(dist string) error {
return cmd.Run() return cmd.Run()
} }
func unregisterDist(dist string) error {
cmd := exec.Command("wsl", "--unregister", dist)
return cmd.Run()
}
func (v *MachineVM) State(bypass bool) (machine.Status, error) { func (v *MachineVM) State(bypass bool) (machine.Status, error) {
if v.isRunning() { if v.isRunning() {
return machine.Running, nil return machine.Running, nil
@ -1470,6 +1531,7 @@ func GetVMInfos() ([]*machine.ListResponse, error) {
listEntry.Port = vm.Port listEntry.Port = vm.Port
listEntry.IdentityPath = vm.IdentityPath listEntry.IdentityPath = vm.IdentityPath
listEntry.Starting = false listEntry.Starting = false
listEntry.UserModeNetworking = vm.UserModeNetworking
running := vm.isRunning() running := vm.isRunning()
listEntry.CreatedAt, listEntry.LastUp, _ = vm.updateTimeStamps(running) listEntry.CreatedAt, listEntry.LastUp, _ = vm.updateTimeStamps(running)
@ -1629,6 +1691,7 @@ func (v *MachineVM) Inspect() (*machine.InspectInfo, error) {
Resources: v.getResources(), Resources: v.getResources(),
SSHConfig: v.SSHConfig, SSHConfig: v.SSHConfig,
State: state, State: state,
UserModeNetworking: v.UserModeNetworking,
}, nil }, nil
} }

View File

@ -0,0 +1,326 @@
//go:build windows
// +build windows
package wsl
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/specgen"
"github.com/sirupsen/logrus"
)
const startUserModeNet = `
set -e
STATE=/mnt/wsl/podman-usermodenet
mkdir -p $STATE
cp -f /mnt/wsl/resolv.conf $STATE/resolv.orig
ip route show default > $STATE/route.dat
ROUTE=$(<$STATE/route.dat)
if [[ $ROUTE =~ .*192\.168\.127\.1.* ]]; then
exit 2
fi
if [[ ! $ROUTE =~ default\ via ]]; then
exit 3
fi
nohup /usr/local/bin/vm -iface podman-usermode -stop-if-exist ignore -url "stdio:$GVPROXY?listen-stdio=accept" > /var/log/vm.log 2> /var/log/vm.err < /dev/null &
echo $! > $STATE/vm.pid
sleep 1
ps -eo args | grep -q -m1 ^/usr/local/bin/vm || exit 42
`
const stopUserModeNet = `
STATE=/mnt/wsl/podman-usermodenet
if [[ ! -f "$STATE/vm.pid" || ! -f "$STATE/route.dat" ]]; then
exit 2
fi
cp -f $STATE/resolv.orig /mnt/wsl/resolv.conf
GPID=$(<$STATE/vm.pid)
kill $GPID > /dev/null
while kill -0 $GPID > /dev/null 2>&1; do
sleep 1
done
ip route del default > /dev/null 2>&1
ROUTE=$(<$STATE/route.dat)
if [[ ! $ROUTE =~ default\ via ]]; then
exit 3
fi
ip route add $ROUTE
rm -rf /mnt/wsl/podman-usermodenet
`
func (v *MachineVM) startUserModeNetworking() error {
if !v.UserModeNetworking {
return nil
}
exe, err := findExecutablePeer(gvProxy)
if err != nil {
return fmt.Errorf("could not locate %s, which is necessary for user-mode networking, please reinstall", gvProxy)
}
flock, err := v.obtainUserModeNetLock()
if err != nil {
return err
}
defer flock.unlock()
running, err := isWSLRunning(userModeDist)
if err != nil {
return err
}
running = running && isGvProxyVMRunning()
// Start or reuse
if !running {
if err := v.launchUserModeNetDist(exe); err != nil {
return err
}
}
if err := createUserModeResolvConf(toDist(v.Name)); err != nil {
return err
}
// Register in-use
err = v.addUserModeNetEntry()
if err != nil {
return err
}
return nil
}
func (v *MachineVM) stopUserModeNetworking(dist string) error {
if !v.UserModeNetworking {
return nil
}
flock, err := v.obtainUserModeNetLock()
if err != nil {
return err
}
defer flock.unlock()
err = v.removeUserModeNetEntry()
if err != nil {
return err
}
count, err := v.cleanupAndCountNetEntries()
if err != nil {
return err
}
// Leave running if still in-use
if count > 0 {
return nil
}
fmt.Println("Stopping user-mode networking...")
err = wslPipe(stopUserModeNet, userModeDist, "bash")
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
switch exitErr.ExitCode() {
case 2:
err = fmt.Errorf("startup state was missing")
case 3:
err = fmt.Errorf("route state is missing a default route")
}
}
logrus.Warnf("problem tearing down user-mode networking cleanly, forcing: %s", err.Error())
}
return terminateDist(userModeDist)
}
func isGvProxyVMRunning() bool {
return wslInvoke(userModeDist, "bash", "-c", "ps -eo args | grep -q -m1 ^/usr/local/bin/vm || exit 42") == nil
}
func (v *MachineVM) launchUserModeNetDist(exeFile string) error {
fmt.Println("Starting user-mode networking...")
exe, err := specgen.ConvertWinMountPath(exeFile)
if err != nil {
return err
}
cmdStr := fmt.Sprintf("GVPROXY=%q\n%s", exe, startUserModeNet)
if err := wslPipe(cmdStr, userModeDist, "bash"); err != nil {
_ = terminateDist(userModeDist)
if exitErr, ok := err.(*exec.ExitError); ok {
switch exitErr.ExitCode() {
case 2:
return fmt.Errorf("another user-mode network is running, only one can be used at a time: shut down all machines and run wsl --shutdown if this is unexpected")
case 3:
err = fmt.Errorf("route state is missing a default route: shutdown all machines and run wsl --shutdown to recover")
}
}
return fmt.Errorf("error setting up user-mode networking: %w", err)
}
return nil
}
func installUserModeDist(dist string, imagePath string) error {
exists, err := isWSLExist(userModeDist)
if err != nil {
return err
}
if !exists {
if err := wslInvoke(dist, "test", "-f", "/usr/local/bin/vm"); err != nil {
return fmt.Errorf("existing machine is too old, can't install user-mode networking dist until machine is reinstalled (using podman machine rm, then podman machine init)")
}
const prompt = "Installing user-mode networking distribution..."
if _, err := provisionWSLDist(userModeDist, imagePath, prompt); err != nil {
return err
}
_ = terminateDist(userModeDist)
}
return nil
}
func createUserModeResolvConf(dist string) error {
err := wslPipe(resolvConfUserNet, dist, "bash", "-c", "(rm -f /etc/resolv.conf; cat > /etc/resolv.conf)")
if err != nil {
return fmt.Errorf("could not create resolv.conf: %w", err)
}
return err
}
func (v *MachineVM) getUserModeNetDir() (string, error) {
vmDataDir, err := machine.GetDataDir(vmtype)
if err != nil {
return "", err
}
dir := filepath.Join(vmDataDir, userModeDist)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("could not create %s directory: %w", userModeDist, err)
}
return dir, nil
}
func (v *MachineVM) getUserModeNetEntriesDir() (string, error) {
netDir, err := v.getUserModeNetDir()
if err != nil {
return "", err
}
dir := filepath.Join(netDir, "entries")
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("could not create %s/entries directory: %w", userModeDist, err)
}
return dir, nil
}
func (v *MachineVM) addUserModeNetEntry() error {
entriesDir, err := v.getUserModeNetEntriesDir()
if err != nil {
return err
}
path := filepath.Join(entriesDir, toDist(v.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)
}
file.Close()
return nil
}
func (v *MachineVM) removeUserModeNetEntry() error {
entriesDir, err := v.getUserModeNetEntriesDir()
if err != nil {
return err
}
path := filepath.Join(entriesDir, toDist(v.Name))
return os.Remove(path)
}
func (v *MachineVM) cleanupAndCountNetEntries() (uint, error) {
entriesDir, err := v.getUserModeNetEntriesDir()
if err != nil {
return 0, err
}
allDists, err := getAllWSLDistros(true)
if err != nil {
return 0, err
}
var count uint = 0
files, err := os.ReadDir(entriesDir)
if err != nil {
return 0, err
}
for _, file := range files {
_, running := allDists[file.Name()]
if !running {
_ = os.Remove(filepath.Join(entriesDir, file.Name()))
continue
}
count++
}
return count, nil
}
func (v *MachineVM) obtainUserModeNetLock() (*fileLock, error) {
dir, err := v.getUserModeNetDir()
if err != nil {
return nil, err
}
var flock *fileLock
lockPath := filepath.Join(dir, "podman-usermodenet.lck")
if flock, err = lockFile(lockPath); err != nil {
return nil, fmt.Errorf("could not lock user-mode networking lock file: %w", err)
}
return flock, nil
}
func changeDistUserModeNetworking(dist string, user string, image string, enable bool) error {
// Only install if user-mode is being enabled and there was an image path passed
if enable && len(image) > 0 {
if err := installUserModeDist(dist, image); err != nil {
return err
}
}
if err := writeWslConf(dist, user); err != nil {
return err
}
if enable {
return appendDisableAutoResolve(dist)
}
return nil
}
func appendDisableAutoResolve(dist string) error {
if err := wslPipe(wslConfUserNet, dist, "sh", "-c", "cat >> /etc/wsl.conf"); err != nil {
return fmt.Errorf("could not append resolv config to wsl.conf: %w", err)
}
return nil
}

View File

@ -8,7 +8,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
@ -345,17 +344,3 @@ func sendQuit(tid uint32) {
postMessage := user32.NewProc("PostThreadMessageW") postMessage := user32.NewProc("PostThreadMessageW")
postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) postMessage.Call(uintptr(tid), WM_QUIT, 0, 0)
} }
func SilentExec(command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
cmd.Stdout = nil
cmd.Stderr = nil
return cmd.Run()
}
func SilentExecCmd(command string, args ...string) *exec.Cmd {
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
return cmd
}

View File

@ -0,0 +1,55 @@
//go:build windows
// +build windows
package wutil
import (
"bufio"
"os/exec"
"strings"
"syscall"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
func SilentExec(command string, args ...string) error {
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
cmd.Stdout = nil
cmd.Stderr = nil
return cmd.Run()
}
func SilentExecCmd(command string, args ...string) *exec.Cmd {
cmd := exec.Command(command, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
return cmd
}
func IsWSLInstalled() bool {
cmd := SilentExecCmd("wsl", "--status")
out, err := cmd.StdoutPipe()
cmd.Stderr = nil
if err != nil {
return false
}
if err = cmd.Start(); err != nil {
return false
}
scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
result := true
for scanner.Scan() {
line := scanner.Text()
// Windows 11 does not set an error exit code when a kernel is not avail
if strings.Contains(line, "kernel file is not found") {
result = false
break
}
}
if err := cmd.Wait(); !result || err != nil {
return false
}
return true
}