From: Luca Vizzarro <luca.vizzarro@arm.com>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>,
Thomas Wilks <thomas.wilks@arm.com>,
Luca Vizzarro <luca.vizzarro@arm.com>,
Paul Szczepanek <paul.szczepanek@arm.com>,
Patrick Robb <probb@iol.unh.edu>
Subject: [PATCH v2 3/3] dts: add packet capture test suite
Date: Tue, 6 May 2025 14:29:33 +0100 [thread overview]
Message-ID: <20250506132933.1580584-4-luca.vizzarro@arm.com> (raw)
In-Reply-To: <20250506132933.1580584-1-luca.vizzarro@arm.com>
From: Thomas Wilks <thomas.wilks@arm.com>
Add a test suite that tests the packet capture framework
through the use of dpdk-dumpcap.
Signed-off-by: Thomas Wilks <thomas.wilks@arm.com>
Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
---
.../dts/tests.TestSuite_packet_capture.rst | 8 +
dts/tests/TestSuite_packet_capture.py | 238 ++++++++++++++++++
2 files changed, 246 insertions(+)
create mode 100644 doc/api/dts/tests.TestSuite_packet_capture.rst
create mode 100644 dts/tests/TestSuite_packet_capture.py
diff --git a/doc/api/dts/tests.TestSuite_packet_capture.rst b/doc/api/dts/tests.TestSuite_packet_capture.rst
new file mode 100644
index 0000000000..3d760d3ae4
--- /dev/null
+++ b/doc/api/dts/tests.TestSuite_packet_capture.rst
@@ -0,0 +1,8 @@
+.. SPDX-License-Identifier: BSD-3-Clause
+
+packet_capture Test Suite
+=========================
+
+.. automodule:: tests.TestSuite_packet_capture
+ :members:
+ :show-inheritance:
diff --git a/dts/tests/TestSuite_packet_capture.py b/dts/tests/TestSuite_packet_capture.py
new file mode 100644
index 0000000000..67a3049b78
--- /dev/null
+++ b/dts/tests/TestSuite_packet_capture.py
@@ -0,0 +1,238 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 Arm Limited
+
+"""Packet capture test suite.
+
+Test the DPDK packet capturing framework through the combined use of testpmd and dumpcap.
+"""
+
+from dataclasses import dataclass, field
+from pathlib import Path, PurePath
+
+from scapy.contrib.lldp import (
+ LLDPDUChassisID,
+ LLDPDUEndOfLLDPDU,
+ LLDPDUPortID,
+ LLDPDUSystemCapabilities,
+ LLDPDUSystemDescription,
+ LLDPDUSystemName,
+ LLDPDUTimeToLive,
+)
+from scapy.layers.inet import IP, TCP, UDP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Dot1Q, Ether
+from scapy.layers.sctp import SCTP
+from scapy.packet import Packet, Raw, raw
+from scapy.utils import rdpcap
+from typing_extensions import Self
+
+from framework.context import get_ctx
+from framework.params import Params
+from framework.remote_session.dpdk_shell import compute_eal_params
+from framework.remote_session.interactive_shell import InteractiveShell
+from framework.remote_session.testpmd_shell import TestPmdShell
+from framework.settings import SETTINGS
+from framework.test_suite import TestSuite, func_test
+from framework.testbed_model.capability import requires
+from framework.testbed_model.cpu import LogicalCoreList
+from framework.testbed_model.os_session import FilePermissions
+from framework.testbed_model.topology import TopologyType
+from framework.testbed_model.traffic_generator.capturing_traffic_generator import (
+ PacketFilteringConfig,
+)
+
+
+@dataclass(kw_only=True)
+class DumpcapParams(Params):
+ """Parameters for the dpdk-dumpcap app.
+
+ Attributes:
+ lcore_list: The list of logical cores to use.
+ file_prefix: The DPDK file prefix that the primary DPDK application is using.
+ interface: The PCI address of the interface to capture.
+ output_pcap_path: The path to the pcapng file to dump captured packets into.
+ packet_filter: A packet filter in libpcap filter syntax.
+ """
+
+ lcore_list: LogicalCoreList | None = field(default=None, metadata=Params.long("lcore"))
+ file_prefix: str | None = None
+ interface: str = field(metadata=Params.short("i"))
+ output_pcap_path: PurePath = field(metadata=Params.convert_value(str) | Params.short("w"))
+ packet_filter: str | None = field(default=None, metadata=Params.short("f"))
+
+
+class Dumpcap(InteractiveShell):
+ """Class to spawn and manage a dpdk-dumpcap process.
+
+ The dpdk-dumpcap is a DPDK app but instead of providing a regular DPDK EAL interface to the
+ user, it replicates the Wireshark dumpcap app.
+ """
+
+ _app_params: DumpcapParams
+
+ def __init__(self, params: DumpcapParams) -> None:
+ """Extends :meth:`~.interactive_shell.InteractiveShell.__init__`."""
+ self.ctx = get_ctx()
+ eal_params = compute_eal_params()
+ params.lcore_list = eal_params.lcore_list
+ params.file_prefix = eal_params.prefix
+
+ super().__init__(self.ctx.sut_node, name=None, privileged=True, app_params=params)
+
+ @property
+ def path(self) -> PurePath:
+ """Path to the shell executable."""
+ return PurePath(self.ctx.dpdk_build.remote_dpdk_build_dir).joinpath("app/dpdk-dumpcap")
+
+ def wait_until_ready(self) -> Self:
+ """Start app and wait until ready."""
+ self.start_application(f"Capturing on '{self._app_params.interface}'")
+ return self
+
+ def close(self) -> None:
+ """Close the application.
+
+ Sends a SIGINT to close the application.
+ """
+ self.send_command("\x03")
+ super().close()
+
+
+@requires(topology_type=TopologyType.two_links)
+class TestPacketCapture(TestSuite):
+ """Packet Capture TestSuite.
+
+ Attributes:
+ packets: List of packets to send for testing pdump.
+ rx_pcap_path: The remote path where to create the RX packets pcap with pdump.
+ tx_pcap_path: The remote path where to create the TX packets pcap with pdump.
+ """
+
+ packets: list[Packet]
+ rx_pcap_path: PurePath
+ tx_pcap_path: PurePath
+
+ def set_up_suite(self) -> None:
+ """Test suite setup.
+
+ Prepare the packets, file paths and queue range to be used in the test suite.
+ """
+ self.packets = [
+ Ether() / IP() / Raw(b"\0" * 60),
+ Ether() / IP() / TCP() / Raw(b"\0" * 60),
+ Ether() / IP() / UDP() / Raw(b"\0" * 60),
+ Ether() / IP() / SCTP() / Raw(b"\0" * 40),
+ Ether() / IPv6() / TCP() / Raw(b"\0" * 60),
+ Ether() / IPv6() / UDP() / Raw(b"\0" * 60),
+ Ether() / IP() / IPv6() / SCTP() / Raw(b"\0" * 40),
+ Ether() / Dot1Q() / IP() / UDP() / Raw(b"\0" * 40),
+ Ether(dst="FF:FF:FF:FF:FF:FF", type=0x88F7) / Raw(b"\0" * 60),
+ Ether(type=0x88CC)
+ / LLDPDUChassisID(subtype=4, id=self.topology.tg_port_egress.mac_address)
+ / LLDPDUPortID(subtype=5, id="Test Id")
+ / LLDPDUTimeToLive(ttl=180)
+ / LLDPDUSystemName(system_name="DTS Test sys")
+ / LLDPDUSystemDescription(description="DTS Test Packet")
+ / LLDPDUSystemCapabilities()
+ / LLDPDUEndOfLLDPDU(),
+ ]
+ self.tx_pcap_path = self._ctx.sut_node.tmp_dir.joinpath("tx.pcapng")
+ self.rx_pcap_path = self._ctx.sut_node.tmp_dir.joinpath("rx.pcapng")
+
+ def _load_pcap_packets(self, remote_pcap_path: PurePath) -> list[Packet]:
+ local_pcap_path = Path(SETTINGS.output_dir).joinpath(remote_pcap_path.name)
+ self._ctx.sut_node.main_session.copy_from(remote_pcap_path, local_pcap_path)
+ return list(rdpcap(str(local_pcap_path)))
+
+ def _send_and_dump(
+ self, packet_filter: str | None = None, rx_only: bool = False
+ ) -> list[Packet]:
+ dumpcap_rx = Dumpcap(
+ DumpcapParams(
+ interface=self.topology.sut_port_ingress.pci,
+ output_pcap_path=self.rx_pcap_path,
+ packet_filter=packet_filter,
+ )
+ ).wait_until_ready()
+ if not rx_only:
+ dumpcap_tx = Dumpcap(
+ DumpcapParams(
+ interface=self.topology.sut_port_egress.pci,
+ output_pcap_path=self.tx_pcap_path,
+ packet_filter=packet_filter,
+ )
+ ).wait_until_ready()
+
+ received_packets = self.send_packets_and_capture(
+ self.packets, PacketFilteringConfig(no_lldp=False)
+ )
+
+ dumpcap_rx.close()
+ self._ctx.sut_node.main_session.change_permissions(
+ self.rx_pcap_path, FilePermissions(0o644)
+ )
+ if not rx_only:
+ dumpcap_tx.close()
+ self._ctx.sut_node.main_session.change_permissions(
+ self.tx_pcap_path, FilePermissions(0o644)
+ )
+
+ return received_packets
+
+ @func_test
+ def test_dumpcap(self) -> None:
+ """Test dumpcap on RX and TX interfaces.
+
+ Steps:
+ * Start up testpmd shell.
+ * Start up dpdk-dumpcap with the default values.
+ * Send packets.
+
+ Verify:
+ * The expected packets are the same as the RX packets.
+ * The TX packets are the same as the packets received from Scapy.
+ """
+ with TestPmdShell() as testpmd:
+ testpmd.start()
+ received_packets = self._send_and_dump()
+
+ expected_packets = self.get_expected_packets(self.packets, sent_from_tg=True)
+ rx_pcap_packets = self._load_pcap_packets(self.rx_pcap_path)
+ self.verify(
+ self.match_all_packets(expected_packets, rx_pcap_packets, verify=False),
+ "RX packets from dumpcap weren't the same as the expected packets.",
+ )
+
+ tx_pcap_packets = self._load_pcap_packets(self.tx_pcap_path)
+ self.verify(
+ self.match_all_packets(tx_pcap_packets, received_packets, verify=False),
+ "TX packets from dumpcap weren't the same as the packets received by Scapy.",
+ )
+
+ @func_test
+ def test_dumpcap_filter(self) -> None:
+ """Test the dumpcap filtering feature.
+
+ Steps:
+ * Start up testpmd shell.
+ * Start up dpdk-dumpcap listening for TCP packets on the RX interface.
+ * Send packets.
+
+ Verify:
+ * The dumped packets did not contain any of the packets meant for filtering.
+ """
+ with TestPmdShell() as testpmd:
+ testpmd.start()
+ self._send_and_dump("tcp", rx_only=True)
+ filtered_packets = [
+ raw(p)
+ for p in self.get_expected_packets(self.packets, sent_from_tg=True)
+ if not p.haslayer(TCP)
+ ]
+
+ rx_pcap_packets = [raw(p) for p in self._load_pcap_packets(self.rx_pcap_path)]
+ for filtered_packet in filtered_packets:
+ self.verify(
+ filtered_packet not in rx_pcap_packets,
+ "Found a packet in the pcap that was meant to be filtered out.",
+ )
--
2.43.0
prev parent reply other threads:[~2025-05-06 13:31 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-03-31 15:57 [PATCH 0/2] " Thomas Wilks
2025-03-31 15:57 ` [PATCH 1/2] " Thomas Wilks
2025-04-10 17:04 ` Dean Marx
2025-04-10 22:46 ` Stephen Hemminger
2025-03-31 15:58 ` [PATCH 2/2] dts: import lldp package in scapy Thomas Wilks
2025-04-10 17:06 ` Dean Marx
2025-05-06 13:29 ` [PATCH v2 0/3] dts: add packet capture test suite Luca Vizzarro
2025-05-06 13:29 ` [PATCH v2 1/3] dts: import lldp package in scapy Luca Vizzarro
2025-05-06 13:29 ` [PATCH v2 2/3] dts: add facility to change file permissions Luca Vizzarro
2025-05-06 13:29 ` Luca Vizzarro [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250506132933.1580584-4-luca.vizzarro@arm.com \
--to=luca.vizzarro@arm.com \
--cc=dev@dpdk.org \
--cc=paul.szczepanek@arm.com \
--cc=probb@iol.unh.edu \
--cc=stephen@networkplumber.org \
--cc=thomas.wilks@arm.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).