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.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:08 +08:00
1 parent 7d2d8c6747
commit 187201a08c
15 files changed
+444 -40

No files matched your search

+32 -15
View File
@@ -163,30 +163,47 @@ class UDPLink(Link):
def _kernel_datapath_eligible(self, node1, node2):
"""
Whether this link can be wired on the kernel datapath (veth pairs
enslaved into a per-link Linux bridge) instead of the uBridge UDP
relay. Every GNS3 filter type has a kernel equivalent — netem for
delay/packet_loss/corrupt and the netem extensions, cls_bpf for bpf,
the eBPF stateful classifier for frequency_drop — so filters no
longer disqualify the kernel path. Capture and markers are served by
uBridge's AF_PACKET modules on the veth host end.
Whether this link can be wired on the kernel datapath (the adapters'
host-side anchors enslaved into a per-link Linux bridge) instead of
the uBridge UDP relay. Every GNS3 filter type has a kernel equivalent
— netem for delay/packet_loss/corrupt and the netem extensions,
cls_bpf for bpf, the eBPF stateful classifier for frequency_drop — so
filters no longer disqualify the kernel path. Capture and markers are
served by uBridge's AF_PACKET modules on the anchor.
Docker adapters are born as veth pairs (unified interface), so the
datapath is a runtime decision — links attach to running containers
too: the compute side enslaves the veth host end (brctl) or attaches
the relay to it (add_nio_ethernet) without touching the container's
interfaces.
Every adapter owns an anchor, so the datapath is a runtime decision —
links attach to running nodes too: the compute side enslaves the
anchor (brctl) or attaches the relay to it (add_nio_ethernet) without
touching the interfaces the node itself uses. Both endpoints must be
on the same compute (there is no kernel link across hosts) and both
must be able to anchor: Docker adapters are born as veth pairs,
QEMU adapters as persistent TAPs, which need uBridge's tap module —
asked of that compute, since an old uBridge leaves QEMU on the relay
datapath and cannot carry a kernel link at all.
"""
if not Config.instance().settings.Server.enable_kernel_datapath:
return False
if node1.node_type != "docker" or node2.node_type != "docker":
return False
if node1.compute.id != node2.compute.id:
return False
if _is_unix_socket_docker(node1) or _is_unix_socket_docker(node2):
return False
return True
return self._kernel_endpoint_ready(node1) and self._kernel_endpoint_ready(node2)
@staticmethod
def _kernel_endpoint_ready(node):
"""
Whether a node can anchor a kernel link on its compute.
"""
if node.node_type == "docker":
return True
if node.node_type != "qemu":
return False
capabilities = node.compute.capabilities or {}
# Strict True: an unreported capability (old uBridge, failed probe)
# keeps the link on the relay, where it always works.
return capabilities.get("ubridge_tap") is True
async def _prepare(self):
"""