| // SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) |
| /* Copyright (C) 2018 Netronome Systems, Inc. */ |
| |
| #include <linux/bitfield.h> |
| #include <net/pkt_cls.h> |
| |
| #include "../nfpcore/nfp_cpp.h" |
| #include "../nfp_app.h" |
| #include "../nfp_net_repr.h" |
| #include "main.h" |
| |
| struct nfp_abm_u32_match { |
| u32 handle; |
| u32 band; |
| u8 mask; |
| u8 val; |
| struct list_head list; |
| }; |
| |
| static bool |
| nfp_abm_u32_check_knode(struct nfp_abm *abm, struct tc_cls_u32_knode *knode, |
| __be16 proto, struct netlink_ext_ack *extack) |
| { |
| struct tc_u32_key *k; |
| unsigned int tos_off; |
| |
| if (knode->exts && tcf_exts_has_actions(knode->exts)) { |
| NL_SET_ERR_MSG_MOD(extack, "action offload not supported"); |
| return false; |
| } |
| if (knode->link_handle) { |
| NL_SET_ERR_MSG_MOD(extack, "linking not supported"); |
| return false; |
| } |
| if (knode->sel->flags != TC_U32_TERMINAL) { |
| NL_SET_ERR_MSG_MOD(extack, |
| "flags must be equal to TC_U32_TERMINAL"); |
| return false; |
| } |
| if (knode->sel->off || knode->sel->offshift || knode->sel->offmask || |
| knode->sel->offoff || knode->fshift) { |
| NL_SET_ERR_MSG_MOD(extack, "variable offsetting not supported"); |
| return false; |
| } |
| if (knode->sel->hoff || knode->sel->hmask) { |
| NL_SET_ERR_MSG_MOD(extack, "hashing not supported"); |
| return false; |
| } |
| if (knode->val || knode->mask) { |
| NL_SET_ERR_MSG_MOD(extack, "matching on mark not supported"); |
| return false; |
| } |
| if (knode->res && knode->res->class) { |
| NL_SET_ERR_MSG_MOD(extack, "setting non-0 class not supported"); |
| return false; |
| } |
| if (knode->res && knode->res->classid >= abm->num_bands) { |
| NL_SET_ERR_MSG_MOD(extack, |
| "classid higher than number of bands"); |
| return false; |
| } |
| if (knode->sel->nkeys != 1) { |
| NL_SET_ERR_MSG_MOD(extack, "exactly one key required"); |
| return false; |
| } |
| |
| switch (proto) { |
| case htons(ETH_P_IP): |
| tos_off = 16; |
| break; |
| case htons(ETH_P_IPV6): |
| tos_off = 20; |
| break; |
| default: |
| NL_SET_ERR_MSG_MOD(extack, "only IP and IPv6 supported as filter protocol"); |
| return false; |
| } |
| |
| k = &knode->sel->keys[0]; |
| if (k->offmask) { |
| NL_SET_ERR_MSG_MOD(extack, "offset mask - variable offsetting not supported"); |
| return false; |
| } |
| if (k->off) { |
| NL_SET_ERR_MSG_MOD(extack, "only DSCP fields can be matched"); |
| return false; |
| } |
| if (k->val & ~k->mask) { |
| NL_SET_ERR_MSG_MOD(extack, "mask does not cover the key"); |
| return false; |
| } |
| if (be32_to_cpu(k->mask) >> tos_off & ~abm->dscp_mask) { |
| NL_SET_ERR_MSG_MOD(extack, "only high DSCP class selector bits can be used"); |
| nfp_err(abm->app->cpp, |
| "u32 offload: requested mask %x FW can support only %x\n", |
| be32_to_cpu(k->mask) >> tos_off, abm->dscp_mask); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /* This filter list -> map conversion is O(n * m), we expect single digit or |
| * low double digit number of prios and likewise for the filters. Also u32 |
| * doesn't report stats, so it's really only setup time cost. |
| */ |
| static unsigned int |
| nfp_abm_find_band_for_prio(struct nfp_abm_link *alink, unsigned int prio) |
| { |
| struct nfp_abm_u32_match *iter; |
| |
| list_for_each_entry(iter, &alink->dscp_map, list) |
| if ((prio & iter->mask) == iter->val) |
| return iter->band; |
| |
| return alink->def_band; |
| } |
| |
| static int nfp_abm_update_band_map(struct nfp_abm_link *alink) |
| { |
| unsigned int i, bits_per_prio, prios_per_word, base_shift; |
| struct nfp_abm *abm = alink->abm; |
| u32 field_mask; |
| |
| alink->has_prio = !list_empty(&alink->dscp_map); |
| |
| bits_per_prio = roundup_pow_of_two(order_base_2(abm->num_bands)); |
| field_mask = (1 << bits_per_prio) - 1; |
| prios_per_word = sizeof(u32) * BITS_PER_BYTE / bits_per_prio; |
| |
| /* FW mask applies from top bits */ |
| base_shift = 8 - order_base_2(abm->num_prios); |
| |
| for (i = 0; i < abm->num_prios; i++) { |
| unsigned int offset; |
| u32 *word; |
| u8 band; |
| |
| word = &alink->prio_map[i / prios_per_word]; |
| offset = (i % prios_per_word) * bits_per_prio; |
| |
| band = nfp_abm_find_band_for_prio(alink, i << base_shift); |
| |
| *word &= ~(field_mask << offset); |
| *word |= band << offset; |
| } |
| |
| /* Qdisc offload status may change if has_prio changed */ |
| nfp_abm_qdisc_offload_update(alink); |
| |
| return nfp_abm_ctrl_prio_map_update(alink, alink->prio_map); |
| } |
| |
| static void |
| nfp_abm_u32_knode_delete(struct nfp_abm_link *alink, |
| struct tc_cls_u32_knode *knode) |
| { |
| struct nfp_abm_u32_match *iter; |
| |
| list_for_each_entry(iter, &alink->dscp_map, list) |
| if (iter->handle == knode->handle) { |
| list_del(&iter->list); |
| kfree(iter); |
| nfp_abm_update_band_map(alink); |
| return; |
| } |
| } |
| |
| static int |
| nfp_abm_u32_knode_replace(struct nfp_abm_link *alink, |
| struct tc_cls_u32_knode *knode, |
| __be16 proto, struct netlink_ext_ack *extack) |
| { |
| struct nfp_abm_u32_match *match = NULL, *iter; |
| unsigned int tos_off; |
| u8 mask, val; |
| int err; |
| |
| if (!nfp_abm_u32_check_knode(alink->abm, knode, proto, extack)) |
| goto err_delete; |
| |
| tos_off = proto == htons(ETH_P_IP) ? 16 : 20; |
| |
| /* Extract the DSCP Class Selector bits */ |
| val = be32_to_cpu(knode->sel->keys[0].val) >> tos_off & 0xff; |
| mask = be32_to_cpu(knode->sel->keys[0].mask) >> tos_off & 0xff; |
| |
| /* Check if there is no conflicting mapping and find match by handle */ |
| list_for_each_entry(iter, &alink->dscp_map, list) { |
| u32 cmask; |
| |
| if (iter->handle == knode->handle) { |
| match = iter; |
| continue; |
| } |
| |
| cmask = iter->mask & mask; |
| if ((iter->val & cmask) == (val & cmask) && |
| iter->band != knode->res->classid) { |
| NL_SET_ERR_MSG_MOD(extack, "conflict with already offloaded filter"); |
| goto err_delete; |
| } |
| } |
| |
| if (!match) { |
| match = kzalloc(sizeof(*match), GFP_KERNEL); |
| if (!match) |
| return -ENOMEM; |
| list_add(&match->list, &alink->dscp_map); |
| } |
| match->handle = knode->handle; |
| match->band = knode->res->classid; |
| match->mask = mask; |
| match->val = val; |
| |
| err = nfp_abm_update_band_map(alink); |
| if (err) |
| goto err_delete; |
| |
| return 0; |
| |
| err_delete: |
| nfp_abm_u32_knode_delete(alink, knode); |
| return -EOPNOTSUPP; |
| } |
| |
| static int nfp_abm_setup_tc_block_cb(enum tc_setup_type type, |
| void *type_data, void *cb_priv) |
| { |
| struct tc_cls_u32_offload *cls_u32 = type_data; |
| struct nfp_repr *repr = cb_priv; |
| struct nfp_abm_link *alink; |
| |
| alink = repr->app_priv; |
| |
| if (type != TC_SETUP_CLSU32) { |
| NL_SET_ERR_MSG_MOD(cls_u32->common.extack, |
| "only offload of u32 classifier supported"); |
| return -EOPNOTSUPP; |
| } |
| if (!tc_cls_can_offload_and_chain0(repr->netdev, &cls_u32->common)) |
| return -EOPNOTSUPP; |
| |
| if (cls_u32->common.protocol != htons(ETH_P_IP) && |
| cls_u32->common.protocol != htons(ETH_P_IPV6)) { |
| NL_SET_ERR_MSG_MOD(cls_u32->common.extack, |
| "only IP and IPv6 supported as filter protocol"); |
| return -EOPNOTSUPP; |
| } |
| |
| switch (cls_u32->command) { |
| case TC_CLSU32_NEW_KNODE: |
| case TC_CLSU32_REPLACE_KNODE: |
| return nfp_abm_u32_knode_replace(alink, &cls_u32->knode, |
| cls_u32->common.protocol, |
| cls_u32->common.extack); |
| case TC_CLSU32_DELETE_KNODE: |
| nfp_abm_u32_knode_delete(alink, &cls_u32->knode); |
| return 0; |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |
| |
| int nfp_abm_setup_cls_block(struct net_device *netdev, struct nfp_repr *repr, |
| struct tc_block_offload *f) |
| { |
| if (f->binder_type != TCF_BLOCK_BINDER_TYPE_CLSACT_EGRESS) |
| return -EOPNOTSUPP; |
| |
| switch (f->command) { |
| case TC_BLOCK_BIND: |
| return tcf_block_cb_register(f->block, |
| nfp_abm_setup_tc_block_cb, |
| repr, repr, f->extack); |
| case TC_BLOCK_UNBIND: |
| tcf_block_cb_unregister(f->block, nfp_abm_setup_tc_block_cb, |
| repr); |
| return 0; |
| default: |
| return -EOPNOTSUPP; |
| } |
| } |