// SPDX-License-Identifier: GPL-2.0
/*
 * ip nexthop
 *
 * Copyright (c) 2017-19 David Ahern <dsahern@gmail.com>
 */

#include <linux/nexthop.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <rt_names.h>
#include <errno.h>

#include "utils.h"
#include "ip_common.h"
#include "nh_common.h"

static struct {
	unsigned int flushed;
	unsigned int groups;
	unsigned int ifindex;
	unsigned int master;
	unsigned int proto;
	unsigned int fdb;
	unsigned int id;
	unsigned int nhid;
} filter;

enum {
	IPNH_LIST,
	IPNH_FLUSH,
};

#define RTM_NHA(h)  ((struct rtattr *)(((char *)(h)) + \
			NLMSG_ALIGN(sizeof(struct nhmsg))))

static struct hlist_head nh_cache[NH_CACHE_SIZE];
static struct rtnl_handle nh_cache_rth = { .fd = -1 };

static void usage(void) __attribute__((noreturn));

static void usage(void)
{
	fprintf(stderr,
		"Usage: ip nexthop { list | flush } [ protocol ID ] SELECTOR\n"
		"       ip nexthop { add | replace } id ID NH [ protocol ID ]\n"
		"       ip nexthop { get | del } id ID\n"
		"       ip nexthop bucket list BUCKET_SELECTOR\n"
		"       ip nexthop bucket get id ID index INDEX\n"
		"SELECTOR := [ id ID ] [ dev DEV ] [ vrf NAME ] [ master DEV ]\n"
		"            [ groups ] [ fdb ]\n"
		"BUCKET_SELECTOR := SELECTOR | [ nhid ID ]\n"
		"NH := { blackhole | [ via ADDRESS ] [ dev DEV ] [ onlink ]\n"
		"        [ encap ENCAPTYPE ENCAPHDR ] |\n"
		"        group GROUP [ fdb ] [ type TYPE [ TYPE_ARGS ] ] }\n"
		"GROUP := [ <id[,weight]>/<id[,weight]>/... ]\n"
		"TYPE := { mpath | resilient }\n"
		"TYPE_ARGS := [ RESILIENT_ARGS ]\n"
		"RESILIENT_ARGS := [ buckets BUCKETS ] [ idle_timer IDLE ]\n"
		"                  [ unbalanced_timer UNBALANCED ]\n"
		"ENCAPTYPE := [ mpls ]\n"
		"ENCAPHDR := [ MPLSLABEL ]\n");
	exit(-1);
}

static int nh_dump_filter(struct nlmsghdr *nlh, int reqlen)
{
	int err;

	if (filter.ifindex) {
		err = addattr32(nlh, reqlen, NHA_OIF, filter.ifindex);
		if (err)
			return err;
	}

	if (filter.groups) {
		err = addattr_l(nlh, reqlen, NHA_GROUPS, NULL, 0);
		if (err)
			return err;
	}

	if (filter.master) {
		err = addattr32(nlh, reqlen, NHA_MASTER, filter.master);
		if (err)
			return err;
	}

	if (filter.fdb) {
		err = addattr_l(nlh, reqlen, NHA_FDB, NULL, 0);
		if (err)
			return err;
	}

	return 0;
}

static int nh_dump_bucket_filter(struct nlmsghdr *nlh, int reqlen)
{
	struct rtattr *nest;
	int err = 0;

	err = nh_dump_filter(nlh, reqlen);
	if (err)
		return err;

	if (filter.id) {
		err = addattr32(nlh, reqlen, NHA_ID, filter.id);
		if (err)
			return err;
	}

	if (filter.nhid) {
		nest = addattr_nest(nlh, reqlen, NHA_RES_BUCKET);
		nest->rta_type |= NLA_F_NESTED;

		err = addattr32(nlh, reqlen, NHA_RES_BUCKET_NH_ID,
				filter.nhid);
		if (err)
			return err;

		addattr_nest_end(nlh, nest);
	}

	return err;
}

static struct rtnl_handle rth_del = { .fd = -1 };

static int delete_nexthop(__u32 id)
{
	struct {
		struct nlmsghdr	n;
		struct nhmsg	nhm;
		char		buf[64];
	} req = {
		.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)),
		.n.nlmsg_flags = NLM_F_REQUEST,
		.n.nlmsg_type = RTM_DELNEXTHOP,
		.nhm.nh_family = AF_UNSPEC,
	};

	req.n.nlmsg_seq = ++rth_del.seq;

	addattr32(&req.n, sizeof(req), NHA_ID, id);

	if (rtnl_talk(&rth_del, &req.n, NULL) < 0)
		return -1;
	return 0;
}

static int flush_nexthop(struct nlmsghdr *nlh, void *arg)
{
	struct nhmsg *nhm = NLMSG_DATA(nlh);
	struct rtattr *tb[NHA_MAX+1];
	__u32 id = 0;
	int len;

	len = nlh->nlmsg_len - NLMSG_SPACE(sizeof(*nhm));
	if (len < 0) {
		fprintf(stderr, "BUG: wrong nlmsg len %d\n", len);
		return -1;
	}

	if (filter.proto && nhm->nh_protocol != filter.proto)
		return 0;

	parse_rtattr(tb, NHA_MAX, RTM_NHA(nhm), len);
	if (tb[NHA_ID])
		id = rta_getattr_u32(tb[NHA_ID]);

	if (id && !delete_nexthop(id))
		filter.flushed++;

	return 0;
}

static int ipnh_flush(unsigned int all)
{
	int rc = -2;

	if (all) {
		filter.groups = 1;
		filter.ifindex = 0;
		filter.master = 0;
	}

	if (rtnl_open(&rth_del, 0) < 0) {
		fprintf(stderr, "Cannot open rtnetlink\n");
		return EXIT_FAILURE;
	}
again:
	if (rtnl_nexthopdump_req(&rth, preferred_family, nh_dump_filter) < 0) {
		perror("Cannot send dump request");
		goto out;
	}

	if (rtnl_dump_filter(&rth, flush_nexthop, stdout) < 0) {
		fprintf(stderr, "Dump terminated. Failed to flush nexthops\n");
		goto out;
	}

	/* if deleting all, then remove groups first */
	if (all && filter.groups) {
		filter.groups = 0;
		goto again;
	}

	rc = 0;
out:
	rtnl_close(&rth_del);
	if (!filter.flushed)
		printf("Nothing to flush\n");
	else
		printf("Flushed %d nexthops\n", filter.flushed);

	return rc;
}

static bool __valid_nh_group_attr(const struct rtattr *g_attr)
{
	int num = RTA_PAYLOAD(g_attr) / sizeof(struct nexthop_grp);

	return num && num * sizeof(struct nexthop_grp) == RTA_PAYLOAD(g_attr);
}

static void print_nh_group(const struct nh_entry *nhe)
{
	int i;

	open_json_array(PRINT_JSON, "group");
	print_string(PRINT_FP, NULL, "%s", "group ");
	for (i = 0; i < nhe->nh_groups_cnt; ++i) {
		open_json_object(NULL);

		if (i)
			print_string(PRINT_FP, NULL, "%s", "/");

		print_uint(PRINT_ANY, "id", "%u", nhe->nh_groups[i].id);
		if (nhe->nh_groups[i].weight)
			print_uint(PRINT_ANY, "weight", ",%u",
				   nhe->nh_groups[i].weight + 1);

		close_json_object();
	}
	print_string(PRINT_FP, NULL, "%s", " ");
	close_json_array(PRINT_JSON, NULL);
}

static const char *nh_group_type_name(__u16 type)
{
	switch (type) {
	case NEXTHOP_GRP_TYPE_MPATH:
		return "mpath";
	case NEXTHOP_GRP_TYPE_RES:
		return "resilient";
	default:
		return "<unknown type>";
	}
}

static void print_nh_group_type(__u16 nh_grp_type)
{
	if (nh_grp_type == NEXTHOP_GRP_TYPE_MPATH)
		/* Do not print type in order not to break existing output. */
		return;

	print_string(PRINT_ANY, "type", "type %s ", nh_group_type_name(nh_grp_type));
}

static void parse_nh_res_group_rta(const struct rtattr *res_grp_attr,
				   struct nha_res_grp *res_grp)
{
	struct rtattr *tb[NHA_RES_GROUP_MAX + 1];
	struct rtattr *rta;

	memset(res_grp, 0, sizeof(*res_grp));
	parse_rtattr_nested(tb, NHA_RES_GROUP_MAX, res_grp_attr);

	if (tb[NHA_RES_GROUP_BUCKETS])
		res_grp->buckets = rta_getattr_u16(tb[NHA_RES_GROUP_BUCKETS]);

	if (tb[NHA_RES_GROUP_IDLE_TIMER]) {
		rta = tb[NHA_RES_GROUP_IDLE_TIMER];
		res_grp->idle_timer = rta_getattr_u32(rta);
	}

	if (tb[NHA_RES_GROUP_UNBALANCED_TIMER]) {
		rta = tb[NHA_RES_GROUP_UNBALANCED_TIMER];
		res_grp->unbalanced_timer = rta_getattr_u32(rta);
	}

	if (tb[NHA_RES_GROUP_UNBALANCED_TIME]) {
		rta = tb[NHA_RES_GROUP_UNBALANCED_TIME];
		res_grp->unbalanced_time = rta_getattr_u64(rta);
	}
}

static void print_nh_res_group(const struct nha_res_grp *res_grp)
{
	struct timeval tv;

	open_json_object("resilient_args");

	print_uint(PRINT_ANY, "buckets", "buckets %u ", res_grp->buckets);

	 __jiffies_to_tv(&tv, res_grp->idle_timer);
	print_tv(PRINT_ANY, "idle_timer", "idle_timer %g ", &tv);

	__jiffies_to_tv(&tv, res_grp->unbalanced_timer);
	print_tv(PRINT_ANY, "unbalanced_timer", "unbalanced_timer %g ", &tv);

	__jiffies_to_tv(&tv, res_grp->unbalanced_time);
	print_tv(PRINT_ANY, "unbalanced_time", "unbalanced_time %g ", &tv);

	close_json_object();
}

static void print_nh_res_bucket(FILE *fp, const struct rtattr *res_bucket_attr)
{
	struct rtattr *tb[NHA_RES_BUCKET_MAX + 1];

	parse_rtattr_nested(tb, NHA_RES_BUCKET_MAX, res_bucket_attr);

	open_json_object("bucket");

	if (tb[NHA_RES_BUCKET_INDEX])
		print_uint(PRINT_ANY, "index", "index %u ",
			   rta_getattr_u16(tb[NHA_RES_BUCKET_INDEX]));

	if (tb[NHA_RES_BUCKET_IDLE_TIME]) {
		struct rtattr *rta = tb[NHA_RES_BUCKET_IDLE_TIME];
		struct timeval tv;

		__jiffies_to_tv(&tv, rta_getattr_u64(rta));
		print_tv(PRINT_ANY, "idle_time", "idle_time %g ", &tv);
	}

	if (tb[NHA_RES_BUCKET_NH_ID])
		print_uint(PRINT_ANY, "nhid", "nhid %u ",
			   rta_getattr_u32(tb[NHA_RES_BUCKET_NH_ID]));

	close_json_object();
}

static void ipnh_destroy_entry(struct nh_entry *nhe)
{
	free(nhe->nh_encap);
	free(nhe->nh_groups);
}

/* parse nhmsg into nexthop entry struct which must be destroyed by
 * ipnh_destroy_enty when it's not needed anymore
 */
static int ipnh_parse_nhmsg(FILE *fp, const struct nhmsg *nhm, int len,
			    struct nh_entry *nhe)
{
	struct rtattr *tb[NHA_MAX+1];
	int err = 0;

	memset(nhe, 0, sizeof(*nhe));
	parse_rtattr_flags(tb, NHA_MAX, RTM_NHA(nhm), len, NLA_F_NESTED);

	if (tb[NHA_ID])
		nhe->nh_id = rta_getattr_u32(tb[NHA_ID]);

	if (tb[NHA_OIF])
		nhe->nh_oif = rta_getattr_u32(tb[NHA_OIF]);

	if (tb[NHA_GROUP_TYPE])
		nhe->nh_grp_type = rta_getattr_u16(tb[NHA_GROUP_TYPE]);

	if (tb[NHA_GATEWAY]) {
		if (RTA_PAYLOAD(tb[NHA_GATEWAY]) > sizeof(nhe->nh_gateway)) {
			fprintf(fp, "<nexthop id %u invalid gateway length %lu>\n",
				nhe->nh_id, RTA_PAYLOAD(tb[NHA_GATEWAY]));
			err = -EINVAL;
			goto out_err;
		}
		nhe->nh_gateway_len = RTA_PAYLOAD(tb[NHA_GATEWAY]);
		memcpy(&nhe->nh_gateway, RTA_DATA(tb[NHA_GATEWAY]),
		       RTA_PAYLOAD(tb[NHA_GATEWAY]));
	}

	if (tb[NHA_ENCAP]) {
		nhe->nh_encap = malloc(RTA_LENGTH(RTA_PAYLOAD(tb[NHA_ENCAP])));
		if (!nhe->nh_encap) {
			err = -ENOMEM;
			goto out_err;
		}
		memcpy(nhe->nh_encap, tb[NHA_ENCAP],
		       RTA_LENGTH(RTA_PAYLOAD(tb[NHA_ENCAP])));
		memcpy(&nhe->nh_encap_type, tb[NHA_ENCAP_TYPE],
		       sizeof(nhe->nh_encap_type));
	}

	if (tb[NHA_GROUP]) {
		if (!__valid_nh_group_attr(tb[NHA_GROUP])) {
			fprintf(fp, "<nexthop id %u invalid nexthop group>",
				nhe->nh_id);
			err = -EINVAL;
			goto out_err;
		}

		nhe->nh_groups = malloc(RTA_PAYLOAD(tb[NHA_GROUP]));
		if (!nhe->nh_groups) {
			err = -ENOMEM;
			goto out_err;
		}
		nhe->nh_groups_cnt = RTA_PAYLOAD(tb[NHA_GROUP]) /
				     sizeof(struct nexthop_grp);
		memcpy(nhe->nh_groups, RTA_DATA(tb[NHA_GROUP]),
		       RTA_PAYLOAD(tb[NHA_GROUP]));
	}

	if (tb[NHA_RES_GROUP]) {
		parse_nh_res_group_rta(tb[NHA_RES_GROUP], &nhe->nh_res_grp);
		nhe->nh_has_res_grp = true;
	}

	nhe->nh_blackhole = !!tb[NHA_BLACKHOLE];
	nhe->nh_fdb = !!tb[NHA_FDB];

	nhe->nh_family = nhm->nh_family;
	nhe->nh_protocol = nhm->nh_protocol;
	nhe->nh_scope = nhm->nh_scope;
	nhe->nh_flags = nhm->nh_flags;

	return 0;

out_err:
	ipnh_destroy_entry(nhe);
	return err;
}

static void __print_nexthop_entry(FILE *fp, const char *jsobj,
				  struct nh_entry *nhe,
				  bool deleted)
{
	SPRINT_BUF(b1);

	open_json_object(jsobj);

	if (deleted)
		print_bool(PRINT_ANY, "deleted", "Deleted ", true);

	print_uint(PRINT_ANY, "id", "id %u ", nhe->nh_id);

	if (nhe->nh_groups)
		print_nh_group(nhe);

	print_nh_group_type(nhe->nh_grp_type);

	if (nhe->nh_has_res_grp)
		print_nh_res_group(&nhe->nh_res_grp);

	if (nhe->nh_encap)
		lwt_print_encap(fp, &nhe->nh_encap_type.rta, nhe->nh_encap);

	if (nhe->nh_gateway_len)
		__print_rta_gateway(fp, nhe->nh_family,
				    format_host(nhe->nh_family,
				    nhe->nh_gateway_len,
				    &nhe->nh_gateway));

	if (nhe->nh_oif)
		print_rta_ifidx(fp, nhe->nh_oif, "dev");

	if (nhe->nh_scope != RT_SCOPE_UNIVERSE || show_details > 0) {
		print_string(PRINT_ANY, "scope", "scope %s ",
			     rtnl_rtscope_n2a(nhe->nh_scope, b1, sizeof(b1)));
	}

	if (nhe->nh_blackhole)
		print_null(PRINT_ANY, "blackhole", "blackhole ", NULL);

	if (nhe->nh_protocol != RTPROT_UNSPEC || show_details > 0) {
		print_string(PRINT_ANY, "protocol", "proto %s ",
			     rtnl_rtprot_n2a(nhe->nh_protocol, b1, sizeof(b1)));
	}

	print_rt_flags(fp, nhe->nh_flags);

	if (nhe->nh_fdb)
		print_null(PRINT_ANY, "fdb", "fdb", NULL);

	close_json_object();
}

static int  __ipnh_get_id(struct rtnl_handle *rthp, __u32 nh_id,
			  struct nlmsghdr **answer)
{
	struct {
		struct nlmsghdr n;
		struct nhmsg	nhm;
		char		buf[1024];
	} req = {
		.n.nlmsg_len	= NLMSG_LENGTH(sizeof(struct nhmsg)),
		.n.nlmsg_flags	= NLM_F_REQUEST,
		.n.nlmsg_type	= RTM_GETNEXTHOP,
		.nhm.nh_family	= preferred_family,
	};

	addattr32(&req.n, sizeof(req), NHA_ID, nh_id);

	return rtnl_talk(rthp, &req.n, answer);
}

static struct hlist_head *ipnh_cache_head(__u32 nh_id)
{
	nh_id ^= nh_id >> 20;
	nh_id ^= nh_id >> 10;

	return &nh_cache[nh_id % NH_CACHE_SIZE];
}

static void ipnh_cache_link_entry(struct nh_entry *nhe)
{
	struct hlist_head *head = ipnh_cache_head(nhe->nh_id);

	hlist_add_head(&nhe->nh_hash, head);
}

static void ipnh_cache_unlink_entry(struct nh_entry *nhe)
{
	hlist_del(&nhe->nh_hash);
}

static struct nh_entry *ipnh_cache_get(__u32 nh_id)
{
	struct hlist_head *head = ipnh_cache_head(nh_id);
	struct nh_entry *nhe;
	struct hlist_node *n;

	hlist_for_each(n, head) {
		nhe = container_of(n, struct nh_entry, nh_hash);
		if (nhe->nh_id == nh_id)
			return nhe;
	}

	return NULL;
}

static int __ipnh_cache_parse_nlmsg(const struct nlmsghdr *n,
				    struct nh_entry *nhe)
{
	int err, len;

	len = n->nlmsg_len - NLMSG_SPACE(sizeof(struct nhmsg));
	if (len < 0) {
		fprintf(stderr, "BUG: wrong nlmsg len %d\n", len);
		return -EINVAL;
	}

	err = ipnh_parse_nhmsg(stderr, NLMSG_DATA(n), len, nhe);
	if (err) {
		fprintf(stderr, "Error parsing nexthop: %s\n", strerror(-err));
		return err;
	}

	return 0;
}

static struct nh_entry *ipnh_cache_add(__u32 nh_id)
{
	struct nlmsghdr *answer = NULL;
	struct nh_entry *nhe = NULL;

	if (nh_cache_rth.fd < 0 && rtnl_open(&nh_cache_rth, 0) < 0) {
		nh_cache_rth.fd = -1;
		goto out;
	}

	if (__ipnh_get_id(&nh_cache_rth, nh_id, &answer) < 0)
		goto out;

	nhe = malloc(sizeof(*nhe));
	if (!nhe)
		goto out;

	if (__ipnh_cache_parse_nlmsg(answer, nhe))
		goto out_free_nhe;

	ipnh_cache_link_entry(nhe);

out:
	free(answer);

	return nhe;

out_free_nhe:
	free(nhe);
	nhe = NULL;
	goto out;
}

static void ipnh_cache_del(struct nh_entry *nhe)
{
	ipnh_cache_unlink_entry(nhe);
	ipnh_destroy_entry(nhe);
	free(nhe);
}

/* update, add or delete a nexthop entry based on nlmsghdr */
static int ipnh_cache_process_nlmsg(const struct nlmsghdr *n,
				    struct nh_entry *new_nhe)
{
	struct nh_entry *nhe;

	nhe = ipnh_cache_get(new_nhe->nh_id);
	switch (n->nlmsg_type) {
	case RTM_DELNEXTHOP:
		if (nhe)
			ipnh_cache_del(nhe);
		ipnh_destroy_entry(new_nhe);
		break;
	case RTM_NEWNEXTHOP:
		if (!nhe) {
			nhe = malloc(sizeof(*nhe));
			if (!nhe) {
				ipnh_destroy_entry(new_nhe);
				return -1;
			}
		} else {
			/* this allows us to save 1 allocation on updates by
			 * reusing the old nh entry, but we need to cleanup its
			 * internal storage
			 */
			ipnh_cache_unlink_entry(nhe);
			ipnh_destroy_entry(nhe);
		}
		memcpy(nhe, new_nhe, sizeof(*nhe));
		ipnh_cache_link_entry(nhe);
		break;
	}

	return 0;
}

void print_cache_nexthop_id(FILE *fp, const char *fp_prefix, const char *jsobj,
			    __u32 nh_id)
{
	struct nh_entry *nhe = ipnh_cache_get(nh_id);

	if (!nhe) {
		nhe = ipnh_cache_add(nh_id);
		if (!nhe)
			return;
	}

	if (fp_prefix)
		print_string(PRINT_FP, NULL, "%s", fp_prefix);
	__print_nexthop_entry(fp, jsobj, nhe, false);
}

int print_cache_nexthop(struct nlmsghdr *n, void *arg, bool process_cache)
{
	struct nhmsg *nhm = NLMSG_DATA(n);
	FILE *fp = (FILE *)arg;
	struct nh_entry nhe;
	int len, err;

	if (n->nlmsg_type != RTM_DELNEXTHOP &&
	    n->nlmsg_type != RTM_NEWNEXTHOP) {
		fprintf(stderr, "Not a nexthop: %08x %08x %08x\n",
			n->nlmsg_len, n->nlmsg_type, n->nlmsg_flags);
		return -1;
	}

	len = n->nlmsg_len - NLMSG_SPACE(sizeof(*nhm));
	if (len < 0) {
		close_json_object();
		fprintf(stderr, "BUG: wrong nlmsg len %d\n", len);
		return -1;
	}

	if (filter.proto && filter.proto != nhm->nh_protocol)
		return 0;

	err = ipnh_parse_nhmsg(fp, nhm, len, &nhe);
	if (err) {
		close_json_object();
		fprintf(stderr, "Error parsing nexthop: %s\n", strerror(-err));
		return -1;
	}
	__print_nexthop_entry(fp, NULL, &nhe, n->nlmsg_type == RTM_DELNEXTHOP);
	print_string(PRINT_FP, NULL, "%s", "\n");
	fflush(fp);

	if (process_cache)
		ipnh_cache_process_nlmsg(n, &nhe);
	else
		ipnh_destroy_entry(&nhe);

	return 0;
}

static int print_nexthop_nocache(struct nlmsghdr *n, void *arg)
{
	return print_cache_nexthop(n, arg, false);
}

int print_nexthop_bucket(struct nlmsghdr *n, void *arg)
{
	struct nhmsg *nhm = NLMSG_DATA(n);
	struct rtattr *tb[NHA_MAX+1];
	FILE *fp = (FILE *)arg;
	int len;

	if (n->nlmsg_type != RTM_DELNEXTHOPBUCKET &&
	    n->nlmsg_type != RTM_NEWNEXTHOPBUCKET) {
		fprintf(stderr, "Not a nexthop bucket: %08x %08x %08x\n",
			n->nlmsg_len, n->nlmsg_type, n->nlmsg_flags);
		return -1;
	}

	len = n->nlmsg_len - NLMSG_SPACE(sizeof(*nhm));
	if (len < 0) {
		close_json_object();
		fprintf(stderr, "BUG: wrong nlmsg len %d\n", len);
		return -1;
	}

	parse_rtattr_flags(tb, NHA_MAX, RTM_NHA(nhm), len, NLA_F_NESTED);

	open_json_object(NULL);

	if (n->nlmsg_type == RTM_DELNEXTHOP)
		print_bool(PRINT_ANY, "deleted", "Deleted ", true);

	if (tb[NHA_ID])
		print_uint(PRINT_ANY, "id", "id %u ",
			   rta_getattr_u32(tb[NHA_ID]));

	if (tb[NHA_RES_BUCKET])
		print_nh_res_bucket(fp, tb[NHA_RES_BUCKET]);

	print_rt_flags(fp, nhm->nh_flags);

	print_string(PRINT_FP, NULL, "%s", "\n");
	close_json_object();
	fflush(fp);

	return 0;
}

static int add_nh_group_attr(struct nlmsghdr *n, int maxlen, char *argv)
{
	struct nexthop_grp *grps = NULL;
	int count = 0, i;
	int err = -1;
	char *sep, *wsep;

	if (*argv != '\0')
		count = 1;

	/* separator is '/' */
	sep = strchr(argv, '/');
	while (sep) {
		count++;
		sep = strchr(sep + 1, '/');
	}

	if (count == 0)
		goto out;

	grps = calloc(count, sizeof(*grps));
	if (!grps)
		goto out;

	for (i = 0; i < count; ++i) {
		sep = strchr(argv, '/');
		if (sep)
			*sep = '\0';

		wsep = strchr(argv, ',');
		if (wsep)
			*wsep = '\0';

		if (get_unsigned(&grps[i].id, argv, 0))
			goto out;
		if (wsep) {
			unsigned int w;

			wsep++;
			if (get_unsigned(&w, wsep, 0) || w == 0 || w > 256)
				invarg("\"weight\" is invalid\n", wsep);
			grps[i].weight = w - 1;
		}

		if (!sep)
			break;

		argv = sep + 1;
	}

	err = addattr_l(n, maxlen, NHA_GROUP, grps, count * sizeof(*grps));
out:
	free(grps);
	return err;
}

static int read_nh_group_type(const char *name)
{
	if (strcmp(name, "mpath") == 0)
		return NEXTHOP_GRP_TYPE_MPATH;
	else if (strcmp(name, "resilient") == 0)
		return NEXTHOP_GRP_TYPE_RES;

	return __NEXTHOP_GRP_TYPE_MAX;
}

static void parse_nh_group_type_res(struct nlmsghdr *n, int maxlen, int *argcp,
				    char ***argvp)
{
	char **argv = *argvp;
	struct rtattr *nest;
	int argc = *argcp;

	if (!NEXT_ARG_OK())
		return;

	nest = addattr_nest(n, maxlen, NHA_RES_GROUP);
	nest->rta_type |= NLA_F_NESTED;

	NEXT_ARG_FWD();
	while (argc > 0) {
		if (strcmp(*argv, "buckets") == 0) {
			__u16 buckets;

			NEXT_ARG();
			if (get_u16(&buckets, *argv, 0))
				invarg("invalid buckets value", *argv);

			addattr16(n, maxlen, NHA_RES_GROUP_BUCKETS, buckets);
		} else if (strcmp(*argv, "idle_timer") == 0) {
			__u32 idle_timer;

			NEXT_ARG();
			if (get_unsigned(&idle_timer, *argv, 0) ||
			    idle_timer >= UINT32_MAX / 100)
				invarg("invalid idle timer value", *argv);

			addattr32(n, maxlen, NHA_RES_GROUP_IDLE_TIMER,
				  idle_timer * 100);
		} else if (strcmp(*argv, "unbalanced_timer") == 0) {
			__u32 unbalanced_timer;

			NEXT_ARG();
			if (get_unsigned(&unbalanced_timer, *argv, 0) ||
			    unbalanced_timer >= UINT32_MAX / 100)
				invarg("invalid unbalanced timer value", *argv);

			addattr32(n, maxlen, NHA_RES_GROUP_UNBALANCED_TIMER,
				  unbalanced_timer * 100);
		} else {
			break;
		}
		argc--; argv++;
	}

	/* argv is currently the first unparsed argument, but ipnh_modify()
	 * will move to the next, so step back.
	 */
	*argcp = argc + 1;
	*argvp = argv - 1;

	addattr_nest_end(n, nest);
}

static void parse_nh_group_type(struct nlmsghdr *n, int maxlen, int *argcp,
				char ***argvp)
{
	char **argv = *argvp;
	int argc = *argcp;
	__u16 type;

	NEXT_ARG();
	type = read_nh_group_type(*argv);
	if (type > NEXTHOP_GRP_TYPE_MAX)
		invarg("\"type\" value is invalid\n", *argv);

	switch (type) {
	case NEXTHOP_GRP_TYPE_MPATH:
		/* No additional arguments */
		break;
	case NEXTHOP_GRP_TYPE_RES:
		parse_nh_group_type_res(n, maxlen, &argc, &argv);
		break;
	}

	*argcp = argc;
	*argvp = argv;

	addattr16(n, maxlen, NHA_GROUP_TYPE, type);
}

static int ipnh_parse_id(const char *argv)
{
	__u32 id;

	if (get_unsigned(&id, argv, 0))
		invarg("invalid id value", argv);
	return id;
}

static int ipnh_modify(int cmd, unsigned int flags, int argc, char **argv)
{
	struct {
		struct nlmsghdr	n;
		struct nhmsg	nhm;
		char		buf[1024];
	} req = {
		.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)),
		.n.nlmsg_flags = NLM_F_REQUEST | flags,
		.n.nlmsg_type = cmd,
		.nhm.nh_family = preferred_family,
	};
	__u32 nh_flags = 0;
	int ret;

	while (argc > 0) {
		if (!strcmp(*argv, "id")) {
			NEXT_ARG();
			addattr32(&req.n, sizeof(req), NHA_ID,
				  ipnh_parse_id(*argv));
		} else if (!strcmp(*argv, "dev")) {
			int ifindex;

			NEXT_ARG();
			ifindex = ll_name_to_index(*argv);
			if (!ifindex)
				invarg("Device does not exist\n", *argv);
			addattr32(&req.n, sizeof(req), NHA_OIF, ifindex);
			if (req.nhm.nh_family == AF_UNSPEC)
				req.nhm.nh_family = AF_INET;
		} else if (strcmp(*argv, "via") == 0) {
			inet_prefix addr;
			int family;

			NEXT_ARG();
			family = read_family(*argv);
			if (family == AF_UNSPEC)
				family = req.nhm.nh_family;
			else
				NEXT_ARG();
			get_addr(&addr, *argv, family);
			if (req.nhm.nh_family == AF_UNSPEC)
				req.nhm.nh_family = addr.family;
			else if (req.nhm.nh_family != addr.family)
				invarg("address family mismatch\n", *argv);
			addattr_l(&req.n, sizeof(req), NHA_GATEWAY,
				  &addr.data, addr.bytelen);
		} else if (strcmp(*argv, "encap") == 0) {
			char buf[1024];
			struct rtattr *rta = (void *)buf;

			rta->rta_type = NHA_ENCAP;
			rta->rta_len = RTA_LENGTH(0);

			lwt_parse_encap(rta, sizeof(buf), &argc, &argv,
					NHA_ENCAP, NHA_ENCAP_TYPE);

			if (rta->rta_len > RTA_LENGTH(0)) {
				addraw_l(&req.n, 1024, RTA_DATA(rta),
					 RTA_PAYLOAD(rta));
			}
		} else if (!strcmp(*argv, "blackhole")) {
			addattr_l(&req.n, sizeof(req), NHA_BLACKHOLE, NULL, 0);
			if (req.nhm.nh_family == AF_UNSPEC)
				req.nhm.nh_family = AF_INET;
		} else if (!strcmp(*argv, "fdb")) {
			addattr_l(&req.n, sizeof(req), NHA_FDB, NULL, 0);
		} else if (!strcmp(*argv, "onlink")) {
			nh_flags |= RTNH_F_ONLINK;
		} else if (!strcmp(*argv, "group")) {
			NEXT_ARG();

			if (add_nh_group_attr(&req.n, sizeof(req), *argv))
				invarg("\"group\" value is invalid\n", *argv);
		} else if (!strcmp(*argv, "type")) {
			parse_nh_group_type(&req.n, sizeof(req), &argc, &argv);
		} else if (matches(*argv, "protocol") == 0) {
			__u32 prot;

			NEXT_ARG();
			if (rtnl_rtprot_a2n(&prot, *argv))
				invarg("\"protocol\" value is invalid\n", *argv);
			req.nhm.nh_protocol = prot;
		} else if (strcmp(*argv, "help") == 0) {
			usage();
		} else {
			invarg("", *argv);
		}
		argc--; argv++;
	}

	req.nhm.nh_flags = nh_flags;

	if (echo_request)
		ret = rtnl_echo_talk(&rth, &req.n, json, print_nexthop_nocache);
	else
		ret = rtnl_talk(&rth, &req.n, NULL);

	if (ret)
		return -2;

	return 0;
}

static int ipnh_get_id(__u32 id)
{
	struct nlmsghdr *answer;

	if (__ipnh_get_id(&rth, id, &answer) < 0)
		return -2;

	new_json_obj(json);

	if (print_nexthop_nocache(answer, (void *)stdout) < 0) {
		delete_json_obj();
		free(answer);
		return -1;
	}

	delete_json_obj();
	fflush(stdout);

	free(answer);

	return 0;
}

static int ipnh_list_flush_id(__u32 id, int action)
{
	int err;

	if (action == IPNH_LIST)
		return ipnh_get_id(id);

	if (rtnl_open(&rth_del, 0) < 0) {
		fprintf(stderr, "Cannot open rtnetlink\n");
		return EXIT_FAILURE;
	}

	err = delete_nexthop(id);
	rtnl_close(&rth_del);

	return err;
}

static int ipnh_list_flush(int argc, char **argv, int action)
{
	unsigned int all = (argc == 0);

	while (argc > 0) {
		if (!matches(*argv, "dev")) {
			NEXT_ARG();
			filter.ifindex = ll_name_to_index(*argv);
			if (!filter.ifindex)
				invarg("Device does not exist\n", *argv);
		} else if (!matches(*argv, "groups")) {
			filter.groups = 1;
		} else if (!matches(*argv, "master")) {
			NEXT_ARG();
			filter.master = ll_name_to_index(*argv);
			if (!filter.master)
				invarg("Device does not exist\n", *argv);
		} else if (matches(*argv, "vrf") == 0) {
			NEXT_ARG();
			if (!name_is_vrf(*argv))
				invarg("Invalid VRF\n", *argv);
			filter.master = ll_name_to_index(*argv);
			if (!filter.master)
				invarg("VRF does not exist\n", *argv);
		} else if (!strcmp(*argv, "id")) {
			NEXT_ARG();
			return ipnh_list_flush_id(ipnh_parse_id(*argv), action);
		} else if (!matches(*argv, "protocol")) {
			__u32 proto;

			NEXT_ARG();
			if (get_unsigned(&proto, *argv, 0))
				invarg("invalid protocol value", *argv);
			filter.proto = proto;
		} else if (!matches(*argv, "fdb")) {
			filter.fdb = 1;
		} else if (matches(*argv, "help") == 0) {
			usage();
		} else {
			invarg("", *argv);
		}
		argc--; argv++;
	}

	if (action == IPNH_FLUSH)
		return ipnh_flush(all);

	if (rtnl_nexthopdump_req(&rth, preferred_family, nh_dump_filter) < 0) {
		perror("Cannot send dump request");
		return -2;
	}

	new_json_obj(json);

	if (rtnl_dump_filter(&rth, print_nexthop_nocache, stdout) < 0) {
		delete_json_obj();
		fprintf(stderr, "Dump terminated\n");
		return -2;
	}

	delete_json_obj();
	fflush(stdout);

	return 0;
}

static int ipnh_get(int argc, char **argv)
{
	__u32 id = 0;

	while (argc > 0) {
		if (!strcmp(*argv, "id")) {
			NEXT_ARG();
			id = ipnh_parse_id(*argv);
		} else  {
			usage();
		}
		argc--; argv++;
	}

	if (!id) {
		usage();
		return -1;
	}

	return ipnh_get_id(id);
}

static int ipnh_bucket_list(int argc, char **argv)
{
	while (argc > 0) {
		if (!matches(*argv, "dev")) {
			NEXT_ARG();
			filter.ifindex = ll_name_to_index(*argv);
			if (!filter.ifindex)
				invarg("Device does not exist\n", *argv);
		} else if (!matches(*argv, "master")) {
			NEXT_ARG();
			filter.master = ll_name_to_index(*argv);
			if (!filter.master)
				invarg("Device does not exist\n", *argv);
		} else if (matches(*argv, "vrf") == 0) {
			NEXT_ARG();
			if (!name_is_vrf(*argv))
				invarg("Invalid VRF\n", *argv);
			filter.master = ll_name_to_index(*argv);
			if (!filter.master)
				invarg("VRF does not exist\n", *argv);
		} else if (!strcmp(*argv, "id")) {
			NEXT_ARG();
			filter.id = ipnh_parse_id(*argv);
		} else if (!strcmp(*argv, "nhid")) {
			NEXT_ARG();
			filter.nhid = ipnh_parse_id(*argv);
		} else if (matches(*argv, "help") == 0) {
			usage();
		} else {
			invarg("", *argv);
		}
		argc--; argv++;
	}

	if (rtnl_nexthop_bucket_dump_req(&rth, preferred_family,
					 nh_dump_bucket_filter) < 0) {
		perror("Cannot send dump request");
		return -2;
	}

	new_json_obj(json);

	if (rtnl_dump_filter(&rth, print_nexthop_bucket, stdout) < 0) {
		delete_json_obj();
		fprintf(stderr, "Dump terminated\n");
		return -2;
	}

	delete_json_obj();
	fflush(stdout);

	return 0;
}

static int ipnh_bucket_get_id(__u32 id, __u16 bucket_index)
{
	struct {
		struct nlmsghdr	n;
		struct nhmsg	nhm;
		char		buf[1024];
	} req = {
		.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct nhmsg)),
		.n.nlmsg_flags = NLM_F_REQUEST,
		.n.nlmsg_type  = RTM_GETNEXTHOPBUCKET,
		.nhm.nh_family = preferred_family,
	};
	struct nlmsghdr *answer;
	struct rtattr *nest;

	addattr32(&req.n, sizeof(req), NHA_ID, id);

	nest = addattr_nest(&req.n, sizeof(req), NHA_RES_BUCKET);
	nest->rta_type |= NLA_F_NESTED;

	addattr16(&req.n, sizeof(req), NHA_RES_BUCKET_INDEX, bucket_index);

	addattr_nest_end(&req.n, nest);

	if (rtnl_talk(&rth, &req.n, &answer) < 0)
		return -2;

	new_json_obj(json);

	if (print_nexthop_bucket(answer, (void *)stdout) < 0) {
		delete_json_obj();
		free(answer);
		return -1;
	}

	delete_json_obj();
	fflush(stdout);

	free(answer);

	return 0;
}

static int ipnh_bucket_get(int argc, char **argv)
{
	bool bucket_valid = false;
	__u16 bucket_index;
	__u32 id = 0;

	while (argc > 0) {
		if (!strcmp(*argv, "id")) {
			NEXT_ARG();
			id = ipnh_parse_id(*argv);
		} else if (!strcmp(*argv, "index")) {
			NEXT_ARG();
			if (get_u16(&bucket_index, *argv, 0))
				invarg("invalid bucket index value", *argv);
			bucket_valid = true;
		} else  {
			usage();
		}
		argc--; argv++;
	}

	if (!id || !bucket_valid) {
		usage();
		return -1;
	}

	return ipnh_bucket_get_id(id, bucket_index);
}

static int do_ipnh_bucket(int argc, char **argv)
{
	if (argc < 1)
		return ipnh_bucket_list(0, NULL);

	if (!matches(*argv, "list") ||
	    !matches(*argv, "show") ||
	    !matches(*argv, "lst"))
		return ipnh_bucket_list(argc-1, argv+1);

	if (!matches(*argv, "get"))
		return ipnh_bucket_get(argc-1, argv+1);

	if (!matches(*argv, "help"))
		usage();

	fprintf(stderr,
		"Command \"%s\" is unknown, try \"ip nexthop help\".\n", *argv);
	exit(-1);
}

int do_ipnh(int argc, char **argv)
{
	if (argc < 1)
		return ipnh_list_flush(0, NULL, IPNH_LIST);

	if (!matches(*argv, "add"))
		return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_EXCL,
				   argc-1, argv+1);
	if (!matches(*argv, "replace"))
		return ipnh_modify(RTM_NEWNEXTHOP, NLM_F_CREATE|NLM_F_REPLACE,
				   argc-1, argv+1);
	if (!matches(*argv, "delete"))
		return ipnh_modify(RTM_DELNEXTHOP, 0, argc-1, argv+1);

	if (!matches(*argv, "list") ||
	    !matches(*argv, "show") ||
	    !matches(*argv, "lst"))
		return ipnh_list_flush(argc-1, argv+1, IPNH_LIST);

	if (!matches(*argv, "get"))
		return ipnh_get(argc-1, argv+1);

	if (!matches(*argv, "flush"))
		return ipnh_list_flush(argc-1, argv+1, IPNH_FLUSH);

	if (!matches(*argv, "bucket"))
		return do_ipnh_bucket(argc-1, argv+1);

	if (!matches(*argv, "help"))
		usage();

	fprintf(stderr,
		"Command \"%s\" is unknown, try \"ip nexthop help\".\n", *argv);
	exit(-1);
}
