/*
 * netlink.c - basic infrastructure for netlink code
 *
 * Heart of the netlink interface implementation.
 */

#include <errno.h>

#include "../internal.h"
#include "netlink.h"
#include "extapi.h"
#include "msgbuff.h"
#include "nlsock.h"
#include "strset.h"

/* Used as reply callback for requests where no reply is expected (e.g. most
 * "set" type commands)
 */
int nomsg_reply_cb(const struct nlmsghdr *nlhdr, void *data __maybe_unused)
{
	const struct genlmsghdr *ghdr = (const struct genlmsghdr *)(nlhdr + 1);

	fprintf(stderr, "received unexpected message: len=%u type=%u cmd=%u\n",
		nlhdr->nlmsg_len, nlhdr->nlmsg_type, ghdr->cmd);
	return MNL_CB_OK;
}

/* standard attribute parser callback; it fills provided array with pointers
 * to attributes like kernel nla_parse(). We must expect to run on top of
 * a newer kernel which may send attributes that we do not know (yet). Rather
 * than treating them as an error, just ignore them.
 */
int attr_cb(const struct nlattr *attr, void *data)
{
	const struct attr_tb_info *tb_info = data;
	uint16_t type = mnl_attr_get_type(attr);

	if (type <= tb_info->max_type)
		tb_info->tb[type] = attr;

	return MNL_CB_OK;
}

/* misc helpers */

const char *get_dev_name(const struct nlattr *nest)
{
	const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {};
	DECLARE_ATTR_TB_INFO(tb);
	int ret;

	if (!nest)
		return NULL;
	ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info);
	if (ret < 0 || !tb[ETHTOOL_A_HEADER_DEV_NAME])
		return "(none)";
	return mnl_attr_get_str(tb[ETHTOOL_A_HEADER_DEV_NAME]);
}

int get_dev_info(const struct nlattr *nest, int *ifindex, char *ifname)
{
	const struct nlattr *tb[ETHTOOL_A_HEADER_MAX + 1] = {};
	const struct nlattr *index_attr;
	const struct nlattr *name_attr;
	DECLARE_ATTR_TB_INFO(tb);
	int ret;

	if (ifindex)
		*ifindex = 0;
	if (ifname)
		memset(ifname, '\0', ALTIFNAMSIZ);

	if (!nest)
		return -EFAULT;
	ret = mnl_attr_parse_nested(nest, attr_cb, &tb_info);
	index_attr = tb[ETHTOOL_A_HEADER_DEV_INDEX];
	name_attr = tb[ETHTOOL_A_HEADER_DEV_NAME];
	if (ret < 0 || (ifindex && !index_attr) || (ifname && !name_attr))
		return -EFAULT;

	if (ifindex)
		*ifindex = mnl_attr_get_u32(index_attr);
	if (ifname) {
		strncpy(ifname, mnl_attr_get_str(name_attr), ALTIFNAMSIZ);
		if (ifname[ALTIFNAMSIZ - 1]) {
			ifname[ALTIFNAMSIZ - 1] = '\0';
			fprintf(stderr, "kernel device name too long: '%s'\n",
				mnl_attr_get_str(name_attr));
			return -EFAULT;
		}
	}
	return 0;
}

/**
 * netlink_cmd_check() - check support for netlink command
 * @ctx:            ethtool command context
 * @cmd:            netlink command id
 * @devname:        device name from user
 * @allow_wildcard: wildcard dumps supported
 *
 * Check if command @cmd is known to be unsupported based on ops information
 * from genetlink family id request. Set nlctx->ioctl_fallback if ethtool
 * should fall back to ioctl, i.e. when we do not know in advance that
 * netlink request is supported. Set nlctx->wildcard_unsupported if "*" was
 * used as device name but the request does not support wildcards (on either
 * side).
 *
 * Return: true if we know the netlink request is not supported and should
 * fail (and possibly fall back) without actually sending it to kernel.
 */
bool netlink_cmd_check(struct cmd_context *ctx, unsigned int cmd,
		       bool allow_wildcard)
{
	bool is_dump = !strcmp(ctx->devname, WILDCARD_DEVNAME);
	uint32_t cap = is_dump ? GENL_CMD_CAP_DUMP : GENL_CMD_CAP_DO;
	struct nl_context *nlctx = ctx->nlctx;

	if (is_dump && !allow_wildcard) {
		nlctx->wildcard_unsupported = true;
		return true;
	}
	if (!nlctx->ops_info) {
		nlctx->ioctl_fallback = true;
		return false;
	}
	if (cmd > ETHTOOL_MSG_USER_MAX || !nlctx->ops_info[cmd].op_flags) {
		nlctx->ioctl_fallback = true;
		return true;
	}

	if (is_dump && !(nlctx->ops_info[cmd].op_flags & GENL_CMD_CAP_DUMP))
		nlctx->wildcard_unsupported = true;

	return !(nlctx->ops_info[cmd].op_flags & cap);
}

struct ethtool_op_policy_query_ctx {
	struct nl_context *nlctx;
	unsigned int op;
	unsigned int op_hdr_attr;

	bool op_policy_found;
	bool hdr_policy_found;
	unsigned int op_policy_idx;
	unsigned int hdr_policy_idx;
	uint64_t flag_mask;
};

static int family_policy_find_op(struct ethtool_op_policy_query_ctx *policy_ctx,
				 const struct nlattr *op_policy)
{
	const struct nlattr *attr;
	unsigned int type;
	int ret;

	type = policy_ctx->nlctx->is_dump ?
		CTRL_ATTR_POLICY_DUMP : CTRL_ATTR_POLICY_DO;

	mnl_attr_for_each_nested(attr, op_policy) {
		const struct nlattr *tb[CTRL_ATTR_POLICY_DUMP_MAX + 1] = {};
		DECLARE_ATTR_TB_INFO(tb);

		if (mnl_attr_get_type(attr) != policy_ctx->op)
			continue;

		ret = mnl_attr_parse_nested(attr, attr_cb, &tb_info);
		if (ret < 0)
			return ret;

		if (!tb[type])
			continue;

		policy_ctx->op_policy_found = true;
		policy_ctx->op_policy_idx = mnl_attr_get_u32(tb[type]);
		break;
	}

	return 0;
}

static int family_policy_cb(const struct nlmsghdr *nlhdr, void *data)
{
	const struct nlattr *tba[NL_POLICY_TYPE_ATTR_MAX + 1] = {};
	DECLARE_ATTR_TB_INFO(tba);
	const struct nlattr *tb[CTRL_ATTR_MAX + 1] = {};
	DECLARE_ATTR_TB_INFO(tb);
	struct ethtool_op_policy_query_ctx *policy_ctx = data;
	const struct nlattr *policy_attr, *attr_attr, *attr;
	unsigned int attr_idx, policy_idx;
	int ret;

	ret = mnl_attr_parse(nlhdr, GENL_HDRLEN, attr_cb, &tb_info);
	if (ret < 0)
		return MNL_CB_ERROR;

	if (!policy_ctx->op_policy_found) {
		if (!tb[CTRL_ATTR_OP_POLICY]) {
			fprintf(stderr, "Error: op policy map not present\n");
			return MNL_CB_ERROR;
		}
		ret = family_policy_find_op(policy_ctx, tb[CTRL_ATTR_OP_POLICY]);
		return ret < 0 ? MNL_CB_ERROR : MNL_CB_OK;
	}

	if (!tb[CTRL_ATTR_POLICY])
		return MNL_CB_OK;

	policy_attr = mnl_attr_get_payload(tb[CTRL_ATTR_POLICY]);
	policy_idx = mnl_attr_get_type(policy_attr);
	attr_attr = mnl_attr_get_payload(policy_attr);
	attr_idx = mnl_attr_get_type(attr_attr);

	ret = mnl_attr_parse_nested(attr_attr, attr_cb, &tba_info);
	if (ret < 0)
		return MNL_CB_ERROR;

	if (policy_idx == policy_ctx->op_policy_idx &&
	    attr_idx == policy_ctx->op_hdr_attr) {
		attr = tba[NL_POLICY_TYPE_ATTR_POLICY_IDX];
		if (!attr) {
			fprintf(stderr,	"Error: no policy index in what was expected to be ethtool header attribute\n");
			return MNL_CB_ERROR;
		}
		policy_ctx->hdr_policy_found = true;
		policy_ctx->hdr_policy_idx = mnl_attr_get_u32(attr);
	}

	if (policy_ctx->hdr_policy_found &&
	    policy_ctx->hdr_policy_idx == policy_idx &&
	    attr_idx == ETHTOOL_A_HEADER_FLAGS) {
		attr = tba[NL_POLICY_TYPE_ATTR_MASK];
		if (!attr) {
			fprintf(stderr,	"Error: validation mask not reported for ethtool header flags\n");
			return MNL_CB_ERROR;
		}

		policy_ctx->flag_mask = mnl_attr_get_u64(attr);
	}

	return MNL_CB_OK;
}

static int read_flags_policy(struct nl_context *nlctx, struct nl_socket *nlsk,
			     unsigned int nlcmd, unsigned int hdrattr)
{
	struct ethtool_op_policy_query_ctx policy_ctx;
	struct nl_msg_buff *msgbuff = &nlsk->msgbuff;
	int ret;

	if (nlctx->ops_info[nlcmd].hdr_policy_loaded)
		return 0;

	memset(&policy_ctx, 0, sizeof(policy_ctx));
	policy_ctx.nlctx = nlctx;
	policy_ctx.op = nlcmd;
	policy_ctx.op_hdr_attr = hdrattr;

	ret = __msg_init(msgbuff, GENL_ID_CTRL, CTRL_CMD_GETPOLICY,
			 NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP, 1);
	if (ret < 0)
		return ret;
	ret = -EMSGSIZE;
	if (ethnla_put_u16(msgbuff, CTRL_ATTR_FAMILY_ID, nlctx->ethnl_fam))
		return ret;
	if (ethnla_put_u32(msgbuff, CTRL_ATTR_OP, nlcmd))
		return ret;

	nlsock_sendmsg(nlsk, NULL);
	nlsock_process_reply(nlsk, family_policy_cb, &policy_ctx);

	nlctx->ops_info[nlcmd].hdr_policy_loaded = 1;
	nlctx->ops_info[nlcmd].hdr_flags = policy_ctx.flag_mask;
	return 0;
}

u32 get_stats_flag(struct nl_context *nlctx, unsigned int nlcmd,
		   unsigned int hdrattr)
{
	if (!nlctx->ctx->show_stats)
		return 0;
	if (nlcmd > ETHTOOL_MSG_USER_MAX ||
	    !(nlctx->ops_info[nlcmd].op_flags & GENL_CMD_CAP_HASPOL))
		return 0;

	if (read_flags_policy(nlctx, nlctx->ethnl_socket, nlcmd, hdrattr) < 0)
		return 0;

	return nlctx->ops_info[nlcmd].hdr_flags & ETHTOOL_FLAG_STATS;
}

/* initialization */

static int genl_read_ops(struct nl_context *nlctx,
			 const struct nlattr *ops_attr)
{
	struct nl_op_info *ops_info;
	struct nlattr *op_attr;
	int ret;

	ops_info = calloc(__ETHTOOL_MSG_USER_CNT, sizeof(ops_info[0]));
	if (!ops_info)
		return -ENOMEM;

	mnl_attr_for_each_nested(op_attr, ops_attr) {
		const struct nlattr *tb[CTRL_ATTR_OP_MAX + 1] = {};
		DECLARE_ATTR_TB_INFO(tb);
		uint32_t op_id;

		ret = mnl_attr_parse_nested(op_attr, attr_cb, &tb_info);
		if (ret < 0)
			goto err;

		if (!tb[CTRL_ATTR_OP_ID] || !tb[CTRL_ATTR_OP_FLAGS])
			continue;
		op_id = mnl_attr_get_u32(tb[CTRL_ATTR_OP_ID]);
		if (op_id >= __ETHTOOL_MSG_USER_CNT)
			continue;

		ops_info[op_id].op_flags =
			mnl_attr_get_u32(tb[CTRL_ATTR_OP_FLAGS]);
	}

	nlctx->ops_info = ops_info;
	return 0;
err:
	free(ops_info);
	return ret;
}

static void find_mc_group(struct nl_context *nlctx, struct nlattr *nest)
{
	const struct nlattr *grp_tb[CTRL_ATTR_MCAST_GRP_MAX + 1] = {};
	DECLARE_ATTR_TB_INFO(grp_tb);
	struct nlattr *grp_attr;
	int ret;

	mnl_attr_for_each_nested(grp_attr, nest) {
		ret = mnl_attr_parse_nested(grp_attr, attr_cb, &grp_tb_info);
		if (ret < 0)
			return;
		if (!grp_tb[CTRL_ATTR_MCAST_GRP_NAME] ||
		    !grp_tb[CTRL_ATTR_MCAST_GRP_ID])
			continue;
		if (strcmp(mnl_attr_get_str(grp_tb[CTRL_ATTR_MCAST_GRP_NAME]),
			   ETHTOOL_MCGRP_MONITOR_NAME))
			continue;
		nlctx->ethnl_mongrp =
			mnl_attr_get_u32(grp_tb[CTRL_ATTR_MCAST_GRP_ID]);
		return;
	}
}

static int __maybe_unused family_info_cb(const struct nlmsghdr *nlhdr,
					 void *data)
{
	struct nl_context *nlctx = data;
	struct nlattr *attr;
	int ret;

	mnl_attr_for_each(attr, nlhdr, GENL_HDRLEN) {
		switch (mnl_attr_get_type(attr)) {
		case CTRL_ATTR_FAMILY_ID:
			nlctx->ethnl_fam = mnl_attr_get_u16(attr);
			break;
		case CTRL_ATTR_OPS:
			ret = genl_read_ops(nlctx, attr);
			if (ret < 0)
				return MNL_CB_ERROR;
			break;
		case CTRL_ATTR_MCAST_GROUPS:
			find_mc_group(nlctx, attr);
			break;
		}
	}

	return MNL_CB_OK;
}

#ifdef TEST_ETHTOOL
static int get_genl_family(struct nl_context *nlctx __maybe_unused,
			   struct nl_socket *nlsk __maybe_unused)
{
	return 0;
}
#else
static int get_genl_family(struct nl_context *nlctx, struct nl_socket *nlsk)
{
	struct nl_msg_buff *msgbuff = &nlsk->msgbuff;
	int ret;

	nlctx->suppress_nlerr = 2;
	ret = __msg_init(msgbuff, GENL_ID_CTRL, CTRL_CMD_GETFAMILY,
			 NLM_F_REQUEST | NLM_F_ACK, 1);
	if (ret < 0)
		goto out;
	ret = -EMSGSIZE;
	if (ethnla_put_strz(msgbuff, CTRL_ATTR_FAMILY_NAME, ETHTOOL_GENL_NAME))
		goto out;

	nlsock_sendmsg(nlsk, NULL);
	nlsock_process_reply(nlsk, family_info_cb, nlctx);
	ret = nlctx->ethnl_fam ? 0 : -EADDRNOTAVAIL;

out:
	nlctx->suppress_nlerr = 0;
	return ret;
}
#endif

int netlink_init(struct cmd_context *ctx)
{
	struct nl_context *nlctx;
	int ret;

	nlctx = calloc(1, sizeof(*nlctx));
	if (!nlctx)
		return -ENOMEM;
	nlctx->ctx = ctx;
	ret = nlsock_init(nlctx, &nlctx->ethnl_socket, NETLINK_GENERIC);
	if (ret < 0)
		goto out_free;
	ret = get_genl_family(nlctx, nlctx->ethnl_socket);
	if (ret < 0)
		goto out_nlsk;

	ctx->nlctx = nlctx;
	return 0;

out_nlsk:
	nlsock_done(nlctx->ethnl_socket);
out_free:
	free(nlctx->ops_info);
	free(nlctx);
	return ret;
}

static void netlink_done(struct cmd_context *ctx)
{
	struct nl_context *nlctx = ctx->nlctx;

	if (!nlctx)
		return;

	nlsock_done(nlctx->ethnl_socket);
	nlsock_done(nlctx->ethnl2_socket);
	nlsock_done(nlctx->rtnl_socket);
	free(nlctx->ops_info);
	free(nlctx);
	ctx->nlctx = NULL;
	cleanup_all_strings();
}

/**
 * netlink_run_handler() - run netlink handler for subcommand
 * @ctx:         command context
 * @nlchk:       netlink capability check
 * @nlfunc:      subcommand netlink handler to call
 * @no_fallback: there is no ioctl fallback handler
 *
 * This function returns only if ioctl() handler should be run as fallback.
 * Otherwise it exits with appropriate return code.
 */
void netlink_run_handler(struct cmd_context *ctx, nl_chk_t nlchk,
			 nl_func_t nlfunc, bool no_fallback)
{
	bool wildcard = ctx->devname && !strcmp(ctx->devname, WILDCARD_DEVNAME);
	bool wildcard_unsupported, ioctl_fallback;
	struct nl_context *nlctx;
	const char *reason;
	int ret;

	if (ctx->nl_disable) {
		reason = "netlink disabled";
		goto no_support;
	}

	if (nlchk && !nlchk(ctx)) {
		reason = "ioctl-only request";
		goto no_support;
	}
	if (ctx->devname && strlen(ctx->devname) >= ALTIFNAMSIZ) {
		fprintf(stderr, "device name '%s' longer than %u characters\n",
			ctx->devname, ALTIFNAMSIZ - 1);
		exit(1);
	}

	if (!nlfunc) {
		reason = "ethtool netlink support for subcommand missing";
		goto no_support;
	}
	if (netlink_init(ctx)) {
		reason = "netlink interface initialization failed";
		goto no_support;
	}
	nlctx = ctx->nlctx;

	ret = nlfunc(ctx);
	wildcard_unsupported = nlctx->wildcard_unsupported;
	ioctl_fallback = nlctx->ioctl_fallback;
	netlink_done(ctx);

	if (no_fallback || ret != -EOPNOTSUPP || !ioctl_fallback) {
		if (wildcard_unsupported)
			fprintf(stderr, "%s\n",
				"subcommand does not support wildcard dump");
		exit(ret >= 0 ? ret : 1);
	}
	if (wildcard_unsupported)
		reason = "subcommand does not support wildcard dump";
	else
		reason = "kernel netlink support for subcommand missing";

no_support:
	if (no_fallback) {
		fprintf(stderr, "%s, subcommand not supported by ioctl\n",
			reason);
		exit(1);
	}
	if (wildcard) {
		fprintf(stderr, "%s, wildcard dump not supported\n", reason);
		exit(1);
	}
	if (ctx->devname && strlen(ctx->devname) >= IFNAMSIZ) {
		fprintf(stderr,
			"%s, device name longer than %u not supported\n",
			reason, IFNAMSIZ - 1);
		exit(1);
	}

	/* fallback to ioctl() */
}
