diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index f8e8b8bb6..eb2ba1193 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -294,6 +294,7 @@ async def update_docker_node_nio( nio.filters.clear() if nio_data.filters: nio.filters = nio_data.filters + nio.markers = nio_data.markers or {} await node.adapter_update_nio_binding(adapter_number, nio) return nio.asdict() @@ -378,6 +379,11 @@ async def start_docker_node_marker( MarkerManager.instance().register( str(project_id), node.id, marker_data.name, marker_data.link_id, marker_data.tag ) + nio = node.get_nio(adapter_number) + if nio: + nio.markers[marker_data.name] = { + "bpf": marker_data.bpf, "tag": marker_data.tag, "link_id": marker_data.link_id + } return {"pcap_file_path": str(pcap_path)} @@ -398,6 +404,9 @@ async def stop_docker_node_marker( await node.stop_marker(adapter_number, marker_data.name) MarkerManager.instance().unregister(node.id, marker_data.name) + nio = node.get_nio(adapter_number) + if nio: + nio.markers.pop(marker_data.name, None) @router.get( diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index 6c8456eeb..e0117339a 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -322,6 +322,7 @@ async def update_qemu_node_nio( if nio_data.filters: nio.filters = nio_data.filters nio.suspend = nio_data.suspend + nio.markers = nio_data.markers or {} await node.adapter_update_nio_binding(adapter_number, nio) return nio.asdict() @@ -407,6 +408,11 @@ async def start_qemu_node_marker( MarkerManager.instance().register( str(project_id), node.id, marker_data.name, marker_data.link_id, marker_data.tag ) + nio = node.get_nio(adapter_number) + if nio: + nio.markers[marker_data.name] = { + "bpf": marker_data.bpf, "tag": marker_data.tag, "link_id": marker_data.link_id + } return {"pcap_file_path": str(pcap_path)} @@ -427,6 +433,9 @@ async def stop_qemu_node_marker( await node.stop_marker(adapter_number, marker_data.name) MarkerManager.instance().unregister(node.id, marker_data.name) + nio = node.get_nio(adapter_number) + if nio: + nio.markers.pop(marker_data.name, None) @router.get( diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py index 60360f579..74b349959 100644 --- a/gns3server/api/routes/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -241,6 +241,7 @@ async def update_vpcs_node_nio( nio.filters.clear() if nio_data.filters: nio.filters = nio_data.filters + nio.markers = nio_data.markers or {} await node.port_update_nio_binding(port_number, nio) return nio.asdict() @@ -329,6 +330,15 @@ async def start_vpcs_node_marker( MarkerManager.instance().register( str(project_id), node.id, marker_data.name, marker_data.link_id, marker_data.tag ) + # Mirror the marker spec onto the NIO so it survives a node restart (the NIO + # persists across stop/start and _ubridge_apply_markers re-applies its markers). + nio = node.get_nio(port_number) + if nio: + nio.markers[marker_data.name] = { + "bpf": marker_data.bpf, + "tag": marker_data.tag, + "link_id": marker_data.link_id, + } return {"pcap_file_path": pcap_path} @@ -350,6 +360,9 @@ async def stop_vpcs_node_marker( await node.stop_marker(port_number, marker_data.name) MarkerManager.instance().unregister(node.id, marker_data.name) + nio = node.get_nio(port_number) + if nio: + nio.markers.pop(marker_data.name, None) @router.get( diff --git a/gns3server/compute/base_manager.py b/gns3server/compute/base_manager.py index 076b802d1..8ef27265c 100644 --- a/gns3server/compute/base_manager.py +++ b/gns3server/compute/base_manager.py @@ -356,6 +356,7 @@ class BaseManager: raise ComputeError(f"Could not create an UDP connection to {rhost}:{rport}: {e}") nio = NIOUDP(lport, rhost, rport) nio.filters = nio_settings.get("filters", {}) + nio.markers = nio_settings.get("markers", {}) nio.suspend = nio_settings.get("suspend", False) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index fc2701ab0..3618fea30 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -1012,10 +1012,12 @@ class BaseNode: await self._ubridge_send(f"bridge start {bridge_name}") await self._ubridge_apply_filters(bridge_name, destination_nio.filters) + await self._ubridge_apply_markers(bridge_name, destination_nio) async def update_ubridge_udp_connection(self, bridge_name, source_nio, destination_nio): if destination_nio: await self._ubridge_apply_filters(bridge_name, destination_nio.filters) + await self._ubridge_apply_markers(bridge_name, destination_nio) async def ubridge_delete_bridge(self, name): """ @@ -1112,6 +1114,34 @@ class BaseNode: await self._ubridge_send(f"bridge delete_packet_filter {bridge_name} {name}") + async def _ubridge_apply_markers(self, bridge_name, nio): + """ + (Re-)apply every traffic-insight marker carried by *nio* to the uBridge + bridge *bridge_name*. Called from ``add_ubridge_udp_connection`` (bridge + creation / node restart) and ``update_ubridge_udp_connection`` (NIO update + — the preceding ``_ubridge_apply_filters`` has already issued + ``reset_packet_filters``, so we must re-add markers to survive the reset). + """ + from gns3server.compute.marker.marker_manager import MarkerManager + + markers = nio.markers if hasattr(nio, 'markers') else {} + if not markers: + return + + manager = MarkerManager.instance() + markers_dir = self.project.markers_working_directory() + for name, spec in markers.items(): + bpf = spec.get("bpf", "") + tag = spec.get("tag") + link_id = spec.get("link_id", "") + pcap_path = os.path.join( + markers_dir, f"{self._id}_{link_id}_{name}.pcap" + ) + await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag) + manager.register( + str(self.project.id), self._id, name, link_id, tag + ) + async def _add_ubridge_ethernet_connection(self, bridge_name, ethernet_interface, block_host_traffic=False): """ Creates a connection with an Ethernet interface in uBridge. diff --git a/gns3server/compute/nios/nio.py b/gns3server/compute/nios/nio.py index 8ad5bd870..6fe57a130 100644 --- a/gns3server/compute/nios/nio.py +++ b/gns3server/compute/nios/nio.py @@ -30,6 +30,7 @@ class NIO: self._capturing = False self._suspended = False self._filters = {} + self._markers = {} self._pcap_output_file = "" self._pcap_data_link_type = "" @@ -118,3 +119,24 @@ class NIO: assert isinstance(new_filters, dict) self._filters = new_filters + + @property + def markers(self): + """ + Returns the traffic-insight markers for this NIO. + + :returns: markers (dictionary: name -> {bpf, tag, link_id}) + """ + + return self._markers + + @markers.setter + def markers(self, new_markers): + """ + Set the traffic-insight markers for this NIO. + + :param new_markers: markers (dictionary: name -> {bpf, tag, link_id}) + """ + + assert isinstance(new_markers, dict) + self._markers = new_markers diff --git a/gns3server/compute/nios/nio_udp.py b/gns3server/compute/nios/nio_udp.py index b7736a39e..e6f1bd8bc 100644 --- a/gns3server/compute/nios/nio_udp.py +++ b/gns3server/compute/nios/nio_udp.py @@ -80,5 +80,6 @@ class NIOUDP(NIO): "rport": self._rport, "rhost": self._rhost, "suspend": self._suspended, - "filters": self._filters + "filters": self._filters, + "markers": self._markers } diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index d0db96395..ff17069d1 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -45,7 +45,7 @@ class UDPLink(Link): def _get_node_filters(self, node1, node2): """ Determine which node gets the active filters applied. - + :returns: Tuple of (node1_filters, node2_filters) """ filter_node = self._get_filter_node() @@ -54,6 +54,26 @@ class UDPLink(Link): self.get_active_filters() if filter_node == node2 else {}, ) + def _markers_for_node(self, node): + """ + Marker specs (name -> {bpf, tag, link_id}) for the markers whose capture + side is ``node`` and that are enabled. Routed by capture_node_id so a + marker only rides the NIO of the node whose uBridge will host it. + """ + return { + name: {"bpf": m["bpf"], "tag": m.get("tag"), "link_id": self._id} + for name, m in self._markers.items() + if m.get("enabled", True) and m.get("capture_node_id") == node.id + } + + def _get_node_markers(self, node1, node2): + """ + Determine which node gets which markers applied. + + :returns: Tuple of (node1_markers, node2_markers) + """ + return self._markers_for_node(node1), self._markers_for_node(node2) + async def create(self): """ Create the link on the nodes @@ -88,6 +108,7 @@ class UDPLink(Link): self._node2_port = response.json["udp_port"] node1_filters, node2_filters = self._get_node_filters(node1, node2) + node1_markers, node2_markers = self._get_node_markers(node1, node2) # Create the tunnel on both side self._link_data.append( @@ -97,6 +118,7 @@ class UDPLink(Link): "rport": self._node2_port, "type": "nio_udp", "filters": node1_filters, + "markers": node1_markers, "suspend": self._suspended, } ) @@ -109,6 +131,7 @@ class UDPLink(Link): "rport": self._node1_port, "type": "nio_udp", "filters": node2_filters, + "markers": node2_markers, "suspend": self._suspended, } ) @@ -133,10 +156,12 @@ class UDPLink(Link): node2 = self._nodes[1]["node"] node1_filters, node2_filters = self._get_node_filters(node1, node2) + node1_markers, node2_markers = self._get_node_markers(node1, node2) adapter_number1 = self._nodes[0]["adapter_number"] port_number1 = self._nodes[0]["port_number"] self._link_data[0]["filters"] = node1_filters + self._link_data[0]["markers"] = node1_markers self._link_data[0]["suspend"] = self._suspended if node1.node_type not in ("ethernet_switch", "ethernet_hub"): await node1.put( @@ -146,6 +171,7 @@ class UDPLink(Link): adapter_number2 = self._nodes[1]["adapter_number"] port_number2 = self._nodes[1]["port_number"] self._link_data[1]["filters"] = node2_filters + self._link_data[1]["markers"] = node2_markers self._link_data[1]["suspend"] = self._suspended if node2.node_type not in ("ethernet_switch", "ethernet_hub"): await node2.put( diff --git a/gns3server/schemas/compute/nios.py b/gns3server/schemas/compute/nios.py index cf69f9e30..f5cf5f073 100644 --- a/gns3server/schemas/compute/nios.py +++ b/gns3server/schemas/compute/nios.py @@ -36,6 +36,7 @@ class UDPNIO(BaseModel): rport: int = Field(..., gt=0, le=65535, description="Remote port") suspend: Optional[bool] = Field(None, description="Suspend the NIO") filters: Optional[dict] = Field(None, description="Packet filters") + markers: Optional[dict] = Field(None, description="Traffic-insight markers") class EthernetNIOType(str, Enum): diff --git a/tests/controller/test_udp_link.py b/tests/controller/test_udp_link.py index fb03947c9..e322627a0 100644 --- a/tests/controller/test_udp_link.py +++ b/tests/controller/test_udp_link.py @@ -78,6 +78,7 @@ async def test_create(project): "rport": 2048, "type": "nio_udp", "filters": {"delay": [10, 0]}, + "markers": {}, "suspend": False, }, timeout=120) @@ -87,6 +88,7 @@ async def test_create(project): "rport": 1024, "type": "nio_udp", "filters": {}, + "markers": {}, "suspend": False, }, timeout=120) @@ -146,6 +148,7 @@ async def test_create_one_side_failure(project): "rport": 2048, "type": "nio_udp", "filters": {}, + "markers": {}, "suspend": False, }, timeout=120) @@ -155,6 +158,7 @@ async def test_create_one_side_failure(project): "rport": 1024, "type": "nio_udp", "filters": {}, + "markers": {}, "suspend": False, }, timeout=120) # The link creation has failed we rollback the nio @@ -345,6 +349,7 @@ async def test_update(project): "rport": 2048, "type": "nio_udp", "suspend": False, + "markers": {}, "filters": {"delay": [10, 0]} }, timeout=120) @@ -354,6 +359,7 @@ async def test_update(project): "rport": 1024, "type": "nio_udp", "suspend": False, + "markers": {}, "filters": {} }, timeout=120) @@ -365,6 +371,7 @@ async def test_update(project): "rport": 2048, "type": "nio_udp", "suspend": False, + "markers": {}, "filters": { "frequency_drop": [5], "bpf": ["icmp[icmptype] == 8"] @@ -425,6 +432,7 @@ async def test_update_suspend(project): "rport": 2048, "type": "nio_udp", "filters": {"frequency_drop": [-1]}, + "markers": {}, "suspend": True }, timeout=120) @@ -434,5 +442,6 @@ async def test_update_suspend(project): "rport": 1024, "type": "nio_udp", "filters": {}, + "markers": {}, "suspend": True }, timeout=120)