Skip to content

Commit 57beb9f

Browse files
authored
Merge pull request #1220 from AkihiroSuda/socket_vmnet_path
vmnet: support detecting Homebrew's socket_vmnet path
2 parents 142c5fd + 9d822a2 commit 57beb9f

File tree

6 files changed

+122
-31
lines changed

6 files changed

+122
-31
lines changed

docs/network.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,23 @@ The configuration steps are different across QEMU and VZ:
5353
### QEMU
5454
#### Managed (192.168.105.0/24)
5555

56-
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)
57-
is required for adding another guest IP that is accessible from the host and other guests.
56+
[`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.
5857

59-
Starting with version v0.7.0 lima can manage the networking daemons automatically. Networks are defined in
60-
`$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default
58+
```bash
59+
# Install socket_vmnet
60+
brew install socket_vmnet
61+
62+
# Set up the sudoers file for launching socket_vmnet from Lima
63+
limactl sudoers >etc_sudoers.d_lima
64+
sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
65+
```
66+
67+
> **Note**
68+
>
69+
> Lima before v0.12 used `vde_vmnet` for managing the networks.
70+
> `vde_vmnet` is still supported but it is deprecated and no longer documented here.
71+
72+
The networks are defined in `$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default
6173
settings:
6274

6375
<details>
@@ -114,9 +126,8 @@ Instances can then reference these networks from their `lima.yaml` file:
114126

115127
```yaml
116128
networks:
117-
# Lima can manage daemons for networks defined in $LIMA_HOME/_config/networks.yaml
118-
# automatically. The socket_vmnet must be installed into
119-
# secure locations only alterable by the "root" user.
129+
# Lima can manage the socket_vmnet daemon for networks defined in $LIMA_HOME/_config/networks.yaml automatically.
130+
# The socket_vmnet binary must be installed into a secure location only alterable by the admin.
120131
# The same applies to vde_switch and vde_vmnet for the deprecated VDE mode.
121132
# - lima: shared
122133
# # MAC address of the instance; lima will pick one based on the instance name,
@@ -126,18 +137,10 @@ networks:
126137
# interface: ""
127138
```
128139

129-
The network daemons are started automatically when the first instance referencing them is started,
140+
The network daemon is started automatically when the first instance referencing them is started,
130141
and will stop automatically once the last instance has stopped. Daemon logs will be stored in the
131142
`$LIMA_HOME/_networks` directory.
132143

133-
Since the commands to start and stop the `socket_vmnet` daemon (or the `vde_vmnet` daemon) requires root, the user either must
134-
have password-less `sudo` enabled, or add the required commands to a `sudoers` file. This can
135-
be done via:
136-
137-
```shell
138-
limactl sudoers | sudo tee /etc/sudoers.d/lima
139-
```
140-
141144
#### Unmanaged
142145
For Lima >= 0.12:
143146
```yaml

examples/vmnet.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Example to enable vmnet.framework for QEMU.
22
# VZ users should refer to experimental/vz.yaml
33

4+
# Usage:
5+
# brew install socket_vmnet
6+
# limactl sudoers >etc_sudoers.d_lima
7+
# sudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima
8+
# limactl start template://vmnet
9+
410
# This example requires Lima v0.7.0 or later.
511
# Older versions of Lima were using a different syntax for supporting vmnet.framework.
612
images:

pkg/networks/config.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,64 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"runtime"
1011
"sync"
1112

1213
"github.com/goccy/go-yaml"
1314
"github.com/lima-vm/lima/pkg/store/dirnames"
1415
"github.com/lima-vm/lima/pkg/store/filenames"
16+
"github.com/lima-vm/lima/pkg/textutil"
17+
"github.com/sirupsen/logrus"
1518
)
1619

17-
//go:embed networks.yaml
18-
var defaultConfig []byte
20+
//go:embed networks.TEMPLATE.yaml
21+
var defaultConfigTemplate string
22+
23+
type defaultConfigTemplateArgs struct {
24+
SocketVMNet string // "/opt/socket_vmnet/bin/socket_vmnet"
25+
}
26+
27+
func defaultConfigBytes() ([]byte, error) {
28+
var args defaultConfigTemplateArgs
29+
candidates := []string{
30+
"/opt/socket_vmnet/bin/socket_vmnet", // the hard-coded path before v0.14
31+
"socket_vmnet",
32+
"/usr/local/opt/socket_vmnet/bin/socket_vmnet", // Homebrew (Intel)
33+
"/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet", // Homebrew (ARM)
34+
}
35+
for _, candidate := range candidates {
36+
if p, err := exec.LookPath(candidate); err == nil {
37+
realP, evalErr := filepath.EvalSymlinks(p)
38+
if evalErr != nil {
39+
return nil, evalErr
40+
}
41+
args.SocketVMNet = realP
42+
break
43+
} else if errors.Is(err, exec.ErrNotFound) || errors.Is(err, os.ErrNotExist) {
44+
logrus.WithError(err).Debugf("Failed to look up socket_vmnet path %q", candidate)
45+
} else {
46+
logrus.WithError(err).Warnf("Failed to look up socket_vmnet path %q", candidate)
47+
}
48+
}
49+
if args.SocketVMNet == "" {
50+
args.SocketVMNet = candidates[0] // the hard-coded path before v0.14
51+
}
52+
return textutil.ExecuteTemplate(defaultConfigTemplate, args)
53+
}
1954

2055
func DefaultConfig() (YAML, error) {
2156
var config YAML
22-
err := yaml.UnmarshalWithOptions(defaultConfig, &config, yaml.Strict())
23-
return config, err
57+
defaultConfig, err := defaultConfigBytes()
58+
if err != nil {
59+
return config, err
60+
}
61+
err = yaml.UnmarshalWithOptions(defaultConfig, &config, yaml.Strict())
62+
if err != nil {
63+
return config, err
64+
}
65+
return config, nil
2466
}
2567

2668
var cache struct {
@@ -56,6 +98,11 @@ func loadCache() {
5698
cache.err = fmt.Errorf("could not create %q directory: %w", configDir, cache.err)
5799
return
58100
}
101+
var defaultConfig []byte
102+
defaultConfig, cache.err = defaultConfigBytes()
103+
if cache.err != nil {
104+
return
105+
}
59106
cache.err = os.WriteFile(configFile, defaultConfig, 0644)
60107
if cache.err != nil {
61108
return

pkg/networks/networks.yaml renamed to pkg/networks/networks.TEMPLATE.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
paths:
1313
# socketVMNet requires Lima >= 0.12 .
1414
# socketVMNet has precedence over vdeVMNet.
15-
socketVMNet: /opt/socket_vmnet/bin/socket_vmnet
15+
socketVMNet: "{{.SocketVMNet}}"
1616
# vdeSwitch and vdeVMNet are DEPRECATED.
1717
vdeSwitch: /opt/vde/bin/vde_switch
1818
vdeVMNet: /opt/vde/bin/vde_vmnet

pkg/networks/sudoers.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ func (config *YAML) VerifySudoAccess(sudoersFile string) error {
8585
}
8686
return fmt.Errorf("passwordLessSudo error: %w", err)
8787
}
88+
hint := fmt.Sprintf("run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)",
89+
os.Args[0], sudoersFile)
8890
b, err := os.ReadFile(sudoersFile)
8991
if err != nil {
9092
// 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 {
9799
}
98100
logrus.Debugf("%q does not exist; passwordLessSudo error: %s", sudoersFile, err)
99101
}
100-
return fmt.Errorf("can't read %q: %s", sudoersFile, err)
102+
return fmt.Errorf("can't read %q: %s (Hint: %s)", sudoersFile, err, hint)
101103
}
102104
sudoers, err := Sudoers()
103105
if err != nil {
104106
return err
105107
}
106108
if string(b) != sudoers {
107-
return fmt.Errorf("sudoers file %q is out of sync and must be regenerated", sudoersFile)
109+
// Happens on upgrading socket_vmnet with Homebrew
110+
return fmt.Errorf("sudoers file %q is out of sync and must be regenerated (Hint: %s)", sudoersFile, hint)
108111
}
109112
return nil
110113
}

pkg/networks/validate.go

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import (
55
"fmt"
66
"io/fs"
77
"os"
8+
"os/user"
89
"path/filepath"
910
"reflect"
11+
"runtime"
12+
"strconv"
1013
"strings"
1114

1215
"github.com/lima-vm/lima/pkg/osutil"
@@ -98,28 +101,57 @@ func validatePath(path string, allowDaemonGroupWritable bool) error {
98101
// should never happen
99102
return fmt.Errorf("could not retrieve stat buffer for %q", path)
100103
}
104+
if runtime.GOOS != "darwin" {
105+
return fmt.Errorf("vmnet code must not be called on non-Darwin") // TODO: move to *_darwin.go
106+
}
107+
// TODO: cache looked up UIDs/GIDs
101108
root, err := osutil.LookupUser("root")
102109
if err != nil {
103110
return err
104111
}
105-
if stat.Uid != root.Uid {
106-
return fmt.Errorf(`%s %q is not owned by %q (uid: %d), but by uid %d`, file, path, root.User, root.Uid, stat.Uid)
112+
adminGroup, err := user.LookupGroup("admin")
113+
if err != nil {
114+
return err
115+
}
116+
adminGid, err := strconv.Atoi(adminGroup.Gid)
117+
if err != nil {
118+
return err
119+
}
120+
owner, err := user.LookupId(strconv.Itoa(int(stat.Uid)))
121+
if err != nil {
122+
return err
123+
}
124+
ownerIsAdmin := owner.Uid == "0"
125+
if !ownerIsAdmin {
126+
ownerGroupIds, err := owner.GroupIds()
127+
if err != nil {
128+
return err
129+
}
130+
for _, g := range ownerGroupIds {
131+
if g == adminGroup.Gid {
132+
ownerIsAdmin = true
133+
break
134+
}
135+
}
136+
}
137+
if !ownerIsAdmin {
138+
return fmt.Errorf(`%s %q owner %dis not an admin`, file, path, stat.Uid)
107139
}
108140
if allowDaemonGroupWritable {
109141
daemon, err := osutil.LookupUser("daemon")
110142
if err != nil {
111143
return err
112144
}
113-
if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != daemon.Gid {
114-
return fmt.Errorf(`%s %q is group-writable and group is neither %q (gid: %d) nor %q (gid: %d), but is gid: %d`,
115-
file, path, root.User, root.Gid, daemon.User, daemon.Gid, stat.Gid)
145+
if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) && stat.Gid != daemon.Gid {
146+
return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin, daemon]`,
147+
file, path, stat.Gid)
116148
}
117149
if fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0010 == 0 || stat.Gid != daemon.Gid) {
118150
return fmt.Errorf(`%s %q is not executable by the %q (gid: %d)" group`, file, path, daemon.User, daemon.Gid)
119151
}
120-
} else if fi.Mode()&020 != 0 && stat.Gid != root.Gid {
121-
return fmt.Errorf(`%s %q is group-writable and group is not %q (gid: %d), but is gid: %d`,
122-
file, path, root.User, root.Gid, stat.Gid)
152+
} else if fi.Mode()&020 != 0 && stat.Gid != root.Gid && stat.Gid != uint32(adminGid) {
153+
return fmt.Errorf(`%s %q is group-writable and group %d is not one of [wheel, admin]`,
154+
file, path, stat.Gid)
123155
}
124156
if fi.Mode()&002 != 0 {
125157
return fmt.Errorf("%s %q is world-writable", file, path)

0 commit comments

Comments
 (0)