Repository: lima-vm/lima
Pull request #851 (2022-05-10 10:41:09)

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.


- - -
## iperf3 benchmark (host -> guest)

Mode                 | Shared (NAT) | Bridged
---------------|--------------|----------
socket_vmnet | 0.66 Gbps      | 1.23 Gbps
vde_vmnet      | 0.27 Gbps      | 0.31 Gbps

Tested on MacBook Pro 2020 (Intel), macOS 12
Lima commit 8db31e8087272da1c848d5d6d23f680004ad7d45 , socket_vmnet v1.0.0-alpha.0, vde_vmnet v0.6.0

Known issue: the throughput of the Shared (NAT) interface can be slower when both the Shared (NAT) and the Bridged interfaces are configured



AkihiroSuda: cc @abiosoft
abiosoft: Will Lima be able to use unmanaged socket_vmnet like the deprecated vde_vmnet?
jandubois: > Will Lima be able to use unmanaged socket_vmnet like the deprecated vde_vmnet?

I have only briefly looked at this PR, and it doesn't seem to be implemented.

I will be interested in unmanged vmnet as well, as experience with Rancher Desktop has shown that the whole `sudoers` mechanism is problematic for a significant subset of users, so requiring admin rights only once during installation is preferable.

I'll do a review of this PR later to see if adding unmanaged socket_vmnet support seems feasible.
jandubois: > I can update PR to support unmanaged socks (by the end of this week or maybe next week)

That would be great; could also be a follow-up PR because the current PR doesn't remove the ability to use unmanaged vde_vmnet daemons; it only switches the managed networks from vde_vmnet to socket_vmnet.
AkihiroSuda: Updated the PR to support unmanaged socks
```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"
```

Currently this requires `socket_vmnet_client` in the PATH.
AkihiroSuda: ## iperf3 benchmark (host -> guest)
- vde_vmnet: 321 Mbps / 318 Mbps
- socket_vmnet: 686 Mbps / 686 Mbps
<details>
<p>

### vde_vmnet

```console
$ iperf3 -c 192.168.105.3
Connecting to host 192.168.105.3, port 5201
[  5] local 192.168.105.1 port 62788 connected to 192.168.105.3 port 5201
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec  42.6 MBytes   357 Mbits/sec                  
[  5]   1.00-2.00   sec  39.7 MBytes   333 Mbits/sec                  
[  5]   2.00-3.00   sec  39.3 MBytes   329 Mbits/sec                  
[  5]   3.00-4.00   sec  37.8 MBytes   317 Mbits/sec                  
[  5]   4.00-5.00   sec  37.2 MBytes   312 Mbits/sec                  
[  5]   5.00-6.00   sec  37.2 MBytes   312 Mbits/sec                  
[  5]   6.00-7.00   sec  37.8 MBytes   317 Mbits/sec                  
[  5]   7.00-8.00   sec  38.4 MBytes   322 Mbits/sec                  
[  5]   8.00-9.00   sec  35.5 MBytes   298 Mbits/sec                  
[  5]   9.00-10.00  sec  36.9 MBytes   309 Mbits/sec                  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec   382 MBytes   321 Mbits/sec                  sender
[  5]   0.00-10.05  sec   381 MBytes   318 Mbits/sec                  receiver

iperf Done.
```

### socket_vmnet
```console
$ iperf3 -c 192.168.105.3
Connecting to host 192.168.105.3, port 5201
[  5] local 192.168.105.1 port 62814 connected to 192.168.105.3 port 5201
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-1.00   sec  85.2 MBytes   715 Mbits/sec                  
[  5]   1.00-2.00   sec  82.7 MBytes   694 Mbits/sec                  
[  5]   2.00-3.00   sec  81.7 MBytes   686 Mbits/sec                  
[  5]   3.00-4.00   sec  82.3 MBytes   690 Mbits/sec                  
[  5]   4.00-5.00   sec  82.0 MBytes   688 Mbits/sec                  
[  5]   5.00-6.00   sec  81.5 MBytes   684 Mbits/sec                  
[  5]   6.00-7.00   sec  79.8 MBytes   669 Mbits/sec                  
[  5]   7.00-8.00   sec  82.2 MBytes   690 Mbits/sec                  
[  5]   8.00-9.00   sec  79.4 MBytes   666 Mbits/sec                  
[  5]   9.00-10.00  sec  80.9 MBytes   678 Mbits/sec                  
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate
[  5]   0.00-10.00  sec   818 MBytes   686 Mbits/sec                  sender
[  5]   0.00-9.99   sec   817 MBytes   686 Mbits/sec                  receiver

iperf Done.
```

</p>
</details>
AkihiroSuda: Rebased, will try to address the review comments next week
AkihiroSuda: This should be reviewable/mergeable now.

cc @jandubois @abiosoft @mook-as
AkihiroSuda: Planning to cut the next release of Lima soon.
Can we merge this?
Compatibility with the legacy vde_vmnet is tested in the CI.
abiosoft: > Planning to cut the next release of Lima soon. Can we merge this? Compatibility with the legacy vde_vmnet is tested in the CI.

I will not be able to properly test this for the next few days as I am occupied with some other things at the moment. 
I should give my feedback by Friday/Saturday (if that is not too late). 
jandubois: I hope to be able to test this by Wednesday.
jandubois: I haven't looked at the code changes yet, but I just started with a quick test, creating an Alpine VM with both a shared and a bridged interface:

```console
$ grep -A 2 networks: ~/.lima/alpine/lima.yaml
networks:
- lima: shared
  lima: bridged
```

I had deleted the `networks.yaml` to let Lima create a new file with the new default content:

```console
$ grep -A 9 networks: ~/.lima/_config/networks.yaml
networks:
  shared:
    mode: shared
    gateway: 192.168.105.1
    dhcpEnd: 192.168.105.254
    netmask: 255.255.255.0
  bridged:
    mode: bridged
    interface: en0
    # bridged mode doesn't have a gateway; dhcp is managed by outside network
```

But both interfaces are created in the DHCP range of the bridged network (192.168.18.0/24):

```console
$ limactl shell alpine ip a | grep -A 2 lima0:
3: lima0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 52:55:55:3d:01:95 brd ff:ff:ff:ff:ff:ff
    inet 192.168.18.111/16 scope global lima0
$ limactl shell alpine ip a | grep -A 2 lima1:
4: lima1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 52:55:55:9b:53:e4 brd ff:ff:ff:ff:ff:ff
    inet 192.168.18.160/16 scope global lima1
```

I also see just a single `socket_vmnet` daemon process for bridged networking, and none for the shared network.

So this is still a regression to the vde_vmnet implementation.

AkihiroSuda: > ```
> $ grep -A 2 networks: ~/.lima/alpine/lima.yaml
> networks:
> - lima: shared
>   lima: bridged
> ```

Probably you meant this:

```yaml
networks:
- lima: shared
- lima: bridged
```

jandubois: > Probably you meant this:
> 
> ```yaml
> networks:
> - lima: shared
> - lima: bridged
> ```

Indeed, I did. I didn't notice because I still got 2 networks. Which happened because I had defined a bridged network in my `default.yaml`, but had forgotten about...

> (And it works for me)

Yes, works for me too now! Thanks!

Will try to do another code review tomorrow.
jandubois: > Will try to do another code review tomorrow.

I have not finished this today; will try to wrap up tomorrow. So far LGTM.
AkihiroSuda: Rebased

Patch:
diff --git a/README.md b/README.md
index f32c7b7e..492c094a 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 c81ac0c4..69f2bc18 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 f3750af6..7512df66 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 079b7fa1..f3065d86 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/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 79ae6dbf..dc233960 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 fa19e5de..3a2c2121 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/limayaml.go b/pkg/limayaml/limayaml.go
index 20ea3a4f..9235b948 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 4c98c0b5..64cc400f 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 c39cb2ac..9cb3fcc4 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/config.go b/pkg/networks/config.go
index 82e44c03..3343207f 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 0ce46c23..c9eb7507 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 e4df76ee..68e9c9f9 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 e4aa983c..6d9b63ab 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 acb01f24..00d242f2 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 0d77371f..a145c524 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 0688df90..77ea8d48 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))
 	}
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 379fcd4e..145f43db 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/hack/test-example.sh b/hack/test-example.sh
index 039f2469..d30bb569 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/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index f31eab03..da9b93c4 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/networks/commands_darwin_test.go b/pkg/networks/commands_darwin_test.go
index a51b8b71..8b3e91f2 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 7fc22be1..0d6803a9 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) {