Skip to content

Commit 36bcb8d

Browse files
committed
docs: document virtio-net data plane tuning
1 parent 6449e41 commit 36bcb8d

4 files changed

Lines changed: 326 additions & 30 deletions

File tree

docs/bridge-networking.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ By default, dstack-vmm uses **user** networking (QEMU's built-in SLIRP stack, no
44

55
## When to use bridge networking
66

7-
- High connection concurrency (passt becomes CPU-bound at ~25K+ concurrent connections)
7+
- High connection concurrency (user-mode networking becomes CPU-bound at ~25K+ concurrent connections)
88
- Workloads that need full L2 network access
99
- Environments where VMs need to be directly reachable on the LAN
1010

@@ -21,11 +21,11 @@ bridge = "virbr0"
2121
### Per-VM override
2222

2323
Individual VMs can override the global networking mode via:
24-
- **CLI**: `vmm-cli.py deploy --net bridge` or `--net passt`
24+
- **CLI**: `vmm-cli.py deploy --net bridge`, `--net user`, or `--net macvtap`
2525
- **Web UI**: Networking dropdown in the deploy dialog
2626
- **API**: `networking: { mode: "bridge" }` in `VmConfiguration`
2727

28-
Only the mode is per-VM; the bridge interface name always comes from the global config.
28+
The bridge interface name comes from the global config unless the node lists it in `cvm.allowed_bridges`. VMs may also override the vhost and queue settings — see [network-data-plane.md](network-data-plane.md).
2929

3030
## Host setup
3131

@@ -143,9 +143,15 @@ mode = "bridge"
143143
bridge = "dstack-br0"
144144
```
145145

146-
### QEMU bridge helper setup (required for both options)
146+
### QEMU bridge helper setup (needed unless every bridge NIC goes through netd)
147147

148148
The bridge helper allows QEMU to create and attach TAP devices without VMM needing root privileges.
149+
It is used only on the single-queue bridge paths; a NIC that `netd` builds never touches it, so a
150+
node that runs `netd` for all of its bridge VMs does not need it at all.
151+
152+
The VMM probes `/usr/lib/qemu/qemu-bridge-helper`, `/usr/libexec/qemu-bridge-helper` and
153+
`/usr/local/libexec/qemu-bridge-helper`. Set `cvm.qemu_bridge_helper` in `vmm.toml` for a path
154+
outside that list.
149155

150156
```bash
151157
# Allow QEMU to use the bridge
@@ -159,12 +165,13 @@ sudo chmod u+s /usr/lib/qemu/qemu-bridge-helper
159165

160166
## How it works
161167

162-
- VMM passes `-netdev bridge,id=net0,br=<bridge>` to QEMU
163-
- QEMU's bridge helper (setuid) creates a TAP device and attaches it to the bridge
168+
- With more than one queue pair, or with libvirt filtering on, `netd` creates the TAP and the VMM passes `-netdev tap,id=net0,ifname=<tap>,...` — this is the usual case on a node running `netd` with multi-vCPU VMs, since queue pairs default to the VM's vCPU count. Without `netd`, a bridge NIC that took that default drops back to one queue pair and takes a helper path below
169+
- Otherwise the VMM passes `-netdev tap,id=net0,br=<bridge>,helper=<qemu-bridge-helper>,vhost=on`, or `-netdev bridge,id=net0,br=<bridge>` when vhost is off or no helper is found
170+
- QEMU's bridge helper (setuid) creates a TAP device and attaches it to the bridge on the two helper paths
164171
- Guest MAC address is derived from SHA256 of the VM ID, with an optional configurable prefix (stable across restarts for DHCP IP consistency)
165172
- The host DHCP server (dnsmasq) assigns an IP to the VM
166-
- When QEMU exits, the TAP device is automatically destroyed
167-
- VMM does not need root or `CAP_NET_ADMIN`
173+
- On the two bridge-helper paths the TAP disappears when QEMU exits; a `netd`-created TAP is persistent and is deleted when the VMM tears the VM's networking down
174+
- The VMM process itself needs neither root nor `CAP_NET_ADMIN` on any path; the `netd` path moves that privilege into a separate root service instead
168175

169176
### MAC address prefix
170177

@@ -194,13 +201,15 @@ The remaining bytes are derived from the VM ID hash. The prefix applies to all n
194201

195202
### Mixing networking modes
196203

197-
Bridge and passt VMs can coexist. Set the global default in `vmm.toml` and override per-VM as needed:
204+
Bridge and user-mode VMs can coexist. Set the global default in `vmm.toml` and override per-VM as needed:
198205

199206
```bash
200-
# Global default is bridge, but deploy this VM with passt
201-
vmm-cli.py deploy --name my-vm --image dstack-0.5.6 --compose app.yaml --net passt
207+
# Global default is bridge, but deploy this VM with user networking
208+
vmm-cli.py deploy --name my-vm --image dstack-0.5.6 --compose app.yaml --net user
202209
```
203210

204-
### vhost-net and TDX
211+
### vhost-net and multiqueue
212+
213+
Bridge NICs use the host kernel's vhost-net data plane by default, and can expose several virtio-net queue pairs. Both are configurable per node and per VM — see [network-data-plane.md](network-data-plane.md) for the knobs, the mode support matrix, and how to pick a queue count.
205214

206-
vhost-net (kernel data plane offload for virtio-net) is **not enabled** for bridge mode. TDX encrypts guest memory, which prevents the host kernel from performing DMA-based packet offload. The default QEMU userspace virtio backend is used instead.
215+
vhost-net works in a TDX guest: the virtio rings and buffers live in shared, unencrypted memory so that a host-side backend can reach them, which is the same mechanism `vhost-vsock-pci` has always relied on.

docs/libvirt-network-filter.md

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ host mechanism.
1414

1515
The measurable acceptance criteria are:
1616

17-
- `network_filter = "none"` preserves the existing QEMU `-netdev bridge`
18-
behavior and does not require `netd` or libvirt.
17+
- `network_filter = "none"` installs no nwfilter binding. It still uses `netd`
18+
for any NIC with more than one queue pair, and a `tap` netdev behind
19+
`qemu-bridge-helper` whenever vhost is on; only a single-queue, non-vhost
20+
bridge NIC keeps the historical `-netdev bridge` path with no `netd` or
21+
libvirt dependency.
1922
- `network_filter = "libvirt"` creates the TAP and filter binding before QEMU
2023
is submitted to Supervisor, and uses QEMU `-netdev tap`.
2124
- A failed TAP or filter setup prevents QEMU from starting and rolls back all
@@ -53,8 +56,11 @@ allowed_macvtap_parents = []
5356

5457
Macvtap is excluded from `allowed_network_modes` by default. Empty bridge and
5558
macvtap-parent allowlists prevent RPC callers from overriding the respective
56-
node defaults. If macvtap is explicitly enabled, callers may select only a
57-
parent in `allowed_macvtap_parents`; the macvtap forwarding mode always comes
59+
node defaults. If macvtap is explicitly enabled, callers may select a
60+
parent listed in `allowed_macvtap_parents`, the node's own configured parent, or
61+
one this VM already holds — restating a value the node would have supplied
62+
anyway grants nothing new. The same applies to `bridge_name` and
63+
`allowed_bridges`. The macvtap forwarding mode always comes
5864
from `[cvm.networking].macvtap_mode` and cannot be selected through deployment
5965
RPCs. These allowlists authorize attachment targets; an nwfilter is not a
6066
substitute for that authorization.
@@ -82,7 +88,8 @@ For libvirt mode, startup is:
8288
2. Create the TAP for the configured QEMU UID and attach it to the bridge.
8389
3. Create a libvirt nwfilter binding for the TAP.
8490
4. Bring the TAP up and return success.
85-
5. Start QEMU directly with `-netdev tap,script=no,downscript=no`.
91+
5. Start QEMU directly with `-netdev tap,script=no,downscript=no`, carrying
92+
`vhost=on|off` and, above one queue pair, `queues=N`.
8693

8794
Teardown stops QEMU first, removes the binding, and deletes the TAP. Operations
8895
are serialized by `netd`. The design intentionally does not add ownership
@@ -100,18 +107,28 @@ validated by libvirt.
100107

101108
## Deployment modes
102109

103-
Production should run one shared service. `netd` reads only the `[netd]`
104-
section, so its root-owned configuration can be small and independent of every
105-
VMM instance:
110+
Production should run one shared service. `netd` reads the `[netd]` section,
111+
plus `cvm.network_filter.mode` if the file has one, so its root-owned
112+
configuration can be small and independent of every VMM instance:
106113

107114
```toml
108115
# /etc/dstack/netd.toml
109116
[netd]
110117
socket = "/run/dstack/netd.sock"
111118
socket_mode = 0o660
112119
libvirt_uri = "qemu:///system"
120+
# Refuse to build a bridge TAP with no nwfilter binding. Required here because
121+
# this file has no [cvm] section for netd to read the node's policy from.
122+
require_filter = true
113123
```
114124

125+
`require_filter` is netd's own copy of the invariant, not a convenience. netd is
126+
the privileged side of the socket, and anything that can reach the socket can
127+
ask for an unfiltered TAP on a host bridge — a request a filtering node has to
128+
refuse in the daemon rather than in its caller. When netd and the VMM share one
129+
`vmm.toml`, leaving it unset derives it from `cvm.network_filter.mode` so the
130+
two cannot drift apart.
131+
115132
Production deployments can use systemd socket activation. The socket unit
116133
owns the filesystem mode and ownership; `netd.socket_mode` applies only to the
117134
standalone bind path.
@@ -162,10 +179,37 @@ sudo dstack-vmm --config ./vmm.toml \
162179
--netd-socket /run/dstack-dev/netd.sock
163180
```
164181

165-
User networking and bridge networking with `mode = "none"` never connect to
166-
`netd`. Libvirt mode fails closed if `netd` is unavailable.
167-
168-
Filtered TAP netdevs currently set `vhost=off`. This keeps the initial backend
169-
on the directly bound TAP path and avoids adding `/dev/vhost-net` permissions
170-
to the QEMU user. It is a deliberate security-first throughput tradeoff; a
171-
future configurable vhost mode requires equivalent filter integration tests.
182+
User networking never asks `netd` to build an interface; the VMM still opens a
183+
short liveness-probe connection to the netd socket on every launch and when
184+
describing a stopped VM. Libvirt mode fails closed if `netd`
185+
is unavailable. Bridge networking with `mode = "none"` connects only when it
186+
needs more than one queue pair, as described below.
187+
188+
Filtered TAP netdevs follow the node's `vhost` and `queues` settings like any
189+
other TAP-backed NIC (see [network-data-plane.md](network-data-plane.md)). The
190+
nwfilter binding is installed on the host TAP interface, so packets traverse it
191+
whether they were written by QEMU or by a vhost worker; filtering is unaffected
192+
by the data plane choice. Enabling vhost does require the QEMU user to be able
193+
to open `/dev/vhost-net`.
194+
195+
`netd` also creates the TAP for unfiltered bridge NICs that ask for more than
196+
one queue pair, because `qemu-bridge-helper` returns a single descriptor and
197+
cannot create a `multi_queue` device. Those TAPs carry no nwfilter binding, so
198+
a multiqueue bridge node needs `netd` even when `network_filter.mode = "none"`.
199+
200+
An empty filter name is what selects that unfiltered TAP, so `mode = "libvirt"`
201+
with an empty `filter` is rejected at config load rather than quietly producing
202+
an unbound TAP.
203+
204+
Removal carries the same distinction: the VMM tells `netd` whether the interface
205+
it is asking about was created with a binding, from a record made when it was
206+
built rather than from configuration that may have changed since. A binding it
207+
was told about must be gone before `netd` returns; otherwise `netd` still asks
208+
libvirt to clear one — an interface name is reused by the same VM, and a
209+
leftover binding's rules would be inherited — but a `libvirtd` it cannot reach
210+
is a warning rather than a failure. So a node with `virsh` installed and no
211+
running `libvirtd` can create and destroy multiqueue TAPs. The flag defaults to
212+
true on the wire, so an older VMM's removals still drop their bindings.
213+
214+
`netd` requires the `virsh` binary to be present whatever the filter mode; it is
215+
`libvirtd` that unfiltered work does not need.

docs/macvtap-networking.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Configure a NIC through node configuration or an authorized VMM RPC request:
1919

2020
`parent` must name an existing host interface. `macvtap_mode` may be
2121
`private`, `bridge`, `vepa`, or `passthru`; an empty value selects `private`.
22+
Macvtap NICs also honour the `vhost` and `queues` settings described in
23+
[network-data-plane.md](network-data-plane.md); netd creates the interface with
24+
matching hardware queues and the launcher opens `/dev/tapN` once per queue.
2225
The configured netd socket permissions apply in the same way as for
2326
libvirt-filtered bridge networking.
2427

@@ -49,8 +52,9 @@ and the same deterministic MAC address passed to QEMU. Netd then:
4952
4. reads its kernel-assigned ifindex and waits for `/dev/tap<ifindex>`; and
5053
5. returns that runtime device path to the VMM.
5154

52-
The per-VM launcher opens the character device, places it at the fd referenced
53-
by QEMU's `-netdev tap,fd=...` argument, and then execs QEMU. This keeps device
55+
The per-VM launcher opens the character device once per queue pair, places the
56+
descriptors at the fds referenced by QEMU's `-netdev tap,fd=...` (or `fds=...`)
57+
argument, and then execs QEMU. This keeps device
5458
paths out of persistent VM
5559
configuration, works with both Supervisor and systemd process managers, and
5660
does not pass network fds through `sudo`.

0 commit comments

Comments
 (0)