// SPDX-License-Identifier: GPL-2.0

#define _GNU_SOURCE

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#include <linux/genetlink.h>
#include <linux/neighbour.h>
#include <linux/netdevice.h>
#include <linux/netlink.h>
#include <linux/mqueue.h>
#include <linux/rtnetlink.h>

#include "kselftest_harness.h"

#include <ynl.h>

struct ext_ack {
	int err;

	__u32 attr_offs;
	__u32 miss_type;
	__u32 miss_nest;
	const char *str;
};

enum get_ea_ret {
	ERROR = -1,
	NO_CTRL = 0,
	FOUND_DONE,
	FOUND_ERR,
	FOUND_EXTACK,
};

static enum get_ea_ret
nl_get_extack(char *buf, size_t n, struct ext_ack *ea)
{
	enum get_ea_ret ret = NO_CTRL;
	const struct nlmsghdr *nlh;
	const struct nlattr *attr;
	ssize_t rem;

	for (rem = n; rem > 0; NLMSG_NEXT(nlh, rem)) {
		nlh = (struct nlmsghdr *)&buf[n - rem];
		if (!NLMSG_OK(nlh, rem))
			return ERROR;

		if (nlh->nlmsg_type == NLMSG_ERROR)
			ret = FOUND_ERR;
		else if (nlh->nlmsg_type == NLMSG_DONE)
			ret = FOUND_DONE;
		else
			continue;

		ea->err = -*(int *)NLMSG_DATA(nlh);

		if (!(nlh->nlmsg_flags & NLM_F_ACK_TLVS))
			return ret;

		ynl_attr_for_each(attr, nlh, sizeof(int)) {
			switch (ynl_attr_type(attr)) {
			case NLMSGERR_ATTR_OFFS:
				ea->attr_offs = ynl_attr_get_u32(attr);
				break;
			case NLMSGERR_ATTR_MISS_TYPE:
				ea->miss_type = ynl_attr_get_u32(attr);
				break;
			case NLMSGERR_ATTR_MISS_NEST:
				ea->miss_nest = ynl_attr_get_u32(attr);
				break;
			case NLMSGERR_ATTR_MSG:
				ea->str = ynl_attr_get_str(attr);
				break;
			}
		}

		return FOUND_EXTACK;
	}

	return ret;
}

static const struct {
	struct nlmsghdr nlhdr;
	struct ndmsg ndm;
	struct nlattr ahdr;
	__u32 val;
} dump_neigh_bad = {
	.nlhdr = {
		.nlmsg_len	= sizeof(dump_neigh_bad),
		.nlmsg_type	= RTM_GETNEIGH,
		.nlmsg_flags	= NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP,
		.nlmsg_seq	= 1,
	},
	.ndm = {
		.ndm_family	= 123,
	},
	.ahdr = {
		.nla_len	= 4 + 4,
		.nla_type	= NDA_FLAGS_EXT,
	},
	.val = -1, // should fail MASK validation
};

TEST(dump_extack)
{
	int netlink_sock;
	int i, cnt, ret;
	char buf[8192];
	int one = 1;
	ssize_t n;

	netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
	ASSERT_GE(netlink_sock, 0);

	n = setsockopt(netlink_sock, SOL_NETLINK, NETLINK_CAP_ACK,
		       &one, sizeof(one));
	ASSERT_EQ(n, 0);
	n = setsockopt(netlink_sock, SOL_NETLINK, NETLINK_EXT_ACK,
		       &one, sizeof(one));
	ASSERT_EQ(n, 0);
	n = setsockopt(netlink_sock, SOL_NETLINK, NETLINK_GET_STRICT_CHK,
		       &one, sizeof(one));
	ASSERT_EQ(n, 0);

	/* Dump so many times we fill up the buffer */
	cnt = 80;
	for (i = 0; i < cnt; i++) {
		n = send(netlink_sock, &dump_neigh_bad,
			 sizeof(dump_neigh_bad), 0);
		ASSERT_EQ(n, sizeof(dump_neigh_bad));
	}

	/* Read out the ENOBUFS */
	n = recv(netlink_sock, buf, sizeof(buf), MSG_DONTWAIT);
	EXPECT_EQ(n, -1);
	EXPECT_EQ(errno, ENOBUFS);

	ret = NO_CTRL;
	for (i = 0; i < cnt; i++) {
		struct ext_ack ea = {};

		n = recv(netlink_sock, buf, sizeof(buf), MSG_DONTWAIT);
		if (n < 0) {
			ASSERT_GE(i, 10);
			break;
		}
		ASSERT_GE(n, (ssize_t)sizeof(struct nlmsghdr));

		ret = nl_get_extack(buf, n, &ea);
		/* Once we fill the buffer we'll see one ENOBUFS followed
		 * by a number of EBUSYs. Then the last recv() will finally
		 * trigger and complete the dump.
		 */
		if (ret == FOUND_ERR && (ea.err == ENOBUFS || ea.err == EBUSY))
			continue;
		EXPECT_EQ(ret, FOUND_EXTACK);
		EXPECT_EQ(ea.err, EINVAL);
		EXPECT_EQ(ea.attr_offs,
			  sizeof(struct nlmsghdr) + sizeof(struct ndmsg));
	}
	/* Make sure last message was a full DONE+extack */
	EXPECT_EQ(ret, FOUND_EXTACK);
}

static const struct {
	struct nlmsghdr nlhdr;
	struct genlmsghdr genlhdr;
	struct nlattr ahdr;
	__u16 val;
	__u16 pad;
} dump_policies = {
	.nlhdr = {
		.nlmsg_len	= sizeof(dump_policies),
		.nlmsg_type	= GENL_ID_CTRL,
		.nlmsg_flags	= NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP,
		.nlmsg_seq	= 1,
	},
	.genlhdr = {
		.cmd		= CTRL_CMD_GETPOLICY,
		.version	= 2,
	},
	.ahdr = {
		.nla_len	= 6,
		.nla_type	= CTRL_ATTR_FAMILY_ID,
	},
	.val = GENL_ID_CTRL,
	.pad = 0,
};

// Sanity check for the test itself, make sure the dump doesn't fit in one msg
TEST(test_sanity)
{
	int netlink_sock;
	char buf[8192];
	ssize_t n;

	netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
	ASSERT_GE(netlink_sock, 0);

	n = send(netlink_sock, &dump_policies, sizeof(dump_policies), 0);
	ASSERT_EQ(n, sizeof(dump_policies));

	n = recv(netlink_sock, buf, sizeof(buf), MSG_DONTWAIT);
	ASSERT_GE(n, (ssize_t)sizeof(struct nlmsghdr));

	n = recv(netlink_sock, buf, sizeof(buf), MSG_DONTWAIT);
	ASSERT_GE(n, (ssize_t)sizeof(struct nlmsghdr));

	close(netlink_sock);
}

TEST(close_in_progress)
{
	int netlink_sock;
	ssize_t n;

	netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
	ASSERT_GE(netlink_sock, 0);

	n = send(netlink_sock, &dump_policies, sizeof(dump_policies), 0);
	ASSERT_EQ(n, sizeof(dump_policies));

	close(netlink_sock);
}

TEST(close_with_ref)
{
	char cookie[NOTIFY_COOKIE_LEN] = {};
	int netlink_sock, mq_fd;
	struct sigevent sigev;
	ssize_t n;

	netlink_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
	ASSERT_GE(netlink_sock, 0);

	n = send(netlink_sock, &dump_policies, sizeof(dump_policies), 0);
	ASSERT_EQ(n, sizeof(dump_policies));

	mq_fd = syscall(__NR_mq_open, "sed", O_CREAT | O_WRONLY, 0600, 0);
	ASSERT_GE(mq_fd, 0);

	memset(&sigev, 0, sizeof(sigev));
	sigev.sigev_notify		= SIGEV_THREAD;
	sigev.sigev_value.sival_ptr	= cookie;
	sigev.sigev_signo		= netlink_sock;

	syscall(__NR_mq_notify, mq_fd, &sigev);

	close(netlink_sock);

	// give mqueue time to fire
	usleep(100 * 1000);
}

TEST_HARNESS_MAIN
