feat(docker): unify adapter interfaces on veth, allow kernel links at runtime

Every Docker adapter is now born as a veth pair at container start
(unified interface) regardless of NIO type: guest end in the container
namespace, host end in the root namespace. Kernel links enslave the host
end into the per-link Linux bridge; relay links attach it to the uBridge
relay via AF_PACKET (bridge add_nio_ethernet) — the TAP path is gone.

This makes the datapath a runtime decision:
- links can be created on running containers (kernel or relay) without
  touching container interfaces — the controller stopped-node
  eligibility check is removed
- project reopen re-prepares relay links and upgrades eligible ones to
  the kernel datapath
- packet capture start/stop now key on the NIO type (not veth presence)
  so relay-over-veth captures use the relay capture, kernel ones the
  AF_PACKET module
- vendor unix-socket containers keep their wiring: no veth is required
  on the relay path
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:08 +08:00
1 parent 61020d3bbc
commit 672efda592
6 files changed
+121 -105

No files matched your search

@@ -125,10 +125,11 @@ async def test_add_ubridge_kernel_connection(vm):
@pytest.mark.asyncio
async def test_add_ubridge_connection_none_nio_stays_on_tap_path(vm):
async def test_add_ubridge_connection_none_nio_creates_veth(vm):
"""
Unconnected adapters keep the relay TAP: the veth variant is only created
for adapters whose NIO is a kernel one at start time.
Unconnected adapters are born as veths too (the unified interface): the
interface exists inside the container with carrier off, and any link
type can be attached later — at runtime — without rebirthing it.
"""
vm._ubridge_hypervisor = MagicMock()
@@ -136,11 +137,42 @@ async def test_add_ubridge_connection_none_nio_stays_on_tap_path(vm):
await vm._add_ubridge_connection(None, 0)
host_ifc, guest_ifc = vm._veth_names(0, 0)
vm._ubridge_hypervisor.assert_has_calls([
call.send("bridge create bridge0"),
call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"),
call.send(f'docker create_veth "{host_ifc}" "{guest_ifc}"'),
call.send(f'link set "{host_ifc}" down'),
call.send(f'docker move_to_ns {guest_ifc} 42 eth0'),
], any_order=True)
assert vm._kernel_veths == {}
assert vm._kernel_veths[(0, 0)] == host_ifc
# no relay bridge and no TAP for an unconnected adapter
assert "bridge0" not in vm._bridges
for c in vm._ubridge_hypervisor.method_calls:
assert "add_nio_tap" not in str(c)
@pytest.mark.asyncio
async def test_add_ubridge_connection_udp_relays_over_veth(vm):
"""
A relay NIO at start: the adapter is born as a veth and the uBridge
relay attaches to its host end via AF_PACKET (add_nio_ethernet) —
no TAP anywhere.
"""
vm._ubridge_hypervisor = MagicMock()
vm._namespace = 42
nio = vm.manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
await vm._add_ubridge_connection(nio, 0)
host_ifc, _ = vm._veth_names(0, 0)
vm._ubridge_hypervisor.assert_has_calls([
call.send(f'docker create_veth "{host_ifc}" "{vm._veth_names(0, 0)[1]}"'),
call.send("bridge create bridge0"),
call.send(f'bridge add_nio_ethernet bridge0 "{host_ifc}"'),
call.send("bridge add_nio_udp bridge0 4242 127.0.0.1 4343"),
call.send("bridge start bridge0"),
], any_order=True)
assert "bridge0" in vm._bridges
@pytest.mark.asyncio
+16 -22
View File
@@ -1541,14 +1541,17 @@ async def test_add_ubridge_connection(vm):
vm._namespace = 42
await vm._add_ubridge_connection(nio, 0)
host_ifc, guest_ifc = vm._veth_names(0, 0)
calls = [
call.send(f'docker create_veth "{host_ifc}" "{guest_ifc}"'),
call.send(f'link set "{host_ifc}" down'),
call.send(f"docker move_to_ns {guest_ifc} 42 eth0"),
call.send("bridge create bridge0"),
call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"),
call.send("docker move_to_ns tap-gns3-e0 42 eth0"),
call.send(f'bridge add_nio_ethernet bridge0 "{host_ifc}"'),
call.send("bridge add_nio_udp bridge0 4242 127.0.0.1 4343"),
call.send('bridge start_capture bridge0 "/tmp/capture.pcap"'),
call.send("bridge start bridge0"),
call.send("bridge set_nio_tap_carrier bridge0 on"),
call.send(f'link set "{host_ifc}" up'),
]
assert "bridge0" in vm._bridges
# We need to check any_order otherwise mock is confused by asyncio
@@ -1571,11 +1574,13 @@ async def test_add_ubridge_connections_with_base_mac_address(vm):
nio = vm.manager.create_nio(nio_params)
await vm._add_ubridge_connection(nio, 1)
_, guest0 = vm._veth_names(0, 0)
_, guest1 = vm._veth_names(1, 0)
calls = [
call.send("bridge create bridge0"),
call.send("bridge create bridge1"),
call.send("docker set_mac_addr tap-gns3-e0 02:42:42:42:42:00"),
call.send("docker set_mac_addr tap-gns3-e0 02:42:42:42:42:01"),
call.send(f"docker set_mac_addr {guest0} 02:42:42:42:42:00"),
call.send(f"docker set_mac_addr {guest1} 02:42:42:42:42:01"),
]
# We need to check any_order otherwise mock is confused by asyncio
@@ -1591,12 +1596,14 @@ async def test_add_ubridge_connection_none_nio(vm):
await vm._add_ubridge_connection(nio, 0)
host_ifc, guest_ifc = vm._veth_names(0, 0)
calls = [
call.send("bridge create bridge0"),
call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"),
call.send("docker move_to_ns tap-gns3-e0 42 eth0"),
call.send(f'docker create_veth "{host_ifc}" "{guest_ifc}"'),
call.send(f'link set "{host_ifc}" down'),
call.send(f"docker move_to_ns {guest_ifc} 42 eth0"),
]
assert "bridge0" in vm._bridges
# unconnected adapter: interface exists (carrier off), no relay bridge
assert "bridge0" not in vm._bridges
# We need to check any_order ortherwise mock is confused by asyncio
vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True)
@@ -1626,19 +1633,6 @@ async def test_add_ubridge_connection_invalid_adapter_number(vm):
await vm._add_ubridge_connection(nio, 12)
@pytest.mark.asyncio
async def test_add_ubridge_connection_no_free_interface(vm):
nio = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}
nio = vm.manager.create_nio(nio)
with pytest.raises(DockerError):
# We create fake ethernet interfaces for docker
interfaces = [f"tap-gns3-e{index}" for index in range(4096)]
with patch("psutil.net_if_addrs", return_value=interfaces):
await vm._add_ubridge_connection(nio, 0)
@pytest.mark.asyncio
async def test_adapter_add_nio_binding_1(vm):
@@ -314,7 +314,7 @@ async def test_move_to_ns_uses_renamed_interface(compute_project, manager):
# adapter 0 should be renamed to mgmt0
move_calls = [c for c in vm._ubridge_hypervisor.method_calls if "move_to_ns" in str(c)]
assert move_calls, "move_to_ns was not sent"
assert call.send("docker move_to_ns tap-gns3-e0 42 mgmt0") in move_calls
assert call.send(f"docker move_to_ns {vm._veth_names(0, 0)[1]} 42 mgmt0") in move_calls
@pytest.mark.asyncio
@@ -326,7 +326,7 @@ async def test_move_to_ns_falls_back_to_eth(compute_project, manager):
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
await vm._add_ubridge_connection(nio, 1)
move_calls = [c for c in vm._ubridge_hypervisor.method_calls if "move_to_ns" in str(c)]
assert call.send("docker move_to_ns tap-gns3-e0 42 eth1") in move_calls
assert call.send(f"docker move_to_ns {vm._veth_names(1, 0)[1]} 42 eth1") in move_calls
# ---------------------------------------------------------------------------
@@ -123,11 +123,16 @@ async def test_kernel_datapath_eligible_with_project_marker_definitions(project)
@pytest.mark.asyncio
async def test_kernel_datapath_not_eligible_for_running_nodes(project):
async def test_kernel_datapath_eligible_for_running_nodes(project):
"""
Docker adapters are born as veths (unified interface), so links attach
to running containers too — no stopped-node requirement.
"""
link, node1, node2 = await _kernel_link(project)
node1._status = "started"
assert link._kernel_datapath_eligible(node1, node2) is False
node2._status = "started"
assert link._kernel_datapath_eligible(node1, node2) is True
@pytest.mark.asyncio