| // 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 <kselftest_harness.h> | |
| #include "tc-user.h" | |
| #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); | |
| EXPECT_TRUE((bool)name); | |
| ksft_print_msg("%16s: ", name ?: "no-name"); | |
| if (q->_len.kind) { | |
| printf("%s ", q->kind); | |
| if (q->options._present.fq_codel) { | |
| struct tc_fq_codel_attrs *fq_codel; | |
| struct tc_fq_codel_xstats *stats; | |
| 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) | |
| printf("target: %dms ", | |
| (fq_codel->target + 500) / 1000); | |
| 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; | |
| } | |
| static 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"; | |
| } | |
| 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(); | |
| ASSERT_NE(NULL, dreq); | |
| 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(); | |
| ASSERT_NE(NULL, dreq); | |
| 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 |