gns3-server/docs/features/qemu-kernel-datapath.md
YueGuobin 187201a08c
feat(controller): offer QEMU the kernel datapath, gated per compute
Kernel eligibility becomes "both endpoints on one compute, each able to
anchor": Docker always (adapters are born as veth pairs), QEMU when that
compute's uBridge can create the persistent TAPs its adapters anchor on. The
question is asked per compute, because the link needs both ends anchored --
so a mixed or half-upgraded deployment degrades to the relay instead of
failing an attach.

probe_tap_support() creates and deletes one throwaway persistent TAP through a
scratch uBridge, cached by binary identity like the tc report; /capabilities
exposes `ubridge_tap` (absent = unknown, which keeps links on the relay).

The compute route for a QEMU adapter NIO took UDPNIO only, so a kernel link
was rejected with 422: its body and response now accept
Union[UDPNIO, BridgeNIO] exactly like Docker's.

Stale "Docker-to-Docker" wording in the config, link and MCP descriptions now
talks about anchors, and docs/features/qemu-kernel-datapath.md documents the
datapath -- verified end-to-end on a real instance: 32/32 kernel-datapath
checks and 29/29 on the relay variant.
2026-10-06 00:21:08 +08:00

8.2 KiB

QEMU kernel datapath

QEMU adapters anchor on a persistent TAP, and links between two QEMU nodes — or a QEMU node and a Docker container — on the same compute are wired in the kernel (per-link Linux bridge) instead of the uBridge UDP relay. The compute side is the same machinery Docker already uses: KernelDatapathMixin (gns3server/compute/kernel_datapath.py) takes an anchor interface and does not care whether it is a veth host end or a TAP.

Architecture

Kernel link between two QEMU nodes on one compute:

 QEMU A (fd)                root namespace                 QEMU B (fd)
 ┌──────────┐   -netdev   ┌──────────────────┐  -netdev   ┌──────────┐
 │ e1000    │◄───tap──────┤  gq{idA}e0p0 ────┼──► gns3{link_id_prefix}
 └──────────┘             │  (bridge port)   │    (bridge port)  │
                          │  per-link kernel │   gq{idB}e0p0     │
                          │  bridge = "cable"│                   │
                          └──────────────────┘

Relay link on the same anchor (cross-compute, mixed node types, or enable_kernel_datapath off):

 ┌──────────┐   tap    ┌────────────────────────────┐   UDP   ┌──────────┐
 │ QEMU A   │◄────────►│ uBridge relay bridge       │◄───────►│ peer     │
 │ (fd)     │ gq…e0p0  │ add_nio_ethernet ↔ add_    │ tunnel  │ (any)    │
 └──────────┘          │ nio_udp                    │         └──────────┘
                       └────────────────────────────┘

The relay attaches the TAP with bridge add_nio_ethernet (AF_PACKET, which does not take the device over) — never bridge add_nio_tap, which would claim the fd QEMU itself holds.

Why a TAP works like a veth host end

Event Docker anchor (veth host end) QEMU anchor (TAP)
Frames the node sends arrive on the veth as PACKET_HOST written to the tap fd, arrive as RX (PACKET_HOST)
Frames the peer sends injected by uBridge, seen as PACKET_OUTGOING injected by uBridge, seen as PACKET_OUTGOING
tc egress qdisc impairs traffic entering the container traffic entering the VM
Capture/marker anchor the interface the same interface

So every filter, capture and marker primitive carries over verbatim, and delay 100 on both anchors still measures ≈200 ms round trip while each direction is impaired exactly once.

Datapath selection

UDPLink._kernel_datapath_eligible needs both endpoints on one compute and both able to anchor:

  • Docker always can (adapters are born as veth pairs);
  • QEMU can when that compute's uBridge reports the tap module — asked per compute through /capabilities (ubridge_tap, probed by creating and deleting one throwaway TAP, cached by binary identity), because an old uBridge leaves QEMU on the legacy socket-netdev datapath and cannot anchor a kernel link. An unknown/failed probe keeps the link on the relay.

Because the -netdev type is fixed when QEMU is launched, this is all-or-nothing per VM: on the TAP datapath every link can attach to (or detach from) a running VM, including switching a link between the two datapaths (delete + re-create, or a project reopen).

Adapter lifecycle

  • Birth (node start, before QEMU is launched) — _prepare_tap_datapath probes the tap module, then _create_taps sweeps any leftover (a persistent TAP outlives a crash) and creates one TAP per adapter: tap create gq{node_id[:8]}e{adapter}p{port} → tap set_owner <uid> (so the unprivileged QEMU process can open it) → link set <tap> down (carrier off until a link attaches). QEMU then opens it as -netdev tap,id=gns3-{adapter},ifname=<tap>,script=no,downscript=no.
  • Life (running) — the TAP is never created or destroyed again. Link create/delete/switch, suspend, capture and markers only change what is attached to it.
  • Death (node stop) — _remove_taps un-persists the TAPs (tap delete; QEMU's open fd keeps each device alive until it exits) and deletes the per-link kernel bridges the node still holds. Deleting a node never runs the link-teardown path, so those bridges would otherwise stay behind as empty orphans; a restart rebuilds them from the NIO.
Operation Kernel-datapath action
Create / attach brctl create (EEXIST tolerated via brctl show) + link set <bridge> up + brctl addif <bridge> <tap> + carrier up + markers + tc netem set (filters)
Delete / detach marker teardown → tc reset <tap> → carrier off → brctl delif → brctl delete (last endpoint wins; EBUSY/ENOENT suppressed). The TAP survives — the adapter keeps it for the next link
Reset (POST /links/{id}/reset) delete + create (re-evaluates eligibility)
Suspend (PUT /links/{id} {"suspend": true}) tap admin-down (writing to a down TAP fd fails with EIO, so the link is genuinely dead) + QMP set_link gns3-N off so the guest notices; resume restores both, and the netem qdisc survives the flap

Capture, markers, filters

Identical to Docker's kernel links, on the TAP: capture start_kernel <tap> (driven by the NIO type, not by a link flag), marker add_kernel <tap> … (only the capture node's end), and one tc netem set per anchor so each direction is impaired once. frequency_drop/quota/window_drop run as the eBPF classifier on the anchor's clsact egress.

Legacy fallback

A uBridge build without the tap module (or without CAP_NET_ADMIN) keeps the previous datapath untouched: local UDP tunnels into uBridge, QEMU launched with -netdev socket, and the node logs a warning that it cannot carry kernel links. Such a node is never offered a kernel link, so nothing fails.

Verified

End-to-end through a real server (isolated instance, two QEMU nodes with a stub binary, the test playing the guest by holding the TAP fds and pushing frames): 32/32 checks on the kernel datapath and 29/29 on the relay variant (enable_kernel_datapath = false). Kernel run highlights:

  • anchors created persistent and owned by the server user, QEMU's netdev is the TAP, no local UDP tunnel left; link_list reports kernel_datapath: true and one per-link bridge holds both taps;
  • frames cross in both directions (~0.1 ms); delay 100 ⇒ netem on both anchors and a measured 100.3 ms one-way;
  • suspend ⇒ anchor DOWN, no traffic, resume ⇒ 100.3 ms again (filter kept);
  • capture ⇒ a 252-byte pcap written by capture start_kernel;
  • link delete ⇒ bridge gone, qdisc clean, anchor alive; re-create ⇒ 0.1 ms with no residual impairment; node stop ⇒ no taps, no bridges left.

Relay run the same script with the flag off: same lifecycle, filter measured at 109.8 ms (applied by uBridge's bridge — no netem on the anchor, as expected), no kernel bridge holding the tap.

Configuration

[Server]
# Wire eligible links (both endpoints on one compute, both able to anchor)
# through kernel interfaces instead of the uBridge UDP relay.
enable_kernel_datapath = True

Known caveat

Anchors are host-side netdevs, so the kernel gives them an IPv6 link-local address and emits MLD/DAD from them; those frames flood into the emulated segment. The hardening requirement (anchors must be pure L2, no L3 identity) is specified for uBridge in docs/design/ubridge-l2-anchor-spec.md — it applies to QEMU TAPs and Docker veths alike.

Roadmap

IOU (its fabric would need a kernel-side port in iol_bridge), Dynamips (hypervisor-created taps, enslavable as they are), and the Ethernet switch / cloud paths (their anchors are ubridge-owned by design) are still on the relay; cross-compute kernel links need VXLAN/GENEVE encapsulation, which is a separate project (it would serve Docker links the same way).