diff --git a/Makefile b/Makefile index 9fe4b5923c..a163de85e3 100644 --- a/Makefile +++ b/Makefile @@ -199,7 +199,7 @@ endif # include this lightweight helper binary. # GV_GITURL=https://github.com/containers/gvisor-tap-vsock.git -GV_SHA=aab0ac9367fc5142f5857c36ac2352bcb3c60ab7 +GV_SHA=407efb5dcdb0f4445935f7360535800b60447544 ### ### Primary entry-point targets diff --git a/cmd/podman-wslkerninst/main.go b/cmd/podman-wslkerninst/main.go index 0485db13c7..8316f7201d 100644 --- a/cmd/podman-wslkerninst/main.go +++ b/cmd/podman-wslkerninst/main.go @@ -11,7 +11,7 @@ import ( "time" "unsafe" - "github.com/containers/podman/v4/pkg/machine/wsl" + "github.com/containers/podman/v4/pkg/machine/wsl/wutil" "github.com/sirupsen/logrus" "golang.org/x/sys/windows/svc/eventlog" ) @@ -49,7 +49,7 @@ func installWslKernel() error { ) backoff := 500 * time.Millisecond for i := 1; i < 6; i++ { - err = wsl.SilentExec("wsl", "--update") + err = wutil.SilentExec("wsl", "--update") if err == nil { break } @@ -87,7 +87,7 @@ func warn(title string, caption string) int { func main() { args := os.Args setupLogging(path.Base(args[0])) - if wsl.IsWSLInstalled() { + if wutil.IsWSLInstalled() { // nothing to do logrus.Info("WSL Kernel already installed") return diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 57cb909437..959cef2bd9 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -27,10 +27,17 @@ var ( } initOpts = machine.InitOptions{} + initOptionalFlags = InitOptionalFlags{} defaultMachineName = machine.DefaultMachineName 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 // because macOS has a much smaller file size limit. const maxMachineNameSize = 30 @@ -110,6 +117,10 @@ func init() { rootfulFlagName := "rootful" 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 { @@ -118,7 +129,7 @@ func initMachine(cmd *cobra.Command, args []string) error { vm machine.VM ) - provider := GetSystemDefaultProvider() + provider := defaultProvider initOpts.Name = defaultMachineName if len(args) > 0 { if len(args[0]) > maxMachineNameSize { @@ -132,6 +143,12 @@ func initMachine(cmd *cobra.Command, args []string) error { for idx, vol := range initOpts.Volumes { 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) if err != nil { return err diff --git a/cmd/podman/machine/list.go b/cmd/podman/machine/list.go index 435771da59..ac87bfa647 100644 --- a/cmd/podman/machine/list.go +++ b/cmd/podman/machine/list.go @@ -178,6 +178,7 @@ func toMachineFormat(vms []*machine.ListResponse) ([]*entities.ListReporter, err response.RemoteUsername = vm.RemoteUsername response.IdentityPath = vm.IdentityPath response.Starting = vm.Starting + response.UserModeNetworking = vm.UserModeNetworking machineResponses = append(machineResponses, response) } diff --git a/cmd/podman/machine/set.go b/cmd/podman/machine/set.go index 1b9e1b2bdb..5c933ea491 100644 --- a/cmd/podman/machine/set.go +++ b/cmd/podman/machine/set.go @@ -32,10 +32,11 @@ var ( ) type SetFlags struct { - CPUs uint64 - DiskSize uint64 - Memory uint64 - Rootful bool + CPUs uint64 + DiskSize uint64 + Memory uint64 + Rootful bool + UserModeNetworking bool } func init() { @@ -72,6 +73,10 @@ func init() { "Memory in MB", ) _ = 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 { @@ -102,6 +107,9 @@ func setMachine(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("disk-size") { setOpts.DiskSize = &setFlags.DiskSize } + if cmd.Flags().Changed("user-mode-networking") { + setOpts.UserModeNetworking = &setFlags.UserModeNetworking + } setErrs, lasterr := vm.Set(vmName, setOpts) for _, err := range setErrs { diff --git a/docs/source/markdown/.gitignore b/docs/source/markdown/.gitignore index 47161e9831..8807c337f6 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -19,7 +19,9 @@ podman-kube-play.1.md podman-login.1.md podman-logout.1.md podman-logs.1.md +podman-machine-init.1.md podman-machine-list.1.md +podman-machine-set.1.md podman-manifest-add.1.md podman-manifest-annotate.1.md podman-manifest-create.1.md diff --git a/docs/source/markdown/options/user-mode-networking.md b/docs/source/markdown/options/user-mode-networking.md new file mode 100644 index 0000000000..dc6129f834 --- /dev/null +++ b/docs/source/markdown/options/user-mode-networking.md @@ -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. diff --git a/docs/source/markdown/podman-machine-init.1.md b/docs/source/markdown/podman-machine-init.1.md.in similarity index 99% rename from docs/source/markdown/podman-machine-init.1.md rename to docs/source/markdown/podman-machine-init.1.md.in index df4ba2f501..e5242f8f4c 100644 --- a/docs/source/markdown/podman-machine-init.1.md +++ b/docs/source/markdown/podman-machine-init.1.md.in @@ -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, means to use the timezone of the machine host. +@@option user-mode-networking + #### **--username** Username to use for executing commands in remote VM. Default value is `core` diff --git a/docs/source/markdown/podman-machine-inspect.1.md b/docs/source/markdown/podman-machine-inspect.1.md index fefe7533b7..6693ec8151 100644 --- a/docs/source/markdown/podman-machine-inspect.1.md +++ b/docs/source/markdown/podman-machine-inspect.1.md @@ -31,6 +31,7 @@ Print results with a Go template. | .Resources ... | Resources used by the machine | | .SSHConfig ... | SSH configuration info for communitating with machine | | .State ... | Machine state | +| .UserModeNetworking | Whether this machine uses user-mode networking | #### **--help** diff --git a/docs/source/markdown/podman-machine-list.1.md.in b/docs/source/markdown/podman-machine-list.1.md.in index 80373906fb..2564c983ac 100644 --- a/docs/source/markdown/podman-machine-list.1.md.in +++ b/docs/source/markdown/podman-machine-list.1.md.in @@ -32,22 +32,23 @@ Change the default output format. This can be of a supported type like 'json' or a Go template. Valid placeholders for the Go template are listed below: -| **Placeholder** | **Description** | -| --------------- | ------------------------------- | -| .CPUs | Number of CPUs | -| .Created | Time since VM creation | -| .Default | Is default machine | -| .DiskSize | Disk size of machine | -| .IdentityPath | Path to ssh identity file | -| .LastUp | Time machine was last up | -| .LastUp | Time since the VM was last run | -| .Memory | Allocated memory for machine | -| .Name | VM name | -| .Port | SSH Port to use to connect to VM| -| .RemoteUsername | VM Username for rootless Podman | -| .Running | Is machine running | -| .Stream | Stream name | -| .VMType | VM type | +| **Placeholder** | **Description** | +| ------------------- | ----------------------------------------- | +| .CPUs | Number of CPUs | +| .Created | Time since VM creation | +| .Default | Is default machine | +| .DiskSize | Disk size of machine | +| .IdentityPath | Path to ssh identity file | +| .LastUp | Time machine was last up | +| .LastUp | Time since the VM was last run | +| .Memory | Allocated memory for machine | +| .Name | VM name | +| .Port | SSH Port to use to connect to VM | +| .RemoteUsername | VM Username for rootless Podman | +| .Running | Is machine running | +| .Stream | Stream name | +| .UserModeNetworking | Whether machine uses user-mode networking | +| .VMType | VM type | #### **--help** diff --git a/docs/source/markdown/podman-machine-set.1.md b/docs/source/markdown/podman-machine-set.1.md.in similarity index 98% rename from docs/source/markdown/podman-machine-set.1.md rename to docs/source/markdown/podman-machine-set.1.md.in index a5ca5033ee..0cc6852c8e 100644 --- a/docs/source/markdown/podman-machine-set.1.md +++ b/docs/source/markdown/podman-machine-set.1.md.in @@ -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 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) this option will also make the API socket, if available, forward to the rootful/rootless socket in the VM. diff --git a/pkg/domain/entities/machine.go b/pkg/domain/entities/machine.go index 4fd0413c9b..bd8be9779c 100644 --- a/pkg/domain/entities/machine.go +++ b/pkg/domain/entities/machine.go @@ -3,20 +3,21 @@ package entities import "github.com/containers/podman/v4/libpod/define" type ListReporter struct { - Name string - Default bool - Created string - Running bool - Starting bool - LastUp string - Stream string - VMType string - CPUs uint64 - Memory string - DiskSize string - Port int - RemoteUsername string - IdentityPath string + Name string + Default bool + Created string + Running bool + Starting bool + LastUp string + Stream string + VMType string + CPUs uint64 + Memory string + DiskSize string + Port int + RemoteUsername string + IdentityPath string + UserModeNetworking bool } // MachineInfo contains info on the machine host and version info diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 1e1021d9a7..5cd44bb712 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -17,22 +17,22 @@ import ( ) type InitOptions struct { - CPUS uint64 - DiskSize uint64 - IgnitionPath string - ImagePath string - Volumes []string - VolumeDriver string - IsDefault bool - Memory uint64 - Name string - TimeZone string - URI url.URL - Username string - ReExec bool - Rootful bool - // The numerical userid of the user that called machine - UID string + CPUS uint64 + DiskSize uint64 + IgnitionPath string + ImagePath string + Volumes []string + VolumeDriver string + IsDefault bool + Memory uint64 + Name string + TimeZone string + URI url.URL + Username string + ReExec bool + Rootful bool + UID string // uid of the user that called machine + UserModeNetworking *bool // nil = use backend/system default, false = disable, true = enable } type Status = string @@ -91,26 +91,28 @@ type Download struct { type ListOptions struct{} type ListResponse struct { - Name string - CreatedAt time.Time - LastUp time.Time - Running bool - Starting bool - Stream string - VMType string - CPUs uint64 - Memory uint64 - DiskSize uint64 - Port int - RemoteUsername string - IdentityPath string + Name string + CreatedAt time.Time + LastUp time.Time + Running bool + Starting bool + Stream string + VMType string + CPUs uint64 + Memory uint64 + DiskSize uint64 + Port int + RemoteUsername string + IdentityPath string + UserModeNetworking bool } type SetOptions struct { - CPUs *uint64 - DiskSize *uint64 - Memory *uint64 - Rootful *bool + CPUs *uint64 + DiskSize *uint64 + Memory *uint64 + Rootful *bool + UserModeNetworking *bool } type SSHOptions struct { @@ -151,15 +153,16 @@ type DistributionDownload interface { CleanCache() error } type InspectInfo struct { - ConfigPath VMFile - ConnectionInfo ConnectionConfig - Created time.Time - Image ImageConfig - LastUp time.Time - Name string - Resources ResourceConfig - SSHConfig SSHConfig - State Status + ConfigPath VMFile + ConnectionInfo ConnectionConfig + Created time.Time + Image ImageConfig + LastUp time.Time + Name string + Resources ResourceConfig + SSHConfig SSHConfig + State Status + UserModeNetworking bool } func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url.URL { diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index a046a8977b..ea73a54568 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -364,6 +364,11 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { if err := v.resizeDisk(opts.DiskSize, originalDiskSize>>(10*3)); err != nil { 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 // copy it into the conf dir if len(opts.IgnitionPath) > 0 { @@ -1152,6 +1157,7 @@ func getVMInfos() ([]*machine.ListResponse, error) { listEntry.IdentityPath = vm.IdentityPath listEntry.CreatedAt = vm.Created listEntry.Starting = vm.Starting + listEntry.UserModeNetworking = true // always true if listEntry.CreatedAt.IsZero() { listEntry.CreatedAt = time.Now() @@ -1609,15 +1615,16 @@ func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { } connInfo.PodmanSocket = podmanSocket return &machine.InspectInfo{ - ConfigPath: v.ConfigPath, - ConnectionInfo: *connInfo, - Created: v.Created, - Image: v.ImageConfig, - LastUp: v.LastUp, - Name: v.Name, - Resources: v.ResourceConfig, - SSHConfig: v.SSHConfig, - State: state, + ConfigPath: v.ConfigPath, + ConnectionInfo: *connInfo, + Created: v.Created, + Image: v.ImageConfig, + LastUp: v.LastUp, + Name: v.Name, + Resources: v.ResourceConfig, + SSHConfig: v.SSHConfig, + State: state, + UserModeNetworking: true, // always true }, nil } diff --git a/pkg/machine/wsl/filelock.go b/pkg/machine/wsl/filelock.go new file mode 100644 index 0000000000..dadc806629 --- /dev/null +++ b/pkg/machine/wsl/filelock.go @@ -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 +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index db14af074a..9d790c9783 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -20,6 +20,7 @@ import ( "github.com/containers/common/pkg/config" "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/storage/pkg/homedir" "github.com/containers/storage/pkg/ioutils" @@ -115,6 +116,15 @@ const wslConf = `[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 const overrideSysusers = `[Service] LoadCredential= @@ -198,10 +208,12 @@ http://docs.microsoft.com/en-us/windows/wsl/install\ ` const ( + gvProxy = "gvproxy.exe" winSShProxy = "win-sshproxy.exe" winSshProxyTid = "win-sshproxy.tid" pipePrefix = "npipe:////./pipe/" globalPipe = "docker_engine" + userModeDist = "podman-net-usermode" ) type Virtualization struct { @@ -241,6 +253,8 @@ type MachineVM struct { machine.SSHConfig // machine version Version int + // Whether to use user-mode networking + UserModeNetworking bool } type ExitCodeError struct { @@ -276,6 +290,11 @@ func (p *Virtualization) NewMachine(opts machine.InitOptions) (machine.VM, error vm.Created = time.Now() vm.LastUp = vm.Created + // Default is false + if opts.UserModeNetworking != nil { + vm.UserModeNetworking = *opts.UserModeNetworking + } + // Add a random port for ssh port, err := utils.GetRandomPort() if err != nil { @@ -395,11 +414,19 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) { 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 { return false, err } + if v.UserModeNetworking { + if err = installUserModeDist(dist, v.ImagePath); err != nil { + _ = unregisterDist(dist) + return false, err + } + } + fmt.Println("Configuring system...") if err = configureSystem(v, dist); err != nil { return false, err @@ -496,21 +523,21 @@ func setupConnections(v *MachineVM, opts machine.InitOptions, sshDir string) err return nil } -func provisionWSLDist(v *MachineVM) (string, error) { +func provisionWSLDist(name string, imagePath string, prompt string) (string, error) { vmDataDir, err := machine.GetDataDir(vmtype) if err != nil { return "", err } distDir := filepath.Join(vmDataDir, "wsldist") - distTarget := filepath.Join(distDir, v.Name) + distTarget := filepath.Join(distDir, name) if err := os.MkdirAll(distDir, 0755); err != nil { return "", fmt.Errorf("could not create wsldist directory: %w", err) } - dist := toDist(v.Name) - fmt.Println("Importing operating system into WSL (this may take a few minutes on a new WSL install)...") - if err = runCmdPassThrough("wsl", "--import", dist, distTarget, v.ImagePath, "--version", "2"); err != nil { + dist := toDist(name) + fmt.Println(prompt) + if err = runCmdPassThrough("wsl", "--import", dist, distTarget, imagePath, "--version", "2"); err != nil { 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) } - // 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 } @@ -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) } - 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 + return changeDistUserModeNetworking(dist, user, "", v.UserModeNetworking) } func configureProxy(dist string, useProxy bool, quiet bool) error { @@ -694,8 +708,16 @@ func installScripts(dist string) error { 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) { - if IsWSLInstalled() { + if wutil.IsWSLInstalled() { 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")) } + 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() } @@ -1029,6 +1072,11 @@ func (v *MachineVM) Start(name string, opts machine.StartOptions) error { return err } + // Startup user-mode networking if enabled + if err := v.startUserModeNetworking(); err != nil { + return err + } + err := wslInvoke(dist, "/root/bootstrap") if err != nil { 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 } +func findExecutablePeer(name string) (string, error) { + exe, err := os.Executable() + if err != nil { + return "", err + } + + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + return "", err + } + + return filepath.Join(filepath.Dir(exe), name), nil +} + func launchWinProxy(v *MachineVM) (bool, string, error) { machinePipe := toDist(v.Name) if !machine.PipeNameAvailable(machinePipe) { @@ -1083,17 +1145,11 @@ func launchWinProxy(v *MachineVM) (bool, string, error) { globalName = true } - exe, err := os.Executable() + command, err := findExecutablePeer(winSShProxy) if err != nil { return globalName, "", err } - exe, err = filepath.EvalSymlinks(exe) - if err != nil { - return globalName, "", err - } - - command := filepath.Join(filepath.Dir(exe), winSShProxy) stateDir, err := getWinProxyStateDir(v) if err != nil { return globalName, "", err @@ -1143,59 +1199,54 @@ func getWinProxyStateDir(v *MachineVM) (string, error) { 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 - } +func IsWSLFeatureEnabled() bool { + return wutil.SilentExec("wsl", "--set-default-version", "2") == nil +} - return true +func isWSLRunning(dist string) (bool, error) { + return wslCheckExists(dist, true) } -func IsWSLFeatureEnabled() bool { - return SilentExec("wsl", "--set-default-version", "2") == nil +func isWSLExist(dist string) (bool, error) { + return wslCheckExists(dist, false) } -func isWSLRunning(dist string) (bool, error) { - cmd := exec.Command("wsl", "-l", "--running", "--quiet") - out, err := cmd.StdoutPipe() +func wslCheckExists(dist string, running bool) (bool, error) { + all, err := getAllWSLDistros(running) if 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 false, err + return nil, err } + + all := make(map[string]struct{}) scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())) - result := false for scanner.Scan() { fields := strings.Fields(scanner.Text()) - if len(fields) > 0 && dist == fields[0] { - result = true - break + if len(fields) > 0 { + all[fields[0]] = struct{}{} } } _ = cmd.Wait() - return result, nil + return all, nil } 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) } + // 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) if err := stopWinProxy(v); err != nil { @@ -1272,6 +1328,11 @@ func terminateDist(dist string) error { 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) { if v.isRunning() { return machine.Running, nil @@ -1470,6 +1531,7 @@ func GetVMInfos() ([]*machine.ListResponse, error) { listEntry.Port = vm.Port listEntry.IdentityPath = vm.IdentityPath listEntry.Starting = false + listEntry.UserModeNetworking = vm.UserModeNetworking running := vm.isRunning() listEntry.CreatedAt, listEntry.LastUp, _ = vm.updateTimeStamps(running) @@ -1624,11 +1686,12 @@ func (v *MachineVM) Inspect() (*machine.InspectInfo, error) { ImagePath: machine.VMFile{Path: v.ImagePath}, ImageStream: v.ImageStream, }, - LastUp: lastUp, - Name: v.Name, - Resources: v.getResources(), - SSHConfig: v.SSHConfig, - State: state, + LastUp: lastUp, + Name: v.Name, + Resources: v.getResources(), + SSHConfig: v.SSHConfig, + State: state, + UserModeNetworking: v.UserModeNetworking, }, nil } diff --git a/pkg/machine/wsl/usermodenet.go b/pkg/machine/wsl/usermodenet.go new file mode 100644 index 0000000000..798c248a16 --- /dev/null +++ b/pkg/machine/wsl/usermodenet.go @@ -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 +} diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go index 8672f2593f..fac85f9b90 100644 --- a/pkg/machine/wsl/util_windows.go +++ b/pkg/machine/wsl/util_windows.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "os" - "os/exec" "path/filepath" "strings" "syscall" @@ -345,17 +344,3 @@ func sendQuit(tid uint32) { postMessage := user32.NewProc("PostThreadMessageW") 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 -} diff --git a/pkg/machine/wsl/wutil/wutil.go b/pkg/machine/wsl/wutil/wutil.go new file mode 100644 index 0000000000..b1fb02afd5 --- /dev/null +++ b/pkg/machine/wsl/wutil/wutil.go @@ -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 +}