From 9d822a291e0079c7fa7e2af37ceea74b9a9ed409 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 1 Dec 2022 19:48:05 +0900 Subject: [PATCH] vmnet: support detecting Homebrew's socket_vmnet path On Homebrew environments, socket_vmnet is installed on: - /usr/local/opt/socket_vmnet/Cellar//bin/socket_vmnet (Intel) - /opt/homebrew/opt/socket_vmnet/Cellar//bin/socket_vmnet (ARM) The binary is usually owned by a non-root admin user. (i.e., the homebrew user) The group is typically set to "admin". https://github.com/Homebrew/homebrew-core/commit/636ac0700e011b23c3e41cc4176b30e01a3af5ae Signed-off-by: Akihiro Suda --- docs/network.md | 35 ++++++------ examples/vmnet.yaml | 6 ++ pkg/networks/config.go | 55 +++++++++++++++++-- .../{networks.yaml => networks.TEMPLATE.yaml} | 2 +- pkg/networks/sudoers.go | 7 ++- pkg/networks/validate.go | 48 +++++++++++++--- 6 files changed, 122 insertions(+), 31 deletions(-) rename pkg/networks/{networks.yaml => networks.TEMPLATE.yaml} (96%) diff --git a/docs/network.md b/docs/network.md index f4d799e1303..c51f82672c0 100644 --- a/docs/network.md +++ b/docs/network.md @@ -53,11 +53,23 @@ The configuration steps are different across QEMU and VZ: ### QEMU #### Managed (192.168.105.0/24) -Either [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) (since Lima v0.12) or [`vde_vmnet`](https://github.com/lima-vm/vde_vmnet) (Deprecated) -is required for adding another guest IP that is accessible from the host and other guests. +[`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) is required for adding another guest IP that is accessible from the host and other guests. -Starting with version v0.7.0 lima can manage the networking daemons automatically. Networks are defined in -`$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default +```bash +# Install socket_vmnet +brew install socket_vmnet + +# Set up the sudoers file for launching socket_vmnet from Lima +limactl sudoers >etc_sudoers.d_lima +sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima +``` + +> **Note** +> +> Lima before v0.12 used `vde_vmnet` for managing the networks. +> `vde_vmnet` is still supported but it is deprecated and no longer documented here. + +The networks are defined in `$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default settings:
@@ -114,9 +126,8 @@ Instances can then reference these networks from their `lima.yaml` file: ```yaml networks: - # Lima can manage daemons for networks defined in $LIMA_HOME/_config/networks.yaml - # automatically. The socket_vmnet must be installed into - # secure locations only alterable by the "root" user. + # Lima can manage the socket_vmnet daemon for networks defined in $LIMA_HOME/_config/networks.yaml automatically. + # The socket_vmnet binary must be installed into a secure location only alterable by the admin. # The same applies to vde_switch and vde_vmnet for the deprecated VDE mode. # - lima: shared # # MAC address of the instance; lima will pick one based on the instance name, @@ -126,18 +137,10 @@ networks: # interface: "" ``` -The network daemons are started automatically when the first instance referencing them is started, +The network daemon is started automatically when the first instance referencing them is started, and will stop automatically once the last instance has stopped. Daemon logs will be stored in the `$LIMA_HOME/_networks` directory. -Since the commands to start and stop the `socket_vmnet` daemon (or the `vde_vmnet` daemon) requires root, the user either must -have password-less `sudo` enabled, or add the required commands to a `sudoers` file. This can -be done via: - -```shell -limactl sudoers | sudo tee /etc/sudoers.d/lima -``` - #### Unmanaged For Lima >= 0.12: ```yaml diff --git a/examples/vmnet.yaml b/examples/vmnet.yaml index c0f96226f44..aae6cf8d42b 100644 --- a/examples/vmnet.yaml +++ b/examples/vmnet.yaml @@ -1,6 +1,12 @@ # Example to enable vmnet.framework for QEMU. # VZ users should refer to experimental/vz.yaml +# Usage: +# brew install socket_vmnet +# limactl sudoers >etc_sudoers.d_lima +# sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima +# limactl start template://vmnet + # This example requires Lima v0.7.0 or later. # Older versions of Lima were using a different syntax for supporting vmnet.framework. images: diff --git a/pkg/networks/config.go b/pkg/networks/config.go index 657a88bb29f..e9072413051 100644 --- a/pkg/networks/config.go +++ b/pkg/networks/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "runtime" "sync" @@ -12,15 +13,56 @@ import ( "github.com/goccy/go-yaml" "github.com/lima-vm/lima/pkg/store/dirnames" "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/lima-vm/lima/pkg/textutil" + "github.com/sirupsen/logrus" ) -//go:embed networks.yaml -var defaultConfig []byte +//go:embed networks.TEMPLATE.yaml +var defaultConfigTemplate string + +type defaultConfigTemplateArgs struct { + SocketVMNet string // "/opt/socket_vmnet/bin/socket_vmnet" +} + +func defaultConfigBytes() ([]byte, error) { + var args defaultConfigTemplateArgs + candidates := []string{ + "/opt/socket_vmnet/bin/socket_vmnet", // the hard-coded path before v0.14 + "socket_vmnet", + "/usr/local/opt/socket_vmnet/bin/socket_vmnet", // Homebrew (Intel) + "/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet", // Homebrew (ARM) + } + for _, candidate := range candidates { + if p, err := exec.LookPath(candidate); err == nil { + realP, evalErr := filepath.EvalSymlinks(p) + if evalErr != nil { + return nil, evalErr + } + args.SocketVMNet = realP + break + } else if errors.Is(err, exec.ErrNotFound) || errors.Is(err, os.ErrNotExist) { + logrus.WithError(err).Debugf("Failed to look up socket_vmnet path %q", candidate) + } else { + logrus.WithError(err).Warnf("Failed to look up socket_vmnet path %q", candidate) + } + } + if args.SocketVMNet == "" { + args.SocketVMNet = candidates[0] // the hard-coded path before v0.14 + } + return textutil.ExecuteTemplate(defaultConfigTemplate, args) +} func DefaultConfig() (YAML, error) { var config YAML - err := yaml.UnmarshalWithOptions(defaultConfig, &config, yaml.Strict()) - return config, err + defaultConfig, err := defaultConfigBytes() + if err != nil { + return config, err + } + err = yaml.UnmarshalWithOptions(defaultConfig, &config, yaml.Strict()) + if err != nil { + return config, err + } + return config, nil } var cache struct { @@ -56,6 +98,11 @@ func loadCache() { cache.err = fmt.Errorf("could not create %q directory: %w", configDir, cache.err) return } + var defaultConfig []byte + defaultConfig, cache.err = defaultConfigBytes() + if cache.err != nil { + return + } cache.err = os.WriteFile(configFile, defaultConfig, 0644) if cache.err != nil { return diff --git a/pkg/networks/networks.yaml b/pkg/networks/networks.TEMPLATE.yaml similarity index 96% rename from pkg/networks/networks.yaml rename to pkg/networks/networks.TEMPLATE.yaml index 68e9c9f9b5c..0032e005910 100644 --- a/pkg/networks/networks.yaml +++ b/pkg/networks/networks.TEMPLATE.yaml @@ -12,7 +12,7 @@ paths: # socketVMNet requires Lima >= 0.12 . # socketVMNet has precedence over vdeVMNet. - socketVMNet: /opt/socket_vmnet/bin/socket_vmnet + socketVMNet: "{{.SocketVMNet}}" # vdeSwitch and vdeVMNet are DEPRECATED. vdeSwitch: /opt/vde/bin/vde_switch vdeVMNet: /opt/vde/bin/vde_vmnet diff --git a/pkg/networks/sudoers.go b/pkg/networks/sudoers.go index 3f023d12fd7..11d5524de02 100644 --- a/pkg/networks/sudoers.go +++ b/pkg/networks/sudoers.go @@ -85,6 +85,8 @@ func (config *YAML) VerifySudoAccess(sudoersFile string) error { } return fmt.Errorf("passwordLessSudo error: %w", err) } + hint := fmt.Sprintf("run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)", + os.Args[0], sudoersFile) b, err := os.ReadFile(sudoersFile) if err != nil { // Default networks.yaml specifies /etc/sudoers.d/lima file. Don't throw an error when the @@ -97,14 +99,15 @@ func (config *YAML) VerifySudoAccess(sudoersFile string) error { } logrus.Debugf("%q does not exist; passwordLessSudo error: %s", sudoersFile, err) } - return fmt.Errorf("can't read %q: %s", sudoersFile, err) + return fmt.Errorf("can't read %q: %s (Hint: %s)", sudoersFile, err, hint) } sudoers, err := Sudoers() if err != nil { return err } if string(b) != sudoers { - return fmt.Errorf("sudoers file %q is out of sync and must be regenerated", sudoersFile) + // Happens on upgrading socket_vmnet with Homebrew + return fmt.Errorf("sudoers file %q is out of sync and must be regenerated (Hint: %s)", sudoersFile, hint) } return nil } diff --git a/pkg/networks/validate.go b/pkg/networks/validate.go index a8d41c33060..ed14c52c885 100644 --- a/pkg/networks/validate.go +++ b/pkg/networks/validate.go @@ -5,8 +5,11 @@ import ( "fmt" "io/fs" "os" + "os/user" "path/filepath" "reflect" + "runtime" + "strconv" "strings" "github.com/lima-vm/lima/pkg/osutil" @@ -98,28 +101,57 @@ func validatePath(path string, allowDaemonGroupWritable bool) error { // should never happen return fmt.Errorf("could not retrieve stat buffer for %q", path) } + if runtime.GOOS != "darwin" { + return fmt.Errorf("vmnet code must not be called on non-Darwin") // TODO: move to *_darwin.go + } + // TODO: cache looked up UIDs/GIDs root, err := osutil.LookupUser("root") if err != nil { return err } - if stat.Uid != root.Uid { - return fmt.Errorf(`%s %q is not owned by %q (uid: %d), but by uid %d`, file, path, root.User, root.Uid, stat.Uid) + adminGroup, err := user.LookupGroup("admin") + if err != nil { + return err + } + adminGid, err := strconv.Atoi(adminGroup.Gid) + if err != nil { + return err + } + owner, err := user.LookupId(strconv.Itoa(int(stat.Uid))) + if err != nil { + return err + } + ownerIsAdmin := owner.Uid == "0" + if !ownerIsAdmin { + ownerGroupIds, err := owner.GroupIds() + if err != nil { + return err + } + for _, g := range ownerGroupIds { + if g == adminGroup.Gid { + ownerIsAdmin = true + break + } + } + } + if !ownerIsAdmin { + return fmt.Errorf(`%s %q owner %dis not an admin`, file, path, stat.Uid) } if allowDaemonGroupWritable { daemon, err := osutil.LookupUser("daemon") if err != nil { return err } - if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != daemon.Gid { - return fmt.Errorf(`%s %q is group-writable and group is neither %q (gid: %d) nor %q (gid: %d), but is gid: %d`, - file, path, root.User, root.Gid, daemon.User, daemon.Gid, stat.Gid) + if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) && stat.Gid != daemon.Gid { + return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin, daemon]`, + file, path, stat.Gid) } if fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0010 == 0 || stat.Gid != daemon.Gid) { return fmt.Errorf(`%s %q is not executable by the %q (gid: %d)" group`, file, path, daemon.User, daemon.Gid) } - } else if fi.Mode()&020 != 0 && stat.Gid != root.Gid { - return fmt.Errorf(`%s %q is group-writable and group is not %q (gid: %d), but is gid: %d`, - file, path, root.User, root.Gid, stat.Gid) + } else if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) { + return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin]`, + file, path, stat.Gid) } if fi.Mode()&002 != 0 { return fmt.Errorf("%s %q is world-writable", file, path)