mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 22:11:14 +02:00
feat(docker): traffic-insight markers on kernel-datapath links via AF_PACKET
Markers no longer disqualify the kernel datapath. They ride the NIO like on the relay path and attach to the veth host end through uBridge's marker add_kernel/delete_kernel/enable_kernel commands (AF_PACKET taps, same BPF/direction/signal semantics as the relay mark filter). - base_node: marker anchors are now polymorphic (relay bridge name or kernel veth host interface); the reconcile loop is datapath-agnostic and DockerVM overrides the add/delete/enable primitives to translate the commands. Marker-name validation extracted for both add paths. - docker_vm: kernel NIO connect/update apply markers on the veth anchor; link deletion tears markers down explicitly (the veth survives the link, unlike a relay bridge whose death drops its filters). - controller: eligibility and start_marker no longer exclude markers or project-level marker definitions; the kernel NIO carries the capture node's marker specs. - NIO creation accepts markers on kernel NIOs (filters still rejected).
This commit is contained in:
8 files changed
+378
-69
No files matched your search
@@ -21,6 +21,7 @@ creation, per-link kernel bridge enslavement, carrier semantics, relay
|
||||
fallback and cleanup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
@@ -78,10 +79,15 @@ def test_create_nio_bridge_rejects_filters(vm):
|
||||
vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE, "filters": {"loss": [10]}})
|
||||
|
||||
|
||||
def test_create_nio_bridge_rejects_markers(vm):
|
||||
def test_create_nio_bridge_accepts_markers(vm):
|
||||
"""
|
||||
Markers ride the kernel NIO (attached to the veth host end via uBridge's
|
||||
AF_PACKET marker module); only filters are rejected.
|
||||
"""
|
||||
|
||||
with pytest.raises(ComputeError):
|
||||
vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE, "markers": {"m": {"bpf": "icmp"}}})
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE, "markers": {"m": {"bpf": "icmp"}}})
|
||||
assert isinstance(nio, NIOBridge)
|
||||
assert nio.markers == {"m": {"bpf": "icmp"}}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -409,6 +415,125 @@ async def test_connect_nio_kernel_restarts_capture(vm):
|
||||
vm._ubridge_send.assert_any_call(f'capture start_kernel {host_ifc} "/tmp/capture.pcap"')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markers (AF_PACKET taps on the veth host end)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _marker_spec(bpf="icmp", tag=1, enabled=True, direction=None, link_id="l1"):
|
||||
return {"bpf": bpf, "tag": tag, "enabled": enabled, "direction": direction,
|
||||
"data_link_type": "DLT_EN10MB", "link_id": link_id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_applies_markers(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, "markers": {"icmp-m": _marker_spec()}})
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
pcap = os.path.join(vm.project.markers_working_directory(), f"{vm.id}_l1_icmp-m.pcap")
|
||||
vm._ubridge_send.assert_any_call(f'marker add_kernel icmp-m {host_ifc} "icmp" tag 1 link l1 pcap "{pcap}"')
|
||||
assert vm._marker_filter_bridges[("icmp-m", "l1")] == host_ifc
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_marker_disabled_at_apply(vm):
|
||||
"""
|
||||
A disabled marker is installed but silenced (marker enable_kernel off) so
|
||||
the UI can flip it back on instantly — same contract as the relay path.
|
||||
"""
|
||||
|
||||
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, "markers": {"icmp-m": _marker_spec(enabled=False)}}
|
||||
)
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
vm._ubridge_send.assert_any_call(f"marker enable_kernel {host_ifc} icmp-m off")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_applies_markers(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)
|
||||
|
||||
nio.markers = {"tcp-m": _marker_spec(bpf="tcp", tag=2, link_id="l2")}
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
|
||||
pcap = os.path.join(vm.project.markers_working_directory(), f"{vm.id}_l2_tcp-m.pcap")
|
||||
vm._ubridge_send.assert_any_call(f'marker add_kernel tcp-m {host_ifc} "tcp" tag 2 link l2 pcap "{pcap}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_marker_toggle_kernel_uses_enable_kernel(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
vm._marker_filter_bridges[("icmp-m", "l1")] = host_ifc
|
||||
|
||||
await vm._ubridge_set_marker_filter_state("icmp-m", False)
|
||||
vm._ubridge_send.assert_any_call(f"marker enable_kernel {host_ifc} icmp-m off")
|
||||
await vm._ubridge_set_marker_filter_state("icmp-m", True)
|
||||
vm._ubridge_send.assert_any_call(f"marker enable_kernel {host_ifc} icmp-m on")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_nio_deletes_markers(vm):
|
||||
"""
|
||||
The veth survives link deletion (it is the adapter interface), so kernel
|
||||
markers must be torn down explicitly — otherwise a deleted link's markers
|
||||
would keep sniffing and signaling.
|
||||
"""
|
||||
|
||||
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
|
||||
vm._marker_filter_bridges[("icmp-m", "l1")] = host_ifc
|
||||
vm._marker_specs[("icmp-m", "l1")] = _marker_spec()
|
||||
vm._ethernet_adapters[0].add_nio(0, vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE}))
|
||||
|
||||
await vm.adapter_remove_nio_binding(0)
|
||||
|
||||
vm._ubridge_send.assert_any_call(f"marker delete_kernel {host_ifc} icmp-m")
|
||||
assert ("icmp-m", "l1") not in vm._marker_filter_bridges
|
||||
assert ("icmp-m", "l1") not in vm._marker_specs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rebuild_marker_filter_kernel(vm):
|
||||
"""
|
||||
The fine-grained rebuild path (bpf/tag/direction change) goes through the
|
||||
overridden primitives: delete_kernel + add_kernel with the new expression.
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
vm._marker_filter_bridges[("icmp-m", "l1")] = host_ifc
|
||||
|
||||
await vm.rebuild_marker_filter("icmp-m", "l1", "tcp", tag=9)
|
||||
|
||||
pcap = os.path.join(vm.project.markers_working_directory(), f"{vm.id}_l1_icmp-m.pcap")
|
||||
vm._ubridge_send.assert_any_call(f"marker delete_kernel {host_ifc} icmp-m")
|
||||
vm._ubridge_send.assert_any_call(f'marker add_kernel icmp-m {host_ifc} "tcp" tag 9 link l1 pcap "{pcap}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_veths(vm):
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Docker links, NIO emission and the filters/markers guards.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller.udp_link import UDPLink
|
||||
@@ -96,20 +96,28 @@ async def test_kernel_datapath_not_eligible_with_filters(project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_markers(project):
|
||||
async def test_kernel_datapath_eligible_with_markers(project):
|
||||
"""
|
||||
Markers no longer disqualify the kernel datapath: they attach to the veth
|
||||
host end via uBridge's AF_PACKET marker module (marker add_kernel).
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._markers = {"m": {"bpf": "icmp"}}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_project_marker_definitions(project):
|
||||
async def test_kernel_datapath_eligible_with_project_marker_definitions(project):
|
||||
"""
|
||||
Project-level marker definitions are inherited by every new link and ride
|
||||
the kernel datapath like private markers.
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
project._marker_definitions = {"global-m": {"bpf": "icmp"}}
|
||||
try:
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
finally:
|
||||
project._marker_definitions = {}
|
||||
|
||||
@@ -209,13 +217,58 @@ async def test_update_filters_rejected_on_kernel_link(project):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_marker_rejected_on_kernel_link(project):
|
||||
async def test_prepare_kernel_link_carries_markers(project):
|
||||
"""
|
||||
Markers on a kernel link ride the capture node's NIO (routed by
|
||||
capture_node_id) exactly like on the relay datapath.
|
||||
"""
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._markers = {"icmp": {"bpf": "icmp", "tag": 1, "capture_node_id": node1.id, "direction": None}}
|
||||
|
||||
entries = await link._prepare()
|
||||
|
||||
by_node = {entry[0].id: entry[3] for entry in entries}
|
||||
assert by_node[node1.id]["type"] == "nio_bridge"
|
||||
assert by_node[node1.id]["markers"]["icmp"]["bpf"] == "icmp"
|
||||
assert by_node[node1.id]["markers"]["icmp"]["tag"] == 1
|
||||
assert by_node[node2.id]["markers"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_marker_on_kernel_link_pushes_nio(project):
|
||||
"""
|
||||
start_marker works on kernel links: the marker is stored and pushed via
|
||||
the NIO update like on the relay datapath (no kernel guard anymore).
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
link._created = True
|
||||
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.start_marker("m", "icmp")
|
||||
bodies = []
|
||||
|
||||
async def put1(path, data=None, **kwargs):
|
||||
bodies.append((node1, data))
|
||||
|
||||
async def put2(path, data=None, **kwargs):
|
||||
bodies.append((node2, data))
|
||||
|
||||
node1.put = put1
|
||||
node2.put = put2
|
||||
|
||||
with patch("gns3server.controller.udp_link.validate_bpf_syntax", return_value={"valid": True, "error": None}):
|
||||
await link.start_marker("icmp", "icmp", tag=3)
|
||||
|
||||
entry = link._markers["icmp"]
|
||||
assert entry["bpf"] == "icmp"
|
||||
assert entry["capture_node_id"] == node1.id # auto-picked: first marker-capable node
|
||||
n1_data = next(data for node, data in bodies if node is node1)
|
||||
n2_data = next(data for node, data in bodies if node is node2)
|
||||
assert n1_data["type"] == "nio_bridge"
|
||||
assert n1_data["filters"] == {}
|
||||
assert n1_data["markers"]["icmp"]["tag"] == 3
|
||||
assert n2_data["markers"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in new issue
Block a user