feat(iol): anchor IOL runner containers on persistent TAPs

IOL runner containers (IOLDockerVM, GNS3_IOL_RUNNER) had no kernel
datapath: their guest leg is the runner's AF_UNIX socket pairs, so every
link rode the uBridge relay. Their Ethernet bays now own persistent TAP
anchors (gx names, the iol_docker key of the shared naming contract —
born at node start like IOU's, before any link, so the switch fast
path's deferred join can never miss them), and the per-port bridge's
topology leg swaps between UDP (relay) and the anchor (kernel link)
through the frozen stop -> delete -> add -> start contract — the unix
binding never re-binds across a swap.

- iol_docker_vm: KernelDatapathMixin hooks, the anchor lifecycle
  (_prepare_tap_datapath at _start_ubridge: probe ubridge_bridge_tap +
  ubridge_tap, sweep leftover, tap create born down), the kernel attach
  (ensure-then-add — tap create is strictly create-only, IFF_TUN_EXCL,
  so the ensure checks /sys/class/net first: an unconditional create
  answers EBUSY on the device the start loop just made), the release
  (port bridge stop + delete_nio_tap before the shared teardown) and the
  stop order the fd holders dictate (port bridges first, then tap
  delete, then the per-link bridges — all before the hypervisor stops).
- vendor_docker_vm: the port-bridge creation factored into
  _ensure_unix_port_bridge (idempotent on the registered name), keeping
  the NIOBridge rejection for the generic unix-socket containers.
- docker_vm: the three _kernel_veths lookups go through the
  _kernel_host_ifc hook — behavior-identical for DockerVM, and it is
  what lets the subclass route without overriding the binding methods.
- kernel_anchor: the iol_docker key (gx) and kernel_anchor_type, the
  controller-side mapping for naming an IOL runner's anchor on the
  switch fast path (the marker check stays inline: this module sits
  below everything and must not import the controller package).
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent c20125c180
commit 4b4a0fc35f
7 files changed
+773 -31

No files matched your search

+11 -8
View File
@@ -1807,16 +1807,17 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
async def _connect_nio(self, adapter_number, nio, port_number=0):
host_ifc = self._kernel_veths.get((adapter_number, port_number))
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
if isinstance(nio, NIOBridge):
if host_ifc is None:
# Only reachable for containers started before the
# unified-veth change (or unix-socket vendor containers,
# rejected earlier): the kernel path needs the veth host end.
# rejected earlier): the kernel path needs an anchor — the
# veth host end, or the persistent TAP of an IOL runner.
raise DockerError(
"Adapter {adapter_number} port {port_number} of container '{name}' has no veth "
"interface (it was started before the unified-veth datapath); "
"Adapter {adapter_number} port {port_number} of container '{name}' has no host-side "
"anchor interface (it was started before the unified-veth datapath); "
"restart the node to attach a kernel link to it".format(
adapter_number=adapter_number, port_number=port_number, name=self.name
)
@@ -1894,7 +1895,7 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
if self.ubridge:
if isinstance(nio, NIOBridge):
if self.status == "started":
host_ifc = self._kernel_veths.get((adapter_number, port_number))
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
if host_ifc is not None:
# Incremental marker + filter reconcile on the anchor
# — the relay-datapath equivalent of the branch below.
@@ -2008,9 +2009,11 @@ class DockerVM(DockerKernelDatapathMixin, BaseNode):
nio = self._ethernet_adapters[adapter_number].get_nio(port_number)
if isinstance(nio, NIOBridge):
# Kernel datapath: no relay bridge exists — capture via uBridge's
# AF_PACKET module bound to the veth host end. Single capture per
# uBridge process: a concurrent second one returns EALREADY.
host_ifc = self._kernel_veths.get((adapter_number, port_number))
# AF_PACKET module bound to the anchor (a veth host end for the
# standard container, a persistent TAP for an IOL runner).
# Single capture per uBridge process: a concurrent second one
# returns EALREADY.
host_ifc = self._kernel_host_ifc(adapter_number, port_number)
await self._ubridge_send(f'capture start_kernel {host_ifc} "{output_file}"')
return
# Relay datapath (even though the adapter interface is a veth, the