From b5e0d5abd0fb2f74b7ddf8faea7a855b5a14ceda Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 10 May 2022 19:19:38 +0900 Subject: [PATCH] vmnet: Support socket_vmnet; deprecate vde_vmnet socket_vmnet is similar to vde_vmnet but does not depend on VDE. https://github.com/lima-vm/socket_vmnet See docs/network.md for how to create networks.yaml with socketVMNet. When both socketVMNet and vdeVMNet (deprecated) are present in the YAML, socketVMNet is chosen. Signed-off-by: Akihiro Suda --- .github/workflows/test.yml | 85 ++++++++++++++++++------ README.md | 3 +- docs/network.md | 73 ++++++++++++--------- examples/default.yaml | 12 ++-- examples/vmnet.yaml | 9 ++- hack/test-example.sh | 20 ++++++ pkg/hostagent/hostagent.go | 68 +++++++++++++++++++- pkg/limayaml/defaults.go | 40 ++++++++---- pkg/limayaml/defaults_test.go | 12 ++-- pkg/limayaml/limayaml.go | 15 +++-- pkg/limayaml/validate.go | 33 +++++++--- pkg/networks/commands.go | 95 ++++++++++++++++++++++++--- pkg/networks/commands_darwin_test.go | 8 +++ pkg/networks/commands_test.go | 96 ++++++++++++++++++++-------- pkg/networks/config.go | 21 ++++++ pkg/networks/networks.go | 9 +-- pkg/networks/networks.yaml | 10 ++- pkg/networks/reconcile/reconcile.go | 22 ++++++- pkg/networks/sudoers.go | 14 +++- pkg/networks/validate.go | 27 +++++++- pkg/qemu/qemu.go | 49 ++++++++++---- 21 files changed, 567 insertions(+), 154 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 379fcd4e27f..145f43db434 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -105,22 +105,6 @@ jobs: time brew update time brew install qemu bash coreutils curl jq time brew upgrade - - name: Install vde_switch and vde_vmnet - env: - VDE_VMNET_VERSION: v0.6.0 - run: | - ( - brew install autoconf automake - cd ~ - git clone https://github.com/lima-vm/vde_vmnet - cd vde_vmnet - git checkout $VDE_VMNET_VERSION - sudo git config --global --add safe.directory /Users/runner/vde_vmnet - sudo make PREFIX=/opt/vde install - ) - ( - limactl sudoers | sudo tee /etc/sudoers.d/lima - ) - name: Cache ~/Library/Caches/lima/download uses: actions/cache@v3 with: @@ -147,15 +131,78 @@ jobs: retry_on: error max_attempts: 3 command: ./hack/test-example.sh examples/experimental/9p.yaml - - name: "Test vmnet.yaml" + # GHA macOS is slow and flaky, so we only test a few YAMLS here. + # Other yamls are tested on Linux instances of Cirrus. + + vmnet: + name: "VMNet test" + runs-on: macos-11 + timeout-minutes: 120 + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.19.x + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Make + run: make + - name: Install + run: make install + - name: Install test dependencies + run: brew install qemu bash coreutils iperf3 + - name: Cache ~/Library/Caches/lima/download + uses: actions/cache@v3 + with: + path: ~/Library/Caches/lima/download + key: ${{ runner.os }}-vmnet + - name: Install vde_switch and vde_vmnet (Deprecated) + env: + VDE_VMNET_VERSION: v0.6.0 + run: | + ( + brew install autoconf automake + cd ~ + git clone https://github.com/lima-vm/vde_vmnet + cd vde_vmnet + git checkout $VDE_VMNET_VERSION + sudo git config --global --add safe.directory /Users/runner/vde_vmnet + sudo make PREFIX=/opt/vde install + ) + limactl sudoers | sudo tee /etc/sudoers.d/lima + - name: Unit test (pkg/networks) with vde_vmnet (Deprecated) + # Set -count=1 to disable cache + run: go test -v -count=1 ./pkg/networks/... + - name: Test vde_vmnet (Deprecated) + uses: nick-invision/retry@v2 + with: + timeout_minutes: 30 + retry_on: error + max_attempts: 3 + command: ./hack/test-example.sh examples/vmnet.yaml + - name: Install socket_vmnet + env: + SOCKET_VMNET_VERSION: v1.0.0-alpha.0 + run: | + ( + cd ~ + git clone https://github.com/lima-vm/socket_vmnet + cd socket_vmnet + git checkout $SOCKET_VMNET_VERSION + sudo git config --global --add safe.directory /Users/runner/socket_vmnet + sudo make PREFIX=/opt/socket_vmnet install + ) + limactl sudoers | sudo tee /etc/sudoers.d/lima + - name: Unit test (pkg/networks) with socket_vmnet + # Set -count=1 to disable cache + run: go test -v -count=1 ./pkg/networks/... + - name: Test socket_vmnet uses: nick-invision/retry@v2 with: timeout_minutes: 30 retry_on: error max_attempts: 3 command: ./hack/test-example.sh examples/vmnet.yaml - # GHA macOS is slow and flaky, so we only test a few YAMLS here. - # Other yamls are tested on Linux instances of Cirrus. upgrade: name: "Upgrade test" diff --git a/README.md b/README.md index f32c7b7e487..492c094a19c 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,8 @@ The `copy` command only works for instances that have been created by lima 0.5.0 The default guest IP 192.168.5.15 is not accessible from the host and other guests. -To add another IP address that is accessible from the host and other virtual machines, enable [`vde_vmnet`](https://github.com/lima-vm/vde_vmnet). +To add another IP address that is accessible from the host and other virtual machines, enable [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) (since Lima v0.12) +or [`vde_vmnet`](https://github.com/lima-vm/vde_vmnet) (Deprecated). See [`./docs/network.md`](./docs/network.md). diff --git a/docs/network.md b/docs/network.md index c81ac0c4687..69f2bc18495 100644 --- a/docs/network.md +++ b/docs/network.md @@ -10,7 +10,7 @@ The guest IP address is set to `192.168.5.15`. This IP address is not accessible from the host by design. -Use [vde_vmnet](https://github.com/lima-vm/vde_vmnet) to allow accessing the guest IP from the host and other guests. +Use VMNet (see below) to allow accessing the guest IP from the host and other guests. ### Host IP (192.168.5.2) @@ -42,50 +42,32 @@ During initial cloud-init bootstrap, `iptables` may not yet be installed. In tha If `useHostResolver` is false, then DNS servers can be configured manually in `lima.yaml` via the `dns` setting. If that list is empty, then Lima will either use the slirp DNS (on Linux), or the nameservers from the first host interface in service order that has an assigned IPv4 address (on macOS). -## `vde_vmnet` (192.168.105.0/24) +## Managed VMNet networks (192.168.105.0/24) -[`vde_vmnet`](https://github.com/lima-vm/vde_vmnet) is required for adding another guest IP that is accessible from -the host and other guests. - -To enable `vde_vmnet` (in addition the user-mode network), add the following lines to the YAML after installing `vde_vmnet`. - -```yaml -networks: - # vnl (virtual network locator) points to the vde_switch socket directory, - # optionally with vde:// prefix - # - vnl: "vde:///var/run/vde.ctl" - # # VDE Switch port number (not TCP/UDP port number). Set to 65535 for PTP mode. - # # Default: 0 - # switchPort: 0 - # # MAC address of the instance; lima will pick one based on the instance name, - # # so DHCP assigned ip addresses should remain constant over instance restarts. - # macAddress: "" - # # Interface name, defaults to "lima0", "lima1", etc. - # interface: "" -``` - -The IP address range is typically `192.168.105.0/24`, but depends on the configuration of `vde_vmnet`. -See [the documentation of `vde_vmnet`](https://github.com/lima-vm/vde_vmnet) for further information. - -## Managed VMNet networks (via vde_vmnet) +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. 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 settings: ```yaml -# Paths to vde executables. Because vde_vmnet is invoked via sudo it should be +# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be # installed where only root can modify/replace it. This means also none of the # parent directories should be writable by the user. # # The varRun directory also must not be writable by the user because it will -# include the vde_vmnet pid files. Those will be terminated via sudo, so replacing -# the pid files would allow killing of arbitrary privileged processes. varRun +# include the socket_vmnet pid file. Those will be terminated via sudo, so replacing +# the pid file would allow killing of arbitrary privileged processes. varRun # however MUST be writable by the daemon user. # # None of the paths segments may be symlinks, why it has to be /private/var # instead of /var etc. paths: +# socketVMNet requires Lima >= 0.12 . +# socketVMNet has precedence over vdeVMNet. + socketVMNet: /opt/socket_vmnet/bin/socket_vmnet +# vdeSwitch and vdeVMNet are DEPRECATED. vdeSwitch: /opt/vde/bin/vde_switch vdeVMNet: /opt/vde/bin/vde_vmnet varRun: /private/var/run/lima @@ -115,8 +97,9 @@ 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. Both vde_switch and vde_vmnet binaries must be installed into + # automatically. The socket_vmnet must be installed into # secure locations only alterable by the "root" user. + # 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, # # so DHCP assigned ip addresses should remain constant over instance restarts. @@ -129,10 +112,38 @@ The network daemons are started automatically when the first instance referencin 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 `vde_vmnet` daemon requires root, the user either must +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 VMNet networks +For Lima >= 0.12: +```yaml +networks: + # Lima can also connect to "unmanaged" networks addressed by "socket". This + # means that the daemons will not be controlled by Lima, but must be started + # before the instance. The interface type (host, shared, or bridged) is + # configured in socket_vmnet and not in lima. + # - socket: "/var/run/socket_vmnet" +``` + +For older Lima releases: +```yaml +networks: + # vnl (virtual network locator) points to the vde_switch socket directory, + # optionally with vde:// prefix + # ⚠️ vnl is deprecated, use socket. + # - vnl: "vde:///var/run/vde.ctl" + # # VDE Switch port number (not TCP/UDP port number). Set to 65535 for PTP mode. + # # Builtin default: 0 + # switchPort: 0 + # # MAC address of the instance; lima will pick one based on the instance name, + # # so DHCP assigned ip addresses should remain constant over instance restarts. + # macAddress: "" + # # Interface name, defaults to "lima0", "lima1", etc. + # interface: "" +``` diff --git a/examples/default.yaml b/examples/default.yaml index f3750af6215..7512df66ab0 100644 --- a/examples/default.yaml +++ b/examples/default.yaml @@ -217,12 +217,13 @@ video: display: null # The instance can get routable IP addresses from the vmnet framework using -# https://github.com/lima-vm/vde_vmnet. +# https://github.com/lima-vm/socket_vmnet. # 🟢 Builtin default: null networks: # Lima can manage daemons for networks defined in $LIMA_HOME/_config/networks.yaml -# automatically. Both vde_switch and vde_vmnet binaries must be installed into +# automatically. The socket_vmnet binary must be installed into # secure locations only alterable by the "root" user. +# 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, # # so DHCP assigned ip addresses should remain constant over instance restarts. @@ -230,12 +231,15 @@ networks: # # Interface name, defaults to "lima0", "lima1", etc. # interface: "" # -# Lima can also connect to "unmanaged" vde networks addressed by "vnl". This +# Lima can also connect to "unmanaged" networks addressed by "socket". This # means that the daemons will not be controlled by Lima, but must be started # before the instance. The interface type (host, shared, or bridged) is -# configured in vde_vmnet and not in lima. +# configured in socket_vmnet and not in lima. +# - socket: "/var/run/socket_vmnet" + # vnl (virtual network locator) points to the vde_switch socket directory, # optionally with vde:// prefix +# ⚠️ vnl is deprecated, use socket. # - vnl: "vde:///var/run/vde.ctl" # # VDE Switch port number (not TCP/UDP port number). Set to 65535 for PTP mode. # # Builtin default: 0 diff --git a/examples/vmnet.yaml b/examples/vmnet.yaml index 079b7fa1f08..f3065d866b5 100644 --- a/examples/vmnet.yaml +++ b/examples/vmnet.yaml @@ -21,7 +21,14 @@ mounts: writable: true networks: # The instance can get routable IP addresses from the vmnet framework using -# https://github.com/lima-vm/vde_vmnet. Available networks are defined in +# https://github.com/lima-vm/socket_vmnet (since Lima v0.12) or +# https://github.com/lima-vm/vde_vmnet (deprecated) . +# +# Available networks are defined in # $LIMA_HOME/_config/networks.yaml. Supported network types are "host", # "shared", or "bridged". +# +# Interface "lima0": shared mode (IP is assigned by macOS's bootpd) - lima: shared +# Interface "lima1": bridged mode (IP is assigned by a DHCP server on the physical network) +# - lima: bridged diff --git a/hack/test-example.sh b/hack/test-example.sh index 039f2469756..d30bb56918c 100755 --- a/hack/test-example.sh +++ b/hack/test-example.sh @@ -23,6 +23,7 @@ declare -A CHECKS=( ["containerd-user"]="1" ["restart"]="1" ["port-forwards"]="1" + ["vmnet"]="" ) case "$NAME" in @@ -41,6 +42,9 @@ case "$NAME" in # ● run-r2b459797f5b04262bfa79984077a65c7.service loaded failed failed /usr/bin/systemctl start man-db-cache-update CHECKS["systemd-strict"]= ;; +"vmnet") + CHECKS["vmnet"]=1 + ;; esac if limactl ls -q | grep -q "$NAME"; then @@ -217,6 +221,22 @@ if [[ -n ${CHECKS["port-forwards"]} ]]; then set +x fi +if [[ -n ${CHECKS["vmnet"]} ]]; then + INFO "Testing vmnet functionality" + guestip="$(limactl shell "$NAME" ip -4 -j addr show dev lima0 | jq -r '.[0].addr_info[0].local')" + INFO "Pinging the guest IP ${guestip}" + set -x + ping -c 3 "$guestip" + set +x + INFO "Benchmarking with iperf3" + set -x + limactl shell "$NAME" sudo apt-get install -y iperf3 + limactl shell "$NAME" iperf3 -s -1 -D + iperf3 -c "$guestip" + set +x + # NOTE: we only test the shared interface here, as the bridged interface cannot be used on GHA (and systemd-networkd-wait-online.service will fail) +fi + if [[ -n ${CHECKS["restart"]} ]]; then INFO "Create file in the guest home directory and verify that it still exists after a restart" # shellcheck disable=SC2016 diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 79ae6dbfe38..dc233960604 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -2,6 +2,7 @@ package hostagent import ( "bufio" + "bytes" "context" "encoding/json" "errors" @@ -14,6 +15,7 @@ import ( "strconv" "strings" "sync" + "text/template" "time" "github.com/digitalocean/go-qemu/qmp" @@ -47,7 +49,7 @@ type HostAgent struct { onClose []func() error // LIFO qExe string - qArgs []string + qArgs []string // May contain text templates like "{{ fd_connect /path/to/sock }}" sigintCh chan os.Signal eventEnc *json.Encoder @@ -241,6 +243,58 @@ func logPipeRoutine(r io.Reader, header string) { } } +type qArgTemplateApplier struct { + files []*os.File +} + +func (a *qArgTemplateApplier) applyTemplate(qArg string) (string, error) { + if !strings.Contains(qArg, "{{") { + return qArg, nil + } + funcMap := template.FuncMap{ + "fd_connect": func(v interface{}) string { + fn := func(v interface{}) (string, error) { + s, ok := v.(string) + if !ok { + return "", fmt.Errorf("non-string argument %+v", v) + } + addr := &net.UnixAddr{ + Net: "unix", + Name: s, + } + conn, err := net.DialUnix("unix", nil, addr) + if err != nil { + return "", err + } + f, err := conn.File() + if err != nil { + return "", err + } + if err := conn.Close(); err != nil { + return "", err + } + a.files = append(a.files, f) + fd := len(a.files) + 2 // the first FD is 3 + return strconv.Itoa(fd), nil + } + res, err := fn(v) + if err != nil { + panic(fmt.Errorf("fd_connect: %w", err)) + } + return res + }, + } + tmpl, err := template.New("").Funcs(funcMap).Parse(qArg) + if err != nil { + return "", err + } + var b bytes.Buffer + if err := tmpl.Execute(&b, nil); err != nil { + return "", nil + } + return b.String(), nil +} + func (a *HostAgent) Run(ctx context.Context) error { defer func() { exitingEv := events.Event{ @@ -271,7 +325,17 @@ func (a *HostAgent) Run(ctx context.Context) error { defer dnsServer.Shutdown() } - qCmd := exec.CommandContext(ctx, a.qExe, a.qArgs...) + var qArgs []string + applier := &qArgTemplateApplier{} + for _, unapplied := range a.qArgs { + applied, err := applier.applyTemplate(unapplied) + if err != nil { + return err + } + qArgs = append(qArgs, applied) + } + qCmd := exec.CommandContext(ctx, a.qExe, qArgs...) + qCmd.ExtraFiles = append(qCmd.ExtraFiles, applier.files...) qStdout, err := qCmd.StdoutPipe() if err != nil { return err diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index fa19e5def1a..3a2c2121c08 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -343,10 +343,10 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { if len(y.Network.VDEDeprecated) > 0 && len(y.Networks) == 0 { for _, vde := range y.Network.VDEDeprecated { network := Network{ - Interface: vde.Name, - MACAddress: vde.MACAddress, - SwitchPort: vde.SwitchPort, - VNL: vde.VNL, + Interface: vde.Name, + MACAddress: vde.MACAddress, + SwitchPortDeprecated: vde.SwitchPort, + VNLDeprecated: vde.VNL, } y.Networks = append(y.Networks, network) } @@ -357,20 +357,38 @@ func FillDefault(y, d, o *LimaYAML, filePath string) { iface := make(map[string]int) for _, nw := range append(append(d.Networks, y.Networks...), o.Networks...) { if i, ok := iface[nw.Interface]; ok { - if nw.VNL != "" { - networks[i].VNL = nw.VNL - networks[i].SwitchPort = nw.SwitchPort + if nw.VNLDeprecated != "" { + networks[i].VNLDeprecated = nw.VNLDeprecated + networks[i].SwitchPortDeprecated = nw.SwitchPortDeprecated + networks[i].Socket = "" + networks[i].Lima = "" + } + if nw.Socket != "" { + if nw.VNLDeprecated != "" { + // We can't return an error, so just log it, and prefer `socket` over `vnl` + logrus.Errorf("Network %q has both vnl=%q and socket=%q fields; ignoring vnl", + nw.Interface, nw.VNLDeprecated, nw.Socket) + } + networks[i].Socket = nw.Socket + networks[i].VNLDeprecated = "" + networks[i].SwitchPortDeprecated = 0 networks[i].Lima = "" } if nw.Lima != "" { - if nw.VNL != "" { + if nw.VNLDeprecated != "" { // We can't return an error, so just log it, and prefer `lima` over `vnl` logrus.Errorf("Network %q has both vnl=%q and lima=%q fields; ignoring vnl", - nw.Interface, nw.VNL, nw.Lima) + nw.Interface, nw.VNLDeprecated, nw.Lima) + } + if nw.Socket != "" { + // We can't return an error, so just log it, and prefer `lima` over `socket` + logrus.Errorf("Network %q has both socket=%q and lima=%q fields; ignoring socket", + nw.Interface, nw.Socket, nw.Lima) } networks[i].Lima = nw.Lima - networks[i].VNL = "" - networks[i].SwitchPort = 0 + networks[i].Socket = "" + networks[i].VNLDeprecated = "" + networks[i].SwitchPortDeprecated = 0 } if nw.MACAddress != "" { networks[i].MACAddress = nw.MACAddress diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index f31eab03455..da9b93c4bf1 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -277,10 +277,10 @@ func TestFillDefault(t *testing.T) { }, Networks: []Network{ { - VNL: "/tmp/vde.ctl", - SwitchPort: 65535, - MACAddress: "11:22:33:44:55:66", - Interface: "def0", + VNLDeprecated: "/tmp/vde.ctl", + SwitchPortDeprecated: 65535, + MACAddress: "11:22:33:44:55:66", + Interface: "def0", }, }, DNS: []net.IP{ @@ -492,8 +492,8 @@ func TestFillDefault(t *testing.T) { // o.Networks[1] is overriding the d.Networks[0].Lima entry for the "def0" interface expect.Networks = append(append(d.Networks, y.Networks...), o.Networks[0]) expect.Networks[0].Lima = o.Networks[1].Lima - expect.Networks[0].VNL = "" - expect.Networks[0].SwitchPort = 0 + expect.Networks[0].VNLDeprecated = "" + expect.Networks[0].SwitchPortDeprecated = 0 // Only highest prio DNS are retained expect.DNS = o.DNS diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 20ea3a4f993..9235b948ec5 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -164,14 +164,17 @@ type PortForward struct { } type Network struct { - // `Lima` and `VNL` are mutually exclusive; exactly one is required + // `Lima`, `Socket`, and `VNL` are mutually exclusive; exactly one is required Lima string `yaml:"lima,omitempty" json:"lima,omitempty"` - // VNL is a Virtual Network Locator (https://github.com/rd235/vdeplug4/commit/089984200f447abb0e825eb45548b781ba1ebccd). + // Socket is a QEMU-compatible socket + Socket string `yaml:"socket,omitempty" json:"socket,omitempty"` + // VNLDeprecated is a Virtual Network Locator (https://github.com/rd235/vdeplug4/commit/089984200f447abb0e825eb45548b781ba1ebccd). // On macOS, only VDE2-compatible form (optionally with vde:// prefix) is supported. - VNL string `yaml:"vnl,omitempty" json:"vnl,omitempty"` - SwitchPort uint16 `yaml:"switchPort,omitempty" json:"switchPort,omitempty"` // VDE Switch port, not TCP/UDP port (only used by VDE networking) - MACAddress string `yaml:"macAddress,omitempty" json:"macAddress,omitempty"` - Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` + // VNLDeprecated is deprecated. Use Socket. + VNLDeprecated string `yaml:"vnl,omitempty" json:"vnl,omitempty"` + SwitchPortDeprecated uint16 `yaml:"switchPort,omitempty" json:"switchPort,omitempty"` // VDE Switch port, not TCP/UDP port (only used by VDE networking) + MACAddress string `yaml:"macAddress,omitempty" json:"macAddress,omitempty"` + Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` } type HostResolver struct { diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 4c98c0b51e5..64cc400f490 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -278,10 +278,13 @@ func validateNetwork(y LimaYAML, warn bool) error { if runtime.GOOS != "darwin" { return fmt.Errorf("field `%s.lima` is only supported on macOS right now", field) } - if nw.VNL != "" { + if nw.Socket != "" { + return fmt.Errorf("field `%s.lima` and field `%s.socket` are mutually exclusive", field, field) + } + if nw.VNLDeprecated != "" { return fmt.Errorf("field `%s.lima` and field `%s.vnl` are mutually exclusive", field, field) } - if nw.SwitchPort != 0 { + if nw.SwitchPortDeprecated != 0 { return fmt.Errorf("field `%s.switchPort` cannot be used with field `%s.lima`", field, field) } config, err := networks.Config() @@ -291,15 +294,27 @@ func validateNetwork(y LimaYAML, warn bool) error { if config.Check(nw.Lima) != nil { return fmt.Errorf("field `%s.lima` references network %q which is not defined in networks.yaml", field, nw.Lima) } + } else if nw.Socket != "" { + if nw.VNLDeprecated != "" { + return fmt.Errorf("field `%s.socket` and field `%s.vnl` are mutually exclusive", field, field) + } + if nw.SwitchPortDeprecated != 0 { + return fmt.Errorf("field `%s.switchPort` cannot be used with field `%s.socket`", field, field) + } + if fi, err := os.Stat(nw.Socket); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } else if err == nil && fi.Mode()&os.ModeSocket == 0 { + return fmt.Errorf("field `%s.socket` %q points to a non-socket file", field, nw.Socket) + } } else { - if nw.VNL == "" { - return fmt.Errorf("field `%s.lima` or field `%s.vnl` must be set", field, field) + if nw.VNLDeprecated == "" { + return fmt.Errorf("field `%s.lima`, field `%s.socket`, or field `%s.vnl` must be set", field, field, field) } // The field is called VDE.VNL in anticipation of QEMU upgrading VDE2 to VDEplug4, // but right now the only valid value on macOS is a path to the vde_switch socket directory, // optionally with vde:// prefix. - if !strings.Contains(nw.VNL, "://") || strings.HasPrefix(nw.VNL, "vde://") { - vdeSwitch := strings.TrimPrefix(nw.VNL, "vde://") + if !strings.Contains(nw.VNLDeprecated, "://") || strings.HasPrefix(nw.VNLDeprecated, "vde://") { + vdeSwitch := strings.TrimPrefix(nw.VNLDeprecated, "vde://") if fi, err := os.Stat(vdeSwitch); err != nil { // negligible when the instance is stopped logrus.WithError(err).Debugf("field `%s.vnl` %q failed stat", field, vdeSwitch) @@ -313,7 +328,7 @@ func validateNetwork(y LimaYAML, warn bool) error { return fmt.Errorf("field `%s.vnl` file %q is not a UNIX socket", field, ctlSocket) } } - if nw.SwitchPort == 65535 { + if nw.SwitchPortDeprecated == 65535 { return fmt.Errorf("field `%s.vnl` points to a non-PTP switch, so the port number must not be 65535", field) } } else { @@ -321,9 +336,9 @@ func validateNetwork(y LimaYAML, warn bool) error { if fi.Mode()&os.ModeSocket == 0 { return fmt.Errorf("field `%s.vnl` %q is not a directory nor a UNIX socket", field, vdeSwitch) } - if nw.SwitchPort != 65535 { + if nw.SwitchPortDeprecated != 65535 { return fmt.Errorf("field `%s.vnl` points to a PTP (switchless) socket %q, so the port number has to be 65535 (got %d)", - field, vdeSwitch, nw.SwitchPort) + field, vdeSwitch, nw.SwitchPortDeprecated) } } } diff --git a/pkg/networks/commands.go b/pkg/networks/commands.go index c39cb2acbde..9cb3fcc4885 100644 --- a/pkg/networks/commands.go +++ b/pkg/networks/commands.go @@ -1,16 +1,21 @@ package networks import ( + "errors" "fmt" + "io/fs" + "os/exec" "path/filepath" + "strings" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/store/dirnames" ) const ( - Switch = "switch" - VMNet = "vmnet" + VDESwitch = "vde_switch" // Deprecated + VDEVMNet = "vde_vmnet" // Deprecated + SocketVMNet = "socket_vmnet" ) // Commands in `sudoers` cannot use quotes, so all arguments are printed via "%s" @@ -23,22 +28,68 @@ func (config *NetworksConfig) Check(name string) error { return fmt.Errorf("network %q is not defined", name) } +// DaemonPath returns the daemon path. +func (config *NetworksConfig) DaemonPath(daemon string) (string, error) { + switch daemon { + case VDESwitch: + return config.Paths.VDESwitch, nil + case VDEVMNet: + return config.Paths.VDEVMNet, nil + case SocketVMNet: + return config.Paths.SocketVMNet, nil + default: + return "", fmt.Errorf("unknown daemon type %q", daemon) + } +} + +// IsDaemonInstalled checks whether the daemon is installed. +func (config *NetworksConfig) IsDaemonInstalled(daemon string) (bool, error) { + p, err := config.DaemonPath(daemon) + if err != nil { + return false, err + } + if p == "" { + return false, nil + } + if _, err := exec.LookPath(p); err != nil { + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + return false, err + } + return true, nil +} + +// Sock returns a socket_vmnet socket. +func (config *NetworksConfig) Sock(name string) string { + return filepath.Join(config.Paths.VarRun, fmt.Sprintf("socket_vmnet.%s", name)) +} + +// VDESock returns a vde socket. +// +// Deprecated. Use Sock. func (config *NetworksConfig) VDESock(name string) string { return filepath.Join(config.Paths.VarRun, fmt.Sprintf("%s.ctl", name)) } func (config *NetworksConfig) PIDFile(name, daemon string) string { - return filepath.Join(config.Paths.VarRun, fmt.Sprintf("%s_%s.pid", name, daemon)) + daemonTrimmed := strings.TrimPrefix(daemon, "vde_") // for compatibility + return filepath.Join(config.Paths.VarRun, fmt.Sprintf("%s_%s.pid", name, daemonTrimmed)) } func (config *NetworksConfig) LogFile(name, daemon, stream string) string { networksDir, _ := dirnames.LimaNetworksDir() - return filepath.Join(networksDir, fmt.Sprintf("%s_%s.%s.log", name, daemon, stream)) + daemonTrimmed := strings.TrimPrefix(daemon, "vde_") // for compatibility + return filepath.Join(networksDir, fmt.Sprintf("%s_%s.%s.log", name, daemonTrimmed, stream)) } func (config *NetworksConfig) User(daemon string) (osutil.User, error) { + if ok, _ := config.IsDaemonInstalled(daemon); !ok { + daemonPath, _ := config.DaemonPath(daemon) + return osutil.User{}, fmt.Errorf("daemon %q (path=%q) is not available", daemon, daemonPath) + } switch daemon { - case Switch: + case VDESwitch: user, err := osutil.LookupUser("daemon") if err != nil { return user, err @@ -47,7 +98,7 @@ func (config *NetworksConfig) User(daemon string) (osutil.User, error) { user.Group = group.Name user.Gid = group.Gid return user, err - case VMNet: + case VDEVMNet, SocketVMNet: return osutil.LookupUser("root") } return osutil.User{}, fmt.Errorf("daemon %q not defined", daemon) @@ -58,15 +109,24 @@ func (config *NetworksConfig) MkdirCmd() string { } func (config *NetworksConfig) StartCmd(name, daemon string) string { + if ok, _ := config.IsDaemonInstalled(daemon); !ok { + panic(fmt.Errorf("daemon %q is not available", daemon)) + } var cmd string switch daemon { - case Switch: + case VDESwitch: + if config.Paths.VDESwitch == "" { + panic("config.Paths.VDESwitch is empty") + } cmd = fmt.Sprintf("%s --pidfile=%s --sock=%s --group=%s --dirmode=0770 --nostdin", - config.Paths.VDESwitch, config.PIDFile(name, Switch), config.VDESock(name), config.Group) - case VMNet: + config.Paths.VDESwitch, config.PIDFile(name, VDESwitch), config.VDESock(name), config.Group) + case VDEVMNet: nw := config.Networks[name] + if config.Paths.VDEVMNet == "" { + panic("config.Paths.VDEVMNet is empty") + } cmd = fmt.Sprintf("%s --pidfile=%s --vde-group=%s --vmnet-mode=%s", - config.Paths.VDEVMNet, config.PIDFile(name, VMNet), config.Group, nw.Mode) + config.Paths.VDEVMNet, config.PIDFile(name, VDEVMNet), config.Group, nw.Mode) switch nw.Mode { case ModeBridged: cmd += fmt.Sprintf(" --vmnet-interface=%s", nw.Interface) @@ -75,6 +135,21 @@ func (config *NetworksConfig) StartCmd(name, daemon string) string { nw.Gateway, nw.DHCPEnd, nw.NetMask) } cmd += " " + config.VDESock(name) + case SocketVMNet: + nw := config.Networks[name] + if config.Paths.SocketVMNet == "" { + panic("config.Paths.SocketVMNet is empty") + } + cmd = fmt.Sprintf("%s --pidfile=%s --socket-group=%s --vmnet-mode=%s", + config.Paths.SocketVMNet, config.PIDFile(name, SocketVMNet), config.Group, nw.Mode) + switch nw.Mode { + case ModeBridged: + cmd += fmt.Sprintf(" --vmnet-interface=%s", nw.Interface) + case ModeHost, ModeShared: + cmd += fmt.Sprintf(" --vmnet-gateway=%s --vmnet-dhcp-end=%s --vmnet-mask=%s", + nw.Gateway, nw.DHCPEnd, nw.NetMask) + } + cmd += " " + config.Sock(name) } return cmd } diff --git a/pkg/networks/commands_darwin_test.go b/pkg/networks/commands_darwin_test.go index a51b8b71a32..8b3e91f2cb7 100644 --- a/pkg/networks/commands_darwin_test.go +++ b/pkg/networks/commands_darwin_test.go @@ -6,6 +6,14 @@ import ( "gotest.tools/v3/assert" ) +func TestSock(t *testing.T) { + config, err := DefaultConfig() + assert.NilError(t, err) + + sock := config.Sock("foo") + assert.Equal(t, sock, "/private/var/run/lima/socket_vmnet.foo") +} + func TestVDESock(t *testing.T) { config, err := DefaultConfig() assert.NilError(t, err) diff --git a/pkg/networks/commands_test.go b/pkg/networks/commands_test.go index 7fc22be1588..0d6803a93b8 100644 --- a/pkg/networks/commands_test.go +++ b/pkg/networks/commands_test.go @@ -43,24 +43,45 @@ func TestUser(t *testing.T) { t.Skip() } - user, err := config.User(Switch) - assert.NilError(t, err) - assert.Equal(t, user.User, "daemon") - assert.Equal(t, user.Group, config.Group) - if runtime.GOOS == "darwin" { - assert.Equal(t, user.Uid, uint32(1)) - } + t.Run("socket_vmnet", func(t *testing.T) { + if ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok { + t.Skipf("socket_vmnet is not installed") + } + user, err := config.User(SocketVMNet) + assert.NilError(t, err) + assert.Equal(t, user.User, "root") + if runtime.GOOS == "darwin" { + assert.Equal(t, user.Group, "wheel") + } else { + assert.Equal(t, user.Group, "root") + } + assert.Equal(t, user.Uid, uint32(0)) + assert.Equal(t, user.Gid, uint32(0)) + }) + + t.Run("vde_vmnet", func(t *testing.T) { + if ok, _ := config.IsDaemonInstalled(VDEVMNet); !ok { + t.Skipf("vde_vmnet is not installed") + } + user, err := config.User(VDESwitch) + assert.NilError(t, err) + assert.Equal(t, user.User, "daemon") + assert.Equal(t, user.Group, config.Group) + if runtime.GOOS == "darwin" { + assert.Equal(t, user.Uid, uint32(1)) + } - user, err = config.User(VMNet) - assert.NilError(t, err) - assert.Equal(t, user.User, "root") - if runtime.GOOS == "darwin" { - assert.Equal(t, user.Group, "wheel") - } else { - assert.Equal(t, user.Group, "root") - } - assert.Equal(t, user.Uid, uint32(0)) - assert.Equal(t, user.Gid, uint32(0)) + user, err = config.User(VDEVMNet) + assert.NilError(t, err) + assert.Equal(t, user.User, "root") + if runtime.GOOS == "darwin" { + assert.Equal(t, user.Group, "wheel") + } else { + assert.Equal(t, user.Group, "root") + } + assert.Equal(t, user.Uid, uint32(0)) + assert.Equal(t, user.Gid, uint32(0)) + }) } func TestMkdirCmd(t *testing.T) { @@ -77,17 +98,36 @@ func TestStartCmd(t *testing.T) { varRunDir := filepath.Join("/", "private", "var", "run", "lima") - cmd := config.StartCmd("shared", Switch) - assert.Equal(t, cmd, "/opt/vde/bin/vde_switch --pidfile="+filepath.Join(varRunDir, "shared_switch.pid")+" "+ - "--sock="+filepath.Join(varRunDir, "shared.ctl")+" --group=everyone --dirmode=0770 --nostdin") - - cmd = config.StartCmd("shared", VMNet) - assert.Equal(t, cmd, "/opt/vde/bin/vde_vmnet --pidfile="+filepath.Join(varRunDir, "shared_vmnet.pid")+" --vde-group=everyone --vmnet-mode=shared "+ - "--vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.254 --vmnet-mask=255.255.255.0 "+filepath.Join(varRunDir, "shared.ctl")) - - cmd = config.StartCmd("bridged", VMNet) - assert.Equal(t, cmd, "/opt/vde/bin/vde_vmnet --pidfile="+filepath.Join(varRunDir, "bridged_vmnet.pid")+" --vde-group=everyone --vmnet-mode=bridged "+ - "--vmnet-interface=en0 "+filepath.Join(varRunDir, "bridged.ctl")) + t.Run("socket_vmnet", func(t *testing.T) { + if ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok { + t.Skipf("socket_vmnet is not installed") + } + + cmd := config.StartCmd("shared", SocketVMNet) + assert.Equal(t, cmd, "/opt/socket_vmnet/bin/socket_vmnet --pidfile="+filepath.Join(varRunDir, "shared_socket_vmnet.pid")+" --socket-group=everyone --vmnet-mode=shared "+ + "--vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.254 --vmnet-mask=255.255.255.0 "+filepath.Join(varRunDir, "socket_vmnet.shared")) + + cmd = config.StartCmd("bridged", SocketVMNet) + assert.Equal(t, cmd, "/opt/socket_vmnet/bin/socket_vmnet --pidfile="+filepath.Join(varRunDir, "bridged_socket_vmnet.pid")+" --socket-group=everyone --vmnet-mode=bridged "+ + "--vmnet-interface=en0 "+filepath.Join(varRunDir, "socket_vmnet.bridged")) + }) + + t.Run("vde_vmnet", func(t *testing.T) { + if ok, _ := config.IsDaemonInstalled(VDEVMNet); !ok { + t.Skipf("vde_vmnet is not installed") + } + cmd := config.StartCmd("shared", VDESwitch) + assert.Equal(t, cmd, "/opt/vde/bin/vde_switch --pidfile="+filepath.Join(varRunDir, "shared_switch.pid")+" "+ + "--sock="+filepath.Join(varRunDir, "shared.ctl")+" --group=everyone --dirmode=0770 --nostdin") + + cmd = config.StartCmd("shared", VDEVMNet) + assert.Equal(t, cmd, "/opt/vde/bin/vde_vmnet --pidfile="+filepath.Join(varRunDir, "shared_vmnet.pid")+" --vde-group=everyone --vmnet-mode=shared "+ + "--vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.254 --vmnet-mask=255.255.255.0 "+filepath.Join(varRunDir, "shared.ctl")) + + cmd = config.StartCmd("bridged", VDEVMNet) + assert.Equal(t, cmd, "/opt/vde/bin/vde_vmnet --pidfile="+filepath.Join(varRunDir, "bridged_vmnet.pid")+" --vde-group=everyone --vmnet-mode=bridged "+ + "--vmnet-interface=en0 "+filepath.Join(varRunDir, "bridged.ctl")) + }) } func TestStopCmd(t *testing.T) { diff --git a/pkg/networks/config.go b/pkg/networks/config.go index 82e44c0352b..3343207f94f 100644 --- a/pkg/networks/config.go +++ b/pkg/networks/config.go @@ -82,6 +82,24 @@ func Config() (NetworksConfig, error) { return cache.config, cache.err } +// Sock returns a socket_vmnet socket. +func Sock(name string) (string, error) { + loadCache() + if cache.err != nil { + return "", cache.err + } + if err := cache.config.Check(name); err != nil { + return "", err + } + if cache.config.Paths.SocketVMNet == "" { + return "", errors.New("socketVMNet is not set") + } + return cache.config.Sock(name), nil +} + +// VDESock returns a vde socket. +// +// Deprecated. Use Sock. func VDESock(name string) (string, error) { loadCache() if cache.err != nil { @@ -90,5 +108,8 @@ func VDESock(name string) (string, error) { if err := cache.config.Check(name); err != nil { return "", err } + if cache.config.Paths.VDEVMNet == "" { + return "", errors.New("vdeVMnet is not set") + } return cache.config.VDESock(name), nil } diff --git a/pkg/networks/networks.go b/pkg/networks/networks.go index 0ce46c23677..c9eb7507634 100644 --- a/pkg/networks/networks.go +++ b/pkg/networks/networks.go @@ -9,10 +9,11 @@ type NetworksConfig struct { } type Paths struct { - VDESwitch string `yaml:"vdeSwitch"` - VDEVMNet string `yaml:"vdeVMNet"` - VarRun string `yaml:"varRun"` - Sudoers string `yaml:"sudoers,omitempty"` + SocketVMNet string `yaml:"socketVMNet"` + VDESwitch string `yaml:"vdeSwitch"` // Deprecated + VDEVMNet string `yaml:"vdeVMNet"` // Deprecated + VarRun string `yaml:"varRun"` + Sudoers string `yaml:"sudoers,omitempty"` } const ( diff --git a/pkg/networks/networks.yaml b/pkg/networks/networks.yaml index e4df76ee75f..68e9c9f9b5c 100644 --- a/pkg/networks/networks.yaml +++ b/pkg/networks/networks.yaml @@ -1,15 +1,19 @@ -# Paths to vde executables. Because vde_vmnet is invoked via sudo it should be +# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be # installed where only root can modify/replace it. This means also none of the # parent directories should be writable by the user. # # The varRun directory also must not be writable by the user because it will -# include the vde_vmnet pid files. Those will be terminated via sudo, so replacing -# the pid files would allow killing of arbitrary privileged processes. varRun +# include the socket_vmnet pid file. Those will be terminated via sudo, so replacing +# the pid file would allow killing of arbitrary privileged processes. varRun # however MUST be writable by the daemon user. # # None of the paths segments may be symlinks, why it has to be /private/var # instead of /var etc. paths: +# socketVMNet requires Lima >= 0.12 . +# socketVMNet has precedence over vdeVMNet. + socketVMNet: /opt/socket_vmnet/bin/socket_vmnet +# vdeSwitch and vdeVMNet are DEPRECATED. vdeSwitch: /opt/vde/bin/vde_switch vdeVMNet: /opt/vde/bin/vde_vmnet varRun: /private/var/run/lima diff --git a/pkg/networks/reconcile/reconcile.go b/pkg/networks/reconcile/reconcile.go index e4aa983ca6d..6d9b63abb56 100644 --- a/pkg/networks/reconcile/reconcile.go +++ b/pkg/networks/reconcile/reconcile.go @@ -178,7 +178,20 @@ func startNetwork(config *networks.NetworksConfig, ctx context.Context, name str if err := validateConfig(config); err != nil { return err } - for _, daemon := range []string{networks.Switch, networks.VMNet} { + var daemons []string + ok, err := config.IsDaemonInstalled(networks.SocketVMNet) + if err != nil { + return err + } + if ok { + daemons = append(daemons, networks.SocketVMNet) + if ok, _ := config.IsDaemonInstalled(networks.VDEVMNet); ok { + logrus.Debugf("Ignoring deprecated vde_vmnet (%q)", networks.VDEVMNet) + } + } else { + daemons = append(daemons, networks.VDESwitch, networks.VDEVMNet) + } + for _, daemon := range daemons { pid, _ := store.ReadPIDFile(config.PIDFile(name, daemon)) if pid == 0 { logrus.Infof("Starting %s daemon for %q network", daemon, name) @@ -194,7 +207,10 @@ func stopNetwork(config *networks.NetworksConfig, name string) error { logrus.Debugf("Make sure %q network is stopped", name) // Don't call validateConfig() until we actually need to stop a daemon because // stopNetwork() may be called even when the vde daemons are not installed. - for _, daemon := range []string{networks.VMNet, networks.Switch} { + for _, daemon := range []string{networks.SocketVMNet, networks.VDEVMNet, networks.VDESwitch} { + if ok, _ := config.IsDaemonInstalled(daemon); !ok { + continue + } pid, _ := store.ReadPIDFile(config.PIDFile(name, daemon)) if pid != 0 { logrus.Infof("Stopping %s daemon for %q network", daemon, name) @@ -211,7 +227,7 @@ func stopNetwork(config *networks.NetworksConfig, name string) error { } } // wait for VMNet to terminate (up to 5s) before stopping Switch, otherwise the socket may not get deleted - if daemon == networks.VMNet { + if daemon == networks.VDEVMNet { startWaiting := time.Now() for { if pid, _ := store.ReadPIDFile(config.PIDFile(name, daemon)); pid == 0 { diff --git a/pkg/networks/sudoers.go b/pkg/networks/sudoers.go index acb01f247db..00d242f204b 100644 --- a/pkg/networks/sudoers.go +++ b/pkg/networks/sudoers.go @@ -30,7 +30,12 @@ func Sudoers() (string, error) { for _, name := range names { sb.WriteRune('\n') sb.WriteString(fmt.Sprintf("# Manage %q network daemons\n", name)) - for _, daemon := range []string{Switch, VMNet} { + for _, daemon := range []string{VDESwitch, VDEVMNet, SocketVMNet} { + if ok, err := config.IsDaemonInstalled(daemon); err != nil { + return "", err + } else if !ok { + continue + } user, err := config.User(daemon) if err != nil { return "", err @@ -53,7 +58,12 @@ func (config *NetworksConfig) passwordLessSudo() error { } // Verify that user/groups for both daemons work without a password, e.g. // %admin ALL = (ALL:ALL) NOPASSWD: ALL - for _, daemon := range []string{Switch, VMNet} { + for _, daemon := range []string{VDESwitch, VDEVMNet, SocketVMNet} { + if ok, err := config.IsDaemonInstalled(daemon); err != nil { + return err + } else if !ok { + continue + } user, err := config.User(daemon) if err != nil { return err diff --git a/pkg/networks/validate.go b/pkg/networks/validate.go index 0d77371f833..a145c524b3b 100644 --- a/pkg/networks/validate.go +++ b/pkg/networks/validate.go @@ -15,6 +15,8 @@ import ( func (config *NetworksConfig) Validate() error { // validate all paths.* values paths := reflect.ValueOf(&config.Paths).Elem() + pathsMap := make(map[string]string, paths.NumField()) + var socketVMNetNotFound, vdeVMNetNotFound, vdeSwitchNotFound bool for i := 0; i < paths.NumField(); i++ { // extract YAML name from struct tag; strip options like "omitempty" name := paths.Type().Field(i).Tag.Get("yaml") @@ -22,19 +24,38 @@ func (config *NetworksConfig) Validate() error { name = name[:i] } path := paths.Field(i).Interface().(string) + pathsMap[name] = path // varPath will be created securely, but any existing parent directories must already be secure if name == "varRun" { path = findBaseDirectory(path) } err := validatePath(path, name == "varRun") if err != nil { - // sudoers file does not need to exist; otherwise `limactl sudoers` couldn't bootstrap - if name == "sudoers" && errors.Is(err, os.ErrNotExist) { - continue + if errors.Is(err, os.ErrNotExist) { + switch name { + // sudoers file does not need to exist; otherwise `limactl sudoers` couldn't bootstrap + case "sudoers": + continue + case "socketVMNet": + socketVMNetNotFound = true + continue + case "vdeVMNet": + vdeVMNetNotFound = true + continue + case "vdeSwitch": + vdeSwitchNotFound = true + continue + } } return fmt.Errorf("networks.yaml field `paths.%s` error: %w", name, err) } } + if socketVMNetNotFound && vdeVMNetNotFound { + return fmt.Errorf("networks.yaml: either %q (`paths.socketVMNet`) or %q (`paths.vdeVMNet`) has to be installed", pathsMap["socketVMNet"], pathsMap["vdeVMNet"]) + } + if socketVMNetNotFound && !vdeVMNetNotFound && vdeSwitchNotFound { + return fmt.Errorf("networks.yaml: %q (`paths.vdeVMNet`) requires %q (`paths.vdeSwitch`) to be installed", pathsMap["vdeVMNet"], pathsMap["vdeSwitch"]) + } // TODO(jandubois): validate network definitions return nil } diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go index 0688df90a68..77ea8d484a0 100644 --- a/pkg/qemu/qemu.go +++ b/pkg/qemu/qemu.go @@ -468,40 +468,67 @@ func Cmdline(cfg Config) (string, []string, error) { args = append(args, "-netdev", fmt.Sprintf("user,id=net0,net=%s,dhcpstart=%s,hostfwd=tcp:127.0.0.1:%d-:22", qemu.SlirpNetwork, qemu.SlirpIPAddress, cfg.SSHLocalPort)) args = append(args, "-device", "virtio-net-pci,netdev=net0,mac="+limayaml.MACAddress(cfg.InstanceDir)) - if len(y.Networks) > 0 && !strings.Contains(string(features.NetdevHelp), "vde") { - return "", nil, fmt.Errorf("netdev \"vde\" is not supported by %s ( Hint: recompile QEMU with `configure --enable-vde` )", exe) - } for i, nw := range y.Networks { var vdeSock string if nw.Lima != "" { - vdeSock, err = networks.VDESock(nw.Lima) + nwCfg, err := networks.Config() + if err != nil { + return "", nil, err + } + socketVMNetOk, err := nwCfg.IsDaemonInstalled(networks.SocketVMNet) if err != nil { return "", nil, err } + if socketVMNetOk { + logrus.Debugf("Using socketVMNet (%q)", nwCfg.Paths.SocketVMNet) + if vdeVMNetOk, _ := nwCfg.IsDaemonInstalled(networks.VDEVMNet); vdeVMNetOk { + logrus.Debugf("Ignoring vdeVMNet (%q), as socketVMNet (%q) is available and has higher precedence", nwCfg.Paths.VDEVMNet, nwCfg.Paths.SocketVMNet) + } + sock, err := networks.Sock(nw.Lima) + if err != nil { + return "", nil, err + } + args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, sock)) + } else if nwCfg.Paths.VDEVMNet != "" { + logrus.Warn("vdeVMNet is deprecated, use socketVMNet instead (See docs/network.md)") + vdeSock, err = networks.VDESock(nw.Lima) + if err != nil { + return "", nil, err + } + } // TODO: should we also validate that the socket exists, or do we rely on the // networks reconciler to throw an error when the network cannot start? - } else { + } else if nw.Socket != "" { + args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, nw.Socket)) + } else if nw.VNLDeprecated != "" { // VDE4 accepts VNL like vde:///var/run/vde.ctl as well as file path like /var/run/vde.ctl . // VDE2 only accepts the latter form. // VDE2 supports macOS but VDE4 does not yet, so we trim vde:// prefix here for VDE2 compatibility. - vdeSock = strings.TrimPrefix(nw.VNL, "vde://") + vdeSock = strings.TrimPrefix(nw.VNLDeprecated, "vde://") if !strings.Contains(vdeSock, "://") { if _, err := os.Stat(vdeSock); err != nil { - return "", nil, fmt.Errorf("cannot use VNL %q: %w", nw.VNL, err) + return "", nil, fmt.Errorf("cannot use VNL %q: %w", nw.VNLDeprecated, err) } // vdeSock is a directory, unless vde.SwitchPort == 65535 (PTP) actualSocket := filepath.Join(vdeSock, "ctl") - if nw.SwitchPort == 65535 { // PTP + if nw.SwitchPortDeprecated == 65535 { // PTP actualSocket = vdeSock } if st, err := os.Stat(actualSocket); err != nil { - return "", nil, fmt.Errorf("cannot use VNL %q: failed to stat %q: %w", nw.VNL, actualSocket, err) + return "", nil, fmt.Errorf("cannot use VNL %q: failed to stat %q: %w", nw.VNLDeprecated, actualSocket, err) } else if st.Mode()&fs.ModeSocket == 0 { - return "", nil, fmt.Errorf("cannot use VNL %q: %q is not a socket: %w", nw.VNL, actualSocket, err) + return "", nil, fmt.Errorf("cannot use VNL %q: %q is not a socket: %w", nw.VNLDeprecated, actualSocket, err) } } + } else { + return "", nil, fmt.Errorf("invalid network spec %+v", nw) + } + if vdeSock != "" { + if !strings.Contains(string(features.NetdevHelp), "vde") { + return "", nil, fmt.Errorf("netdev \"vde\" is not supported by %s ( Hint: recompile QEMU with `configure --enable-vde` )", exe) + } + args = append(args, "-netdev", fmt.Sprintf("vde,id=net%d,sock=%s", i+1, vdeSock)) } - args = append(args, "-netdev", fmt.Sprintf("vde,id=net%d,sock=%s", i+1, vdeSock)) args = append(args, "-device", fmt.Sprintf("virtio-net-pci,netdev=net%d,mac=%s", i+1, nw.MACAddress)) }