| // SPDX-License-Identifier: GPL-2.0-only |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/netfilter.h> |
| #include <linux/rhashtable.h> |
| #include <linux/netdevice.h> |
| #include <net/flow_offload.h> |
| #include <net/netfilter/nf_flow_table.h> |
| |
| struct flow_offload_xdp_ft { |
| struct list_head head; |
| struct nf_flowtable *ft; |
| struct rcu_head rcuhead; |
| }; |
| |
| struct flow_offload_xdp { |
| struct hlist_node hnode; |
| unsigned long net_device_addr; |
| struct list_head head; |
| }; |
| |
| #define NF_XDP_HT_BITS 4 |
| static DEFINE_HASHTABLE(nf_xdp_hashtable, NF_XDP_HT_BITS); |
| static DEFINE_MUTEX(nf_xdp_hashtable_lock); |
| |
| /* caller must hold rcu read lock */ |
| struct nf_flowtable *nf_flowtable_by_dev(const struct net_device *dev) |
| { |
| unsigned long key = (unsigned long)dev; |
| struct flow_offload_xdp *iter; |
| |
| hash_for_each_possible_rcu(nf_xdp_hashtable, iter, hnode, key) { |
| if (key == iter->net_device_addr) { |
| struct flow_offload_xdp_ft *ft_elem; |
| |
| /* The user is supposed to insert a given net_device |
| * just into a single nf_flowtable so we always return |
| * the first element here. |
| */ |
| ft_elem = list_first_or_null_rcu(&iter->head, |
| struct flow_offload_xdp_ft, |
| head); |
| return ft_elem ? ft_elem->ft : NULL; |
| } |
| } |
| |
| return NULL; |
| } |
| |
| static int nf_flowtable_by_dev_insert(struct nf_flowtable *ft, |
| const struct net_device *dev) |
| { |
| struct flow_offload_xdp *iter, *elem = NULL; |
| unsigned long key = (unsigned long)dev; |
| struct flow_offload_xdp_ft *ft_elem; |
| |
| ft_elem = kzalloc(sizeof(*ft_elem), GFP_KERNEL_ACCOUNT); |
| if (!ft_elem) |
| return -ENOMEM; |
| |
| ft_elem->ft = ft; |
| |
| mutex_lock(&nf_xdp_hashtable_lock); |
| |
| hash_for_each_possible(nf_xdp_hashtable, iter, hnode, key) { |
| if (key == iter->net_device_addr) { |
| elem = iter; |
| break; |
| } |
| } |
| |
| if (!elem) { |
| elem = kzalloc(sizeof(*elem), GFP_KERNEL_ACCOUNT); |
| if (!elem) |
| goto err_unlock; |
| |
| elem->net_device_addr = key; |
| INIT_LIST_HEAD(&elem->head); |
| hash_add_rcu(nf_xdp_hashtable, &elem->hnode, key); |
| } |
| list_add_tail_rcu(&ft_elem->head, &elem->head); |
| |
| mutex_unlock(&nf_xdp_hashtable_lock); |
| |
| return 0; |
| |
| err_unlock: |
| mutex_unlock(&nf_xdp_hashtable_lock); |
| kfree(ft_elem); |
| |
| return -ENOMEM; |
| } |
| |
| static void nf_flowtable_by_dev_remove(struct nf_flowtable *ft, |
| const struct net_device *dev) |
| { |
| struct flow_offload_xdp *iter, *elem = NULL; |
| unsigned long key = (unsigned long)dev; |
| |
| mutex_lock(&nf_xdp_hashtable_lock); |
| |
| hash_for_each_possible(nf_xdp_hashtable, iter, hnode, key) { |
| if (key == iter->net_device_addr) { |
| elem = iter; |
| break; |
| } |
| } |
| |
| if (elem) { |
| struct flow_offload_xdp_ft *ft_elem, *ft_next; |
| |
| list_for_each_entry_safe(ft_elem, ft_next, &elem->head, head) { |
| if (ft_elem->ft == ft) { |
| list_del_rcu(&ft_elem->head); |
| kfree_rcu(ft_elem, rcuhead); |
| } |
| } |
| |
| if (list_empty(&elem->head)) |
| hash_del_rcu(&elem->hnode); |
| else |
| elem = NULL; |
| } |
| |
| mutex_unlock(&nf_xdp_hashtable_lock); |
| |
| if (elem) { |
| synchronize_rcu(); |
| kfree(elem); |
| } |
| } |
| |
| int nf_flow_offload_xdp_setup(struct nf_flowtable *flowtable, |
| struct net_device *dev, |
| enum flow_block_command cmd) |
| { |
| switch (cmd) { |
| case FLOW_BLOCK_BIND: |
| return nf_flowtable_by_dev_insert(flowtable, dev); |
| case FLOW_BLOCK_UNBIND: |
| nf_flowtable_by_dev_remove(flowtable, dev); |
| return 0; |
| } |
| |
| WARN_ON_ONCE(1); |
| return 0; |
| } |