/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * lib/route/netconf.c		netconf
 *
 *	This library is free software; you can redistribute it and/or
 *	modify it under the terms of the GNU Lesser General Public
 *	License as published by the Free Software Foundation version 2.1
 *	of the License.
 *
 * Copyright (c) 2017 David Ahern <dsa@cumulusnetworks.com>
 */

/**
 * @ingroup rtnl
 * @defgroup netconf Netconf
 * @brief
 *
 * @{
 */

#include <netlink-private/netlink.h>
#include <netlink/netlink.h>
#include <netlink/utils.h>
#include <netlink/route/netconf.h>
#include <linux/netconf.h>
#include <linux/socket.h>
#include <netlink/hashtable.h>

/** @cond SKIP */
#define NETCONF_ATTR_FAMILY		0x0001
#define NETCONF_ATTR_IFINDEX		0x0002
#define NETCONF_ATTR_RP_FILTER		0x0004
#define NETCONF_ATTR_FWDING		0x0008
#define NETCONF_ATTR_MC_FWDING		0x0010
#define NETCONF_ATTR_PROXY_NEIGH	0x0020
#define NETCONF_ATTR_IGNORE_RT_LINKDWN	0x0040
#define NETCONF_ATTR_INPUT		0x0080

struct rtnl_netconf
{
	NLHDR_COMMON

	int	family;
	int	ifindex;
	int	rp_filter;
	int	forwarding;
	int	mc_forwarding;
	int	proxy_neigh;
	int	ignore_routes_linkdown;
	int	input;
};

static struct nl_cache_ops rtnl_netconf_ops;
static struct nl_object_ops netconf_obj_ops;
/** @endcond */

static struct nla_policy devconf_ipv4_policy[NETCONFA_MAX+1] = {
	[NETCONFA_IFINDEX]	 = { .type = NLA_S32 },
	[NETCONFA_FORWARDING]	 = { .type = NLA_S32 },
	[NETCONFA_MC_FORWARDING] = { .type = NLA_S32 },
	[NETCONFA_RP_FILTER]	 = { .type = NLA_S32 },
	[NETCONFA_PROXY_NEIGH]	 = { .type = NLA_S32 },
	[NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN]  = { .type = NLA_S32 },
};

static struct nla_policy devconf_ipv6_policy[NETCONFA_MAX+1] = {
	[NETCONFA_IFINDEX]	 = { .type = NLA_S32 },
	[NETCONFA_FORWARDING]	 = { .type = NLA_S32 },
	[NETCONFA_MC_FORWARDING] = { .type = NLA_S32 },
	[NETCONFA_PROXY_NEIGH]	 = { .type = NLA_S32 },
	[NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN]  = { .type = NLA_S32 },
};

static struct nla_policy devconf_mpls_policy[NETCONFA_MAX+1] = {
	[NETCONFA_IFINDEX]	 = { .type = NLA_S32 },
	[NETCONFA_INPUT]	 = { .type = NLA_S32 },
};

static struct rtnl_netconf *rtnl_netconf_alloc(void)
{
	return (struct rtnl_netconf *) nl_object_alloc(&netconf_obj_ops);
}

static int netconf_clone(struct nl_object *_dst, struct nl_object *_src)
{
	struct rtnl_netconf *dst = nl_object_priv(_dst);
	struct rtnl_netconf *src = nl_object_priv(_src);

	*dst = *src;

	return 0;
}

static int netconf_msg_parser(struct nl_cache_ops *ops, struct sockaddr_nl *who,
			      struct nlmsghdr *nlh, struct nl_parser_param *pp)
{
	struct nlattr *tb[NETCONFA_MAX+1], *attr;
	struct rtnl_netconf *nc;
	struct netconfmsg *ncm;
	int err;

	ncm = nlmsg_data(nlh);
	switch (ncm->ncm_family) {
	case AF_INET:
		err = nlmsg_parse(nlh, sizeof(*ncm), tb, NETCONFA_MAX,
				  devconf_ipv4_policy);
		if (err < 0)
			return err;
		break;
	case AF_INET6:
		err = nlmsg_parse(nlh, sizeof(*ncm), tb, NETCONFA_MAX,
				  devconf_ipv6_policy);
		if (err < 0)
			return err;
		break;
	case AF_MPLS:
		err = nlmsg_parse(nlh, sizeof(*ncm), tb, NETCONFA_MAX,
				  devconf_mpls_policy);
		if (err < 0)
			return err;
		break;
	default:
		printf("unexpected netconf family: %d\n", ncm->ncm_family);
		return -1;
	}

	if (!tb[NETCONFA_IFINDEX])
		return -1;

	nc = rtnl_netconf_alloc();
	if (!nc)
		return -NLE_NOMEM;

	nc->ce_msgtype = nlh->nlmsg_type;
	nc->family = ncm->ncm_family;
	nc->ifindex = nla_get_s32(tb[NETCONFA_IFINDEX]);

	nc->ce_mask = NETCONF_ATTR_FAMILY | NETCONF_ATTR_IFINDEX;


	if (tb[NETCONFA_RP_FILTER]) {
		attr = tb[NETCONFA_RP_FILTER];
		nc->rp_filter = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_RP_FILTER;
	}

	if (tb[NETCONFA_FORWARDING]) {
		attr = tb[NETCONFA_FORWARDING];
		nc->forwarding = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_FWDING;
	}

	if (tb[NETCONFA_MC_FORWARDING]) {
		attr = tb[NETCONFA_MC_FORWARDING];
		nc->mc_forwarding = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_MC_FWDING;
	}

	if (tb[NETCONFA_PROXY_NEIGH]) {
		attr = tb[NETCONFA_PROXY_NEIGH];
		nc->proxy_neigh = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_PROXY_NEIGH;
	}

	if (tb[NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN]) {
		attr = tb[NETCONFA_IGNORE_ROUTES_WITH_LINKDOWN];
		nc->ignore_routes_linkdown = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_IGNORE_RT_LINKDWN;
	}

	if (tb[NETCONFA_INPUT]) {
		attr = tb[NETCONFA_INPUT];
		nc->input = nla_get_s32(attr);
		nc->ce_mask |= NETCONF_ATTR_INPUT;
	}

	err = pp->pp_cb((struct nl_object *) nc, pp);

	rtnl_netconf_put(nc);
	return err;
}

static int netconf_request_update(struct nl_cache *cache, struct nl_sock *sk)
{
	struct netconfmsg nc = {
		.ncm_family = cache->c_iarg1,
	};

	return nl_send_simple(sk, RTM_GETNETCONF, NLM_F_DUMP, &nc, sizeof(nc));
}

static void netconf_dump_line(struct nl_object *obj, struct nl_dump_params *p)
{
	struct rtnl_netconf *nc = (struct rtnl_netconf *) obj;
	struct nl_cache *link_cache;
	char buf[64];

	switch(nc->family) {
	case AF_INET:
		nl_dump(p, "ipv4 ");
		break;
	case AF_INET6:
		nl_dump(p, "ipv6 ");
		break;
	case AF_MPLS:
		nl_dump(p, "mpls ");
		break;
	default:
		return;
	}

	switch(nc->ifindex) {
	case NETCONFA_IFINDEX_ALL:
		nl_dump(p, "all ");
		break;
	case NETCONFA_IFINDEX_DEFAULT:
		nl_dump(p, "default ");
		break;
	default:
		link_cache = nl_cache_mngt_require_safe("route/link");
		if (link_cache) {
			nl_dump(p, "dev %s ",
				rtnl_link_i2name(link_cache, nc->ifindex,
						 buf, sizeof(buf)));
			nl_cache_put(link_cache);
		} else
			nl_dump(p, "dev %d ", nc->ifindex);
	}

	if (nc->ce_mask & NETCONF_ATTR_FWDING) {
		nl_dump(p, "forwarding %s ",
			nc->forwarding ? "on" : "off");
	}

	if (nc->ce_mask & NETCONF_ATTR_RP_FILTER) {
		if (nc->rp_filter == 0)
			nl_dump(p, "rp_filter off ");
		else if (nc->rp_filter == 1)
			nl_dump(p, "rp_filter strict ");
		else if (nc->rp_filter == 2)
			nl_dump(p, "rp_filter loose ");
		else
			nl_dump(p, "rp_filter unknown-mode ");
	}

	if (nc->ce_mask & NETCONF_ATTR_MC_FWDING) {
		nl_dump(p, "mc_forwarding %s ",
			nc->mc_forwarding ? "on" : "off");
	}

	if (nc->ce_mask & NETCONF_ATTR_PROXY_NEIGH)
		nl_dump(p, "proxy_neigh %d ", nc->proxy_neigh);

	if (nc->ce_mask & NETCONF_ATTR_IGNORE_RT_LINKDWN) {
		nl_dump(p, "ignore_routes_with_linkdown %s ",
			nc->ignore_routes_linkdown ? "on" : "off");
	}

	if (nc->ce_mask & NETCONF_ATTR_INPUT)
		nl_dump(p, "input %s ", nc->input ? "on" : "off");

	nl_dump(p, "\n");
}

static const struct trans_tbl netconf_attrs[] = {
	__ADD(NETCONF_ATTR_FAMILY, family),
	__ADD(NETCONF_ATTR_IFINDEX, ifindex),
	__ADD(NETCONF_ATTR_RP_FILTER, rp_filter),
	__ADD(NETCONF_ATTR_FWDING, forwarding),
	__ADD(NETCONF_ATTR_MC_FWDING, mc_forwarding),
	__ADD(NETCONF_ATTR_PROXY_NEIGH, proxy_neigh),
	__ADD(NETCONF_ATTR_IGNORE_RT_LINKDWN, ignore_routes_with_linkdown),
	__ADD(NETCONF_ATTR_INPUT, input),
};

static char *netconf_attrs2str(int attrs, char *buf, size_t len)
{
	return __flags2str(attrs, buf, len, netconf_attrs,
			   ARRAY_SIZE(netconf_attrs));
}

static void netconf_keygen(struct nl_object *obj, uint32_t *hashkey,
			   uint32_t table_sz)
{
	struct rtnl_netconf *nc = (struct rtnl_netconf *) obj;
	unsigned int nckey_sz;
	struct nc_hash_key {
		int        nc_family;
		int        nc_index;
	} __attribute__((packed)) nckey;

	nckey_sz = sizeof(nckey);
	nckey.nc_family = nc->family;
	nckey.nc_index = nc->ifindex;

	*hashkey = nl_hash(&nckey, nckey_sz, 0) % table_sz;

	NL_DBG(5, "netconf %p key (dev %d fam %d) keysz %d, hash 0x%x\n",
	       nc, nckey.nc_index, nckey.nc_family, nckey_sz, *hashkey);
}

static uint64_t netconf_compare(struct nl_object *_a, struct nl_object *_b,
			     uint64_t attrs, int flags)
{
	struct rtnl_netconf *a = (struct rtnl_netconf *) _a;
	struct rtnl_netconf *b = (struct rtnl_netconf *) _b;
	uint64_t diff = 0;

#define NETCONF_DIFF(ATTR, EXPR) ATTR_DIFF(attrs, NETCONF_ATTR_##ATTR, a, b, EXPR)

	diff |= NETCONF_DIFF(FAMILY,	a->family != b->family);
	diff |= NETCONF_DIFF(IFINDEX,	a->ifindex != b->ifindex);
	diff |= NETCONF_DIFF(RP_FILTER,	a->rp_filter != b->rp_filter);
	diff |= NETCONF_DIFF(FWDING,	a->forwarding != b->forwarding);
	diff |= NETCONF_DIFF(MC_FWDING,	a->mc_forwarding != b->mc_forwarding);
	diff |= NETCONF_DIFF(PROXY_NEIGH, a->proxy_neigh != b->proxy_neigh);
	diff |= NETCONF_DIFF(IGNORE_RT_LINKDWN,
			a->ignore_routes_linkdown != b->ignore_routes_linkdown);
	diff |= NETCONF_DIFF(INPUT,	a->input != b->input);

#undef NETCONF_DIFF

	return diff;
}

static int netconf_update(struct nl_object *old_obj, struct nl_object *new_obj)
{
	struct rtnl_netconf *new_nc = (struct rtnl_netconf *) new_obj;
	struct rtnl_netconf *old_nc = (struct rtnl_netconf *) old_obj;
	int action = new_obj->ce_msgtype;

	switch(action) {
	case RTM_NEWNETCONF:
		if (new_nc->family != old_nc->family ||
		    new_nc->ifindex != old_nc->ifindex)
			return -NLE_OPNOTSUPP;

		if (new_nc->ce_mask & NETCONF_ATTR_RP_FILTER)
			old_nc->rp_filter = new_nc->rp_filter;
		if (new_nc->ce_mask & NETCONF_ATTR_FWDING)
			old_nc->forwarding = new_nc->forwarding;
		if (new_nc->ce_mask & NETCONF_ATTR_MC_FWDING)
			old_nc->mc_forwarding = new_nc->mc_forwarding;
		if (new_nc->ce_mask & NETCONF_ATTR_PROXY_NEIGH)
			old_nc->proxy_neigh = new_nc->proxy_neigh;
		if (new_nc->ce_mask & NETCONF_ATTR_IGNORE_RT_LINKDWN)
			old_nc->ignore_routes_linkdown = new_nc->ignore_routes_linkdown;

		break;
	default:
		return -NLE_OPNOTSUPP;
	}

	return NLE_SUCCESS;
}

/**
 * @name Cache Management
 * @{
 */

int rtnl_netconf_alloc_cache(struct nl_sock *sk, struct nl_cache **result)
{
	return nl_cache_alloc_and_fill(&rtnl_netconf_ops, sk, result);
}

/**
 * Search netconf in cache
 * @arg cache		netconf cache
 * @arg family		Address family of interest
 * @arg ifindex		Interface index of interest
 *
 * Searches netconf cache previously allocated with rtnl_netconf_alloc_cache()
 * for given index and family
 *
 * The reference counter is incremented before returning the netconf entry,
 * therefore the reference must be given back with rtnl_netconf_put() after
 * usage.
 *
 * @return netconf object or NULL if no match was found.
 */
struct rtnl_netconf *rtnl_netconf_get_by_idx(struct nl_cache *cache, int family,
					     int ifindex)
{
	struct rtnl_netconf *nc;

	if (!ifindex || !family || cache->c_ops != &rtnl_netconf_ops)
		return NULL;

	nl_list_for_each_entry(nc, &cache->c_items, ce_list) {
		if (nc->ifindex == ifindex &&
		    nc->family == family) {
			nl_object_get((struct nl_object *) nc);
			return nc;
		}
	}

	return NULL;
}

void rtnl_netconf_put(struct rtnl_netconf *nc)
{
	nl_object_put((struct nl_object *) nc);
}

/**
 * Search netconf in cache
 * @arg cache		netconf cache
 * @arg family		Address family of interest
 *
 * Searches netconf cache previously allocated with rtnl_netconf_alloc_cache()
 * for "all" netconf settings for given family
 *
 * The reference counter is incremented before returning the netconf entry,
 * therefore the reference must be given back with rtnl_netconf_put() after
 * usage.
 *
 * @return netconf object or NULL if no match was found.
 */
struct rtnl_netconf *rtnl_netconf_get_all(struct nl_cache *cache, int family)
{
	return rtnl_netconf_get_by_idx(cache, family, NETCONFA_IFINDEX_ALL);
}

/**
 * Search netconf in cache
 * @arg cache		netconf cache
 * @arg family		Address family of interest
 *
 * Searches netconf cache previously allocated with rtnl_netconf_alloc_cache()
 * for "default" netconf settings for given family
 *
 * The reference counter is incremented before returning the netconf entry,
 * therefore the reference must be given back with rtnl_netconf_put() after
 * usage.
 *
 * @return netconf object or NULL if no match was found.
 */
struct rtnl_netconf *rtnl_netconf_get_default(struct nl_cache *cache, int family)
{
	return rtnl_netconf_get_by_idx(cache, family, NETCONFA_IFINDEX_DEFAULT);
}

/** @} */

/**
 * @name Attributes
 * @{
 */

int rtnl_netconf_get_family(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_FAMILY))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->family;
	return 0;
}
int rtnl_netconf_get_ifindex(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_IFINDEX))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->ifindex;
	return 0;
}
int rtnl_netconf_get_forwarding(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_FWDING))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->forwarding;
	return 0;
}
int rtnl_netconf_get_mc_forwarding(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_MC_FWDING))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->mc_forwarding;
	return 0;
}
int rtnl_netconf_get_rp_filter(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_RP_FILTER))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->rp_filter;
	return 0;
}
int rtnl_netconf_get_proxy_neigh(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_PROXY_NEIGH))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->proxy_neigh;
	return 0;
}
int rtnl_netconf_get_ignore_routes_linkdown(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_IGNORE_RT_LINKDWN))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->ignore_routes_linkdown;
	return 0;
}
int rtnl_netconf_get_input(struct rtnl_netconf *nc, int *val)
{
	if (!nc)
		return -NLE_INVAL;
	if (!(nc->ce_mask & NETCONF_ATTR_INPUT))
		return -NLE_MISSING_ATTR;
	if (val)
		*val = nc->input;
	return 0;
}


/** @} */

static struct nl_object_ops netconf_obj_ops = {
	.oo_name		= "route/netconf",
	.oo_size		= sizeof(struct rtnl_netconf),
	.oo_clone		= netconf_clone,
	.oo_dump = {
	    [NL_DUMP_LINE] 	= netconf_dump_line,
	    [NL_DUMP_DETAILS] 	= netconf_dump_line,
	},
	.oo_compare		= netconf_compare,
	.oo_keygen		= netconf_keygen,
	.oo_update		= netconf_update,
	.oo_attrs2str		= netconf_attrs2str,
	.oo_id_attrs		= (NETCONF_ATTR_FAMILY      |
				   NETCONF_ATTR_IFINDEX)
};

static struct nl_af_group netconf_groups[] = {
	{ AF_INET,	RTNLGRP_IPV4_NETCONF },
	{ AF_INET6,	RTNLGRP_IPV6_NETCONF },
	{ AF_MPLS,	RTNLGRP_MPLS_NETCONF },
	{ END_OF_GROUP_LIST },
};

static struct nl_cache_ops rtnl_netconf_ops = {
	.co_name		= "route/netconf",
	.co_hdrsize		= sizeof(struct netconfmsg),
	.co_msgtypes		= {
					{ RTM_NEWNETCONF, NL_ACT_NEW, "new" },
					{ RTM_DELNETCONF, NL_ACT_DEL, "del" },
					{ RTM_GETNETCONF, NL_ACT_GET, "get" },
					END_OF_MSGTYPES_LIST,
				  },
	.co_protocol		= NETLINK_ROUTE,
	.co_groups		= netconf_groups,
	.co_request_update      = netconf_request_update,
	.co_msg_parser          = netconf_msg_parser,
	.co_obj_ops		= &netconf_obj_ops,
};

static void __init netconf_init(void)
{
	nl_cache_mngt_register(&rtnl_netconf_ops);
}

static void __exit netconf_exit(void)
{
	nl_cache_mngt_unregister(&rtnl_netconf_ops);
}

/** @} */
