/* SPDX-License-Identifier: LGPL-2.1-only */
/*
 * lib/route/mdb.c		Multicast Database
 */

#include <netlink-private/netlink.h>
#include <netlink/netlink.h>
#include <netlink/route/mdb.h>
#include <netlink/utils.h>
#include <linux/if_bridge.h>

/** @cond SKIP */
#define MDB_ATTR_IFINDEX         0x000001
#define MDB_ATTR_ENTRIES         0x000002

static struct rtnl_mdb_entry *rtnl_mdb_entry_alloc(void);
static void rtnl_mdb_entry_free(struct rtnl_mdb_entry *mdb_entry);

static struct nl_cache_ops rtnl_mdb_ops;
static struct nl_object_ops mdb_obj_ops;
/** @endcond */

static void mdb_constructor(struct nl_object *obj)
{
	struct rtnl_mdb *_mdb = (struct rtnl_mdb *) obj;

	nl_init_list_head(&_mdb->mdb_entry_list);
}

static void mdb_free_data(struct nl_object *obj)
{
	struct rtnl_mdb *mdb = (struct rtnl_mdb *)obj;
	struct rtnl_mdb_entry *mdb_entry;
	struct rtnl_mdb_entry *mdb_entry_safe;

	nl_list_for_each_entry_safe(mdb_entry, mdb_entry_safe,
				    &mdb->mdb_entry_list, mdb_list)
		rtnl_mdb_entry_free(mdb_entry);
}

static int mdb_entry_equal(struct rtnl_mdb_entry *a, struct rtnl_mdb_entry *b)
{
	return    a->ifindex == b->ifindex
	       && a->vid == b->vid
	       && a->proto == b->proto
	       && a->state == b->state
	       && nl_addr_cmp(a->addr, b->addr) == 0;
}

static uint64_t mdb_compare(struct nl_object *_a, struct nl_object *_b,
			    uint64_t attrs, int flags)
{
	struct rtnl_mdb *a = (struct rtnl_mdb *) _a;
	struct rtnl_mdb *b = (struct rtnl_mdb *) _b;
	struct rtnl_mdb_entry *a_entry, *b_entry;
	uint64_t diff = 0;

#define MDB_DIFF(ATTR, EXPR) ATTR_DIFF(attrs, MDB_ATTR_##ATTR, a, b, EXPR)
	diff |= MDB_DIFF(IFINDEX, a->ifindex != b->ifindex);
#undef MDB_DIFF

	a_entry = nl_list_entry(a->mdb_entry_list.next, struct rtnl_mdb_entry, mdb_list);
	b_entry = nl_list_entry(b->mdb_entry_list.next, struct rtnl_mdb_entry, mdb_list);
	while (1) {
		if (   &a_entry->mdb_list == &a->mdb_entry_list
		    || &b_entry->mdb_list == &b->mdb_entry_list) {
			if (   &a_entry->mdb_list != &a->mdb_entry_list
			    || &b_entry->mdb_list != &b->mdb_entry_list)
				diff |= MDB_ATTR_ENTRIES;
			break;
		}
		if (!mdb_entry_equal(a_entry, b_entry)) {
			diff |= MDB_ATTR_ENTRIES;
			break;
		}
		a_entry = nl_list_entry(a_entry->mdb_list.next, struct rtnl_mdb_entry, mdb_list);
		b_entry = nl_list_entry(b_entry->mdb_list.next, struct rtnl_mdb_entry, mdb_list);
	}

	return diff;
}

static struct rtnl_mdb_entry *mdb_entry_clone(struct rtnl_mdb_entry *src)
{
	struct rtnl_mdb_entry *dst = rtnl_mdb_entry_alloc();
	if (!dst)
		return NULL;

	dst->ifindex = src->ifindex;
	dst->state = src->state;
	dst->vid = src->vid;
	dst->proto = src->proto;

	dst->addr = nl_addr_clone(src->addr);
	if (dst->addr == NULL) {
		free(dst);
		return NULL;
	}

	return dst;
}

static int mdb_clone(struct nl_object *_dst, struct nl_object *_src)
{
	struct rtnl_mdb *dst = nl_object_priv(_dst);
	struct rtnl_mdb *src = nl_object_priv(_src);
	struct rtnl_mdb_entry *entry;

	nl_init_list_head(&dst->mdb_entry_list);

	nl_list_for_each_entry(entry, &src->mdb_entry_list, mdb_list) {
		struct rtnl_mdb_entry *copy = mdb_entry_clone(entry);

		if (!copy)
			return -NLE_NOMEM;

		rtnl_mdb_add_entry(dst, copy);
	}

	return 0;
}

static int mdb_update(struct nl_object *old_obj, struct nl_object *new_obj)
{
	struct rtnl_mdb *old = (struct rtnl_mdb *) old_obj;
	struct rtnl_mdb *new = (struct rtnl_mdb *) new_obj;
	struct rtnl_mdb_entry *entry, *old_entry;
	int action = new_obj->ce_msgtype;

	if (new->ifindex != old->ifindex)
		return -NLE_OPNOTSUPP;

	switch (action) {
	case RTM_NEWMDB:
		nl_list_for_each_entry(entry, &new->mdb_entry_list, mdb_list) {
			struct rtnl_mdb_entry *copy = mdb_entry_clone(entry);

			if (!copy)
				return -NLE_NOMEM;

			rtnl_mdb_add_entry(old, copy);
		}
		break;
	case RTM_DELMDB:
		entry = nl_list_first_entry(&new->mdb_entry_list,
		                            struct rtnl_mdb_entry,
		                            mdb_list);
		nl_list_for_each_entry(old_entry, &old->mdb_entry_list, mdb_list) {
			if (   old_entry->ifindex == entry->ifindex
			    && !nl_addr_cmp(old_entry->addr, entry->addr)) {
				nl_list_del(&old_entry->mdb_list);
				break;
			}
		}
		break;
	}

	return NLE_SUCCESS;
}

static struct nla_policy mdb_policy[MDBA_MAX + 1] = {
	[MDBA_MDB] = {.type = NLA_NESTED},
};

static struct nla_policy mdb_db_policy[MDBA_MDB_MAX + 1] = {
	[MDBA_MDB_ENTRY] = {.type = NLA_NESTED},
};

static int mdb_msg_parser(struct nl_cache_ops *ops, struct sockaddr_nl *who,
			  struct nlmsghdr *nlh, struct nl_parser_param *pp)
{
	int err = 0;
	int rem = 0;
	struct nlattr *tb[MDBA_MAX + 1];
	struct br_port_msg *port;
	struct nlattr *nla;
	struct br_mdb_entry *e;
	_nl_auto_rtnl_mdb struct rtnl_mdb *mdb = rtnl_mdb_alloc();

	if (!mdb)
		return -NLE_NOMEM;

	err = nlmsg_parse(nlh, sizeof(struct br_port_msg), tb, MDBA_MAX,
	                  mdb_policy);
	if (err < 0)
		return err;

	mdb->ce_msgtype = nlh->nlmsg_type;

	port = nlmsg_data(nlh);
	mdb->ifindex = port->ifindex;
	mdb->ce_mask |= MDB_ATTR_IFINDEX;

	if (tb[MDBA_MDB]) {
		struct nlattr *db_attr[MDBA_MDB_MAX+1];

		err = nla_parse_nested(db_attr, MDBA_MDB_MAX, tb[MDBA_MDB],
				       mdb_db_policy);
		if (err < 0)
			return err;
		rem = nla_len(tb[MDBA_MDB]);

		for (nla = nla_data(tb[MDBA_MDB]); nla_ok(nla, rem);
		     nla = nla_next(nla, &rem)) {
			int rm = nla_len(nla);
			struct nlattr *nla2;

			for (nla2 = nla_data(nla); nla_ok(nla2, rm);
			     nla2 = nla_next(nla2, &rm)) {
				_nl_auto_nl_addr struct nl_addr *addr = NULL;
				struct rtnl_mdb_entry *entry;
				uint16_t proto;

				e = nla_data(nla2);

				proto = ntohs(e->addr.proto);

				if (proto == ETH_P_IP) {
					addr = nl_addr_build(
						AF_INET, &e->addr.u.ip4,
						sizeof(e->addr.u.ip4));
				} else if (proto == ETH_P_IPV6) {
					addr = nl_addr_build(
						AF_INET6, &e->addr.u.ip6,
						sizeof(e->addr.u.ip6));
				} else {
					addr = nl_addr_build(
						AF_LLC, e->addr.u.mac_addr,
						sizeof(e->addr.u.mac_addr));
				}
				if (!addr)
					return -NLE_NOMEM;

				entry = rtnl_mdb_entry_alloc();
				if (!entry)
					return -NLE_NOMEM;

				mdb->ce_mask |= MDB_ATTR_ENTRIES;

				entry->ifindex = e->ifindex;
				entry->vid = e->vid;
				entry->state = e->state;
				entry->proto = ntohs(e->addr.proto);
				entry->addr = _nl_steal_pointer(&addr);
				rtnl_mdb_add_entry(mdb, entry);
			}
		}
	}

	return pp->pp_cb((struct nl_object *) mdb, pp);
}

static int mdb_request_update(struct nl_cache *cache, struct nl_sock *sk)
{
	return nl_rtgen_request(sk, RTM_GETMDB, AF_BRIDGE, NLM_F_DUMP);
}

static void mdb_entry_dump_line(struct rtnl_mdb_entry *entry,
                                struct nl_dump_params *p)
{
	char buf[INET6_ADDRSTRLEN];

	nl_dump(p, "port %d ", entry->ifindex);
	nl_dump(p, "vid %d ", entry->vid);
	nl_dump(p, "proto 0x%04x ", entry->proto);
	nl_dump(p, "address %s\n", nl_addr2str(entry->addr, buf, sizeof(buf)));
}

static void mdb_dump_line(struct nl_object *obj, struct nl_dump_params *p)
{
	struct rtnl_mdb *mdb = (struct rtnl_mdb *) obj;
	struct rtnl_mdb_entry *_mdb;

	nl_dump(p, "dev %d \n", mdb->ifindex);

	nl_list_for_each_entry(_mdb, &mdb->mdb_entry_list, mdb_list) {
		p->dp_ivar = NH_DUMP_FROM_ONELINE;
		mdb_entry_dump_line(_mdb, p);
	}
}

static void mdb_dump_details(struct nl_object *obj, struct nl_dump_params *p)
{
	mdb_dump_line(obj, p);
}

static void mdb_dump_stats(struct nl_object *obj, struct nl_dump_params *p)
{
	mdb_dump_details(obj, p);
}

void rtnl_mdb_put(struct rtnl_mdb *mdb)
{
	nl_object_put((struct nl_object *) mdb);
}

/** @} */

/**
 * @name Cache Management
 * @{
 */
int rtnl_mdb_alloc_cache(struct nl_sock *sk, struct nl_cache **result)
{
	return nl_cache_alloc_and_fill(&rtnl_mdb_ops, sk, result);
}

/**
 * Build a neighbour cache including all MDB entries currently configured in the kernel.
 * @arg sock		Netlink socket.
 * @arg result		Pointer to store resulting cache.
 * @arg flags		Flags to apply to cache before filling
 *
 * Allocates a new MDB cache, initializes it properly and updates it
 * to include all Multicast Database entries currently configured in the kernel.
 *
 * @return 0 on success or a negative error code.
 */
int rtnl_mdb_alloc_cache_flags(struct nl_sock *sock, struct nl_cache **result,
			       unsigned int flags)
{
	struct nl_cache *cache;
	int err;

	cache = nl_cache_alloc(&rtnl_mdb_ops);
	if (!cache)
		return -NLE_NOMEM;

	nl_cache_set_flags(cache, flags);

	if (sock && (err = nl_cache_refill(sock, cache)) < 0) {
		nl_cache_free(cache);
		return err;
	}

	*result = cache;
	return 0;
}

/** @} */

/**
 * @name Attributes
 * @{
 */
uint32_t rtnl_mdb_get_ifindex(struct rtnl_mdb *mdb)
{
	return mdb->ifindex;
}

void rtnl_mdb_add_entry(struct rtnl_mdb *mdb, struct rtnl_mdb_entry *entry)
{
	nl_list_add_tail(&entry->mdb_list, &mdb->mdb_entry_list);
}

void rtnl_mdb_foreach_entry(struct rtnl_mdb *mdb,
                            void (*cb)(struct rtnl_mdb_entry *, void *),
                            void *arg)
{
	struct rtnl_mdb_entry *entry;

	nl_list_for_each_entry(entry, &mdb->mdb_entry_list, mdb_list) {
		cb(entry, arg);
	}
}

int rtnl_mdb_entry_get_ifindex(struct rtnl_mdb_entry *mdb_entry)
{
	return mdb_entry->ifindex;
}

int rtnl_mdb_entry_get_vid(struct rtnl_mdb_entry *mdb_entry)
{
	return mdb_entry->vid;
}

int rtnl_mdb_entry_get_state(struct rtnl_mdb_entry *mdb_entry)
{
	return mdb_entry->state;
}

struct nl_addr *rtnl_mdb_entry_get_addr(struct rtnl_mdb_entry *mdb_entry)
{
	return mdb_entry->addr;
}

uint16_t rtnl_mdb_entry_get_proto(struct rtnl_mdb_entry *mdb_entry)
{
	return mdb_entry->proto;
}

/** @} */

static struct nl_object_ops mdb_obj_ops = {
	.oo_name = "route/mdb",
	.oo_size = sizeof(struct rtnl_mdb),
	.oo_constructor = mdb_constructor,
	.oo_dump = {
	            [NL_DUMP_LINE]    = mdb_dump_line,
	            [NL_DUMP_DETAILS] = mdb_dump_details,
	            [NL_DUMP_STATS]   = mdb_dump_stats,
	},
	.oo_clone = mdb_clone,
	.oo_compare = mdb_compare,
	.oo_update = mdb_update,
	.oo_free_data = mdb_free_data,
};

struct rtnl_mdb *rtnl_mdb_alloc(void)
{
	return (struct rtnl_mdb *) nl_object_alloc(&mdb_obj_ops);
}

static struct rtnl_mdb_entry *rtnl_mdb_entry_alloc(void)
{
	struct rtnl_mdb_entry *mdb;

	mdb = calloc(1, sizeof(struct rtnl_mdb_entry));
	if (!mdb)
		return NULL;

	nl_init_list_head(&mdb->mdb_list);

	return mdb;

}

static void rtnl_mdb_entry_free(struct rtnl_mdb_entry *mdb_entry)
{
	nl_list_del(&mdb_entry->mdb_list);
	nl_addr_put(mdb_entry->addr);
	free(mdb_entry);
}

static struct nl_af_group mdb_groups[] = {
	{AF_BRIDGE, RTNLGRP_MDB},
	{END_OF_GROUP_LIST},
};

static struct nl_cache_ops rtnl_mdb_ops = {
	.co_name = "route/mdb",
	.co_hdrsize = sizeof(struct br_port_msg),
	.co_msgtypes = {
	                { RTM_NEWMDB, NL_ACT_NEW, "new"},
	                { RTM_DELMDB, NL_ACT_DEL, "del"},
	                { RTM_GETMDB, NL_ACT_GET, "get"},
	                END_OF_MSGTYPES_LIST,
	                },
	.co_protocol = NETLINK_ROUTE,
	.co_groups = mdb_groups,
	.co_request_update = mdb_request_update,
	.co_msg_parser = mdb_msg_parser,
	.co_obj_ops = &mdb_obj_ops,
};

static void __init mdb_init(void)
{
	nl_cache_mngt_register(&rtnl_mdb_ops);
}

static void __exit mdb_exit(void)
{
	nl_cache_mngt_unregister(&rtnl_mdb_ops);
}

/** @} */
