Skip to content

Commit

Permalink
Merge pull request #1220 from AkihiroSuda/socket_vmnet_path
Browse files Browse the repository at this point in the history
vmnet: support detecting Homebrew's socket_vmnet path
  • Loading branch information
AkihiroSuda authored Dec 12, 2022
2 parents 142c5fd + 9d822a2 commit 57beb9f
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 31 deletions.
35 changes: 19 additions & 16 deletions docs/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<details>
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions examples/vmnet.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
55 changes: 51 additions & 4 deletions pkg/networks/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,64 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"

"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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions pkg/networks/sudoers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
48 changes: 40 additions & 8 deletions pkg/networks/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"

"github.com/lima-vm/lima/pkg/osutil"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 57beb9f

Please sign in to comment.