mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-12 13:35:31 +03:00
feat(docker): packet capture on kernel-datapath links via AF_PACKET
Replace the capture rejection with wiring to uBridge's capture module: an AF_PACKET socket bound to the veth host end writes into the same pcap file the relay path uses (capture start_kernel <if> "<pcap>" / capture stop_kernel), so the pcap streaming to clients is unchanged. - capture survives node restarts: _connect_nio re-arms it when the NIO is already capturing, mirroring the relay path - uBridge's kernel capture is a singleton per process: a concurrent second capture on the same node surfaces uBridge's EALREADY error - update the datapath docs: filters and markers remain relay-only, capture is now served on both datapaths
This commit is contained in:
parent
58b97b4ce5
commit
f6b869be63
@ -1823,8 +1823,9 @@ class DockerVM(BaseNode):
|
||||
whose host end will be enslaved to the per-link kernel bridge, and
|
||||
whose guest end becomes the container interface (the role the TAP
|
||||
plays on the relay path). No uBridge relay bridge is created — frames
|
||||
flow entirely in the kernel, which is also why filters, markers and
|
||||
capture are unavailable on this datapath.
|
||||
flow entirely in the kernel. Filters and markers are unavailable on
|
||||
this datapath (they live in the uBridge relay); packet capture is
|
||||
provided by uBridge's AF_PACKET module on the veth host end.
|
||||
"""
|
||||
|
||||
try:
|
||||
@ -1942,6 +1943,10 @@ class DockerVM(BaseNode):
|
||||
await self._ubridge_send(f'brctl show "{nio.bridge}"')
|
||||
await self._ubridge_send(f'link set "{nio.bridge}" up')
|
||||
await self._ubridge_send(f'brctl addif "{nio.bridge}" "{host_ifc}"')
|
||||
if nio.capturing:
|
||||
# Restore a capture that was active before a node restart
|
||||
# (mirrors the relay path's start_capture in _connect_nio).
|
||||
await self._ubridge_send(f'capture start_kernel {host_ifc} "{nio.pcap_output_file}"')
|
||||
return
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
@ -2132,9 +2137,16 @@ class DockerVM(BaseNode):
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot start the packet capture: uBridge is not running")
|
||||
host_ifc = self._kernel_veths.get((adapter_number, port_number))
|
||||
if host_ifc is not None:
|
||||
# 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.
|
||||
await self._ubridge_send(f'capture start_kernel {host_ifc} "{output_file}"')
|
||||
return
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(f'bridge start_capture {bridge_name} "{output_file}"')
|
||||
|
||||
async def _stop_ubridge_capture(self, adapter_number, port_number=0):
|
||||
@ -2145,9 +2157,13 @@ class DockerVM(BaseNode):
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot stop the packet capture: uBridge is not running")
|
||||
if (adapter_number, port_number) in self._kernel_veths:
|
||||
# Idempotent on the uBridge side (no active capture is OK).
|
||||
await self._ubridge_send("capture stop_kernel")
|
||||
return
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(f"bridge stop_capture {bridge_name}")
|
||||
|
||||
async def start_capture(self, adapter_number, output_file, port_number=0):
|
||||
@ -2162,11 +2178,6 @@ class DockerVM(BaseNode):
|
||||
nio = self.get_nio(adapter_number, port_number)
|
||||
if nio.capturing:
|
||||
raise DockerError(f"Packet capture is already activated on adapter {adapter_number}")
|
||||
if isinstance(nio, NIOBridge):
|
||||
raise DockerError(
|
||||
"Packet capture is not supported on kernel-datapath links (there is no uBridge "
|
||||
"relay in the forwarding path); delete and recreate the link to capture it"
|
||||
)
|
||||
|
||||
nio.start_packet_capture(output_file)
|
||||
if self.status == "started" and self.ubridge:
|
||||
|
||||
@ -116,7 +116,8 @@ class Link:
|
||||
"""
|
||||
Whether this link is wired on the kernel datapath (veth pairs
|
||||
enslaved into a per-link Linux bridge — no uBridge relay in the
|
||||
forwarding path, so filters, markers and capture are unavailable).
|
||||
forwarding path, so filters and markers are unavailable; capture is
|
||||
served by uBridge's AF_PACKET module).
|
||||
"""
|
||||
return any(d.get("type") == "nio_bridge" for d in (getattr(self, "_link_data", None) or []))
|
||||
|
||||
|
||||
@ -135,8 +135,9 @@ class UDPLink(Link):
|
||||
"""
|
||||
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. The kernel path has no userspace relay, so filters, markers
|
||||
and packet capture — which all live in the relay — disqualify it.
|
||||
relay. The kernel path has no userspace relay, so filters and
|
||||
markers — which live in the relay — disqualify it (capture is
|
||||
served separately by uBridge's AF_PACKET module).
|
||||
It is host-local (same compute) and requires the adapter interfaces
|
||||
to be created as veths at container start, hence stopped nodes only.
|
||||
"""
|
||||
|
||||
@ -75,7 +75,8 @@ class BridgeNIO(BaseModel):
|
||||
Kernel-datapath bridge Network Input/Output properties. The NIO tells the
|
||||
node to enslave its veth host end into the named kernel bridge instead of
|
||||
wiring a uBridge UDP relay. ``filters`` and ``markers`` must stay empty on
|
||||
this datapath; they are accepted (and rejected server-side) only because
|
||||
this datapath (rejected server-side); packet capture is supported via
|
||||
uBridge's AF_PACKET module. Both are accepted as fields only because
|
||||
link update payloads always carry the full NIO dictionary.
|
||||
"""
|
||||
|
||||
|
||||
@ -339,15 +339,76 @@ async def test_update_nio_kernel_suspend_toggles_carrier(vm):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_capture_rejected_on_kernel_link(vm):
|
||||
async def test_start_capture_kernel_uses_af_packet(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
with pytest.raises(DockerError, match="kernel-datapath"):
|
||||
await vm.start_capture(0, "/tmp/capture.pcap")
|
||||
|
||||
assert nio.capturing is True
|
||||
vm._ubridge_send.assert_any_call(f'capture start_kernel {host_ifc} "/tmp/capture.pcap"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_capture_kernel(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
await vm.start_capture(0, "/tmp/capture.pcap")
|
||||
|
||||
await vm.stop_capture(0)
|
||||
|
||||
assert nio.capturing is False
|
||||
vm._ubridge_send.assert_any_call("capture stop_kernel")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_capture_second_concurrent_fails(vm):
|
||||
"""
|
||||
uBridge's kernel capture is a singleton per process: a second concurrent
|
||||
capture returns EALREADY and must surface to the caller.
|
||||
"""
|
||||
|
||||
async def send(command):
|
||||
if "capture start_kernel" in command:
|
||||
raise UbridgeError("Could not start kernel capture: Operation already in progress")
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=send)
|
||||
vm.status = "started"
|
||||
vm._kernel_veths[(0, 0)] = vm._veth_names(0, 0)[0]
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
with pytest.raises(UbridgeError, match="already in progress"):
|
||||
await vm.start_capture(0, "/tmp/capture.pcap")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_restarts_capture(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
nio.start_packet_capture("/tmp/capture.pcap")
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
vm._ubridge_send.assert_any_call(f'capture start_kernel {host_ifc} "/tmp/capture.pcap"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_veths(vm):
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user