gns3-server/docs/features/qemu-kernel-datapath.md
YueGuobin 7a9a514ea3
docs: kernel-datapath family review pass — accuracy fixes and pinned claims
A correctness read-through of the five delivered datapath documents, plus
the two code comments that carried the same stale vocabulary.

Datapath selection: the VendorDockerVM selector is `console_type=docker_exec`
alone (`GNS3_SKIP_INIT` and friends are behavior knobs of the chosen class);
the cross-compute and anchor-less fallbacks are named for what they are; the
Ethernet-switch endpoint is the documented exception to the anchoring rule,
and the IOL runner container is called out as the Docker node that does not
anchor on a veth.

Filter vocabulary: the stateful eBPF program evaluates four modes
(nth → quota → window → flow), GNS3 exposes three of them, and window_drop
joins the kernel-only list; the correl suffixes gate on the `rate` token as
the extension-build marker.

group_fwd_mask: the 0xfffd port mask is applied inside uBridge's `brctl addif`
itself, not as a separate command — the link-creation sequences and the
operation tables no longer show an arrow the server never sends. Same class
of fix for `link set up` on the IOL anchor (addif already brought it up; the
command only pushes a suspended anchor back down) and for the QEMU carrier
replication (QMP `set_link`, gated by `replicate_network_connection_state`).

Lifecycles and stop orders re-worded around the invariant rather than the
node type: the fd holder must release before the devices are deleted, which
is QEMU's process-first order, IOU's uBridge-first order and Dynamips'
hypervisor-NIO-first order. Deferred wiring for a stopped endpoint is
documented as the common path it is, and the Dynamips "stop is not a close"
note now says what recovery from a dead hypervisor actually requires.

Dynamips specifics: a c7200 needs an adapter in slot 1 (slot 0 is the fixed
I/O slot), the tap-fd direction is stated correctly (writes inject into the
kernel, reads return what the kernel transmits), and the anchor registry is
name-keyed against the controller's port matrix by convention, not derivation.

IOL port bridge naming: `bridge{bay}` on unit 0, `bridge{bay}_{unit}`
elsewhere; the leg swap happens on link delete/re-create or a datapath
switch, while suspend is an in-place NIO update and a project reopen is a
fresh build.

Verified and Roadmap sections: the in-repo e2e suite is distinguished from
the isolated-instance development harness (IOU), assertions are stated with
their bounds and capability gates, and the roadmap entries collapse into the
one remaining project — VXLAN/GENEVE for cross-compute links.

Code comments: `flow` added to the documented mode order in the eBPF apply
path, and the `nio_bridge` module docstring corrected on which filter types
run on the kernel datapath (bpf via cls_bpf, frequency_drop/quota/window_drop
via the eBPF classifier — neither is relay-only any more).
2026-10-06 00:21:10 +08:00

23 KiB
Raw Blame History

This documentation is AI-generated with reference to actual code and verified against real-kernel testing. AI can make mistakes — please verify against the source code when in doubt.

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 the same compute. Interface names follow the shared contract of gns3server/utils/kernel_anchor.py: gq{node_id[:8]}e{adapter}p{port} for the persistent TAP (one per QEMU adapter, port always 0) and gns3{link_id[:11]} for the bridge; the concrete names in the diagrams use node ids 3f2a1b9c / 7c9d4e2f and link id prefix 5d3c2b1a4e6. A mixed Docker↔QEMU link is the same bridge with the Docker end's gv veth host end as its other anchor — the shared mixin is anchor-agnostic; the pure-veth picture is docker-kernel-datapath.md. Terms the diagrams use:

Term Meaning
uBridge the per-node helper process gns3-server spawns with elevated capabilities; its hypervisor command surface executes all host-side wiring (TAPs, bridges, carrier, tc, taps) — on a kernel link it builds and observes the path but never carries frames
anchor the host-side interface a link attaches to — here the gq persistent TAP
persistent TAP the TAP uBridge creates per QEMU adapter at node start (tap create, tap set_owner); it outlives restarts and crashes, and QEMU opens it by name as its -netdev tap
per-link bridge the Linux bridge gns3{link_id[:11]} that stands in for the cable; the two anchors are its only ports
bridge port an interface enslaved to a Linux bridge (brctl addif) — it no longer sends or receives on its own: frames arriving on it go to the bridge's forwarding logic, and the bridge transmits through it
tap fd a TAP's file descriptor: reads return frames the kernel received on the device, writes inject frames into it — on this datapath QEMU itself holds it
FDB the bridge's forwarding database (learned MAC → port); unknown, broadcast and multicast destinations are flooded
01:80:c2 / group_fwd_mask the IEEE 802.1D reserved multicast range (STP, LACP, LLDP, 802.1X, PAUSE); the bridge forwards it only per the port's group_fwd_mask — 0xfffd passes everything except PAUSE/PFC
tc / qdisc traffic-control objects the kernel inserts into the anchor's egress path — stages frames pass through in flight, not external packet consumers; uBridge only installs them over netlink
clsact the tc classifier carrier on the anchor; its egress chain is where the drop classifiers run
eBPF classifier the stateful drop modes (every-Nth, quota, time window) at clsact egress prio 1
bpf_drop match-drop BPF expressions at clsact egress prio 10–99
netem the impairment qdisc behind clsact: delay, loss, rate, corrupt, duplicate, …
AF_PACKET tap uBridge's packet socket bound to the anchor; the kernel clones every frame the interface sends or receives to it (the tcpdump mechanism) — read-only copies for capture and markers, the original stays on the kernel path
carrier the anchor's admin state (link set up/down); suspend is carrier down plus QMP set_link off so the guest notices
QMP the QEMU machine protocol — how the server drives a running QEMU (link state on suspend/resume) without restarting it
nio_bridge the NIO record the controller posts to each endpoint node: bridge name, filters, markers, suspend flag
flowchart LR
    subgraph GA["guest A (in the VM)"]
        nA["NIC 0"]
    end
    subgraph RootNS["root namespace"]
        gqA["gq3f2a1b9ce0p0 — anchor"]
        BR["gns35d3c2b1a4e6 — per-link bridge"]
        gqB["gq7c9d4e2fe0p0 — anchor"]
        UBA["uBridge A"]
        UBB["uBridge B"]
    end
    subgraph QPA["QEMU A process"]
        ndA["netdev gns3-0 (tap fd)"]
    end
    subgraph QPB["QEMU B process"]
        ndB["netdev gns3-0 (tap fd)"]
    end
    subgraph GB["guest B (in the VM)"]
        nB["NIC 0"]
    end
    nA <-->|"virtio / e1000"| ndA
    ndA <-->|"tap fd"| gqA
    gqA <-->|"bridge port"| BR
    BR <-->|"bridge port"| gqB
    gqB <-->|"tap fd"| ndB
    ndB <-->|"virtio / e1000"| nB
    gqA -. "frame copies" .-> UBA
    UBA -. "qdisc install" .-> gqA
    gqB -. "frame copies" .-> UBB
    UBB -. "qdisc install" .-> gqB

Everything in the root namespace is built the same way: gns3-server creates the persistent TAPs at node start (before QEMU is launched), the per-link bridge and its memberships at link creation, and every later change (carrier, capture, markers, filters) by sending hypervisor commands to the node's uBridge process — the privileged agent that turns them into netlink/ioctl operations. That build flow is the Link creation sequence below; this diagram is the steady state that remains, where the only uBridge relationships left are the two dashed kinds.

uBridge sits off the forwarding path — the dashed edges are control and observation, never data flow. The qdisc install edge is uBridge configuring the kernel over netlink; the kernel then runs those qdiscs in-flight on the anchor's egress (stages of the transmit path, not packet consumers). The frame copies edge is the AF_PACKET tap, there only while a capture or markers is active (a kernel link leaves no uBridge endpoint on the anchor otherwise): the kernel clones every frame the TAP sends or receives to uBridge's packet socket (the tcpdump mechanism) — read-only copies for capture and markers; the originals never leave the kernel. The only userspace on the path is the two QEMU processes pumping their own tap fds — the irreducible VM leg, one fd crossing at each end per direction.

Relay link between the same two QEMU nodes on the same single compute — what every link becomes when enable_kernel_datapath is off (mixed node types and cross-compute wiring fall back too; there the UDP hop rides the real network instead of loopback, the copy count unchanged):

flowchart LR
    subgraph GA["guest A (in the VM)"]
        EA["NIC sends"]
    end
    subgraph QA["QEMU A — userspace"]
        NDA["netdev tap fd"]
    end
    subgraph KERN["kernel — root ns"]
        TA["gq3f2a1b9ce0p0 — anchor"]
        UDP["UDP sockets (loopback)"]
        TB["gq7c9d4e2fe0p0 — anchor"]
    end
    subgraph UBA["uBridge A — userspace"]
        NEA["AF_PACKET nio"]
        ENA["relay + userspace filters"]
        NUA["UDP nio"]
    end
    subgraph UBB["uBridge B — userspace"]
        NUB["UDP nio"]
        ENB["relay + userspace filters"]
        NEB["AF_PACKET nio"]
    end
    subgraph QB["QEMU B — userspace"]
        NDB["netdev tap fd"]
    end
    subgraph GB["guest B (in the VM)"]
        EB["NIC receives"]
    end
    EA -->|"virtio"| NDA
    NDA -->|"copy 1: user→kernel"| TA
    TA -->|"copy 2: kernel→user"| NEA
    NEA --> ENA --> NUA
    NUA -->|"copy 3: user→kernel"| UDP
    UDP -->|"copy 4: kernel→user"| NUB
    NUB --> ENB --> NEB
    NEB -->|"copy 5: user→kernel"| TB
    TB -->|"copy 6: kernel→user"| NDB
    NDB -->|"virtio IRQ"| EB

Drawn one-way; the reverse direction is the mirror image. A relayed frame crosses the kernel/user boundary six times each way (twelve per round trip): the two halves of the irreducible VM leg — QEMU's tap-fd write at the sending end, its read at the receiving end — plus the four the relay adds around them (AF_PACKET read, UDP send, UDP receive, AF_PACKET inject). The kernel datapath's only crossings are the VM leg's two; the relay's filters run in userspace, on the frames between the copies.

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. And unlike the IOL runner container, whose anchors idle on the relay, the anchor here is the netdev itself: it stays up and carries as the relay's AF_PACKET endpoint — QEMU has no other leg (the legacy socket-netdev fallback below is the one shape without the TAP).

Frame path for one A → B frame. The impairment chain fires on the egress of the destination end's TAP, in order clsact → netem (a classifier drop means netem never sees the frame); each direction is impaired exactly once and B → A is symmetric on gqA — hence delay 100 ≈200 ms round trip (measured 100.3 ms one-way). Each one-way trip crosses the kernel/user boundary exactly twice — the sending QEMU's tap-fd write and the receiving QEMU's tap-fd read, the irreducible VM leg:

flowchart LR
    subgraph ANS["guest A (in the VM)"]
        S1["NIC sends"]
    end
    subgraph QPA["QEMU A — userspace"]
        S2["tap fd to kernel"]
    end
    subgraph ROOT["root namespace"]
        subgraph GQA["gqA — ingress (its egress chain serves B→A)"]
            S3["ingress"]
        end
        subgraph BRP["per-link bridge"]
            G1{"reserved 01:80:c2?"}
            D["dropped"]
            S4["FDB lookup / flood"]
        end
        subgraph GQB["gqB — egress chain (clsact, then netem)"]
            S5["eBPF prio 1"]
            S6["bpf_drop prio 10-99"]
            S7["netem qdisc"]
        end
        T["capture / markers"]
    end
    subgraph QPB["QEMU B — userspace"]
        S8["tap fd to QEMU"]
    end
    subgraph BNS["guest B (in the VM)"]
        S9["NIC receives"]
    end
    S1 -->|"virtio"| S2
    S2 -->|"tap fd, copy 1: user→kernel"| S3
    S3 --> G1
    G1 -->|"PAUSE 0x01"| D
    G1 -->|"0xfffd passes"| S4
    S4 --> S5
    S5 --> S6 --> S7
    S7 -->|"tap fd, copy 2: kernel→user"| S8
    S8 -->|"virtio IRQ"| S9
    S3 -. "AF_PACKET tap" .-> T
    S7 -. "AF_PACKET tap" .-> T

What each stage drops: the reserved-range gate passes LACP, LLDP, 802.1X and STP per the port mask but never PAUSE/PFC (Link-local frames below); the clsact/netem stages drop or impair per the installed filters (Capture, markers, filters below). The AF_PACKET taps observe both anchors but never a clsact-dropped frame. Copies 1–2 are the whole path's only kernel/user crossings — both halves of the emulation's own TAP-fd leg (QEMU A's write, QEMU B's read); every stage between them runs in the kernel.

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 (an Ethernet-switch endpoint is the exception — it absorbs the peer's anchor into its own bridge, so the switch side needs no anchoring capability of its own; docs/features/ethernet-switch-kernel-datapath.md):

  • Docker can (adapters are born as veth pairs), except the IOL runner container, whose unix-socket guest leg anchors on a TAP under its own capability pair (docs/features/iol-docker-kernel-datapath.md);
  • 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).

Both endpoints attach concurrently and compute the same bridge name independently (a pure function of the link id), so either may win the brctl create race — the loser verifies the bridge exists (brctl show) and continues. The topology above's link, at attach time:

sequenceDiagram
    participant API as REST API
    participant C as Controller
    participant CA as QEMU node A
    participant CB as QEMU node B
    participant UA as uBridge A
    participant UB as uBridge B

    API->>C: POST /links
    C->>C: eligibility check
    C->>C: bridge = gns3 + link_id[:11]
    C--)CA: nio_bridge
    C--)CB: nio_bridge (same bridge)
    par QEMU node A
        CA->>UA: brctl create
        CA->>UA: link set bridge up
        CA->>UA: brctl addif gq3f2a1b9ce0p0
        CA->>UA: capture / markers if any
        CA->>UA: tc netem + bpf + eBPF
        CA->>UA: link set gq3f2a1b9ce0p0 up
    and QEMU node B
        CB->>UB: same on gq7c9d4e2fe0p0
    end
    Note over UA,UB: steady state — kernel forwards, uBridge taps

Every arrow to uBridge is one _ubridge_send command line on the node's local hypervisor console; uBridge executes it (netlink/ioctl) and replies OK or an error synchronously before the next command is sent — those replies are omitted for brevity. The per-port group_fwd_mask 0xfffd is deliberately not an arrow: uBridge applies it inside brctl addif itself (Link-local frames below). uBridge never initiates anything: it is a pure executor, and the only data ever flowing back (capture/marker frame copies) rides separate AF_PACKET sockets, not this console.

The TAP already exists at attach time — created at node start, before QEMU launched (Adapter lifecycle below) — which is what makes the attach runtime-safe on a running VM: anchors, bridge memberships and qdiscs all change around the -netdev tap QEMU already holds, and the only QEMU traffic is carrier replication — QMP set_link on attach, detach and suspend/resume, gated by the node's replicate_network_connection_state option (default on), so the guest's carrier follows the link.

A mixed Docker↔QEMU link runs the identical sequence per end — each node's uBridge receives the same commands on its own anchor, the Docker end's gv veth host end in place of the gq TAP; only the anchor name differs (the mixin is anchor-agnostic, which is the point of the shared gns3server/compute/kernel_datapath.py).

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) — the QEMU process is stopped first, then _remove_taps un-persists the TAPs (tap delete) and deletes the per-link kernel bridges the node still holds. The order matters: uBridge's tap delete refuses a device another process holds open ("Device or resource busy") and that best-effort delete is suppressed, so deleting the TAPs while QEMU still ran leaked every persistent TAP (process side first, then the devices — the same lesson as IOU's stop order, where uBridge itself holds the anchors and is made to release them before tap delete). Stopping a node never runs the link-teardown path — links survive stops by design, and project close is the same (it stops nodes without deleting links) — so those bridges would otherwise stay behind as empty orphans; a restart rebuilds them from the NIO. Deleting a node from the project is not that case: the controller deletes its links first, so the teardown does run.
Operation Kernel-datapath action
Create / attach brctl create (EEXIST tolerated via brctl show) + link set <bridge> up + brctl addif <bridge> <tap>, then capture and markers and filters on the anchor (tc netem set, bpf/eBPF drops), carrier up last
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.

The bridge stands in for a cable, but the kernel's br_handle_frame() does not forward the IEEE 802.1D reserved range (01:80:c2:00:00:00-0f) by default — LACP, LLDP/DCBX and 802.1X would never cross a kernel link while the UDP relay carries them fine. uBridge therefore opens every port's per-port group_fwd_mask (IFLA_BRPORT_GROUP_FWD_MASK) to 0xfffd at brctl addif time (Linux 4.15+, best-effort on older kernels). The bridge-level knob cannot do this: BR_GROUPFWD_RESTRICTED rejects bits 0-2, so LACP is reachable per port only.

Result on kernel links: ordinary multicast, STP/RSTP, LACP, 802.1X and LLDP/DCBX all cross; 802.3x PAUSE / PFC does not — case 0x01 in br_handle_frame() drops it unconditionally and no mask can enable it. That is a documented limit, not a regression (PFC's hardware semantics are out of emulation's reach anyway); a lab that needs PAUSE frames on the wire must use a relay link. Verified live by tests/e2e/test_docker_link_local_frames.py (per-MAC guest-to-guest matrix); the contract and kernel references live in docs/design/ubridge-link-local-frame-forwarding-spec.md.

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; the link object reports kernel_datapath: true (the POST /links response) 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.

The repo suite tests/e2e/test_qemu_kernel_datapath.py covers the same ground with a real guest: two IOSv routers (a real qcow2 image, linked clone, the real IOS CLI on the telnet console) — the full kernel lifecycle (anchors at start, per-link bridge membership, pure-L2 anchors, the §E.2 idle-silence window with the guests shut, real ICMP, netem delay, suspend, capture, link delete/re-create, node stop/start rewiring, zero residue on project delete) plus the relay negative control, where the anchor is up and carrying as the relay's AF_PACKET endpoint (QEMU has no other leg — the TAP is the netdev on the relay too). It needs an L3 IOSv image in the images directory's QEMU subfolder; a raw node must also name hda_disk_interface (the default none attaches no frontend device and the VM comes up diskless — the IOSv appliance boots virtio).

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 and is delivered: anchors and per-link bridges come up with addrgenmode none, no addresses, and an idle anchor is silent (asserted in the e2e after a 2.5 s settle).

One residual is accepted and documented there: enslaving a port makes the kernel announce the bridge's multicast memberships once — an IGMPv3 plus one or two MLDv2 reports in the first second — which floods into the segment like any L2 control frame. It stops; it is outside the L2-only command's reach (no addresses involved, IPv4 has no per-device IGMP switch).

Roadmap

The rest of the node family has landed on the same mixin: IOU — see docs/features/iou-kernel-datapath.md (Ethernet bays anchor on persistent TAPs bound to the IOL fabric, iol_bridge add_nio_tap per docs/design/ubridge-iol-tap-anchor-spec.md; serial links stay relay, and one userspace hop on the IOU leg is irreducible — the fabric is Unix sockets); Dynamips — see docs/features/dynamips-kernel-datapath.md (the hypervisor opens uBridge-created TAPs with nio create_tap: the same external-fd-holder shape QEMU uses; serial/ATM/POS ports stay relay); the Ethernet switch — see docs/features/ethernet-switch-kernel-datapath.md (the switch absorbs a peer's anchor into its own kernel bridge; switch-to-switch cascades too); the IOL runner container — see docs/features/iol-docker-kernel-datapath.md (persistent TAP anchors through the generic bridge module's swappable TAP leg; the unix-socket guest leg keeps one userspace hop, like IOU's fabric).

Still on the relay: the cloud paths (their anchors are ubridge-owned by design); and cross-compute kernel links need VXLAN/GENEVE encapsulation, which is a separate project (it would serve Docker links the same way).