diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index a4a2ce365..24d7813ae 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -1073,7 +1073,7 @@ class BaseNode: ) i += 1 - async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None): + async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None): """ Attach a `mark` packet filter to a uBridge bridge for traffic insight. @@ -1104,6 +1104,11 @@ class BaseNode: ) if tag is not None: cmd += f" tag {tag}" + # Per-link attribution (contract §3.2): when one ubridge bridge serves + # several GNS3 links (e.g. IOU's per-node bridge), bridge+filter collide, + # so the link id is the only way to tell signals — and pcap files — apart. + if link_id: + cmd += f" link {link_id}" cmd += ' pcap "{path}"'.format(path=pcap_path) # Let BPF compile errors propagate — the marker is the user's intent, so a # bad expression must surface instead of being silently dropped. @@ -1133,7 +1138,7 @@ class BaseNode: markers_dir, f"{self._id}_{link_id}_{name}.pcap" ) try: - await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag) + await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id) except UbridgeError as e: # Swallow BPF compile errors (warn + skip) so a single bad # expression can't break link creation / node restart — mirrors diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index d36795af3..11bffb69a 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -1137,6 +1137,11 @@ class IOUVM(BaseNode): ) if tag is not None: cmd += f" tag {tag}" + # IOU uses one per-node IOL-BRIDGE for every link, so bridge+filter + # are identical across this node's links — `link` is the only way the + # controller can tell their signals apart (contract §3.2). + if link_id: + cmd += f" link {link_id}" cmd += ' pcap "{path}"'.format(path=pcap_path) try: await self._ubridge_send(cmd) diff --git a/gns3server/compute/marker/marker_listener.py b/gns3server/compute/marker/marker_listener.py index d8bfa3303..f22f59bff 100644 --- a/gns3server/compute/marker/marker_listener.py +++ b/gns3server/compute/marker/marker_listener.py @@ -79,6 +79,7 @@ class MarkerListener(asyncio.DatagramProtocol): return # "-" means the field was unset on the ubridge side (see contract §3.3). + link = kv.get("link") tag = kv.get("tag") length = kv.get("len") @@ -89,10 +90,16 @@ class MarkerListener(asyncio.DatagramProtocol): ) return + # `link=` is the authoritative per-link id (opaque, set by gns3server at + # filter install time). It disambiguates signals that share a node+filter + # across several links; fall back to the registry's link only for legacy + # signals that carry no `link=`. + signal_link = link if link and link != "-" else None + event = { "project_id": project_id, "node_id": node_id, - "link_id": link_id, + "link_id": signal_link or link_id, "filter": filter_name, # Prefer the value carried in the signal; fall back to the one we registered. "tag": tag if tag and tag != "-" else registered_tag, diff --git a/tests/compute/marker/test_marker_manager.py b/tests/compute/marker/test_marker_manager.py index dc8e826ff..4a2796002 100644 --- a/tests/compute/marker/test_marker_manager.py +++ b/tests/compute/marker/test_marker_manager.py @@ -161,6 +161,28 @@ class TestMarkerListener: lis.datagram_received(b"MARK 2.0 node=n filter=f tag=- len=20\n", None) assert fmgr.events[0][1]["tag"] == 42 + def test_link_in_signal_overrides_registry_link(self): + # Per-link attribution (contract §3.2/§3.3): the signal's `link=` is + # authoritative and must disambiguate links sharing a node+filter — + # e.g. several links on one IOU node under the same global marker name. + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "registry-link", tag=1) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received( + b"MARK 3.0 node=n filter=f link=signal-link tag=1 len=42\n", None + ) + assert fmgr.events[0][1]["link_id"] == "signal-link" + + def test_link_dash_falls_back_to_registry_link(self): + # Legacy signals that carry no link fall back to the registry's link_id. + fmgr = FakeMarkerManager() + fmgr.register("p", "n", "f", "registry-link", tag=1) + lis = MarkerListener(fmgr) + lis.connection_made(None) + lis.datagram_received(b"MARK 3.0 node=n filter=f link=- tag=1 len=42\n", None) + assert fmgr.events[0][1]["link_id"] == "registry-link" + def test_exception_does_not_kill_listener(self): fmgr = FakeMarkerManager() lis = MarkerListener(fmgr)