// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2021 Red Hat GmbH
 *
 * Author: Florian Westphal <fw@strlen.de>
 */

#include <linux/bpf.h>
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/skbuff.h>
#include <linux/errno.h>
#include <linux/netlink.h>
#include <linux/slab.h>

#include <linux/netfilter.h>

#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_hook.h>

#include <net/netfilter/nf_tables.h>
#include <net/sock.h>

static const struct nla_policy nfnl_hook_nla_policy[NFNLA_HOOK_MAX + 1] = {
	[NFNLA_HOOK_HOOKNUM]	= NLA_POLICY_MAX(NLA_BE32, 255),
	[NFNLA_HOOK_PRIORITY]	= { .type = NLA_U32 },
	[NFNLA_HOOK_DEV]	= { .type = NLA_STRING,
				    .len = IFNAMSIZ - 1 },
	[NFNLA_HOOK_FUNCTION_NAME] = { .type = NLA_NUL_STRING,
				       .len = KSYM_NAME_LEN, },
	[NFNLA_HOOK_MODULE_NAME] = { .type = NLA_NUL_STRING,
				     .len = MODULE_NAME_LEN, },
	[NFNLA_HOOK_CHAIN_INFO] = { .type = NLA_NESTED, },
};

static int nf_netlink_dump_start_rcu(struct sock *nlsk, struct sk_buff *skb,
				     const struct nlmsghdr *nlh,
				     struct netlink_dump_control *c)
{
	int err;

	if (!try_module_get(THIS_MODULE))
		return -EINVAL;

	rcu_read_unlock();
	err = netlink_dump_start(nlsk, skb, nlh, c);
	rcu_read_lock();
	module_put(THIS_MODULE);

	return err;
}

struct nfnl_dump_hook_data {
	char devname[IFNAMSIZ];
	unsigned long headv;
	u8 hook;
};

static struct nlattr *nfnl_start_info_type(struct sk_buff *nlskb, enum nfnl_hook_chaintype t)
{
	struct nlattr *nest = nla_nest_start(nlskb, NFNLA_HOOK_CHAIN_INFO);
	int ret;

	if (!nest)
		return NULL;

	ret = nla_put_be32(nlskb, NFNLA_HOOK_INFO_TYPE, htonl(t));
	if (ret == 0)
		return nest;

	nla_nest_cancel(nlskb, nest);
	return NULL;
}

static int nfnl_hook_put_bpf_prog_info(struct sk_buff *nlskb,
				       const struct nfnl_dump_hook_data *ctx,
				       unsigned int seq,
				       const struct bpf_prog *prog)
{
	struct nlattr *nest, *nest2;
	int ret;

	if (!IS_ENABLED(CONFIG_NETFILTER_BPF_LINK))
		return 0;

	if (WARN_ON_ONCE(!prog))
		return 0;

	nest = nfnl_start_info_type(nlskb, NFNL_HOOK_TYPE_BPF);
	if (!nest)
		return -EMSGSIZE;

	nest2 = nla_nest_start(nlskb, NFNLA_HOOK_INFO_DESC);
	if (!nest2)
		goto cancel_nest;

	ret = nla_put_be32(nlskb, NFNLA_HOOK_BPF_ID, htonl(prog->aux->id));
	if (ret)
		goto cancel_nest;

	nla_nest_end(nlskb, nest2);
	nla_nest_end(nlskb, nest);
	return 0;

cancel_nest:
	nla_nest_cancel(nlskb, nest);
	return -EMSGSIZE;
}

static int nfnl_hook_put_nft_info_desc(struct sk_buff *nlskb, const char *tname,
				       const char *name, u8 family)
{
	struct nlattr *nest;

	nest = nla_nest_start(nlskb, NFNLA_HOOK_INFO_DESC);
	if (!nest ||
	    nla_put_string(nlskb, NFNLA_CHAIN_TABLE, tname) ||
	    nla_put_string(nlskb, NFNLA_CHAIN_NAME, name) ||
	    nla_put_u8(nlskb, NFNLA_CHAIN_FAMILY, family)) {
		nla_nest_cancel(nlskb, nest);
		return -EMSGSIZE;
	}
	nla_nest_end(nlskb, nest);
	return 0;
}

static int nfnl_hook_put_nft_chain_info(struct sk_buff *nlskb,
					const struct nfnl_dump_hook_data *ctx,
					unsigned int seq,
					struct nft_chain *chain)
{
	struct net *net = sock_net(nlskb->sk);
	struct nlattr *nest;
	int ret = 0;

	if (WARN_ON_ONCE(!chain))
		return 0;

	if (!nft_is_active(net, chain))
		return 0;

	nest = nfnl_start_info_type(nlskb, NFNL_HOOK_TYPE_NFTABLES);
	if (!nest)
		return -EMSGSIZE;

	ret = nfnl_hook_put_nft_info_desc(nlskb, chain->table->name,
					  chain->name, chain->table->family);
	if (ret) {
		nla_nest_cancel(nlskb, nest);
		return ret;
	}

	nla_nest_end(nlskb, nest);
	return 0;
}

static int nfnl_hook_put_nft_ft_info(struct sk_buff *nlskb,
				     const struct nfnl_dump_hook_data *ctx,
				     unsigned int seq,
				     struct nf_flowtable *nf_ft)
{
	struct nft_flowtable *ft =
		container_of(nf_ft, struct nft_flowtable, data);
	struct net *net = sock_net(nlskb->sk);
	struct nlattr *nest;
	int ret = 0;

	if (WARN_ON_ONCE(!nf_ft))
		return 0;

	if (!nft_is_active(net, ft))
		return 0;

	nest = nfnl_start_info_type(nlskb, NFNL_HOOK_TYPE_NFT_FLOWTABLE);
	if (!nest)
		return -EMSGSIZE;

	ret = nfnl_hook_put_nft_info_desc(nlskb, ft->table->name,
					  ft->name, ft->table->family);
	if (ret) {
		nla_nest_cancel(nlskb, nest);
		return ret;
	}

	nla_nest_end(nlskb, nest);
	return 0;
}

static int nfnl_hook_dump_one(struct sk_buff *nlskb,
			      const struct nfnl_dump_hook_data *ctx,
			      const struct nf_hook_ops *ops,
			      int family, unsigned int seq)
{
	u16 event = nfnl_msg_type(NFNL_SUBSYS_HOOK, NFNL_MSG_HOOK_GET);
	unsigned int portid = NETLINK_CB(nlskb).portid;
	struct nlmsghdr *nlh;
	int ret = -EMSGSIZE;
	u32 hooknum;
#ifdef CONFIG_KALLSYMS
	char sym[KSYM_SYMBOL_LEN];
	char *module_name;
#endif
	nlh = nfnl_msg_put(nlskb, portid, seq, event,
			   NLM_F_MULTI, family, NFNETLINK_V0, 0);
	if (!nlh)
		goto nla_put_failure;

#ifdef CONFIG_KALLSYMS
	ret = snprintf(sym, sizeof(sym), "%ps", ops->hook);
	if (ret >= sizeof(sym)) {
		ret = -EINVAL;
		goto nla_put_failure;
	}

	module_name = strstr(sym, " [");
	if (module_name) {
		char *end;

		*module_name = '\0';
		module_name += 2;
		end = strchr(module_name, ']');
		if (end) {
			*end = 0;

			ret = nla_put_string(nlskb, NFNLA_HOOK_MODULE_NAME, module_name);
			if (ret)
				goto nla_put_failure;
		}
	}

	ret = nla_put_string(nlskb, NFNLA_HOOK_FUNCTION_NAME, sym);
	if (ret)
		goto nla_put_failure;
#endif

	if (ops->pf == NFPROTO_INET && ops->hooknum == NF_INET_INGRESS)
		hooknum = NF_NETDEV_INGRESS;
	else
		hooknum = ops->hooknum;

	ret = nla_put_be32(nlskb, NFNLA_HOOK_HOOKNUM, htonl(hooknum));
	if (ret)
		goto nla_put_failure;

	ret = nla_put_be32(nlskb, NFNLA_HOOK_PRIORITY, htonl(ops->priority));
	if (ret)
		goto nla_put_failure;

	switch (ops->hook_ops_type) {
	case NF_HOOK_OP_NF_TABLES:
		ret = nfnl_hook_put_nft_chain_info(nlskb, ctx, seq, ops->priv);
		break;
	case NF_HOOK_OP_BPF:
		ret = nfnl_hook_put_bpf_prog_info(nlskb, ctx, seq, ops->priv);
		break;
	case NF_HOOK_OP_NFT_FT:
		ret = nfnl_hook_put_nft_ft_info(nlskb, ctx, seq, ops->priv);
		break;
	case NF_HOOK_OP_UNDEFINED:
		break;
	default:
		WARN_ON_ONCE(1);
		break;
	}

	if (ret)
		goto nla_put_failure;

	nlmsg_end(nlskb, nlh);
	return 0;
nla_put_failure:
	nlmsg_trim(nlskb, nlh);
	return ret;
}

static const struct nf_hook_entries *
nfnl_hook_entries_head(u8 pf, unsigned int hook, struct net *net, const char *dev)
{
	const struct nf_hook_entries *hook_head = NULL;
#if defined(CONFIG_NETFILTER_INGRESS) || defined(CONFIG_NETFILTER_EGRESS)
	struct net_device *netdev;
#endif

	switch (pf) {
	case NFPROTO_IPV4:
		if (hook >= ARRAY_SIZE(net->nf.hooks_ipv4))
			return ERR_PTR(-EINVAL);
		hook_head = rcu_dereference(net->nf.hooks_ipv4[hook]);
		break;
	case NFPROTO_IPV6:
		if (hook >= ARRAY_SIZE(net->nf.hooks_ipv6))
			return ERR_PTR(-EINVAL);
		hook_head = rcu_dereference(net->nf.hooks_ipv6[hook]);
		break;
	case NFPROTO_ARP:
#ifdef CONFIG_NETFILTER_FAMILY_ARP
		if (hook >= ARRAY_SIZE(net->nf.hooks_arp))
			return ERR_PTR(-EINVAL);
		hook_head = rcu_dereference(net->nf.hooks_arp[hook]);
#endif
		break;
	case NFPROTO_BRIDGE:
#ifdef CONFIG_NETFILTER_FAMILY_BRIDGE
		if (hook >= ARRAY_SIZE(net->nf.hooks_bridge))
			return ERR_PTR(-EINVAL);
		hook_head = rcu_dereference(net->nf.hooks_bridge[hook]);
#endif
		break;
#if defined(CONFIG_NETFILTER_INGRESS) || defined(CONFIG_NETFILTER_EGRESS)
	case NFPROTO_NETDEV:
		if (hook >= NF_NETDEV_NUMHOOKS)
			return ERR_PTR(-EOPNOTSUPP);

		if (!dev)
			return ERR_PTR(-ENODEV);

		netdev = dev_get_by_name_rcu(net, dev);
		if (!netdev)
			return ERR_PTR(-ENODEV);

#ifdef CONFIG_NETFILTER_INGRESS
		if (hook == NF_NETDEV_INGRESS)
			return rcu_dereference(netdev->nf_hooks_ingress);
#endif
#ifdef CONFIG_NETFILTER_EGRESS
		if (hook == NF_NETDEV_EGRESS)
			return rcu_dereference(netdev->nf_hooks_egress);
#endif
		fallthrough;
#endif
	default:
		return ERR_PTR(-EPROTONOSUPPORT);
	}

	return hook_head;
}

static int nfnl_hook_dump(struct sk_buff *nlskb,
			  struct netlink_callback *cb)
{
	struct nfgenmsg *nfmsg = nlmsg_data(cb->nlh);
	struct nfnl_dump_hook_data *ctx = cb->data;
	int err, family = nfmsg->nfgen_family;
	struct net *net = sock_net(nlskb->sk);
	struct nf_hook_ops * const *ops;
	const struct nf_hook_entries *e;
	unsigned int i = cb->args[0];

	rcu_read_lock();

	e = nfnl_hook_entries_head(family, ctx->hook, net, ctx->devname);
	if (!e)
		goto done;

	if (IS_ERR(e)) {
		cb->seq++;
		goto done;
	}

	if ((unsigned long)e != ctx->headv || i >= e->num_hook_entries)
		cb->seq++;

	ops = nf_hook_entries_get_hook_ops(e);

	for (; i < e->num_hook_entries; i++) {
		err = nfnl_hook_dump_one(nlskb, ctx, ops[i], family,
					 cb->nlh->nlmsg_seq);
		if (err)
			break;
	}

done:
	nl_dump_check_consistent(cb, nlmsg_hdr(nlskb));
	rcu_read_unlock();
	cb->args[0] = i;
	return nlskb->len;
}

static int nfnl_hook_dump_start(struct netlink_callback *cb)
{
	const struct nfgenmsg *nfmsg = nlmsg_data(cb->nlh);
	const struct nlattr * const *nla = cb->data;
	struct nfnl_dump_hook_data *ctx = NULL;
	struct net *net = sock_net(cb->skb->sk);
	u8 family = nfmsg->nfgen_family;
	char name[IFNAMSIZ] = "";
	const void *head;
	u32 hooknum;

	hooknum = ntohl(nla_get_be32(nla[NFNLA_HOOK_HOOKNUM]));
	if (hooknum > 255)
		return -EINVAL;

	if (family == NFPROTO_NETDEV) {
		if (!nla[NFNLA_HOOK_DEV])
			return -EINVAL;

		nla_strscpy(name, nla[NFNLA_HOOK_DEV], sizeof(name));
	}

	rcu_read_lock();
	/* Not dereferenced; for consistency check only */
	head = nfnl_hook_entries_head(family, hooknum, net, name);
	rcu_read_unlock();

	if (head && IS_ERR(head))
		return PTR_ERR(head);

	ctx = kzalloc_obj(*ctx);
	if (!ctx)
		return -ENOMEM;

	strscpy(ctx->devname, name, sizeof(ctx->devname));
	ctx->headv = (unsigned long)head;
	ctx->hook = hooknum;

	cb->seq = 1;
	cb->data = ctx;

	return 0;
}

static int nfnl_hook_dump_stop(struct netlink_callback *cb)
{
	kfree(cb->data);
	return 0;
}

static int nfnl_hook_get(struct sk_buff *skb,
			 const struct nfnl_info *info,
			 const struct nlattr * const nla[])
{
	if (!nla[NFNLA_HOOK_HOOKNUM])
		return -EINVAL;

	if (info->nlh->nlmsg_flags & NLM_F_DUMP) {
		struct netlink_dump_control c = {
			.start = nfnl_hook_dump_start,
			.done = nfnl_hook_dump_stop,
			.dump = nfnl_hook_dump,
			.module = THIS_MODULE,
			.data = (void *)nla,
		};

		return nf_netlink_dump_start_rcu(info->sk, skb, info->nlh, &c);
	}

	return -EOPNOTSUPP;
}

static const struct nfnl_callback nfnl_hook_cb[NFNL_MSG_HOOK_MAX] = {
	[NFNL_MSG_HOOK_GET] = {
		.call		= nfnl_hook_get,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFNLA_HOOK_MAX,
		.policy		= nfnl_hook_nla_policy
	},
};

static const struct nfnetlink_subsystem nfhook_subsys = {
	.name				= "nfhook",
	.subsys_id			= NFNL_SUBSYS_HOOK,
	.cb_count			= NFNL_MSG_HOOK_MAX,
	.cb				= nfnl_hook_cb,
};

MODULE_ALIAS_NFNL_SUBSYS(NFNL_SUBSYS_HOOK);

static int __init nfnetlink_hook_init(void)
{
	return nfnetlink_subsys_register(&nfhook_subsys);
}

static void __exit nfnetlink_hook_exit(void)
{
	nfnetlink_subsys_unregister(&nfhook_subsys);
}

module_init(nfnetlink_hook_init);
module_exit(nfnetlink_hook_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Florian Westphal <fw@strlen.de>");
MODULE_DESCRIPTION("nfnetlink_hook: list registered netfilter hooks");
