Subject: [PATCH net-next 05/10] tools: ynl: convert tc and tc-filter-add samples to selftest

From: Jakub Kicinski <[email-masked]>
Date: Thu, 5 Mar 2026 18:08:56 -0800

Convert tc.c and tc-filter-add.c to produce KTAP output with
kselftest_harness. Merge the two tests together. They both
test TC one is testing qdisc and the other classifiers but
they can easily live in a single selftest.

Make the test spawn a new netns, and run the operations on
lo to avoid onerous setup and cleanup.

  TAP version 13
  1..2
  # Starting 2 tests from 1 test cases.
  #  RUN           tc.qdisc ...
  #               lo: fq_codel  limit: 10240p target: 5ms new_flow_cnt: 0
  #            OK  tc.qdisc
  ok 1 tc.qdisc
  #  RUN           tc.flower ...
  # flower pref 1 proto: 0x8100
  # flower:
  #   vlan_id: 100
  #   vlan_prio: 5
  #   num_of_vlans: 3
  # action order: 1 vlan push id 200 protocol 0x8100 priority 0
  # action order: 2 vlan push id 300 protocol 0x8100 priority 0
  #            OK  tc.flower
  ok 2 tc.flower
  # PASSED: 2 / 2 tests passed.
  # Totals: pass:2 fail:0 xfail:0 xpass:0 skip:0 error:0

Signed-off-by: Jakub Kicinski <[email-masked]>
---
 tools/net/ynl/tests/Makefile        |   4 +-
 tools/net/ynl/tests/tc-filter-add.c | 335 -----------------------
 tools/net/ynl/tests/tc.c            | 403 +++++++++++++++++++++++++---
 tools/net/ynl/tests/config          |   6 +
 4 files changed, 372 insertions(+), 376 deletions(-)
 delete mode 100644 tools/net/ynl/tests/tc-filter-add.c

diff --git a/tools/net/ynl/tests/Makefile b/tools/net/ynl/tests/Makefile
index 08d1146d91ce..524092a8de7e 100644
--- a/tools/net/ynl/tests/Makefile
+++ b/tools/net/ynl/tests/Makefile
@@ -22,6 +22,7 @@ TEST_GEN_PROGS := \
 	netdev \
 	ovs \
 	rt-link \
+	tc \
 # end of TEST_GEN_PROGS
 
 BINS := \
@@ -29,13 +30,10 @@ BINS := \
 	ethtool \
 	rt-addr \
 	rt-route \
-	tc \
-	tc-filter-add \
 # end of BINS
 
 CFLAGS_netdev:=$(CFLAGS_netdev) $(CFLAGS_rt-link)
 CFLAGS_ovs:=$(CFLAGS_ovs_datapath)
-CFLAGS_tc-filter-add:=$(CFLAGS_tc)
 
 include $(wildcard *.d)
 
diff --git a/tools/net/ynl/tests/tc-filter-add.c b/tools/net/ynl/tests/tc-filter-add.c
deleted file mode 100644
index 97871e9e9edc..000000000000
--- a/tools/net/ynl/tests/tc-filter-add.c
+++ /dev/null
@@ -1,335 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <arpa/inet.h>
-#include <linux/pkt_sched.h>
-#include <linux/tc_act/tc_vlan.h>
-#include <linux/tc_act/tc_gact.h>
-#include <linux/if_ether.h>
-#include <net/if.h>
-
-#include <ynl.h>
-
-#include "tc-user.h"
-
-#define TC_HANDLE (0xFFFF << 16)
-
-const char *vlan_act_name(struct tc_vlan *p)
-{
-	switch (p->v_action) {
-	case TCA_VLAN_ACT_POP:
-		return "pop";
-	case TCA_VLAN_ACT_PUSH:
-		return "push";
-	case TCA_VLAN_ACT_MODIFY:
-		return "modify";
-	default:
-		break;
-	}
-
-	return "not supported";
-}
-
-const char *gact_act_name(struct tc_gact *p)
-{
-	switch (p->action) {
-	case TC_ACT_SHOT:
-		return "drop";
-	case TC_ACT_OK:
-		return "ok";
-	case TC_ACT_PIPE:
-		return "pipe";
-	default:
-		break;
-	}
-
-	return "not supported";
-}
-
-static void print_vlan(struct tc_act_vlan_attrs *vlan)
-{
-	printf("%s ", vlan_act_name(vlan->parms));
-	if (vlan->_present.push_vlan_id)
-		printf("id %u ", vlan->push_vlan_id);
-	if (vlan->_present.push_vlan_protocol)
-		printf("protocol %#x ", ntohs(vlan->push_vlan_protocol));
-	if (vlan->_present.push_vlan_priority)
-		printf("priority %u ", vlan->push_vlan_priority);
-}
-
-static void print_gact(struct tc_act_gact_attrs *gact)
-{
-	struct tc_gact *p = gact->parms;
-
-	printf("%s ", gact_act_name(p));
-}
-
-static void flower_print(struct tc_flower_attrs *flower, const char *kind)
-{
-	struct tc_act_attrs *a;
-	unsigned int i;
-
-	printf("%s:\n", kind);
-
-	if (flower->_present.key_vlan_id)
-		printf("  vlan_id: %u\n", flower->key_vlan_id);
-	if (flower->_present.key_vlan_prio)
-		printf("  vlan_prio: %u\n", flower->key_vlan_prio);
-	if (flower->_present.key_num_of_vlans)
-		printf("  num_of_vlans: %u\n", flower->key_num_of_vlans);
-
-	for (i = 0; i < flower->_count.act; i++) {
-		a = &flower->act[i];
-		printf("action order: %i %s ", i + 1, a->kind);
-		if (a->options._present.vlan)
-			print_vlan(&a->options.vlan);
-		else if (a->options._present.gact)
-			print_gact(&a->options.gact);
-		printf("\n");
-	}
-	printf("\n");
-}
-
-static void tc_filter_print(struct tc_gettfilter_rsp *f)
-{
-	struct tc_options_msg *opt = &f->options;
-
-	if (opt->_present.flower)
-		flower_print(&opt->flower, f->kind);
-	else if (f->_len.kind)
-		printf("%s pref %u proto: %#x\n", f->kind,
-		       (f->_hdr.tcm_info >> 16),
-			ntohs(TC_H_MIN(f->_hdr.tcm_info)));
-}
-
-static int tc_filter_add(struct ynl_sock *ys, int ifi)
-{
-	struct tc_newtfilter_req *req;
-	struct tc_act_attrs *acts;
-	struct tc_vlan p = {
-		.action = TC_ACT_PIPE,
-		.v_action = TCA_VLAN_ACT_PUSH
-	};
-	__u16 flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE;
-	int ret;
-
-	req = tc_newtfilter_req_alloc();
-	if (!req) {
-		fprintf(stderr, "tc_newtfilter_req_alloc failed\n");
-		return -1;
-	}
-	memset(req, 0, sizeof(*req));
-
-	acts = tc_act_attrs_alloc(3);
-	if (!acts) {
-		fprintf(stderr, "tc_act_attrs_alloc\n");
-		tc_newtfilter_req_free(req);
-		return -1;
-	}
-	memset(acts, 0, sizeof(*acts) * 3);
-
-	req->_hdr.tcm_ifindex = ifi;
-	req->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
-	req->_hdr.tcm_info = TC_H_MAKE(1 << 16, htons(ETH_P_8021Q));
-	req->chain = 0;
-
-	tc_newtfilter_req_set_nlflags(req, flags);
-	tc_newtfilter_req_set_kind(req, "flower");
-	tc_newtfilter_req_set_options_flower_key_vlan_id(req, 100);
-	tc_newtfilter_req_set_options_flower_key_vlan_prio(req, 5);
-	tc_newtfilter_req_set_options_flower_key_num_of_vlans(req, 3);
-
-	__tc_newtfilter_req_set_options_flower_act(req, acts, 3);
-
-	/* Skip action at index 0 because in TC, the action array
-	 * index starts at 1, with each index defining the action's
-	 * order. In contrast, in YNL indexed arrays start at index 0.
-	 */
-	tc_act_attrs_set_kind(&acts[1], "vlan");
-	tc_act_attrs_set_options_vlan_parms(&acts[1], &p, sizeof(p));
-	tc_act_attrs_set_options_vlan_push_vlan_id(&acts[1], 200);
-	tc_act_attrs_set_kind(&acts[2], "vlan");
-	tc_act_attrs_set_options_vlan_parms(&acts[2], &p, sizeof(p));
-	tc_act_attrs_set_options_vlan_push_vlan_id(&acts[2], 300);
-
-	tc_newtfilter_req_set_options_flower_flags(req, 0);
-	tc_newtfilter_req_set_options_flower_key_eth_type(req, htons(0x8100));
-
-	ret = tc_newtfilter(ys, req);
-	if (ret)
-		fprintf(stderr, "tc_newtfilter: %s\n", ys->err.msg);
-
-	tc_newtfilter_req_free(req);
-
-	return ret;
-}
-
-static int tc_filter_show(struct ynl_sock *ys, int ifi)
-{
-	struct tc_gettfilter_req_dump *req;
-	struct tc_gettfilter_list *rsp;
-
-	req = tc_gettfilter_req_dump_alloc();
-	if (!req) {
-		fprintf(stderr, "tc_gettfilter_req_dump_alloc failed\n");
-		return -1;
-	}
-	memset(req, 0, sizeof(*req));
-
-	req->_hdr.tcm_ifindex = ifi;
-	req->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
-	req->_present.chain = 1;
-	req->chain = 0;
-
-	rsp = tc_gettfilter_dump(ys, req);
-	tc_gettfilter_req_dump_free(req);
-	if (!rsp) {
-		fprintf(stderr, "YNL: %s\n", ys->err.msg);
-		return -1;
-	}
-
-	if (ynl_dump_empty(rsp))
-		fprintf(stderr, "Error: no filters reported\n");
-	else
-		ynl_dump_foreach(rsp, flt) tc_filter_print(flt);
-
-	tc_gettfilter_list_free(rsp);
-
-	return 0;
-}
-
-static int tc_filter_del(struct ynl_sock *ys, int ifi)
-{
-	struct tc_deltfilter_req *req;
-	__u16 flags = NLM_F_REQUEST;
-	int ret;
-
-	req = tc_deltfilter_req_alloc();
-	if (!req) {
-		fprintf(stderr, "tc_deltfilter_req_alloc failed\n");
-		return -1;
-	}
-	memset(req, 0, sizeof(*req));
-
-	req->_hdr.tcm_ifindex = ifi;
-	req->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
-	req->_hdr.tcm_info = TC_H_MAKE(1 << 16, htons(ETH_P_8021Q));
-	tc_deltfilter_req_set_nlflags(req, flags);
-
-	ret = tc_deltfilter(ys, req);
-	if (ret)
-		fprintf(stderr, "tc_deltfilter failed: %s\n", ys->err.msg);
-
-	tc_deltfilter_req_free(req);
-
-	return ret;
-}
-
-static int tc_clsact_add(struct ynl_sock *ys, int ifi)
-{
-	struct tc_newqdisc_req *req;
-	__u16 flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE;
-	int ret;
-
-	req = tc_newqdisc_req_alloc();
-	if (!req) {
-		fprintf(stderr, "tc_newqdisc_req_alloc failed\n");
-		return -1;
-	}
-	memset(req, 0, sizeof(*req));
-
-	req->_hdr.tcm_ifindex = ifi;
-	req->_hdr.tcm_parent = TC_H_CLSACT;
-	req->_hdr.tcm_handle = TC_HANDLE;
-	tc_newqdisc_req_set_nlflags(req, flags);
-	tc_newqdisc_req_set_kind(req, "clsact");
-
-	ret = tc_newqdisc(ys, req);
-	if (ret)
-		fprintf(stderr, "tc_newqdisc failed: %s\n", ys->err.msg);
-
-	tc_newqdisc_req_free(req);
-
-	return ret;
-}
-
-static int tc_clsact_del(struct ynl_sock *ys, int ifi)
-{
-	struct tc_delqdisc_req *req;
-	__u16 flags = NLM_F_REQUEST;
-	int ret;
-
-	req = tc_delqdisc_req_alloc();
-	if (!req) {
-		fprintf(stderr, "tc_delqdisc_req_alloc failed\n");
-		return -1;
-	}
-	memset(req, 0, sizeof(*req));
-
-	req->_hdr.tcm_ifindex = ifi;
-	req->_hdr.tcm_parent = TC_H_CLSACT;
-	req->_hdr.tcm_handle = TC_HANDLE;
-	tc_delqdisc_req_set_nlflags(req, flags);
-
-	ret = tc_delqdisc(ys, req);
-	if (ret)
-		fprintf(stderr, "tc_delqdisc failed: %s\n", ys->err.msg);
-
-	tc_delqdisc_req_free(req);
-
-	return ret;
-}
-
-static int tc_filter_config(struct ynl_sock *ys, int ifi)
-{
-	int ret = 0;
-
-	if (tc_filter_add(ys, ifi))
-		return -1;
-
-	ret = tc_filter_show(ys, ifi);
-
-	if (tc_filter_del(ys, ifi))
-		return -1;
-
-	return ret;
-}
-
-int main(int argc, char **argv)
-{
-	struct ynl_error yerr;
-	struct ynl_sock *ys;
-	int ifi, ret = 0;
-
-	if (argc < 2) {
-		fprintf(stderr, "Usage: %s <interface_name>\n", argv[0]);
-		return 1;
-	}
-	ifi = if_nametoindex(argv[1]);
-	if (!ifi) {
-		perror("if_nametoindex");
-		return 1;
-	}
-
-	ys = ynl_sock_create(&ynl_tc_family, &yerr);
-	if (!ys) {
-		fprintf(stderr, "YNL: %s\n", yerr.msg);
-		return 1;
-	}
-
-	if (tc_clsact_add(ys, ifi)) {
-		ret = 2;
-		goto err_destroy;
-	}
-
-	if (tc_filter_config(ys, ifi))
-		ret = 3;
-
-	if (tc_clsact_del(ys, ifi))
-		ret = 4;
-
-err_destroy:
-	ynl_sock_destroy(ys);
-	return ret;
-}
diff --git a/tools/net/ynl/tests/tc.c b/tools/net/ynl/tests/tc.c
index 0bfff0fdd792..77e6fb69c61e 100644
--- a/tools/net/ynl/tests/tc.c
+++ b/tools/net/ynl/tests/tc.c
@@ -1,21 +1,34 @@
 // SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <sched.h>
 #include <stdio.h>
 #include <string.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+#include <linux/pkt_sched.h>
+#include <linux/tc_act/tc_vlan.h>
+#include <linux/tc_act/tc_gact.h>
+#include <linux/if_ether.h>
+#include <net/if.h>
 
 #include <ynl.h>
 
-#include <net/if.h>
+#include <kselftest_harness.h>
 
 #include "tc-user.h"
 
-static void tc_qdisc_print(struct tc_getqdisc_rsp *q)
+#define TC_HANDLE (0xFFFF << 16)
+
+static bool tc_qdisc_print(struct __test_metadata *_metadata,
+			   struct tc_getqdisc_rsp *q)
 {
+	bool was_fq_codel = false;
 	char ifname[IF_NAMESIZE];
 	const char *name;
 
 	name = if_indextoname(q->_hdr.tcm_ifindex, ifname);
-	if (name)
-		printf("%16s: ", name);
+	EXPECT_TRUE((bool)name);
+	ksft_print_msg("%16s: ", name ?: "no-name");
 
 	if (q->_len.kind) {
 		printf("%s  ", q->kind);
@@ -27,6 +40,11 @@ static void tc_qdisc_print(struct tc_getqdisc_rsp *q)
 			fq_codel = &q->options.fq_codel;
 			stats = q->stats2.app.fq_codel;
 
+			EXPECT_EQ(true,
+				  fq_codel->_present.limit &&
+				  fq_codel->_present.target &&
+				  q->stats2.app._len.fq_codel);
+
 			if (fq_codel->_present.limit)
 				printf("limit: %dp ", fq_codel->limit);
 			if (fq_codel->_present.target)
@@ -35,46 +53,355 @@ static void tc_qdisc_print(struct tc_getqdisc_rsp *q)
 			if (q->stats2.app._len.fq_codel)
 				printf("new_flow_cnt: %d ",
 				       stats->qdisc_stats.new_flow_count);
+			was_fq_codel = true;
 		}
 	}
-
 	printf("\n");
+
+	return was_fq_codel;
 }
 
-int main(int argc, char **argv)
+static const char *vlan_act_name(struct tc_vlan *p)
 {
-	struct tc_getqdisc_req_dump *req;
-	struct tc_getqdisc_list *rsp;
-	struct ynl_error yerr;
-	struct ynl_sock *ys;
-
-	ys = ynl_sock_create(&ynl_tc_family, &yerr);
-	if (!ys) {
-		fprintf(stderr, "YNL: %s\n", yerr.msg);
-		return 1;
+	switch (p->v_action) {
+	case TCA_VLAN_ACT_POP:
+		return "pop";
+	case TCA_VLAN_ACT_PUSH:
+		return "push";
+	case TCA_VLAN_ACT_MODIFY:
+		return "modify";
+	default:
+		break;
 	}
 
-	req = tc_getqdisc_req_dump_alloc();
-	if (!req)
-		goto err_destroy;
-
-	rsp = tc_getqdisc_dump(ys, req);
-	tc_getqdisc_req_dump_free(req);
-	if (!rsp)
-		goto err_close;
-
-	if (ynl_dump_empty(rsp))
-		fprintf(stderr, "Error: no addresses reported\n");
-	ynl_dump_foreach(rsp, qdisc)
-		tc_qdisc_print(qdisc);
-	tc_getqdisc_list_free(rsp);
-
-	ynl_sock_destroy(ys);
-	return 0;
-
-err_close:
-	fprintf(stderr, "YNL: %s\n", ys->err.msg);
-err_destroy:
-	ynl_sock_destroy(ys);
-	return 2;
+	return "not supported";
 }
+
+static const char *gact_act_name(struct tc_gact *p)
+{
+	switch (p->action) {
+	case TC_ACT_SHOT:
+		return "drop";
+	case TC_ACT_OK:
+		return "ok";
+	case TC_ACT_PIPE:
+		return "pipe";
+	default:
+		break;
+	}
+
+	return "not supported";
+}
+
+static void print_vlan(struct tc_act_vlan_attrs *vlan)
+{
+	printf("%s ", vlan_act_name(vlan->parms));
+	if (vlan->_present.push_vlan_id)
+		printf("id %u ", vlan->push_vlan_id);
+	if (vlan->_present.push_vlan_protocol)
+		printf("protocol %#x ", ntohs(vlan->push_vlan_protocol));
+	if (vlan->_present.push_vlan_priority)
+		printf("priority %u ", vlan->push_vlan_priority);
+}
+
+static void print_gact(struct tc_act_gact_attrs *gact)
+{
+	struct tc_gact *p = gact->parms;
+
+	printf("%s ", gact_act_name(p));
+}
+
+static void flower_print(struct tc_flower_attrs *flower, const char *kind)
+{
+	struct tc_act_attrs *a;
+	unsigned int i;
+
+	ksft_print_msg("%s:\n", kind);
+
+	if (flower->_present.key_vlan_id)
+		ksft_print_msg("  vlan_id: %u\n", flower->key_vlan_id);
+	if (flower->_present.key_vlan_prio)
+		ksft_print_msg("  vlan_prio: %u\n", flower->key_vlan_prio);
+	if (flower->_present.key_num_of_vlans)
+		ksft_print_msg("  num_of_vlans: %u\n",
+			       flower->key_num_of_vlans);
+
+	for (i = 0; i < flower->_count.act; i++) {
+		a = &flower->act[i];
+		ksft_print_msg("action order: %i %s ", i + 1, a->kind);
+		if (a->options._present.vlan)
+			print_vlan(&a->options.vlan);
+		else if (a->options._present.gact)
+			print_gact(&a->options.gact);
+		printf("\n");
+	}
+}
+
+static void tc_filter_print(struct __test_metadata *_metadata,
+			     struct tc_gettfilter_rsp *f)
+{
+	struct tc_options_msg *opt = &f->options;
+
+	if (opt->_present.flower) {
+		EXPECT_TRUE((bool)f->_len.kind);
+		flower_print(&opt->flower, f->kind);
+	} else if (f->_len.kind) {
+		ksft_print_msg("%s pref %u proto: %#x\n", f->kind,
+			       (f->_hdr.tcm_info >> 16),
+			       ntohs(TC_H_MIN(f->_hdr.tcm_info)));
+	}
+}
+
+static int tc_clsact_add(struct ynl_sock *ys, int ifi)
+{
+	struct tc_newqdisc_req *req;
+	int ret;
+
+	req = tc_newqdisc_req_alloc();
+	if (!req)
+		return -1;
+	memset(req, 0, sizeof(*req));
+
+	req->_hdr.tcm_ifindex = ifi;
+	req->_hdr.tcm_parent = TC_H_CLSACT;
+	req->_hdr.tcm_handle = TC_HANDLE;
+	tc_newqdisc_req_set_nlflags(req,
+				    NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE);
+	tc_newqdisc_req_set_kind(req, "clsact");
+
+	ret = tc_newqdisc(ys, req);
+	tc_newqdisc_req_free(req);
+
+	return ret;
+}
+
+static int tc_clsact_del(struct ynl_sock *ys, int ifi)
+{
+	struct tc_delqdisc_req *req;
+	int ret;
+
+	req = tc_delqdisc_req_alloc();
+	if (!req)
+		return -1;
+	memset(req, 0, sizeof(*req));
+
+	req->_hdr.tcm_ifindex = ifi;
+	req->_hdr.tcm_parent = TC_H_CLSACT;
+	req->_hdr.tcm_handle = TC_HANDLE;
+	tc_delqdisc_req_set_nlflags(req, NLM_F_REQUEST);
+
+	ret = tc_delqdisc(ys, req);
+	tc_delqdisc_req_free(req);
+
+	return ret;
+}
+
+static int tc_filter_add(struct ynl_sock *ys, int ifi)
+{
+	struct tc_newtfilter_req *req;
+	struct tc_act_attrs *acts;
+	struct tc_vlan p = {
+		.action = TC_ACT_PIPE,
+		.v_action = TCA_VLAN_ACT_PUSH
+	};
+	int ret;
+
+	req = tc_newtfilter_req_alloc();
+	if (!req)
+		return -1;
+	memset(req, 0, sizeof(*req));
+
+	acts = tc_act_attrs_alloc(3);
+	if (!acts) {
+		tc_newtfilter_req_free(req);
+		return -1;
+	}
+	memset(acts, 0, sizeof(*acts) * 3);
+
+	req->_hdr.tcm_ifindex = ifi;
+	req->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
+	req->_hdr.tcm_info = TC_H_MAKE(1 << 16, htons(ETH_P_8021Q));
+	req->chain = 0;
+
+	tc_newtfilter_req_set_nlflags(req, NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE);
+	tc_newtfilter_req_set_kind(req, "flower");
+	tc_newtfilter_req_set_options_flower_key_vlan_id(req, 100);
+	tc_newtfilter_req_set_options_flower_key_vlan_prio(req, 5);
+	tc_newtfilter_req_set_options_flower_key_num_of_vlans(req, 3);
+
+	__tc_newtfilter_req_set_options_flower_act(req, acts, 3);
+
+	/* Skip action at index 0 because in TC, the action array
+	 * index starts at 1, with each index defining the action's
+	 * order. In contrast, in YNL indexed arrays start at index 0.
+	 */
+	tc_act_attrs_set_kind(&acts[1], "vlan");
+	tc_act_attrs_set_options_vlan_parms(&acts[1], &p, sizeof(p));
+	tc_act_attrs_set_options_vlan_push_vlan_id(&acts[1], 200);
+	tc_act_attrs_set_kind(&acts[2], "vlan");
+	tc_act_attrs_set_options_vlan_parms(&acts[2], &p, sizeof(p));
+	tc_act_attrs_set_options_vlan_push_vlan_id(&acts[2], 300);
+
+	tc_newtfilter_req_set_options_flower_flags(req, 0);
+	tc_newtfilter_req_set_options_flower_key_eth_type(req, htons(0x8100));
+
+	ret = tc_newtfilter(ys, req);
+	tc_newtfilter_req_free(req);
+
+	return ret;
+}
+
+static int tc_filter_del(struct ynl_sock *ys, int ifi)
+{
+	struct tc_deltfilter_req *req;
+	int ret;
+
+	req = tc_deltfilter_req_alloc();
+	if (!req)
+		return -1;
+	memset(req, 0, sizeof(*req));
+
+	req->_hdr.tcm_ifindex = ifi;
+	req->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
+	req->_hdr.tcm_info = TC_H_MAKE(1 << 16, htons(ETH_P_8021Q));
+	tc_deltfilter_req_set_nlflags(req, NLM_F_REQUEST);
+
+	ret = tc_deltfilter(ys, req);
+	tc_deltfilter_req_free(req);
+
+	return ret;
+}
+
+FIXTURE(tc)
+{
+	struct ynl_sock *ys;
+	int ifindex;
+};
+
+FIXTURE_SETUP(tc)
+{
+	struct ynl_error yerr;
+	int ret;
+
+	ret = unshare(CLONE_NEWNET);
+	ASSERT_EQ(0, ret);
+
+	self->ifindex = 1; /* loopback */
+
+	self->ys = ynl_sock_create(&ynl_tc_family, &yerr);
+	ASSERT_NE(NULL, self->ys) {
+		TH_LOG("failed to create tc socket: %s", yerr.msg);
+	}
+}
+
+FIXTURE_TEARDOWN(tc)
+{
+	ynl_sock_destroy(self->ys);
+}
+
+TEST_F(tc, qdisc)
+{
+	struct tc_getqdisc_req_dump *dreq;
+	struct tc_newqdisc_req *add_req;
+	struct tc_delqdisc_req *del_req;
+	struct tc_getqdisc_list *rsp;
+	bool found = false;
+	int ret;
+
+	add_req = tc_newqdisc_req_alloc();
+	ASSERT_NE(NULL, add_req);
+	memset(add_req, 0, sizeof(*add_req));
+
+	add_req->_hdr.tcm_ifindex = self->ifindex;
+	add_req->_hdr.tcm_parent = TC_H_ROOT;
+	tc_newqdisc_req_set_nlflags(add_req,
+				    NLM_F_REQUEST | NLM_F_CREATE);
+	tc_newqdisc_req_set_kind(add_req, "fq_codel");
+
+	ret = tc_newqdisc(self->ys, add_req);
+	tc_newqdisc_req_free(add_req);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("qdisc add failed: %s", self->ys->err.msg);
+	}
+
+	dreq = tc_getqdisc_req_dump_alloc();
+	rsp = tc_getqdisc_dump(self->ys, dreq);
+	tc_getqdisc_req_dump_free(dreq);
+	ASSERT_NE(NULL, rsp) {
+		TH_LOG("dump failed: %s", self->ys->err.msg);
+	}
+	ASSERT_FALSE(ynl_dump_empty(rsp));
+
+	ynl_dump_foreach(rsp, qdisc) {
+		found |= tc_qdisc_print(_metadata, qdisc);
+	}
+	tc_getqdisc_list_free(rsp);
+	EXPECT_TRUE(found);
+
+	del_req = tc_delqdisc_req_alloc();
+	ASSERT_NE(NULL, del_req);
+	memset(del_req, 0, sizeof(*del_req));
+
+	del_req->_hdr.tcm_ifindex = self->ifindex;
+	del_req->_hdr.tcm_parent = TC_H_ROOT;
+	tc_delqdisc_req_set_nlflags(del_req, NLM_F_REQUEST);
+
+	ret = tc_delqdisc(self->ys, del_req);
+	tc_delqdisc_req_free(del_req);
+	EXPECT_EQ(0, ret) {
+		TH_LOG("qdisc del failed: %s", self->ys->err.msg);
+	}
+}
+
+TEST_F(tc, flower)
+{
+	struct tc_gettfilter_req_dump *dreq;
+	struct tc_gettfilter_list *rsp;
+	bool found = false;
+	int ret;
+
+	ret = tc_clsact_add(self->ys, self->ifindex);
+	if (ret)
+		SKIP(return, "clsact not supported: %s", self->ys->err.msg);
+
+	ret = tc_filter_add(self->ys, self->ifindex);
+	ASSERT_EQ(0, ret) {
+		TH_LOG("filter add failed: %s", self->ys->err.msg);
+	}
+
+	dreq = tc_gettfilter_req_dump_alloc();
+	memset(dreq, 0, sizeof(*dreq));
+	dreq->_hdr.tcm_ifindex = self->ifindex;
+	dreq->_hdr.tcm_parent = TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS);
+	dreq->_present.chain = 1;
+	dreq->chain = 0;
+
+	rsp = tc_gettfilter_dump(self->ys, dreq);
+	tc_gettfilter_req_dump_free(dreq);
+	ASSERT_NE(NULL, rsp) {
+		TH_LOG("filter dump failed: %s", self->ys->err.msg);
+	}
+
+	ynl_dump_foreach(rsp, flt) {
+		tc_filter_print(_metadata, flt);
+		if (flt->options._present.flower) {
+			EXPECT_EQ(100, flt->options.flower.key_vlan_id);
+			EXPECT_EQ(5, flt->options.flower.key_vlan_prio);
+			found = true;
+		}
+	}
+	tc_gettfilter_list_free(rsp);
+	EXPECT_TRUE(found);
+
+	ret = tc_filter_del(self->ys, self->ifindex);
+	EXPECT_EQ(0, ret) {
+		TH_LOG("filter del failed: %s", self->ys->err.msg);
+	}
+
+	ret = tc_clsact_del(self->ys, self->ifindex);
+	EXPECT_EQ(0, ret) {
+		TH_LOG("clsact del failed: %s", self->ys->err.msg);
+	}
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/net/ynl/tests/config b/tools/net/ynl/tests/config
index b4c58d86a6c2..75c0fe72391f 100644
--- a/tools/net/ynl/tests/config
+++ b/tools/net/ynl/tests/config
@@ -1,7 +1,13 @@
 CONFIG_DUMMY=m
 CONFIG_INET_DIAG=y
 CONFIG_IPV6=y
+CONFIG_NET_ACT_VLAN=m
+CONFIG_NET_CLS_ACT=y
+CONFIG_NET_CLS_FLOWER=m
+CONFIG_NET_SCH_FQ_CODEL=m
+CONFIG_NET_SCH_INGRESS=m
 CONFIG_NET_NS=y
+CONFIG_NET_SCHED=y
 CONFIG_NETDEVSIM=m
 CONFIG_NETKIT=y
 CONFIG_OPENVSWITCH=m
-- 
2.53.0