All of lore.kernel.org
 help / color / mirror / Atom feed
From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>
Subject: [PATCH v6 11/11] test: add unit tests for rtap PMD
Date: Sat, 14 Feb 2026 15:44:20 -0800	[thread overview]
Message-ID: <20260214234726.188947-12-stephen@networkplumber.org> (raw)
In-Reply-To: <20260214234726.188947-1-stephen@networkplumber.org>

Add a comprehensive test suite for the rtap poll mode driver covering:

  - Device creation and configuration
  - Device info query
  - Link status and link up/down control
  - Statistics collection and reset
  - Promiscuous and all-multicast mode toggling
  - MAC address and MTU configuration
  - Multi-queue setup, teardown, and queue count reduction
  - Mismatched Rx/Tx queue count rejection
  - Queue reconfiguration (stop, reconfigure, restart)
  - Rx path: inject packet via AF_PACKET, receive via DPDK
  - Tx path: transmit via DPDK, capture via AF_PACKET
  - Multi-segment Tx with chained mbufs
  - Multi-segment Rx with small-buffer mempool
  - Offload capability reporting and configuration
  - Tx UDP checksum offload end-to-end verification
  - File descriptor leak detection after device close
  - Command-line created device testing

Tests require CAP_NET_ADMIN or root privileges

Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
 app/test/meson.build     |    1 +
 app/test/test_pmd_rtap.c | 2620 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 2621 insertions(+)
 create mode 100644 app/test/test_pmd_rtap.c

diff --git a/app/test/meson.build b/app/test/meson.build
index 48874037eb..5855077066 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -143,6 +143,7 @@ source_file_deps = {
     'test_pie.c': ['sched'],
     'test_pmd_perf.c': ['ethdev', 'net'] + packet_burst_generator_deps,
     'test_pmd_ring.c': ['net_ring', 'ethdev', 'bus_vdev'],
+    'test_pmd_rtap.c': ['net_rtap', 'ethdev', 'bus_vdev'],
     'test_pmd_ring_perf.c': ['ethdev', 'net_ring', 'bus_vdev'],
     'test_pmu.c': ['pmu'],
     'test_power.c': ['power', 'power_acpi', 'power_kvm_vm', 'power_intel_pstate',
diff --git a/app/test/test_pmd_rtap.c b/app/test/test_pmd_rtap.c
new file mode 100644
index 0000000000..a78adcd647
--- /dev/null
+++ b/app/test/test_pmd_rtap.c
@@ -0,0 +1,2620 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Stephen Hemminger
+ */
+#include <errno.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/ioctl.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <linux/if_ether.h>
+#include <linux/if_packet.h>
+
+#include <rte_common.h>
+#include <rte_stdatomic.h>
+#include <rte_ethdev.h>
+#include <rte_bus_vdev.h>
+#include <rte_ether.h>
+#include <rte_errno.h>
+#include <rte_mbuf.h>
+#include <rte_mempool.h>
+#include <rte_ip.h>
+#include <rte_udp.h>
+#include <rte_epoll.h>
+
+#include "test.h"
+
+#define SOCKET0 0
+#define RING_SIZE 256
+#define NB_MBUF 4096
+#define MAX_MULTI_QUEUES 4
+
+#define RTAP_DRIVER_NAME "net_rtap"
+#define TEST_TAP_NAME "rtap_test0"
+
+/* TX/RX test parameters */
+#define TEST_PKT_PAYLOAD_LEN 64
+#define TEST_MAGIC_BYTE 0xAB
+#define RX_BURST_MAX 32
+#define TX_RX_TIMEOUT_US 100000  /* 100ms */
+#define TX_RX_POLL_US 1000       /* 1ms between polls */
+
+static struct rte_mempool *mp;
+static int rtap_port0 = -1;
+static int rtap_port1 = -1;
+
+/* ========== Helper Functions ========== */
+
+static int
+check_rtap_available(void)
+{
+	int fd = open("/dev/net/tun", O_RDWR);
+	if (fd < 0) {
+		printf("Cannot access /dev/net/tun: %s\n", strerror(errno));
+		printf("RTAP PMD tests require CAP_NET_ADMIN or root privileges\n");
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
+/* Configure port with specified number of queue pairs */
+static int
+port_configure(int port, uint16_t nb_queues, const struct rte_eth_conf *conf)
+{
+	struct rte_eth_conf null_conf;
+
+	if (conf == NULL) {
+		memset(&null_conf, 0, sizeof(null_conf));
+		conf = &null_conf;
+	}
+
+	if (rte_eth_dev_configure(port, nb_queues, nb_queues, conf) < 0) {
+		printf("Configure failed for port %d with %u queues\n",
+		       port, nb_queues);
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Setup queue pairs for a port */
+static int
+port_setup_queues(int port, uint16_t nb_queues, uint16_t ring_size,
+		  struct rte_mempool *mempool)
+{
+	for (uint16_t q = 0; q < nb_queues; q++) {
+		if (rte_eth_tx_queue_setup(port, q, ring_size, SOCKET0, NULL) < 0) {
+			printf("TX queue %u setup failed port %d\n", q, port);
+			return -1;
+		}
+
+		if (rte_eth_rx_queue_setup(port, q, ring_size, SOCKET0,
+					   NULL, mempool) < 0) {
+			printf("RX queue %u setup failed port %d\n", q, port);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+/* Stop, configure, setup queues, and start a port */
+static int
+port_reconfigure(int port, uint16_t nb_queues, const struct rte_eth_conf *conf,
+		 uint16_t ring_size, struct rte_mempool *mempool)
+{
+	int ret;
+
+	ret = rte_eth_dev_stop(port);
+	if (ret != 0) {
+		printf("Error stopping port %d: %s\n", port, rte_strerror(-ret));
+		return -1;
+	}
+
+	if (port_configure(port, nb_queues, conf) < 0)
+		return -1;
+
+	if (port_setup_queues(port, nb_queues, ring_size, mempool) < 0)
+		return -1;
+
+	if (rte_eth_dev_start(port) < 0) {
+		printf("Error starting port %d\n", port);
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Restore port to clean single-queue started state */
+static int
+restore_single_queue(int port)
+{
+	return port_reconfigure(port, 1, NULL, RING_SIZE, mp);
+}
+
+/* Verify link status matches expected */
+static int
+verify_link_status(int port, uint8_t expected_status)
+{
+	struct rte_eth_link link;
+	int ret;
+
+	ret = rte_eth_link_get(port, &link);
+	if (ret < 0) {
+		printf("Error getting link status: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	if (link.link_status != expected_status) {
+		printf("Error: link should be %s but is %s\n",
+		       expected_status ? "UP" : "DOWN",
+		       link.link_status ? "UP" : "DOWN");
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Get device info with error checking */
+static int
+get_dev_info(int port, struct rte_eth_dev_info *dev_info)
+{
+	int ret = rte_eth_dev_info_get(port, dev_info);
+	if (ret != 0) {
+		printf("Error getting device info for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+	return 0;
+}
+
+/* Reset and verify stats are zero */
+static int
+reset_and_verify_stats_zero(int port)
+{
+	struct rte_eth_stats stats;
+	int ret;
+
+	ret = rte_eth_stats_reset(port);
+	if (ret != 0) {
+		printf("Error: stats_reset failed for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	ret = rte_eth_stats_get(port, &stats);
+	if (ret != 0) {
+		printf("Error: stats_get failed for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	if (stats.ipackets != 0 || stats.opackets != 0 ||
+	    stats.ibytes != 0 || stats.obytes != 0 ||
+	    stats.ierrors != 0 || stats.oerrors != 0) {
+		printf("Error: port %d stats are not zero after reset\n", port);
+		return -1;
+	}
+
+	return 0;
+}
+
+/* Drain all pending RX packets from a port */
+static void
+drain_rx_queue(int port, uint16_t queue_id)
+{
+	struct rte_mbuf *drain[RX_BURST_MAX];
+	uint16_t n;
+
+	do {
+		n = rte_eth_rx_burst(port, queue_id, drain, RX_BURST_MAX);
+		rte_pktmbuf_free_bulk(drain, n);
+	} while (n > 0);
+}
+
+/* Set Ethernet address to broadcast */
+static inline void
+eth_addr_bcast(struct rte_ether_addr *addr)
+{
+	memset(addr, 0xff, RTE_ETHER_ADDR_LEN);
+}
+
+/* Bring TAP interface up using ioctl */
+static int
+tap_set_up(const char *ifname)
+{
+	struct ifreq ifr;
+	int sock, ret = -1;
+
+	sock = socket(AF_INET, SOCK_DGRAM, 0);
+	if (sock < 0)
+		return -1;
+
+	memset(&ifr, 0, sizeof(ifr));
+	strlcpy(ifr.ifr_name, ifname, IFNAMSIZ);
+
+	if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0)
+		goto out;
+
+	ifr.ifr_flags |= IFF_UP;
+
+	if (ioctl(sock, SIOCSIFFLAGS, &ifr) < 0)
+		goto out;
+
+	ret = 0;
+out:
+	close(sock);
+	return ret;
+}
+
+/* Open an AF_PACKET socket bound to the TAP interface */
+static int
+open_tap_socket(const char *ifname)
+{
+	int sock, ifindex;
+	struct sockaddr_ll sll;
+
+	sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
+	if (sock < 0) {
+		printf("socket(AF_PACKET) failed: %s\n", strerror(errno));
+		return -1;
+	}
+
+	ifindex = if_nametoindex(ifname);
+	if (ifindex == 0) {
+		printf("if_nametoindex(%s) failed: %s\n", ifname, strerror(errno));
+		close(sock);
+		return -1;
+	}
+
+	memset(&sll, 0, sizeof(sll));
+	sll.sll_family = AF_PACKET;
+	sll.sll_ifindex = ifindex;
+	sll.sll_protocol = htons(ETH_P_ALL);
+
+	if (bind(sock, (struct sockaddr *)&sll, sizeof(sll)) < 0) {
+		printf("bind() failed: %s\n", strerror(errno));
+		close(sock);
+		return -1;
+	}
+
+	return sock;
+}
+
+/* Setup TAP socket with non-blocking mode and bring interface up */
+static int
+setup_tap_socket_nb(const char *ifname)
+{
+	int sock, flags;
+
+	if (tap_set_up(ifname) < 0) {
+		printf("Failed to bring TAP interface up\n");
+		return -1;
+	}
+
+	sock = open_tap_socket(ifname);
+	if (sock < 0)
+		return -1;
+
+	flags = fcntl(sock, F_GETFL, 0);
+	fcntl(sock, F_SETFL, flags | O_NONBLOCK);
+
+	return sock;
+}
+
+/* Build a basic test packet with broadcast dest and magic byte payload */
+static void
+build_test_packet(uint8_t *pkt, size_t pkt_len,
+		  const struct rte_ether_addr *src_mac,
+		  const struct rte_ether_addr *dst_mac)
+{
+	struct rte_ether_hdr *eth = (struct rte_ether_hdr *)pkt;
+
+	if (dst_mac)
+		memcpy(&eth->dst_addr, dst_mac, RTE_ETHER_ADDR_LEN);
+	else
+		eth_addr_bcast(&eth->dst_addr);
+
+	if (src_mac)
+		memcpy(&eth->src_addr, src_mac, RTE_ETHER_ADDR_LEN);
+	else
+		rte_eth_random_addr(eth->src_addr.addr_bytes);
+
+	eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
+	memset(pkt + RTE_ETHER_HDR_LEN, TEST_MAGIC_BYTE,
+	       pkt_len - RTE_ETHER_HDR_LEN);
+}
+
+/* Poll AF_PACKET socket for a packet matching the given pattern */
+static ssize_t
+poll_tap_socket(int sock, uint8_t *buf, size_t buf_size,
+		uint8_t match_byte, size_t match_offset)
+{
+	struct timeval tv;
+	fd_set fds;
+	int elapsed = 0;
+	ssize_t rx_len;
+
+	while (elapsed < TX_RX_TIMEOUT_US) {
+		FD_ZERO(&fds);
+		FD_SET(sock, &fds);
+		tv.tv_sec = 0;
+		tv.tv_usec = TX_RX_POLL_US;
+
+		if (select(sock + 1, &fds, NULL, NULL, &tv) > 0) {
+			rx_len = recv(sock, buf, buf_size, 0);
+			if (rx_len > 0 && (size_t)rx_len > match_offset &&
+			    buf[match_offset] == match_byte)
+				return rx_len;
+		}
+		elapsed += TX_RX_POLL_US;
+	}
+
+	return 0;  /* Timeout */
+}
+
+/* Receive packets from DPDK port, filtering for our test packets */
+static uint16_t
+receive_test_packets(int port, uint16_t queue_id, struct rte_mbuf **rx_mbufs,
+		     uint16_t max_pkts, size_t expected_len, uint8_t magic_byte)
+{
+	uint16_t nb_rx = 0;
+	int elapsed = 0;
+
+	while (elapsed < TX_RX_TIMEOUT_US && nb_rx < max_pkts) {
+		struct rte_mbuf *burst[RX_BURST_MAX];
+		uint16_t n = rte_eth_rx_burst(port, queue_id, burst, RX_BURST_MAX);
+
+		for (uint16_t i = 0; i < n; i++) {
+			uint8_t *d = rte_pktmbuf_mtod(burst[i], uint8_t *);
+
+			if (burst[i]->pkt_len == expected_len &&
+			    d[RTE_ETHER_HDR_LEN] == magic_byte) {
+				rx_mbufs[nb_rx++] = burst[i];
+				if (nb_rx >= max_pkts)
+					break;
+			} else {
+				rte_pktmbuf_free(burst[i]);
+			}
+		}
+
+		if (nb_rx > 0)
+			break;
+
+		usleep(TX_RX_POLL_US);
+		elapsed += TX_RX_POLL_US;
+	}
+
+	return nb_rx;
+}
+
+/* Wait for event with timeout using polling */
+static int
+wait_for_event(RTE_ATOMIC(int) *event_flag, int initial_count, int timeout_us)
+{
+	int elapsed = 0;
+
+	while (elapsed < timeout_us) {
+		if (rte_atomic_load_explicit(event_flag, rte_memory_order_seq_cst) > initial_count)
+			return 0;
+		usleep(TX_RX_POLL_US);
+		elapsed += TX_RX_POLL_US;
+	}
+
+	return -1;  /* Timeout */
+}
+
+/* Count open file descriptors */
+static int
+count_open_fds(void)
+{
+	DIR *d;
+	struct dirent *de;
+	int count = 0;
+
+	d = opendir("/proc/self/fd");
+	if (d == NULL)
+		return -1;
+
+	while ((de = readdir(d)) != NULL) {
+		if (de->d_name[0] != '.')
+			count++;
+	}
+	closedir(d);
+	return count - 1;  /* Subtract dirfd itself */
+}
+
+/* ========== Test Functions ========== */
+
+static int
+test_ethdev_configure_port(int port)
+{
+	struct rte_eth_link link;
+	int ret;
+
+	if (port_reconfigure(port, 1, NULL, RING_SIZE, mp) < 0)
+		return -1;
+
+	ret = rte_eth_link_get(port, &link);
+	if (ret < 0) {
+		printf("Link get failed for port %u: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+test_get_stats(int port)
+{
+	printf("Testing rtap PMD stats_get port %d\n", port);
+	return reset_and_verify_stats_zero(port);
+}
+
+static int
+test_stats_reset(int port)
+{
+	printf("Testing rtap PMD stats_reset port %d\n", port);
+	return reset_and_verify_stats_zero(port);
+}
+
+static int
+test_dev_info(int port)
+{
+	struct rte_eth_dev_info dev_info;
+
+	printf("Testing rtap PMD dev_info_get port %d\n", port);
+
+	if (get_dev_info(port, &dev_info) < 0)
+		return -1;
+
+	if (dev_info.max_rx_queues == 0 || dev_info.max_tx_queues == 0) {
+		printf("Error: invalid max queue values\n");
+		return -1;
+	}
+
+	if (dev_info.max_mac_addrs != 1) {
+		printf("Error: expected max_mac_addrs = 1, got %u\n",
+		       dev_info.max_mac_addrs);
+		return -1;
+	}
+
+	printf("  driver_name: %s\n", dev_info.driver_name);
+	printf("  if_index: %u\n", dev_info.if_index);
+	printf("  max_rx_queues: %u\n", dev_info.max_rx_queues);
+	printf("  max_tx_queues: %u\n", dev_info.max_tx_queues);
+
+	return 0;
+}
+
+static int
+test_link_status(int port)
+{
+	struct rte_eth_link link;
+	int ret;
+
+	printf("Testing rtap PMD link status port %d\n", port);
+
+	ret = rte_eth_link_get(port, &link);
+	if (ret < 0) {
+		printf("Error getting link status for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	printf("  link_status: %s\n", link.link_status ? "UP" : "DOWN");
+	printf("  link_speed: %u\n", link.link_speed);
+	printf("  link_duplex: %s\n",
+	       link.link_duplex ? "full-duplex" : "half-duplex");
+
+	return 0;
+}
+
+static int
+test_set_link_up_down(int port)
+{
+	int ret;
+
+	printf("Testing rtap PMD link up/down port %d\n", port);
+
+	ret = rte_eth_dev_set_link_down(port);
+	if (ret < 0) {
+		printf("Error setting link down for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	if (verify_link_status(port, RTE_ETH_LINK_DOWN) < 0)
+		return -1;
+
+	ret = rte_eth_dev_set_link_up(port);
+	if (ret < 0) {
+		printf("Error setting link up for port %d: %s\n",
+		       port, rte_strerror(-ret));
+		return -1;
+	}
+
+	if (verify_link_status(port, RTE_ETH_LINK_UP) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int
+test_promiscuous_mode(int port)
+{
+	int ret;
+
+	printf("Testing rtap PMD promiscuous mode port %d\n", port);
+
+	ret = rte_eth_promiscuous_enable(port);
+	if (ret < 0) {
+		printf("Error enabling promiscuous mode: %s\n",
+		       rte_strerror(-ret));
+		return -1;
+	}
+
+	if (rte_eth_promiscuous_get(port) != 1) {
+		printf("Error: promiscuous mode should be enabled\n");
+		return -1;
+	}
+
+	ret = rte_eth_promiscuous_disable(port);
+	if (ret < 0) {
+		printf("Error disabling promiscuous mode: %s\n",
+		       rte_strerror(-ret));
+		return -1;
+	}
+
+	if (rte_eth_promiscuous_get(port) != 0) {
+		printf("Error: promiscuous mode should be disabled\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+test_allmulticast_mode(int port)
+{
+	int ret;
+
+	printf("Testing rtap PMD allmulticast mode port %d\n", port);
+
+	ret = rte_eth_allmulticast_enable(port);
+	if (ret < 0) {
+		printf("Error enabling allmulticast mode: %s\n",
+		       rte_strerror(-ret));
+		return -1;
+	}
+
+	if (rte_eth_allmulticast_get(port) != 1) {
+		printf("Error: allmulticast mode should be enabled\n");
+		return -1;
+	}
+
+	ret = rte_eth_allmulticast_disable(port);
+	if (ret < 0) {
+		printf("Error disabling allmulticast mode: %s\n",
+		       rte_strerror(-ret));
+		return -1;
+	}
+
+	if (rte_eth_allmulticast_get(port) != 0) {
+		printf("Error: allmulticast mode should be disabled\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+test_mac_address(int port)
+{
+	struct rte_ether_addr mac_addr;
+	struct rte_ether_addr new_mac = {
+		.addr_bytes = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55}
+	};
+	int ret;
+
+	printf("Testing rtap PMD MAC address port %d\n", port);
+
+	ret = rte_eth_macaddr_get(port, &mac_addr);
+	if (ret < 0) {
+		printf("Error getting MAC address: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	printf("  Current MAC: " RTE_ETHER_ADDR_PRT_FMT "\n",
+	       RTE_ETHER_ADDR_BYTES(&mac_addr));
+
+	ret = rte_eth_dev_default_mac_addr_set(port, &new_mac);
+	if (ret < 0) {
+		printf("Error setting MAC address: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	ret = rte_eth_macaddr_get(port, &mac_addr);
+	if (ret < 0) {
+		printf("Error getting MAC address: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	if (!rte_is_same_ether_addr(&mac_addr, &new_mac)) {
+		printf("Error: MAC address not set correctly\n");
+		return -1;
+	}
+
+	printf("  New MAC: " RTE_ETHER_ADDR_PRT_FMT "\n",
+	       RTE_ETHER_ADDR_BYTES(&mac_addr));
+
+	return 0;
+}
+
+static int
+test_mtu_set(int port)
+{
+	uint16_t orig_mtu;
+	uint16_t new_mtu = 9000;
+	int ret;
+
+	printf("Testing rtap PMD MTU set port %d\n", port);
+
+	ret = rte_eth_dev_get_mtu(port, &orig_mtu);
+	if (ret < 0) {
+		printf("Error getting MTU: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	printf("  Original MTU: %u\n", orig_mtu);
+
+	ret = rte_eth_dev_set_mtu(port, new_mtu);
+	if (ret < 0) {
+		printf("Warning: setting MTU to %u failed: %s\n",
+		       new_mtu, rte_strerror(-ret));
+		return 0;
+	}
+
+	uint16_t current_mtu;
+	ret = rte_eth_dev_get_mtu(port, &current_mtu);
+	if (ret < 0) {
+		printf("Error getting MTU: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	printf("  New MTU: %u\n", current_mtu);
+	rte_eth_dev_set_mtu(port, orig_mtu);
+
+	return 0;
+}
+
+static int
+test_queue_reconfigure(int port)
+{
+	struct rte_eth_dev_info dev_info;
+	int ret;
+
+	printf("Testing rtap PMD queue reconfigure port %d\n", port);
+
+	if (port_reconfigure(port, 2, NULL, RING_SIZE, mp) < 0)
+		return -1;
+
+	ret = rte_eth_dev_info_get(port, &dev_info);
+	if (ret != 0) {
+		printf("Error getting device info: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	printf("  Configured with %u rx and %u tx queues\n",
+	       dev_info.nb_rx_queues, dev_info.nb_tx_queues);
+
+	if (restore_single_queue(port) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int
+test_multiqueue(int port)
+{
+	struct rte_eth_dev_info dev_info;
+	uint16_t queue_counts[] = { 1, 2, MAX_MULTI_QUEUES };
+
+	printf("Testing rtap PMD multi-queue port %d\n", port);
+
+	for (unsigned int t = 0; t < RTE_DIM(queue_counts); t++) {
+		uint16_t nb_queues = queue_counts[t];
+
+		printf("  Configuring %u queue pair(s)\n", nb_queues);
+
+		if (port_reconfigure(port, nb_queues, NULL, RING_SIZE, mp) < 0)
+			return -1;
+
+		if (get_dev_info(port, &dev_info) < 0)
+			return -1;
+
+		if (dev_info.nb_rx_queues != nb_queues ||
+		    dev_info.nb_tx_queues != nb_queues) {
+			printf("Error: queue count mismatch\n");
+			return -1;
+		}
+
+		if (reset_and_verify_stats_zero(port) < 0)
+			return -1;
+
+		/* Verify per-queue xstats are zero */
+		int num_xstats = rte_eth_xstats_get(port, NULL, 0);
+		if (num_xstats > 0) {
+			struct rte_eth_xstat *xstats = malloc(sizeof(*xstats) * num_xstats);
+			struct rte_eth_xstat_name *xstat_names =
+				malloc(sizeof(*xstat_names) * num_xstats);
+
+			if (xstats == NULL || xstat_names == NULL) {
+				free(xstats);
+				free(xstat_names);
+				printf("Error: xstats alloc failed\n");
+				return -1;
+			}
+
+			rte_eth_xstats_get_names(port, xstat_names, num_xstats);
+			rte_eth_xstats_get(port, xstats, num_xstats);
+
+			for (int x = 0; x < num_xstats; x++) {
+				if (strstr(xstat_names[x].name, "_q") != NULL &&
+				    xstats[x].value != 0) {
+					printf("Error: xstat %s = %" PRIu64 " not zero\n",
+					       xstat_names[x].name, xstats[x].value);
+					free(xstats);
+					free(xstat_names);
+					return -1;
+				}
+			}
+			free(xstats);
+			free(xstat_names);
+		}
+
+		if (verify_link_status(port, RTE_ETH_LINK_UP) < 0)
+			return -1;
+
+		printf("    %u queue pair(s): OK\n", nb_queues);
+	}
+
+	if (restore_single_queue(port) < 0) {
+		printf("Error restoring single queue\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+test_multiqueue_reduce(int port)
+{
+	printf("Testing rtap PMD queue reduction port %d\n", port);
+
+	if (port_reconfigure(port, MAX_MULTI_QUEUES, NULL, RING_SIZE, mp) < 0)
+		return -1;
+
+	printf("  Started with %d queues, reducing to 2\n", MAX_MULTI_QUEUES);
+
+	if (port_reconfigure(port, 2, NULL, RING_SIZE, mp) < 0)
+		return -1;
+
+	if (reset_and_verify_stats_zero(port) < 0)
+		return -1;
+
+	printf("  Reduced to 2 queues: OK\n");
+	printf("  Reducing to 1 queue\n");
+
+	if (restore_single_queue(port) < 0)
+		return -1;
+
+	if (reset_and_verify_stats_zero(port) < 0)
+		return -1;
+
+	printf("  Reduced to 1 queue: OK\n");
+	return 0;
+}
+
+static int
+test_multiqueue_mismatch(int port)
+{
+	int ret;
+	struct { uint16_t rx; uint16_t tx; } mismatch[] = {
+		{ 1, 2 }, { 2, 1 }, { 4, 2 }, { 1, 4 },
+	};
+
+	printf("Testing rtap PMD mismatched queue rejection port %d\n", port);
+
+	ret = rte_eth_dev_stop(port);
+	if (ret != 0) {
+		printf("Error stopping port: %s\n", rte_strerror(-ret));
+		return -1;
+	}
+
+	for (unsigned int i = 0; i < RTE_DIM(mismatch); i++) {
+		struct rte_eth_conf null_conf;
+		memset(&null_conf, 0, sizeof(null_conf));
+
+		ret = rte_eth_dev_configure(port, mismatch[i].rx,
+					    mismatch[i].tx, &null_conf);
+		if (ret == 0) {
+			printf("Error: configure(%u rx, %u tx) should fail\n",
+			       mismatch[i].rx, mismatch[i].tx);
+			rte_eth_dev_stop(port);
+			return -1;
+		}
+		printf("  Rejected %u rx / %u tx: OK\n",
+		       mismatch[i].rx, mismatch[i].tx);
+	}
+
+	if (restore_single_queue(port) < 0) {
+		printf("Error restoring single queue\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+test_rx_inject(int port)
+{
+	struct rte_ether_addr mac;
+	struct rte_mbuf *rx_mbufs[RX_BURST_MAX];
+	uint8_t pkt[RTE_ETHER_HDR_LEN + TEST_PKT_PAYLOAD_LEN];
+	int sock = -1;
+	uint16_t nb_rx;
+	int ret = -1;
+
+	printf("Testing rtap PMD RX (inject via AF_PACKET)\n");
+
+	if (restore_single_queue(port) < 0) {
+		printf("Failed to restore single queue config\n");
+		return -1;
+	}
+
+	if (rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to get MAC address\n");
+		return -1;
+	}
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		return -1;
+
+	build_test_packet(pkt, sizeof(pkt), NULL, &mac);
+	drain_rx_queue(port, 0);
+	rte_eth_stats_reset(port);
+
+	if (send(sock, pkt, sizeof(pkt), 0) < 0) {
+		printf("send() failed: %s\n", strerror(errno));
+		goto out;
+	}
+
+	nb_rx = receive_test_packets(port, 0, rx_mbufs, 1, sizeof(pkt),
+				     TEST_MAGIC_BYTE);
+
+	if (nb_rx == 0) {
+		printf("No packet received after %d us\n", TX_RX_TIMEOUT_US);
+		goto out;
+	}
+
+	uint8_t *rx_data = rte_pktmbuf_mtod(rx_mbufs[0], uint8_t *);
+	if (rx_data[RTE_ETHER_HDR_LEN] != TEST_MAGIC_BYTE) {
+		printf("Payload mismatch\n");
+		goto free_rx;
+	}
+
+	struct rte_eth_stats stats;
+	rte_eth_stats_get(port, &stats);
+	if (stats.ipackets == 0) {
+		printf("RX stats not updated\n");
+		goto free_rx;
+	}
+
+	printf("  RX inject test PASSED (received %u packets)\n", nb_rx);
+	ret = 0;
+
+free_rx:
+	for (uint16_t i = 0; i < nb_rx; i++)
+		rte_pktmbuf_free(rx_mbufs[i]);
+out:
+	close(sock);
+	return ret;
+}
+
+static int
+test_tx_capture(int port)
+{
+	struct rte_ether_addr mac;
+	struct rte_mbuf *tx_mbuf;
+	struct rte_ether_hdr *eth;
+	uint8_t rx_buf[256];
+	int sock = -1;
+	uint16_t nb_tx;
+	ssize_t rx_len;
+	int ret = -1;
+
+	printf("Testing rtap PMD TX (capture via AF_PACKET)\n");
+
+	if (restore_single_queue(port) < 0) {
+		printf("Failed to restore single queue config\n");
+		return -1;
+	}
+
+	if (rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to get MAC address\n");
+		return -1;
+	}
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		return -1;
+
+	tx_mbuf = rte_pktmbuf_alloc(mp);
+	if (tx_mbuf == NULL) {
+		printf("Failed to allocate mbuf\n");
+		goto out;
+	}
+
+	eth = rte_pktmbuf_mtod(tx_mbuf, struct rte_ether_hdr *);
+	eth_addr_bcast(&eth->dst_addr);
+	memcpy(&eth->src_addr, &mac, RTE_ETHER_ADDR_LEN);
+	eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
+
+	uint8_t *payload = (uint8_t *)(eth + 1);
+	memset(payload, TEST_MAGIC_BYTE, TEST_PKT_PAYLOAD_LEN);
+
+	tx_mbuf->data_len = RTE_ETHER_HDR_LEN + TEST_PKT_PAYLOAD_LEN;
+	tx_mbuf->pkt_len = tx_mbuf->data_len;
+
+	rte_eth_stats_reset(port);
+
+	nb_tx = rte_eth_tx_burst(port, 0, &tx_mbuf, 1);
+	if (nb_tx != 1) {
+		printf("TX failed\n");
+		rte_pktmbuf_free(tx_mbuf);
+		goto out;
+	}
+
+	rx_len = poll_tap_socket(sock, rx_buf, sizeof(rx_buf),
+				 TEST_MAGIC_BYTE, RTE_ETHER_HDR_LEN);
+
+	if (rx_len <= 0) {
+		printf("No packet captured after %d us\n", TX_RX_TIMEOUT_US);
+		goto out;
+	}
+
+	struct rte_eth_stats stats;
+	rte_eth_stats_get(port, &stats);
+	if (stats.opackets == 0) {
+		printf("TX stats not updated\n");
+		goto out;
+	}
+
+	printf("  TX capture test PASSED (captured %zd bytes)\n", rx_len);
+	ret = 0;
+
+out:
+	close(sock);
+	return ret;
+}
+
+#define MSEG_NUM_SEGS 3
+#define MSEG_SEG_LEN 40
+
+static int
+test_tx_multiseg(int port)
+{
+	struct rte_ether_addr mac;
+	struct rte_mbuf *head, *seg, *prev;
+	struct rte_ether_hdr *eth;
+	uint8_t rx_buf[512];
+	int sock = -1;
+	uint16_t nb_tx;
+	ssize_t rx_len;
+	int ret = -1;
+	uint16_t total_payload = MSEG_NUM_SEGS * MSEG_SEG_LEN;
+
+	printf("Testing rtap PMD multi-segment TX\n");
+
+	if (restore_single_queue(port) < 0 ||
+	    rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to setup test\n");
+		return -1;
+	}
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		return -1;
+
+	head = rte_pktmbuf_alloc(mp);
+	if (head == NULL) {
+		printf("Failed to allocate head mbuf\n");
+		goto out;
+	}
+
+	eth = rte_pktmbuf_mtod(head, struct rte_ether_hdr *);
+	eth_addr_bcast(&eth->dst_addr);
+	memcpy(&eth->src_addr, &mac, RTE_ETHER_ADDR_LEN);
+	eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
+
+	uint8_t *p = (uint8_t *)(eth + 1);
+	memset(p, 0xA0, MSEG_SEG_LEN);
+	head->data_len = RTE_ETHER_HDR_LEN + MSEG_SEG_LEN;
+	head->pkt_len = RTE_ETHER_HDR_LEN + total_payload;
+	head->nb_segs = MSEG_NUM_SEGS;
+
+	prev = head;
+	for (int i = 1; i < MSEG_NUM_SEGS; i++) {
+		seg = rte_pktmbuf_alloc(mp);
+		if (seg == NULL) {
+			printf("Failed to allocate segment %d\n", i);
+			rte_pktmbuf_free(head);
+			goto out;
+		}
+		p = rte_pktmbuf_mtod(seg, uint8_t *);
+		memset(p, 0xA0 + i, MSEG_SEG_LEN);
+		seg->data_len = MSEG_SEG_LEN;
+		prev->next = seg;
+		prev = seg;
+	}
+
+	rte_eth_stats_reset(port);
+
+	nb_tx = rte_eth_tx_burst(port, 0, &head, 1);
+	if (nb_tx != 1) {
+		printf("TX failed for multi-seg packet\n");
+		rte_pktmbuf_free(head);
+		goto out;
+	}
+
+	rx_len = poll_tap_socket(sock, rx_buf, sizeof(rx_buf),
+				 0xA0, RTE_ETHER_HDR_LEN);
+
+	if (rx_len <= 0) {
+		printf("No packet captured\n");
+		goto out;
+	}
+
+	for (int i = 0; i < MSEG_NUM_SEGS; i++) {
+		int off = RTE_ETHER_HDR_LEN + i * MSEG_SEG_LEN;
+		uint8_t expected = 0xA0 + i;
+		if (rx_buf[off] != expected) {
+			printf("Segment %d mismatch\n", i);
+			goto out;
+		}
+	}
+
+	struct rte_eth_stats stats;
+	rte_eth_stats_get(port, &stats);
+	if (stats.opackets == 0) {
+		printf("TX stats not updated\n");
+		goto out;
+	}
+
+	printf("  Multi-seg TX test PASSED (%d segs, captured %zd bytes)\n",
+	       MSEG_NUM_SEGS, rx_len);
+	ret = 0;
+
+out:
+	close(sock);
+	return ret;
+}
+
+#define MSEG_RX_BUF_SIZE 256
+#define MSEG_RX_POOL_SIZE 4096
+#define MSEG_RX_PKT_PAYLOAD 200
+
+static int
+test_rx_multiseg(int port)
+{
+	struct rte_mempool *small_mp = NULL;
+	struct rte_ether_addr mac;
+	struct rte_mbuf *rx_mbufs[RX_BURST_MAX];
+	uint8_t pkt[RTE_ETHER_HDR_LEN + MSEG_RX_PKT_PAYLOAD];
+	int sock = -1;
+	uint16_t nb_rx;
+	int ret = -1;
+
+	printf("Testing rtap PMD multi-segment RX\n");
+
+	if (rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to get MAC address\n");
+		return -1;
+	}
+
+	small_mp = rte_pktmbuf_pool_create("small_mbuf_pool",
+			MSEG_RX_POOL_SIZE, 32, 0, MSEG_RX_BUF_SIZE,
+			rte_socket_id());
+	if (small_mp == NULL) {
+		printf("Failed to create small mempool\n");
+		return -1;
+	}
+
+	if (port_reconfigure(port, 1, NULL, RING_SIZE, small_mp) < 0)
+		goto free_pool;
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		goto restore;
+
+	drain_rx_queue(port, 0);
+
+	build_test_packet(pkt, sizeof(pkt), NULL, &mac);
+	memset(pkt + RTE_ETHER_HDR_LEN, 0xDD, MSEG_RX_PKT_PAYLOAD);
+
+	if (send(sock, pkt, sizeof(pkt), 0) < 0) {
+		printf("send() failed: %s\n", strerror(errno));
+		goto close_sock;
+	}
+
+	nb_rx = receive_test_packets(port, 0, rx_mbufs, 1, sizeof(pkt), 0xDD);
+
+	if (nb_rx == 0) {
+		printf("No packet received\n");
+		goto close_sock;
+	}
+
+	struct rte_mbuf *m = rx_mbufs[0];
+	printf("  Received: pkt_len=%u nb_segs=%u\n", m->pkt_len, m->nb_segs);
+
+	if (m->nb_segs < 2) {
+		printf("  Expected multi-segment mbuf, got %u segments\n",
+		       m->nb_segs);
+	}
+
+	if (m->pkt_len < sizeof(pkt)) {
+		printf("  Packet too short: %u < %zu\n", m->pkt_len, sizeof(pkt));
+		goto free_rx;
+	}
+
+	/* Verify payload across segments */
+	uint32_t offset = 0;
+	struct rte_mbuf *seg = m;
+	uint32_t seg_off = 0;
+	int payload_ok = 1;
+
+	while (seg != NULL && offset < m->pkt_len) {
+		if (seg_off >= seg->data_len) {
+			seg = seg->next;
+			seg_off = 0;
+			continue;
+		}
+		if (offset >= RTE_ETHER_HDR_LEN &&
+		    offset < RTE_ETHER_HDR_LEN + MSEG_RX_PKT_PAYLOAD) {
+			uint8_t *d = rte_pktmbuf_mtod_offset(seg, uint8_t *,
+							      seg_off);
+			if (*d != 0xDD) {
+				printf("  Payload mismatch at offset %u\n", offset);
+				payload_ok = 0;
+				break;
+			}
+		}
+		offset++;
+		seg_off++;
+	}
+
+	if (!payload_ok)
+		goto free_rx;
+
+	printf("  Multi-seg RX test PASSED (%u segments)\n", m->nb_segs);
+	ret = 0;
+
+free_rx:
+	for (uint16_t i = 0; i < nb_rx; i++)
+		rte_pktmbuf_free(rx_mbufs[i]);
+
+close_sock:
+	close(sock);
+
+restore:
+	restore_single_queue(port);
+
+free_pool:
+	rte_mempool_free(small_mp);
+	return ret;
+}
+
+static int
+test_offload_config(int port)
+{
+	struct rte_eth_dev_info dev_info;
+	struct rte_eth_conf conf;
+	int ret;
+
+	printf("Testing rtap PMD offload configuration port %d\n", port);
+
+	if (get_dev_info(port, &dev_info) < 0)
+		return -1;
+
+	uint64_t expected_tx = RTE_ETH_TX_OFFLOAD_MULTI_SEGS |
+			       RTE_ETH_TX_OFFLOAD_UDP_CKSUM |
+			       RTE_ETH_TX_OFFLOAD_TCP_CKSUM |
+			       RTE_ETH_TX_OFFLOAD_TCP_TSO;
+
+	if ((dev_info.tx_offload_capa & expected_tx) != expected_tx) {
+		printf("Missing TX offload capabilities\n");
+		return -1;
+	}
+
+	printf("  TX offload capa: 0x%" PRIx64 " OK\n",
+	       dev_info.tx_offload_capa);
+
+	memset(&conf, 0, sizeof(conf));
+	conf.txmode.offloads = RTE_ETH_TX_OFFLOAD_UDP_CKSUM |
+			       RTE_ETH_TX_OFFLOAD_TCP_CKSUM;
+
+	ret = port_reconfigure(port, 1, &conf, RING_SIZE, mp);
+	if (ret < 0) {
+		printf("Configure with TX offloads failed\n");
+		goto restore;
+	}
+
+	printf("  TX offload configuration: OK\n");
+
+restore:
+	restore_single_queue(port);
+	return ret;
+}
+
+#define CSUM_PKT_PAYLOAD 32
+
+static int
+test_tx_csum_offload(int port)
+{
+	struct rte_ether_addr mac;
+	struct rte_mbuf *tx_mbuf;
+	uint8_t rx_buf[256];
+	int sock = -1;
+	uint16_t nb_tx, pkt_len;
+	ssize_t rx_len = 0;
+	int ret = -1;
+
+	printf("Testing rtap PMD TX checksum offload\n");
+
+	if (restore_single_queue(port) < 0 ||
+	    rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to setup test\n");
+		return -1;
+	}
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		return -1;
+
+	tx_mbuf = rte_pktmbuf_alloc(mp);
+	if (tx_mbuf == NULL) {
+		printf("Failed to allocate mbuf\n");
+		goto out;
+	}
+
+	/* Build Eth + IPv4 + UDP + payload */
+	struct rte_ether_hdr *eth = rte_pktmbuf_mtod(tx_mbuf,
+						      struct rte_ether_hdr *);
+	eth_addr_bcast(&eth->dst_addr);
+	memcpy(&eth->src_addr, &mac, RTE_ETHER_ADDR_LEN);
+	eth->ether_type = htons(RTE_ETHER_TYPE_IPV4);
+
+	struct rte_ipv4_hdr *ip = (struct rte_ipv4_hdr *)(eth + 1);
+	memset(ip, 0, sizeof(*ip));
+	ip->version_ihl = 0x45;
+	ip->total_length = htons(sizeof(*ip) + sizeof(struct rte_udp_hdr) +
+				 CSUM_PKT_PAYLOAD);
+	ip->time_to_live = 64;
+	ip->next_proto_id = IPPROTO_UDP;
+	ip->src_addr = htonl(0x0a000001);
+	ip->dst_addr = htonl(0x0a000002);
+	ip->hdr_checksum = 0;
+	ip->hdr_checksum = rte_ipv4_cksum(ip);
+
+	struct rte_udp_hdr *udp = (struct rte_udp_hdr *)(ip + 1);
+	udp->src_port = htons(1234);
+	udp->dst_port = htons(5678);
+	udp->dgram_len = htons(sizeof(*udp) + CSUM_PKT_PAYLOAD);
+	udp->dgram_cksum = 0;
+
+	uint8_t *payload = (uint8_t *)(udp + 1);
+	memset(payload, 0xCC, CSUM_PKT_PAYLOAD);
+
+	pkt_len = sizeof(*eth) + sizeof(*ip) + sizeof(*udp) + CSUM_PKT_PAYLOAD;
+	tx_mbuf->data_len = pkt_len;
+	tx_mbuf->pkt_len = pkt_len;
+
+	tx_mbuf->ol_flags = RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_UDP_CKSUM;
+	tx_mbuf->l2_len = sizeof(*eth);
+	tx_mbuf->l3_len = sizeof(*ip);
+	udp->dgram_cksum = rte_ipv4_phdr_cksum(ip, tx_mbuf->ol_flags);
+
+	rte_eth_stats_reset(port);
+
+	nb_tx = rte_eth_tx_burst(port, 0, &tx_mbuf, 1);
+	if (nb_tx != 1) {
+		printf("TX failed\n");
+		rte_pktmbuf_free(tx_mbuf);
+		goto out;
+	}
+
+	rx_len = poll_tap_socket(sock, rx_buf, sizeof(rx_buf),
+				 0x45, sizeof(*eth));
+
+	if (rx_len <= 0) {
+		printf("No packet captured\n");
+		goto out;
+	}
+
+	unsigned int cksum_off = sizeof(*eth) + sizeof(*ip) +
+				 offsetof(struct rte_udp_hdr, dgram_cksum);
+	uint16_t captured_cksum;
+
+	memcpy(&captured_cksum, &rx_buf[cksum_off], sizeof(captured_cksum));
+
+	if (captured_cksum == 0)
+		printf("  Warning: UDP checksum is zero\n");
+	else
+		printf("  UDP cksum=0x%04x\n", ntohs(captured_cksum));
+
+	struct rte_eth_stats stats;
+	rte_eth_stats_get(port, &stats);
+	if (stats.opackets == 0) {
+		printf("TX stats not updated\n");
+		goto out;
+	}
+
+	printf("  TX csum offload PASSED (captured %zd bytes)\n", rx_len);
+	ret = 0;
+
+out:
+	close(sock);
+	return ret;
+}
+
+#define FLOOD_RING_SIZE 64
+#define FLOOD_NUM_PKTS 1000
+#define FLOOD_PKT_SIZE 128
+
+static int
+test_imissed_counter(int port)
+{
+	struct rte_eth_stats stats_before, stats_after, stats_after_reset;
+	struct rte_ether_addr mac;
+	uint8_t pkt[FLOOD_PKT_SIZE];
+	int sock = -1;
+	int ret = -1;
+
+	printf("Testing rtap PMD imissed counter port %d\n", port);
+
+	if (rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to get MAC address\n");
+		return -1;
+	}
+
+	if (port_reconfigure(port, 1, NULL, FLOOD_RING_SIZE, mp) < 0)
+		goto restore;
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		goto restore;
+
+	drain_rx_queue(port, 0);
+
+	ret = rte_eth_stats_reset(port);
+	if (ret != 0) {
+		printf("Failed to reset stats: %s\n", rte_strerror(-ret));
+		goto close_sock;
+	}
+
+	ret = rte_eth_stats_get(port, &stats_before);
+	if (ret != 0) {
+		printf("Failed to get baseline stats: %s\n", rte_strerror(-ret));
+		goto close_sock;
+	}
+
+	printf("  Flooding with %d packets (ring size %d)\n",
+	       FLOOD_NUM_PKTS, FLOOD_RING_SIZE);
+
+	build_test_packet(pkt, FLOOD_PKT_SIZE, &mac, &mac);
+
+	for (int i = 0; i < FLOOD_NUM_PKTS; i++) {
+		if (send(sock, pkt, FLOOD_PKT_SIZE, 0) < 0) {
+			printf("send() failed after %d packets: %s\n",
+			       i, strerror(errno));
+			goto close_sock;
+		}
+	}
+
+	usleep(100000);  /* 100ms */
+
+	/* Drain whatever we can receive */
+	{
+		struct rte_mbuf *burst[RX_BURST_MAX];
+		uint16_t total_rx = 0;
+		int attempts = 0;
+
+		while (attempts++ < 100) {
+			uint16_t n = rte_eth_rx_burst(port, 0, burst, RX_BURST_MAX);
+			if (n > 0) {
+				rte_pktmbuf_free_bulk(burst, n);
+				total_rx += n;
+			} else {
+				usleep(1000);
+			}
+		}
+		printf("  Received %u packets out of %d sent\n",
+		       total_rx, FLOOD_NUM_PKTS);
+	}
+
+	ret = rte_eth_stats_get(port, &stats_after);
+	if (ret != 0) {
+		printf("Failed to get stats after flood: %s\n", rte_strerror(-ret));
+		goto close_sock;
+	}
+
+	printf("  Stats: ipackets=%"PRIu64" imissed=%"PRIu64"\n",
+	       stats_after.ipackets, stats_after.imissed);
+
+	if (stats_after.ipackets == 0) {
+		printf("  ERROR: No packets received\n");
+		goto close_sock;
+	}
+
+	if (stats_after.imissed == 0) {
+		printf("  WARNING: No packets marked as missed\n");
+	} else {
+		printf("  SUCCESS: imissed counter working (%"PRIu64" drops)\n",
+		       stats_after.imissed);
+	}
+
+	/* Test stats_reset clears imissed counter */
+	printf("  Testing stats_reset for imissed counter\n");
+	ret = rte_eth_stats_reset(port);
+	if (ret != 0) {
+		printf("  ERROR: stats_reset failed: %s\n", rte_strerror(-ret));
+		goto close_sock;
+	}
+
+	ret = rte_eth_stats_get(port, &stats_after_reset);
+	if (ret != 0) {
+		printf("  ERROR: stats_get after reset failed: %s\n", rte_strerror(-ret));
+		goto close_sock;
+	}
+
+	if (stats_after_reset.imissed != 0 || stats_after_reset.ipackets != 0) {
+		printf("  ERROR: stats not reset properly\n");
+		goto close_sock;
+	}
+
+	printf("  Stats reset: OK (all counters zeroed)\n");
+	printf("  imissed counter test PASSED\n");
+	ret = 0;
+
+close_sock:
+	close(sock);
+
+restore:
+	restore_single_queue(port);
+	return ret;
+}
+
+#define LSC_TIMEOUT_US 500000  /* 500ms */
+#define LSC_POLL_US    1000    /* 1ms between polls */
+
+#define RXQ_INTR_TIMEOUT_MS 500  /* 500ms */
+
+static RTE_ATOMIC(int) lsc_event_count;
+static RTE_ATOMIC(int) lsc_last_status;
+
+static int
+test_lsc_callback(uint16_t port_id, enum rte_eth_event_type type,
+		  void *param __rte_unused, void *ret_param __rte_unused)
+{
+	struct rte_eth_link link;
+
+	if (type != RTE_ETH_EVENT_INTR_LSC)
+		return 0;
+
+	if (rte_eth_link_get_nowait(port_id, &link) < 0) {
+		printf("  Link get nowait failed\n");
+		return 0;
+	}
+
+	rte_atomic_store_explicit(&lsc_last_status, link.link_status, rte_memory_order_relaxed);
+	rte_atomic_fetch_add_explicit(&lsc_event_count, 1, rte_memory_order_seq_cst);
+
+	printf("    LSC event #%d: port %u link %s\n",
+	       rte_atomic_load_explicit(&lsc_event_count, rte_memory_order_relaxed),
+	       port_id,
+	       link.link_status ? "UP" : "DOWN");
+
+	return 0;
+}
+
+static int
+test_lsc_interrupt(int port)
+{
+	struct rte_eth_conf lsc_conf;
+	int initial_count;
+	int ret = -1;
+
+	printf("Testing rtap PMD link state interrupt port %d\n", port);
+
+	memset(&lsc_conf, 0, sizeof(lsc_conf));
+	lsc_conf.intr_conf.lsc = 1;
+
+	if (port_reconfigure(port, 1, &lsc_conf, RING_SIZE, mp) < 0)
+		goto restore;
+
+	ret = rte_eth_dev_callback_register(port, RTE_ETH_EVENT_INTR_LSC,
+					    test_lsc_callback, NULL);
+	if (ret < 0) {
+		printf("Failed to register LSC callback: %s\n",
+		       rte_strerror(-ret));
+		goto restore;
+	}
+
+	rte_atomic_store_explicit(&lsc_event_count, 0, rte_memory_order_relaxed);
+	rte_atomic_store_explicit(&lsc_last_status, -1, rte_memory_order_relaxed);
+
+	if (verify_link_status(port, RTE_ETH_LINK_UP) < 0) {
+		ret = -1;
+		goto stop;
+	}
+
+	printf("  Link is UP, setting link DOWN\n");
+	initial_count = rte_atomic_load_explicit(&lsc_event_count, rte_memory_order_seq_cst);
+
+	ret = rte_eth_dev_set_link_down(port);
+	if (ret < 0) {
+		printf("Set link down failed: %s\n", rte_strerror(-ret));
+		goto stop;
+	}
+
+	if (wait_for_event(&lsc_event_count, initial_count, LSC_TIMEOUT_US) < 0) {
+		printf("  No LSC event received for link DOWN after %d us\n",
+		       LSC_TIMEOUT_US);
+		if (verify_link_status(port, RTE_ETH_LINK_DOWN) < 0) {
+			ret = -1;
+			goto stop;
+		}
+		printf("  Link status is DOWN (verified via polling)\n");
+	} else {
+		printf("  LSC event received for link DOWN\n");
+		if (rte_atomic_load_explicit(&lsc_last_status, rte_memory_order_seq_cst) != RTE_ETH_LINK_DOWN) {
+			printf("  ERROR: expected DOWN status in callback\n");
+			ret = -1;
+			goto stop;
+		}
+	}
+
+	printf("  Setting link UP\n");
+	initial_count = rte_atomic_load_explicit(&lsc_event_count, rte_memory_order_seq_cst);
+
+	ret = rte_eth_dev_set_link_up(port);
+	if (ret < 0) {
+		printf("Set link up failed: %s\n", rte_strerror(-ret));
+		goto stop;
+	}
+
+	if (wait_for_event(&lsc_event_count, initial_count, LSC_TIMEOUT_US) < 0) {
+		printf("  No LSC event received for link UP after %d us\n",
+		       LSC_TIMEOUT_US);
+		if (verify_link_status(port, RTE_ETH_LINK_UP) < 0) {
+			ret = -1;
+			goto stop;
+		}
+		printf("  Link status is UP (verified via polling)\n");
+	} else {
+		printf("  LSC event received for link UP\n");
+		if (rte_atomic_load_explicit(&lsc_last_status, rte_memory_order_seq_cst) != RTE_ETH_LINK_UP) {
+			printf("  ERROR: expected UP status in callback\n");
+			ret = -1;
+			goto stop;
+		}
+	}
+
+	printf("  LSC interrupt test PASSED (total events: %d)\n",
+	       rte_atomic_load_explicit(&lsc_event_count, rte_memory_order_relaxed));
+	ret = 0;
+
+stop:
+	rte_eth_dev_stop(port);
+	rte_eth_dev_callback_unregister(port, RTE_ETH_EVENT_INTR_LSC,
+					test_lsc_callback, NULL);
+
+restore:
+	restore_single_queue(port);
+	return ret;
+}
+
+static int
+test_rxq_interrupt(int port)
+{
+	struct rte_eth_conf rxq_conf;
+	struct rte_ether_addr mac;
+	uint8_t pkt[RTE_ETHER_HDR_LEN + TEST_PKT_PAYLOAD_LEN];
+	int sock = -1;
+	int epfd = -1;
+	int ret = -1;
+
+	printf("Testing rtap PMD RX queue interrupt port %d\n", port);
+
+	if (rte_eth_macaddr_get(port, &mac) < 0) {
+		printf("Failed to get MAC address\n");
+		return -1;
+	}
+
+	memset(&rxq_conf, 0, sizeof(rxq_conf));
+	rxq_conf.intr_conf.rxq = 1;
+
+	if (port_reconfigure(port, 1, &rxq_conf, RING_SIZE, mp) < 0)
+		goto restore;
+
+	/* Enable interrupt for queue 0 */
+	ret = rte_eth_dev_rx_intr_enable(port, 0);
+	if (ret < 0) {
+		printf("  rx_intr_enable failed: %s\n", rte_strerror(-ret));
+		goto restore;
+	}
+
+	/* Add queue 0's eventfd to the per-thread epoll set */
+	ret = rte_eth_dev_rx_intr_ctl_q(port, RTE_EPOLL_PER_THREAD,
+					RTE_INTR_EVENT_ADD, 0, NULL);
+	if (ret < 0) {
+		printf("  rx_intr_ctl_q(ADD) failed: %s\n", rte_strerror(-ret));
+		printf("  (epoll may not be available in this environment)\n");
+		epfd = -1;
+	} else {
+		epfd = RTE_EPOLL_PER_THREAD;
+	}
+
+	sock = setup_tap_socket_nb(TEST_TAP_NAME);
+	if (sock < 0)
+		goto disable_intr;
+
+	drain_rx_queue(port, 0);
+	build_test_packet(pkt, sizeof(pkt), NULL, &mac);
+
+	printf("  Injecting test packet\n");
+	if (send(sock, pkt, sizeof(pkt), 0) < 0) {
+		printf("send() failed: %s\n", strerror(errno));
+		goto close_sock;
+	}
+
+	/* Wait for the Rx interrupt via epoll */
+	if (epfd != -1) {
+		struct rte_epoll_event event;
+		int nfds;
+
+		nfds = rte_epoll_wait(epfd, &event, 1, RXQ_INTR_TIMEOUT_MS);
+		if (nfds < 0) {
+			printf("  rte_epoll_wait failed: %s\n",
+			       rte_strerror(-nfds));
+			printf("  (Falling back to polling verification)\n");
+		} else if (nfds == 0) {
+			printf("  WARNING: epoll timeout - no Rx interrupt received\n");
+			printf("  (This may be expected in some test environments)\n");
+		} else {
+			printf("  Rx interrupt received via epoll: OK\n");
+		}
+	}
+
+	/* Verify the packet actually arrived */
+	{
+		struct rte_mbuf *rx_mbufs[RX_BURST_MAX];
+		uint16_t nb_rx;
+		int elapsed = 0;
+
+		while (elapsed < TX_RX_TIMEOUT_US) {
+			nb_rx = rte_eth_rx_burst(port, 0, rx_mbufs, RX_BURST_MAX);
+			if (nb_rx > 0) {
+				rte_pktmbuf_free_bulk(rx_mbufs, nb_rx);
+				printf("  Packet received successfully\n");
+				break;
+			}
+			usleep(TX_RX_POLL_US);
+			elapsed += TX_RX_POLL_US;
+		}
+
+		if (nb_rx == 0) {
+			printf("  ERROR: No packet received\n");
+			ret = -1;
+			goto close_sock;
+		}
+	}
+
+	printf("  RX queue interrupt test PASSED\n");
+	ret = 0;
+
+close_sock:
+	close(sock);
+
+disable_intr:
+	rte_eth_dev_rx_intr_disable(port, 0);
+
+	if (epfd != -1)
+		rte_eth_dev_rx_intr_ctl_q(port, RTE_EPOLL_PER_THREAD,
+					  RTE_INTR_EVENT_DEL, 0, NULL);
+
+restore:
+	restore_single_queue(port);
+	return ret;
+}
+
+/* ========== Extended Statistics (xstats) Tests ========== */
+
+static int
+test_xstats_get_names(int port)
+{
+	struct rte_eth_xstat_name *names = NULL;
+	int count, ret = -1;
+
+	printf("Testing xstats_get_names for port %d\n", port);
+
+	/* Get number of xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count < 0) {
+		printf("Error: xstats_get_names returned %d\n", count);
+		return -1;
+	}
+
+	printf("  Total xstats count: %d\n", count);
+
+	if (count == 0) {
+		printf("  Warning: no xstats available\n");
+		return 0;
+	}
+
+	/* Allocate and retrieve names */
+	names = calloc(count, sizeof(*names));
+	if (names == NULL) {
+		printf("  Error: failed to allocate memory for xstats names\n");
+		return -1;
+	}
+
+	ret = rte_eth_xstats_get_names(port, names, count);
+	if (ret < 0 || ret != count) {
+		printf("  Error: xstats_get_names returned %d, expected %d\n",
+		       ret, count);
+		goto cleanup;
+	}
+
+	/* Verify naming convention for a few stats */
+	bool found_rx_size = false;
+	bool found_tx_size = false;
+	bool found_rx_broadcast = false;
+	bool found_tx_broadcast = false;
+
+	for (int i = 0; i < count; i++) {
+		if (strstr(names[i].name, "rx_q") && strstr(names[i].name, "size_"))
+			found_rx_size = true;
+		if (strstr(names[i].name, "tx_q") && strstr(names[i].name, "size_"))
+			found_tx_size = true;
+		if (strstr(names[i].name, "rx_q") && strstr(names[i].name, "broadcast"))
+			found_rx_broadcast = true;
+		if (strstr(names[i].name, "tx_q") && strstr(names[i].name, "broadcast"))
+			found_tx_broadcast = true;
+	}
+
+	if (!found_rx_size || !found_tx_size || !found_rx_broadcast || !found_tx_broadcast) {
+		printf("  Error: expected xstats not found\n");
+		printf("    rx_size=%d tx_size=%d rx_broadcast=%d tx_broadcast=%d\n",
+		       found_rx_size, found_tx_size, found_rx_broadcast, found_tx_broadcast);
+		ret = -1;
+		goto cleanup;
+	}
+
+	printf("  xstats naming convention verified: OK\n");
+	ret = 0;
+
+cleanup:
+	free(names);
+	return ret;
+}
+
+static int
+test_xstats_get_values(int port)
+{
+	struct rte_eth_xstat *xstats = NULL;
+	int count, ret = -1;
+
+	printf("Testing xstats_get values for port %d\n", port);
+
+	/* Get number of xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		return -1;
+	}
+
+	/* Allocate and retrieve values */
+	xstats = calloc(count, sizeof(*xstats));
+	if (xstats == NULL) {
+		printf("  Error: failed to allocate memory for xstats\n");
+		return -1;
+	}
+
+	ret = rte_eth_xstats_get(port, xstats, count);
+	if (ret < 0 || ret != count) {
+		printf("  Error: xstats_get returned %d, expected %d\n", ret, count);
+		goto cleanup;
+	}
+
+	/* Verify IDs are sequential */
+	for (int i = 0; i < count; i++) {
+		if (xstats[i].id != (uint64_t)i) {
+			printf("  Error: xstats[%d].id=%"PRIu64", expected %d\n",
+			       i, xstats[i].id, i);
+			ret = -1;
+			goto cleanup;
+		}
+	}
+
+	printf("  xstats IDs are sequential: OK\n");
+	ret = 0;
+
+cleanup:
+	free(xstats);
+	return ret;
+}
+
+static int
+test_xstats_reset(int port)
+{
+	struct rte_eth_xstat *xstats = NULL;
+	int count, ret = -1;
+
+	printf("Testing xstats_reset for port %d\n", port);
+
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		return -1;
+	}
+
+	xstats = calloc(count, sizeof(*xstats));
+	if (xstats == NULL) {
+		printf("  Error: failed to allocate memory for xstats\n");
+		return -1;
+	}
+
+	/* Reset xstats */
+	ret = rte_eth_xstats_reset(port);
+	if (ret != 0) {
+		printf("  Error: xstats_reset returned %d\n", ret);
+		goto cleanup;
+	}
+
+	/* Verify all xstats are zero */
+	ret = rte_eth_xstats_get(port, xstats, count);
+	if (ret < 0) {
+		printf("  Error: xstats_get after reset returned %d\n", ret);
+		goto cleanup;
+	}
+
+	for (int i = 0; i < count; i++) {
+		if (xstats[i].value != 0) {
+			printf("  Error: xstats[%d].value=%"PRIu64", expected 0\n",
+			       i, xstats[i].value);
+			ret = -1;
+			goto cleanup;
+		}
+	}
+
+	printf("  All xstats reset to zero: OK\n");
+	ret = 0;
+
+cleanup:
+	free(xstats);
+	return ret;
+}
+
+static int
+test_xstats_packet_sizes(int port)
+{
+	struct rte_eth_xstat_name *names = NULL;
+	struct rte_eth_xstat *xstats = NULL;
+	struct rte_eth_dev_info dev_info;
+	struct rte_mbuf *tx_mbufs[4];
+	uint16_t pkt_sizes[] = {60, 100, 200, 500}; /* Will result in 64, 128, 256, 512 on wire */
+	int count, ret = -1;
+	int sock = -1;
+	char ifname[IFNAMSIZ];
+
+	printf("Testing xstats packet size buckets for port %d\n", port);
+
+	if (get_dev_info(port, &dev_info) < 0)
+		return -1;
+
+	snprintf(ifname, sizeof(ifname), "rtap_test%d", port);
+	sock = setup_tap_socket_nb(ifname);
+	if (sock < 0) {
+		printf("  Warning: cannot setup TAP socket, skipping test\n");
+		return 0;
+	}
+
+	/* Reset stats */
+	if (rte_eth_xstats_reset(port) != 0) {
+		printf("  Error: xstats_reset failed\n");
+		goto cleanup;
+	}
+
+	/* Send packets of different sizes */
+	for (unsigned int i = 0; i < RTE_DIM(pkt_sizes); i++) {
+		tx_mbufs[i] = rte_pktmbuf_alloc(mp);
+		if (tx_mbufs[i] == NULL) {
+			printf("  Error: failed to allocate mbuf\n");
+			goto cleanup;
+		}
+
+		uint8_t *pkt = rte_pktmbuf_mtod(tx_mbufs[i], uint8_t *);
+		build_test_packet(pkt, pkt_sizes[i], NULL, NULL);
+		tx_mbufs[i]->pkt_len = pkt_sizes[i];
+		tx_mbufs[i]->data_len = pkt_sizes[i];
+	}
+
+	uint16_t nb_tx = rte_eth_tx_burst(port, 0, tx_mbufs, RTE_DIM(tx_mbufs));
+	if (nb_tx != RTE_DIM(tx_mbufs)) {
+		printf("  Error: sent %u/%zu packets\n", nb_tx, RTE_DIM(tx_mbufs));
+		goto cleanup;
+	}
+
+	usleep(10000); /* Allow packets to be processed */
+
+	/* Get xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		goto cleanup;
+	}
+
+	names = calloc(count, sizeof(*names));
+	xstats = calloc(count, sizeof(*xstats));
+	if (names == NULL || xstats == NULL) {
+		printf("  Error: failed to allocate memory\n");
+		goto cleanup;
+	}
+
+	rte_eth_xstats_get_names(port, names, count);
+	rte_eth_xstats_get(port, xstats, count);
+
+	/* Find and verify size bucket stats */
+	int buckets_found = 0;
+	for (int i = 0; i < count; i++) {
+		if (strstr(names[i].name, "tx_q0_size_64_packets") && xstats[i].value > 0)
+			buckets_found++;
+		else if (strstr(names[i].name, "tx_q0_size_65_to_127_packets") && xstats[i].value > 0)
+			buckets_found++;
+		else if (strstr(names[i].name, "tx_q0_size_128_to_255_packets") && xstats[i].value > 0)
+			buckets_found++;
+		else if (strstr(names[i].name, "tx_q0_size_256_to_511_packets") && xstats[i].value > 0)
+			buckets_found++;
+		else if (strstr(names[i].name, "tx_q0_size_512_to_1023_packets") && xstats[i].value > 0)
+			buckets_found++;
+	}
+
+	if (buckets_found < 4) {
+		printf("  Warning: only found %d size buckets with packets\n", buckets_found);
+		printf("  (This may be expected if packets were coalesced)\n");
+	} else {
+		printf("  Packet size buckets populated: OK\n");
+	}
+
+	ret = 0;
+
+cleanup:
+	free(names);
+	free(xstats);
+	if (sock >= 0)
+		close(sock);
+	return ret;
+}
+
+static int
+test_xstats_packet_types(int port)
+{
+	struct rte_eth_xstat_name *names = NULL;
+	struct rte_eth_xstat *xstats = NULL;
+	struct rte_eth_dev_info dev_info;
+	struct rte_mbuf *tx_mbufs[3];
+	struct rte_ether_addr bcast, mcast, ucast;
+	int count, ret = -1;
+	int sock = -1;
+	char ifname[IFNAMSIZ];
+
+	printf("Testing xstats packet type classification for port %d\n", port);
+
+	if (get_dev_info(port, &dev_info) < 0)
+		return -1;
+
+	snprintf(ifname, sizeof(ifname), "rtap_test%d", port);
+	sock = setup_tap_socket_nb(ifname);
+	if (sock < 0) {
+		printf("  Warning: cannot setup TAP socket, skipping test\n");
+		return 0;
+	}
+
+	/* Reset stats */
+	if (rte_eth_xstats_reset(port) != 0) {
+		printf("  Error: xstats_reset failed\n");
+		goto cleanup;
+	}
+
+	/* Prepare addresses */
+	eth_addr_bcast(&bcast);
+	mcast.addr_bytes[0] = 0x01;
+	mcast.addr_bytes[1] = 0x00;
+	mcast.addr_bytes[2] = 0x5e;
+	mcast.addr_bytes[3] = 0x00;
+	mcast.addr_bytes[4] = 0x00;
+	mcast.addr_bytes[5] = 0x01;
+	rte_eth_random_addr(ucast.addr_bytes);
+	ucast.addr_bytes[0] &= 0xfe; /* Clear multicast bit */
+
+	/* Send broadcast, multicast, and unicast packets */
+	for (int i = 0; i < 3; i++) {
+		tx_mbufs[i] = rte_pktmbuf_alloc(mp);
+		if (tx_mbufs[i] == NULL) {
+			printf("  Error: failed to allocate mbuf\n");
+			goto cleanup;
+		}
+
+		uint8_t *pkt = rte_pktmbuf_mtod(tx_mbufs[i], uint8_t *);
+		struct rte_ether_addr *dst = (i == 0) ? &bcast : (i == 1) ? &mcast : &ucast;
+		build_test_packet(pkt, 64, NULL, dst);
+		tx_mbufs[i]->pkt_len = 64;
+		tx_mbufs[i]->data_len = 64;
+	}
+
+	uint16_t nb_tx = rte_eth_tx_burst(port, 0, tx_mbufs, 3);
+	if (nb_tx != 3) {
+		printf("  Error: sent %u/3 packets\n", nb_tx);
+		goto cleanup;
+	}
+
+	usleep(10000);
+
+	/* Get xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		goto cleanup;
+	}
+
+	names = calloc(count, sizeof(*names));
+	xstats = calloc(count, sizeof(*xstats));
+	if (names == NULL || xstats == NULL) {
+		printf("  Error: failed to allocate memory\n");
+		goto cleanup;
+	}
+
+	rte_eth_xstats_get_names(port, names, count);
+	rte_eth_xstats_get(port, xstats, count);
+
+	/* Verify packet type stats */
+	bool found_bcast = false, found_mcast = false, found_ucast = false;
+	for (int i = 0; i < count; i++) {
+		if (strstr(names[i].name, "tx_q0_broadcast_packets") && xstats[i].value > 0)
+			found_bcast = true;
+		else if (strstr(names[i].name, "tx_q0_multicast_packets") && xstats[i].value > 0)
+			found_mcast = true;
+		else if (strstr(names[i].name, "tx_q0_unicast_packets") && xstats[i].value > 0)
+			found_ucast = true;
+	}
+
+	if (!found_bcast || !found_mcast || !found_ucast) {
+		printf("  Error: packet type classification failed\n");
+		printf("    broadcast=%d multicast=%d unicast=%d\n",
+		       found_bcast, found_mcast, found_ucast);
+		ret = -1;
+		goto cleanup;
+	}
+
+	printf("  Packet type classification: OK\n");
+	ret = 0;
+
+cleanup:
+	free(names);
+	free(xstats);
+	if (sock >= 0)
+		close(sock);
+	return ret;
+}
+
+static int
+test_xstats_multiqueue(int port)
+{
+	struct rte_eth_xstat_name *names = NULL;
+	struct rte_eth_xstat *xstats = NULL;
+	int count, ret = -1;
+	const uint16_t nb_queues = 2;
+
+	printf("Testing xstats with multiple queues for port %d\n", port);
+
+	/* Reconfigure with 2 queues */
+	if (port_reconfigure(port, nb_queues, NULL, RING_SIZE, mp) < 0) {
+		printf("  Error: failed to reconfigure port with %u queues\n", nb_queues);
+		return -1;
+	}
+
+	/* Reset stats */
+	if (rte_eth_xstats_reset(port) != 0) {
+		printf("  Error: xstats_reset failed\n");
+		goto restore;
+	}
+
+	/* Get xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		goto restore;
+	}
+
+	names = calloc(count, sizeof(*names));
+	xstats = calloc(count, sizeof(*xstats));
+	if (names == NULL || xstats == NULL) {
+		printf("  Error: failed to allocate memory\n");
+		goto restore;
+	}
+
+	rte_eth_xstats_get_names(port, names, count);
+	rte_eth_xstats_get(port, xstats, count);
+
+	/* Verify we have stats for both queues */
+	bool found_q0 = false, found_q1 = false;
+	for (int i = 0; i < count; i++) {
+		if (strstr(names[i].name, "_q0_"))
+			found_q0 = true;
+		if (strstr(names[i].name, "_q1_"))
+			found_q1 = true;
+	}
+
+	if (!found_q0 || !found_q1) {
+		printf("  Error: missing stats for queues (q0=%d, q1=%d)\n",
+		       found_q0, found_q1);
+		ret = -1;
+		goto cleanup;
+	}
+
+	printf("  Multi-queue xstats present: OK\n");
+	ret = 0;
+
+cleanup:
+	free(names);
+	free(xstats);
+restore:
+	restore_single_queue(port);
+	return ret;
+}
+
+static int
+test_xstats_offload_stats(int port)
+{
+	struct rte_eth_xstat_name *names = NULL;
+	struct rte_eth_xstat *xstats = NULL;
+	struct rte_eth_dev_info dev_info;
+	struct rte_mbuf *mb;
+	struct rte_ether_hdr *eth;
+	struct rte_ipv4_hdr *ip;
+	struct rte_udp_hdr *udp;
+	int count, ret = -1;
+	int sock = -1;
+	char ifname[IFNAMSIZ];
+
+	printf("Testing xstats offload statistics for port %d\n", port);
+
+	if (get_dev_info(port, &dev_info) < 0)
+		return -1;
+
+	snprintf(ifname, sizeof(ifname), "rtap_test%d", port);
+	sock = setup_tap_socket_nb(ifname);
+	if (sock < 0) {
+		printf("  Warning: cannot setup TAP socket, skipping test\n");
+		return 0;
+	}
+
+	/* Reset stats */
+	if (rte_eth_xstats_reset(port) != 0) {
+		printf("  Error: xstats_reset failed\n");
+		goto cleanup;
+	}
+
+	/* Create packet with checksum offload request */
+	mb = rte_pktmbuf_alloc(mp);
+	if (mb == NULL) {
+		printf("  Error: failed to allocate mbuf\n");
+		goto cleanup;
+	}
+
+	eth = rte_pktmbuf_mtod(mb, struct rte_ether_hdr *);
+	ip = (struct rte_ipv4_hdr *)(eth + 1);
+	udp = (struct rte_udp_hdr *)(ip + 1);
+
+	/* Build packet headers */
+	rte_eth_random_addr(eth->dst_addr.addr_bytes);
+	rte_eth_random_addr(eth->src_addr.addr_bytes);
+	eth->ether_type = rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4);
+
+	ip->version_ihl = 0x45;
+	ip->total_length = rte_cpu_to_be_16(sizeof(*ip) + sizeof(*udp) + 20);
+	ip->src_addr = rte_cpu_to_be_32(0x0a000001);
+	ip->dst_addr = rte_cpu_to_be_32(0x0a000002);
+
+	udp->src_port = rte_cpu_to_be_16(1234);
+	udp->dst_port = rte_cpu_to_be_16(5678);
+	udp->dgram_len = rte_cpu_to_be_16(sizeof(*udp) + 20);
+
+	mb->l2_len = sizeof(*eth);
+	mb->l3_len = sizeof(*ip);
+	mb->l4_len = sizeof(*udp);
+	mb->ol_flags = RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_UDP_CKSUM | RTE_MBUF_F_TX_IP_CKSUM;
+	mb->data_len = sizeof(*eth) + sizeof(*ip) + sizeof(*udp) + 20;
+	mb->pkt_len = mb->data_len;
+
+	uint16_t nb_tx = rte_eth_tx_burst(port, 0, &mb, 1);
+	if (nb_tx != 1) {
+		printf("  Error: failed to transmit packet\n");
+		rte_pktmbuf_free(mb);
+		goto cleanup;
+	}
+
+	usleep(10000);
+
+	/* Get xstats */
+	count = rte_eth_xstats_get_names(port, NULL, 0);
+	if (count <= 0) {
+		printf("  Error: no xstats available\n");
+		goto cleanup;
+	}
+
+	names = calloc(count, sizeof(*names));
+	xstats = calloc(count, sizeof(*xstats));
+	if (names == NULL || xstats == NULL) {
+		printf("  Error: failed to allocate memory\n");
+		goto cleanup;
+	}
+
+	rte_eth_xstats_get_names(port, names, count);
+	rte_eth_xstats_get(port, xstats, count);
+
+	/* Look for checksum offload stat */
+	bool found_cksum_offload = false;
+	for (int i = 0; i < count; i++) {
+		if (strstr(names[i].name, "tx_q0_checksum_offload_packets") &&
+		    xstats[i].value > 0) {
+			found_cksum_offload = true;
+			break;
+		}
+	}
+
+	if (!found_cksum_offload) {
+		printf("  Warning: checksum offload stat not incremented\n");
+		printf("  (This may be expected if offload was not used)\n");
+	} else {
+		printf("  Checksum offload stat incremented: OK\n");
+	}
+
+	ret = 0;
+
+cleanup:
+	free(names);
+	free(xstats);
+	if (sock >= 0)
+		close(sock);
+	return ret;
+}
+
+static int
+test_fd_leak(void)
+{
+	int fd_before, fd_after;
+	int port = -1;
+	int ret;
+
+	printf("Testing rtap PMD file descriptor leak\n");
+
+	fd_before = count_open_fds();
+	if (fd_before < 0) {
+		printf("Cannot count open fds\n");
+		return -1;
+	}
+
+	printf("  Open fds before: %d\n", fd_before);
+
+	if (rte_vdev_init("net_rtap_fdtest", "iface=rtap_fdtest") < 0) {
+		printf("Failed to create net_rtap_fdtest\n");
+		return -1;
+	}
+
+	uint16_t p;
+	RTE_ETH_FOREACH_DEV(p) {
+		struct rte_eth_dev_info info;
+		if (rte_eth_dev_info_get(p, &info) != 0)
+			continue;
+		if (p == (uint16_t)rtap_port0 || p == (uint16_t)rtap_port1)
+			continue;
+		if (strstr(info.driver_name, "rtap") != NULL) {
+			port = p;
+			break;
+		}
+	}
+
+	if (port < 0) {
+		printf("Failed to find fd-test port\n");
+		rte_vdev_uninit("net_rtap_fdtest");
+		return -1;
+	}
+
+	if (port_reconfigure(port, 2, NULL, RING_SIZE, mp) < 0)
+		goto cleanup;
+
+	ret = rte_eth_dev_stop(port);
+	if (ret != 0)
+		printf("Warning: stop returned %d\n", ret);
+
+	rte_eth_dev_close(port);
+	rte_vdev_uninit("net_rtap_fdtest");
+
+	fd_after = count_open_fds();
+	printf("  Open fds after:  %d\n", fd_after);
+
+	if (fd_after != fd_before) {
+		printf("  ERROR: fd leak detected: %d fds leaked\n",
+		       fd_after - fd_before);
+		return -1;
+	}
+
+	printf("  fd leak test PASSED\n");
+	return 0;
+
+cleanup:
+	rte_eth_dev_stop(port);
+	rte_eth_dev_close(port);
+	rte_vdev_uninit("net_rtap_fdtest");
+	return -1;
+}
+
+static void
+test_rtap_cleanup(void)
+{
+	int ret;
+
+	if (rtap_port0 >= 0) {
+		ret = rte_eth_dev_stop(rtap_port0);
+		if (ret != 0)
+			printf("Error: failed to stop port %u: %s\n",
+			       rtap_port0, rte_strerror(-ret));
+		rte_eth_dev_close(rtap_port0);
+	}
+
+	if (rtap_port1 >= 0) {
+		ret = rte_eth_dev_stop(rtap_port1);
+		if (ret != 0)
+			printf("Error: failed to stop port %u: %s\n",
+			       rtap_port1, rte_strerror(-ret));
+		rte_eth_dev_close(rtap_port1);
+	}
+
+	rte_mempool_free(mp);
+	rte_vdev_uninit("net_rtap0");
+	rte_vdev_uninit("net_rtap1");
+}
+
+static int
+test_pmd_rtap_setup(void)
+{
+	uint16_t nb_ports;
+
+	if (check_rtap_available() < 0) {
+		printf("RTAP not available, skipping tests\n");
+		return TEST_SKIPPED;
+	}
+
+	nb_ports = rte_eth_dev_count_avail();
+	printf("nb_ports before rtap creation=%d\n", (int)nb_ports);
+
+	mp = rte_pktmbuf_pool_create("mbuf_pool", NB_MBUF, 32,
+			0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
+	if (mp == NULL) {
+		printf("Failed to create mempool\n");
+		return TEST_FAILED;
+	}
+
+	if (rte_vdev_init("net_rtap0", "iface=rtap_test0") < 0) {
+		printf("Failed to create net_rtap0\n");
+		rte_mempool_free(mp);
+		return TEST_FAILED;
+	}
+
+	if (rte_vdev_init("net_rtap1", "iface=rtap_test1") < 0) {
+		printf("Failed to create net_rtap1\n");
+		rte_vdev_uninit("net_rtap0");
+		rte_mempool_free(mp);
+		return TEST_FAILED;
+	}
+
+	uint16_t port;
+	RTE_ETH_FOREACH_DEV(port) {
+		struct rte_eth_dev_info dev_info;
+		int ret = rte_eth_dev_info_get(port, &dev_info);
+		if (ret != 0)
+			continue;
+
+		if (strstr(dev_info.driver_name, "rtap") != NULL ||
+		    strstr(dev_info.driver_name, "RTAP") != NULL) {
+			if (rtap_port0 < 0)
+				rtap_port0 = port;
+			else if (rtap_port1 < 0)
+				rtap_port1 = port;
+		}
+	}
+
+	if (rtap_port0 < 0) {
+		printf("Failed to find rtap ports\n");
+		test_rtap_cleanup();
+		return TEST_FAILED;
+	}
+
+	printf("rtap_port0=%d rtap_port1=%d\n", rtap_port0, rtap_port1);
+	return TEST_SUCCESS;
+}
+
+static int
+test_ethdev_configure_ports(void)
+{
+	TEST_ASSERT((test_ethdev_configure_port(rtap_port0) == 0),
+			"test ethdev configure port rtap_port0 failed");
+
+	if (rtap_port1 >= 0) {
+		TEST_ASSERT((test_ethdev_configure_port(rtap_port1) == 0),
+				"test ethdev configure port rtap_port1 failed");
+	}
+
+	return TEST_SUCCESS;
+}
+
+static int
+test_command_line_rtap_port(void)
+{
+	int port, cmdl_port = -1;
+	int ret;
+
+	printf("Testing command line created rtap port\n");
+
+	RTE_ETH_FOREACH_DEV(port) {
+		struct rte_eth_dev_info dev_info;
+
+		ret = rte_eth_dev_info_get(port, &dev_info);
+		if (ret != 0)
+			continue;
+
+		if (port == rtap_port0 || port == rtap_port1)
+			continue;
+
+		if (strstr(dev_info.driver_name, "rtap") != NULL ||
+		    strstr(dev_info.driver_name, "RTAP") != NULL) {
+			printf("Found command line rtap port=%d\n", port);
+			cmdl_port = port;
+			break;
+		}
+	}
+
+	if (cmdl_port != -1) {
+		TEST_ASSERT((test_ethdev_configure_port(cmdl_port) == 0),
+				"test ethdev configure cmdl_port failed");
+		TEST_ASSERT((test_stats_reset(cmdl_port) == 0),
+				"test stats reset cmdl_port failed");
+		TEST_ASSERT((test_get_stats(cmdl_port) == 0),
+				"test get stats cmdl_port failed");
+		TEST_ASSERT((rte_eth_dev_stop(cmdl_port) == 0),
+				"test stop cmdl_port failed");
+	}
+
+	return TEST_SUCCESS;
+}
+
+/* Test case wrappers */
+#define TEST_CASE_WRAPPER(name, func) \
+	static int test_##name##_for_port(void) { \
+		TEST_ASSERT(func(rtap_port0) == 0, #name " failed"); \
+		return TEST_SUCCESS; \
+	}
+
+TEST_CASE_WRAPPER(get_stats, test_get_stats)
+TEST_CASE_WRAPPER(stats_reset, test_stats_reset)
+TEST_CASE_WRAPPER(dev_info, test_dev_info)
+TEST_CASE_WRAPPER(link_status, test_link_status)
+TEST_CASE_WRAPPER(link_up_down, test_set_link_up_down)
+TEST_CASE_WRAPPER(promiscuous, test_promiscuous_mode)
+TEST_CASE_WRAPPER(allmulticast, test_allmulticast_mode)
+TEST_CASE_WRAPPER(mac_address, test_mac_address)
+TEST_CASE_WRAPPER(mtu, test_mtu_set)
+TEST_CASE_WRAPPER(multiqueue, test_multiqueue)
+TEST_CASE_WRAPPER(multiqueue_reduce, test_multiqueue_reduce)
+TEST_CASE_WRAPPER(multiqueue_mismatch, test_multiqueue_mismatch)
+TEST_CASE_WRAPPER(queue_reconfigure, test_queue_reconfigure)
+TEST_CASE_WRAPPER(rx_inject, test_rx_inject)
+TEST_CASE_WRAPPER(tx_capture, test_tx_capture)
+TEST_CASE_WRAPPER(tx_multiseg, test_tx_multiseg)
+TEST_CASE_WRAPPER(rx_multiseg, test_rx_multiseg)
+TEST_CASE_WRAPPER(offload_config, test_offload_config)
+TEST_CASE_WRAPPER(tx_csum_offload, test_tx_csum_offload)
+TEST_CASE_WRAPPER(stats_imissed, test_imissed_counter)
+TEST_CASE_WRAPPER(lsc_interrupt, test_lsc_interrupt)
+TEST_CASE_WRAPPER(rxq_interrupt, test_rxq_interrupt)
+TEST_CASE_WRAPPER(xstats_get_names, test_xstats_get_names)
+TEST_CASE_WRAPPER(xstats_get_values, test_xstats_get_values)
+TEST_CASE_WRAPPER(xstats_reset, test_xstats_reset)
+TEST_CASE_WRAPPER(xstats_packet_sizes, test_xstats_packet_sizes)
+TEST_CASE_WRAPPER(xstats_packet_types, test_xstats_packet_types)
+TEST_CASE_WRAPPER(xstats_multiqueue, test_xstats_multiqueue)
+TEST_CASE_WRAPPER(xstats_offload_stats, test_xstats_offload_stats)
+
+static int
+test_fd_leak_for_port(void)
+{
+	TEST_ASSERT(test_fd_leak() == 0, "test fd leak failed");
+	return TEST_SUCCESS;
+}
+
+static struct
+unit_test_suite test_pmd_rtap_suite = {
+	.setup = test_pmd_rtap_setup,
+	.teardown = test_rtap_cleanup,
+	.suite_name = "Test Pmd RTAP Unit Test Suite",
+	.unit_test_cases = {
+		TEST_CASE(test_ethdev_configure_ports),
+		TEST_CASE(test_dev_info_for_port),
+		TEST_CASE(test_link_status_for_port),
+		TEST_CASE(test_link_up_down_for_port),
+		TEST_CASE(test_get_stats_for_port),
+		TEST_CASE(test_stats_reset_for_port),
+		TEST_CASE(test_stats_imissed_for_port),
+		TEST_CASE(test_promiscuous_for_port),
+		TEST_CASE(test_allmulticast_for_port),
+		TEST_CASE(test_mac_address_for_port),
+		TEST_CASE(test_mtu_for_port),
+		TEST_CASE(test_multiqueue_for_port),
+		TEST_CASE(test_multiqueue_reduce_for_port),
+		TEST_CASE(test_multiqueue_mismatch_for_port),
+		TEST_CASE(test_queue_reconfigure_for_port),
+		TEST_CASE(test_rx_inject_for_port),
+		TEST_CASE(test_tx_capture_for_port),
+		TEST_CASE(test_tx_multiseg_for_port),
+		TEST_CASE(test_rx_multiseg_for_port),
+		TEST_CASE(test_offload_config_for_port),
+		TEST_CASE(test_tx_csum_offload_for_port),
+		TEST_CASE(test_lsc_interrupt_for_port),
+		TEST_CASE(test_rxq_interrupt_for_port),
+		TEST_CASE(test_xstats_get_names_for_port),
+		TEST_CASE(test_xstats_get_values_for_port),
+		TEST_CASE(test_xstats_reset_for_port),
+		TEST_CASE(test_xstats_packet_sizes_for_port),
+		TEST_CASE(test_xstats_packet_types_for_port),
+		TEST_CASE(test_xstats_multiqueue_for_port),
+		TEST_CASE(test_xstats_offload_stats_for_port),
+		TEST_CASE(test_fd_leak_for_port),
+		TEST_CASE(test_command_line_rtap_port),
+		TEST_CASES_END()
+	}
+};
+
+static int
+test_pmd_rtap(void)
+{
+	return unit_test_suite_runner(&test_pmd_rtap_suite);
+}
+
+REGISTER_FAST_TEST(rtap_pmd_autotest, NOHUGE_OK, ASAN_OK, test_pmd_rtap);
-- 
2.51.0


  parent reply	other threads:[~2026-02-14 23:48 UTC|newest]

Thread overview: 72+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-12-10 21:23 [RFC 0/8] ioring: network driver Stephen Hemminger
2024-12-10 21:23 ` [RFC 1/8] net/ioring: introduce new driver Stephen Hemminger
2024-12-10 21:23 ` [RFC 2/8] net/ioring: implement link state Stephen Hemminger
2024-12-10 21:23 ` [RFC 3/8] net/ioring: implement control functions Stephen Hemminger
2024-12-10 21:23 ` [RFC 4/8] net/ioring: implement management functions Stephen Hemminger
2024-12-10 21:23 ` [RFC 5/8] net/ioring: implement primary secondary fd passing Stephen Hemminger
2024-12-10 21:23 ` [RFC 6/8] net/ioring: implement receive and transmit Stephen Hemminger
2024-12-10 21:23 ` [RFC 7/8] net/ioring: add VLAN support Stephen Hemminger
2024-12-10 21:23 ` [RFC 8/8] net/ioring: implement statistics Stephen Hemminger
2024-12-11 11:34 ` [RFC 0/8] ioring: network driver Konstantin Ananyev
2024-12-11 15:03   ` Stephen Hemminger
2024-12-12 19:06     ` Konstantin Ananyev
2024-12-19 15:40       ` Morten Brørup
2024-12-20 14:34         ` Konstantin Ananyev
2024-12-20 16:19           ` Stephen Hemminger
2024-12-11 16:28 ` [PATCH v2 " Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 1/8] net/ioring: introduce new driver Stephen Hemminger
2024-12-28 16:39     ` Morten Brørup
2024-12-11 16:28   ` [PATCH v2 2/8] net/ioring: implement link state Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 3/8] net/ioring: implement control functions Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 4/8] net/ioring: implement management functions Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 5/8] net/ioring: implement primary secondary fd passing Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 6/8] net/ioring: implement receive and transmit Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 7/8] net/ioring: add VLAN support Stephen Hemminger
2024-12-11 16:28   ` [PATCH v2 8/8] net/ioring: implement statistics Stephen Hemminger
2025-03-11 23:51 ` [PATCH v3 0/9] ioring PMD device Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 1/9] net/ioring: introduce new driver Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 2/9] net/ioring: implement link state Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 3/9] net/ioring: implement control functions Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 4/9] net/ioring: implement management functions Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 5/9] net/ioring: implement secondary process support Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 6/9] net/ioring: implement receive and transmit Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 7/9] net/ioring: add VLAN support Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 8/9] net/ioring: implement statistics Stephen Hemminger
2025-03-11 23:51   ` [PATCH v3 9/9] net/ioring: support multi-segment Rx and Tx Stephen Hemminger
2025-03-13 21:50 ` [PATCH v4 00/10] new ioring PMD Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 01/10] net/ioring: introduce new driver Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 02/10] net/ioring: implement link state Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 03/10] net/ioring: implement control functions Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 04/10] net/ioring: implement management functions Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 05/10] net/ioring: implement secondary process support Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 06/10] net/ioring: implement receive and transmit Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 07/10] net/ioring: implement statistics Stephen Hemminger
2025-03-13 21:50   ` [PATCH v4 08/10] net/ioring: support multi-segment Rx and Tx Stephen Hemminger
2025-03-13 21:51   ` [PATCH v4 09/10] net/ioring: support Tx checksum and segment offload Stephen Hemminger
2025-03-13 21:51   ` [PATCH v4 10/10] net/ioring: add support for Rx offload Stephen Hemminger
2026-02-09 18:38 ` [PATCH v5 00/10] net/rtap: add io_uring based TAP driver Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 01/10] net/rtap: add driver skeleton and documentation Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 02/10] net/rtap: add TAP device creation and queue management Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 03/10] net/rtap: add Rx/Tx with scatter/gather support Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 04/10] net/rtap: add statistics and device info Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 05/10] net/rtap: add link and device management operations Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 06/10] net/rtap: add checksum and TSO offload support Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 07/10] net/rtap: add link state change interrupt Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 08/10] net/rtap: add multi-process support Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 09/10] net/rtap: add Rx interrupt support Stephen Hemminger
2026-02-09 18:39   ` [PATCH v5 10/10] test: add unit tests for rtap PMD Stephen Hemminger
2026-02-10  9:18   ` [PATCH v5 00/10] net/rtap: add io_uring based TAP driver Morten Brørup
2026-02-14 23:44 ` [PATCH v6 00/11] " Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 01/11] net/rtap: add driver skeleton and documentation Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 02/11] net/rtap: add TAP device creation and queue management Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 03/11] net/rtap: add Rx/Tx with scatter/gather support Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 04/11] net/rtap: add statistics and device info Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 05/11] net/rtap: add link and device management operations Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 06/11] net/rtap: add checksum and TSO offload support Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 07/11] net/rtap: add multi-process support Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 08/11] net/rtap: add link state change interrupt Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 09/11] net/rtap: add Rx interrupt support Stephen Hemminger
2026-02-14 23:44   ` [PATCH v6 10/11] net/rtap: add extended statistics support Stephen Hemminger
2026-02-14 23:44   ` Stephen Hemminger [this message]
2026-02-15  8:58   ` [PATCH v6 00/11] net/rtap: add io_uring based TAP driver Konstantin Ananyev
2026-02-15 17:08     ` Stephen Hemminger

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=20260214234726.188947-12-stephen@networkplumber.org \
    --to=stephen@networkplumber.org \
    --cc=dev@dpdk.org \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.