Commit eedbc685 authored by Paolo Abeni's avatar Paolo Abeni Committed by David S. Miller
Browse files

selftests: add PM netlink functional tests



This introduces basic self-tests for the PM netlink,
checking the basic APIs and possible exceptional
values.

Signed-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
Signed-off-by: default avatarMat Martineau <mathew.j.martineau@linux.intel.com>
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parent 01cacb00
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
mptcp_connect
pm_nl_ctl
*.pcap
+4 −3
Original line number Diff line number Diff line
# SPDX-License-Identifier: GPL-2.0

top_srcdir = ../../../../..
KSFT_KHDR_INSTALL := 1

CFLAGS =  -Wall -Wl,--no-as-needed -O2 -g
CFLAGS =  -Wall -Wl,--no-as-needed -O2 -g  -I$(top_srcdir)/usr/include

TEST_PROGS := mptcp_connect.sh
TEST_PROGS := mptcp_connect.sh pm_netlink.sh

TEST_GEN_FILES = mptcp_connect
TEST_GEN_FILES = mptcp_connect pm_nl_ctl

TEST_FILES := settings

+130 −0
Original line number Diff line number Diff line
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0

ksft_skip=4
ret=0

usage() {
	echo "Usage: $0 [ -h ]"
}


while getopts "$optstring" option;do
	case "$option" in
	"h")
		usage $0
		exit 0
		;;
	"?")
		usage $0
		exit 1
		;;
	esac
done

sec=$(date +%s)
rndh=$(printf %x $sec)-$(mktemp -u XXXXXX)
ns1="ns1-$rndh"
err=$(mktemp)
ret=0

cleanup()
{
	rm -f $out
	ip netns del $ns1
}

ip -Version > /dev/null 2>&1
if [ $? -ne 0 ];then
	echo "SKIP: Could not run test without ip tool"
	exit $ksft_skip
fi

trap cleanup EXIT

ip netns add $ns1 || exit $ksft_skip
ip -net $ns1 link set lo up
ip netns exec $ns1 sysctl -q net.mptcp.enabled=1

check()
{
	local cmd="$1"
	local expected="$2"
	local msg="$3"
	local out=`$cmd 2>$err`
	local cmd_ret=$?

	printf "%-50s %s" "$msg"
	if [ $cmd_ret -ne 0 ]; then
		echo "[FAIL] command execution '$cmd' stderr "
		cat $err
		ret=1
	elif [ "$out" = "$expected" ]; then
		echo "[ OK ]"
	else
		echo -n "[FAIL] "
		echo "expected '$expected' got '$out'"
		ret=1
	fi
}

check "ip netns exec $ns1 ./pm_nl_ctl dump" "" "defaults addr list"
check "ip netns exec $ns1 ./pm_nl_ctl limits" "accept 0
subflows 0" "defaults limits"

ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.1
ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.2 flags subflow dev lo
ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.3 flags signal,backup
check "ip netns exec $ns1 ./pm_nl_ctl get 1" "id 1 flags  10.0.1.1 " "simple add/get addr"

check "ip netns exec $ns1 ./pm_nl_ctl dump" \
"id 1 flags  10.0.1.1
id 2 flags subflow dev lo 10.0.1.2
id 3 flags signal,backup 10.0.1.3 " "dump addrs"

ip netns exec $ns1 ./pm_nl_ctl del 2
check "ip netns exec $ns1 ./pm_nl_ctl get 2" "" "simple del addr"
check "ip netns exec $ns1 ./pm_nl_ctl dump" \
"id 1 flags  10.0.1.1
id 3 flags signal,backup 10.0.1.3 " "dump addrs after del"

ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.3
check "ip netns exec $ns1 ./pm_nl_ctl get 4" "" "duplicate addr"

ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.4 id 10 flags signal
check "ip netns exec $ns1 ./pm_nl_ctl get 4" "id 4 flags signal 10.0.1.4 " "id addr increment"

for i in `seq 5 9`; do
	ip netns exec $ns1 ./pm_nl_ctl add 10.0.1.$i flags signal >/dev/null 2>&1
done
check "ip netns exec $ns1 ./pm_nl_ctl get 9" "id 9 flags signal 10.0.1.9 " "hard addr limit"
check "ip netns exec $ns1 ./pm_nl_ctl get 10" "" "above hard addr limit"

for i in `seq 9 256`; do
	ip netns exec $ns1 ./pm_nl_ctl del $i
	ip netns exec $ns1 ./pm_nl_ctl add 10.0.0.9
done
check "ip netns exec $ns1 ./pm_nl_ctl dump" "id 1 flags  10.0.1.1
id 3 flags signal,backup 10.0.1.3
id 4 flags signal 10.0.1.4
id 5 flags signal 10.0.1.5
id 6 flags signal 10.0.1.6
id 7 flags signal 10.0.1.7
id 8 flags signal 10.0.1.8 " "id limit"

ip netns exec $ns1 ./pm_nl_ctl flush
check "ip netns exec $ns1 ./pm_nl_ctl dump" "" "flush addrs"

ip netns exec $ns1 ./pm_nl_ctl limits 9 1
check "ip netns exec $ns1 ./pm_nl_ctl limits" "accept 0
subflows 0" "rcv addrs above hard limit"

ip netns exec $ns1 ./pm_nl_ctl limits 1 9
check "ip netns exec $ns1 ./pm_nl_ctl limits" "accept 0
subflows 0" "subflows above hard limit"

ip netns exec $ns1 ./pm_nl_ctl limits 8 8
check "ip netns exec $ns1 ./pm_nl_ctl limits" "accept 8
subflows 8" "set limits"

exit $ret
+616 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0

#include <errno.h>
#include <error.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>

#include <arpa/inet.h>
#include <net/if.h>

#include <linux/rtnetlink.h>
#include <linux/genetlink.h>

#include "linux/mptcp.h"

#ifndef MPTCP_PM_NAME
#define MPTCP_PM_NAME		"mptcp_pm"
#endif

static void syntax(char *argv[])
{
	fprintf(stderr, "%s add|get|del|flush|dump|accept [<args>]\n", argv[0]);
	fprintf(stderr, "\tadd [flags signal|subflow|backup] [id <nr>] [dev <name>] <ip>\n");
	fprintf(stderr, "\tdel <id>\n");
	fprintf(stderr, "\tget <id>\n");
	fprintf(stderr, "\tflush\n");
	fprintf(stderr, "\tdump\n");
	fprintf(stderr, "\tlimits [<rcv addr max> <subflow max>]\n");
	exit(0);
}

static int init_genl_req(char *data, int family, int cmd, int version)
{
	struct nlmsghdr *nh = (void *)data;
	struct genlmsghdr *gh;
	int off = 0;

	nh->nlmsg_type = family;
	nh->nlmsg_flags = NLM_F_REQUEST;
	nh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
	off += NLMSG_ALIGN(sizeof(*nh));

	gh = (void *)(data + off);
	gh->cmd = cmd;
	gh->version = version;
	off += NLMSG_ALIGN(sizeof(*gh));
	return off;
}

static void nl_error(struct nlmsghdr *nh)
{
	struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nh);
	int len = nh->nlmsg_len - sizeof(*nh);
	uint32_t off;

	if (len < sizeof(struct nlmsgerr))
		error(1, 0, "netlink error message truncated %d min %ld", len,
		      sizeof(struct nlmsgerr));

	if (!err->error) {
		/* check messages from kernel */
		struct rtattr *attrs = (struct rtattr *)NLMSG_DATA(nh);

		while (RTA_OK(attrs, len)) {
			if (attrs->rta_type == NLMSGERR_ATTR_MSG)
				fprintf(stderr, "netlink ext ack msg: %s\n",
					(char *)RTA_DATA(attrs));
			if (attrs->rta_type == NLMSGERR_ATTR_OFFS) {
				memcpy(&off, RTA_DATA(attrs), 4);
				fprintf(stderr, "netlink err off %d\n",
					(int)off);
			}
			attrs = RTA_NEXT(attrs, len);
		}
	} else {
		fprintf(stderr, "netlink error %d", err->error);
	}
}

/* do a netlink command and, if max > 0, fetch the reply  */
static int do_nl_req(int fd, struct nlmsghdr *nh, int len, int max)
{
	struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
	socklen_t addr_len;
	void *data = nh;
	int rem, ret;
	int err = 0;

	nh->nlmsg_len = len;
	ret = sendto(fd, data, len, 0, (void *)&nladdr, sizeof(nladdr));
	if (ret != len)
		error(1, errno, "send netlink: %uB != %uB\n", ret, len);
	if (max == 0)
		return 0;

	addr_len = sizeof(nladdr);
	rem = ret = recvfrom(fd, data, max, 0, (void *)&nladdr, &addr_len);
	if (ret < 0)
		error(1, errno, "recv netlink: %uB\n", ret);

	/* Beware: the NLMSG_NEXT macro updates the 'rem' argument */
	for (; NLMSG_OK(nh, rem); nh = NLMSG_NEXT(nh, rem)) {
		if (nh->nlmsg_type == NLMSG_ERROR) {
			nl_error(nh);
			err = 1;
		}
	}
	if (err)
		error(1, 0, "bailing out due to netlink error[s]");
	return ret;
}

static int genl_parse_getfamily(struct nlmsghdr *nlh)
{
	struct genlmsghdr *ghdr = NLMSG_DATA(nlh);
	int len = nlh->nlmsg_len;
	struct rtattr *attrs;

	if (nlh->nlmsg_type != GENL_ID_CTRL)
		error(1, errno, "Not a controller message, len=%d type=0x%x\n",
		      nlh->nlmsg_len, nlh->nlmsg_type);

	len -= NLMSG_LENGTH(GENL_HDRLEN);

	if (len < 0)
		error(1, errno, "wrong controller message len %d\n", len);

	if (ghdr->cmd != CTRL_CMD_NEWFAMILY)
		error(1, errno, "Unknown controller command %d\n", ghdr->cmd);

	attrs = (struct rtattr *) ((char *) ghdr + GENL_HDRLEN);
	while (RTA_OK(attrs, len)) {
		if (attrs->rta_type == CTRL_ATTR_FAMILY_ID)
			return *(__u16 *)RTA_DATA(attrs);
		attrs = RTA_NEXT(attrs, len);
	}

	error(1, errno, "can't find CTRL_ATTR_FAMILY_ID attr");
	return -1;
}

static int resolve_mptcp_pm_netlink(int fd)
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	struct nlmsghdr *nh;
	struct rtattr *rta;
	int namelen;
	int off = 0;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, GENL_ID_CTRL, CTRL_CMD_GETFAMILY, 0);

	rta = (void *)(data + off);
	namelen = strlen(MPTCP_PM_NAME) + 1;
	rta->rta_type = CTRL_ATTR_FAMILY_NAME;
	rta->rta_len = RTA_LENGTH(namelen);
	memcpy(RTA_DATA(rta), MPTCP_PM_NAME, namelen);
	off += NLMSG_ALIGN(rta->rta_len);

	do_nl_req(fd, nh, off, sizeof(data));
	return genl_parse_getfamily((void *)data);
}

int add_addr(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	struct rtattr *rta, *nest;
	struct nlmsghdr *nh;
	u_int16_t family;
	u_int32_t flags;
	int nest_start;
	u_int8_t id;
	int off = 0;
	int arg;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, MPTCP_PM_CMD_ADD_ADDR,
			    MPTCP_PM_VER);

	if (argc < 3)
		syntax(argv);

	nest_start = off;
	nest = (void *)(data + off);
	nest->rta_type = NLA_F_NESTED | MPTCP_PM_ATTR_ADDR;
	nest->rta_len = RTA_LENGTH(0);
	off += NLMSG_ALIGN(nest->rta_len);

	/* addr data */
	rta = (void *)(data + off);
	if (inet_pton(AF_INET, argv[2], RTA_DATA(rta))) {
		family = AF_INET;
		rta->rta_type = MPTCP_PM_ADDR_ATTR_ADDR4;
		rta->rta_len = RTA_LENGTH(4);
	} else if (inet_pton(AF_INET6, argv[2], RTA_DATA(rta))) {
		family = AF_INET6;
		rta->rta_type = MPTCP_PM_ADDR_ATTR_ADDR6;
		rta->rta_len = RTA_LENGTH(16);
	} else
		error(1, errno, "can't parse ip %s", argv[2]);
	off += NLMSG_ALIGN(rta->rta_len);

	/* family */
	rta = (void *)(data + off);
	rta->rta_type = MPTCP_PM_ADDR_ATTR_FAMILY;
	rta->rta_len = RTA_LENGTH(2);
	memcpy(RTA_DATA(rta), &family, 2);
	off += NLMSG_ALIGN(rta->rta_len);

	for (arg = 3; arg < argc; arg++) {
		if (!strcmp(argv[arg], "flags")) {
			char *tok, *str;

			/* flags */
			flags = 0;
			if (++arg >= argc)
				error(1, 0, " missing flags value");

			/* do not support flag list yet */
			for (str = argv[arg]; (tok = strtok(str, ","));
			     str = NULL) {
				if (!strcmp(tok, "subflow"))
					flags |= MPTCP_PM_ADDR_FLAG_SUBFLOW;
				else if (!strcmp(tok, "signal"))
					flags |= MPTCP_PM_ADDR_FLAG_SIGNAL;
				else if (!strcmp(tok, "backup"))
					flags |= MPTCP_PM_ADDR_FLAG_BACKUP;
				else
					error(1, errno,
					      "unknown flag %s", argv[arg]);
			}

			rta = (void *)(data + off);
			rta->rta_type = MPTCP_PM_ADDR_ATTR_FLAGS;
			rta->rta_len = RTA_LENGTH(4);
			memcpy(RTA_DATA(rta), &flags, 4);
			off += NLMSG_ALIGN(rta->rta_len);
		} else if (!strcmp(argv[arg], "id")) {
			if (++arg >= argc)
				error(1, 0, " missing id value");

			id = atoi(argv[arg]);
			rta = (void *)(data + off);
			rta->rta_type = MPTCP_PM_ADDR_ATTR_ID;
			rta->rta_len = RTA_LENGTH(1);
			memcpy(RTA_DATA(rta), &id, 1);
			off += NLMSG_ALIGN(rta->rta_len);
		} else if (!strcmp(argv[arg], "dev")) {
			int32_t ifindex;

			if (++arg >= argc)
				error(1, 0, " missing dev name");

			ifindex = if_nametoindex(argv[arg]);
			if (!ifindex)
				error(1, errno, "unknown device %s", argv[arg]);

			rta = (void *)(data + off);
			rta->rta_type = MPTCP_PM_ADDR_ATTR_IF_IDX;
			rta->rta_len = RTA_LENGTH(4);
			memcpy(RTA_DATA(rta), &ifindex, 4);
			off += NLMSG_ALIGN(rta->rta_len);
		} else
			error(1, 0, "unknown keyword %s", argv[arg]);
	}
	nest->rta_len = off - nest_start;

	do_nl_req(fd, nh, off, 0);
	return 0;
}

int del_addr(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	struct rtattr *rta, *nest;
	struct nlmsghdr *nh;
	int nest_start;
	u_int8_t id;
	int off = 0;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, MPTCP_PM_CMD_DEL_ADDR,
			    MPTCP_PM_VER);

	/* the only argument is the address id */
	if (argc != 3)
		syntax(argv);

	id = atoi(argv[2]);

	nest_start = off;
	nest = (void *)(data + off);
	nest->rta_type = NLA_F_NESTED | MPTCP_PM_ATTR_ADDR;
	nest->rta_len =  RTA_LENGTH(0);
	off += NLMSG_ALIGN(nest->rta_len);

	/* build a dummy addr with only the ID set */
	rta = (void *)(data + off);
	rta->rta_type = MPTCP_PM_ADDR_ATTR_ID;
	rta->rta_len = RTA_LENGTH(1);
	memcpy(RTA_DATA(rta), &id, 1);
	off += NLMSG_ALIGN(rta->rta_len);
	nest->rta_len = off - nest_start;

	do_nl_req(fd, nh, off, 0);
	return 0;
}

static void print_addr(struct rtattr *attrs, int len)
{
	uint16_t family = 0;
	char str[1024];
	uint32_t flags;
	uint8_t id;

	while (RTA_OK(attrs, len)) {
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_FAMILY)
			memcpy(&family, RTA_DATA(attrs), 2);
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_ADDR4) {
			if (family != AF_INET)
				error(1, errno, "wrong IP (v4) for family %d",
				      family);
			inet_ntop(AF_INET, RTA_DATA(attrs), str, sizeof(str));
			printf("%s ", str);
		}
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_ADDR6) {
			if (family != AF_INET6)
				error(1, errno, "wrong IP (v6) for family %d",
				      family);
			inet_ntop(AF_INET6, RTA_DATA(attrs), str, sizeof(str));
			printf("%s ", str);
		}
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_ID) {
			memcpy(&id, RTA_DATA(attrs), 1);
			printf("id %d ", id);
		}
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_FLAGS) {
			memcpy(&flags, RTA_DATA(attrs), 4);

			printf("flags ");
			if (flags & MPTCP_PM_ADDR_FLAG_SIGNAL) {
				printf("signal");
				flags &= ~MPTCP_PM_ADDR_FLAG_SIGNAL;
				if (flags)
					printf(",");
			}

			if (flags & MPTCP_PM_ADDR_FLAG_SUBFLOW) {
				printf("subflow");
				flags &= ~MPTCP_PM_ADDR_FLAG_SUBFLOW;
				if (flags)
					printf(",");
			}

			if (flags & MPTCP_PM_ADDR_FLAG_BACKUP) {
				printf("backup");
				flags &= ~MPTCP_PM_ADDR_FLAG_BACKUP;
				if (flags)
					printf(",");
			}

			/* bump unknown flags, if any */
			if (flags)
				printf("0x%x", flags);
			printf(" ");
		}
		if (attrs->rta_type == MPTCP_PM_ADDR_ATTR_IF_IDX) {
			char name[IF_NAMESIZE], *ret;
			int32_t ifindex;

			memcpy(&ifindex, RTA_DATA(attrs), 4);
			ret = if_indextoname(ifindex, name);
			if (ret)
				printf("dev %s ", ret);
			else
				printf("dev unknown/%d", ifindex);
		}

		attrs = RTA_NEXT(attrs, len);
	}
	printf("\n");
}

static void print_addrs(struct nlmsghdr *nh, int pm_family, int total_len)
{
	struct rtattr *attrs;

	for (; NLMSG_OK(nh, total_len); nh = NLMSG_NEXT(nh, total_len)) {
		int len = nh->nlmsg_len;

		if (nh->nlmsg_type == NLMSG_DONE)
			break;
		if (nh->nlmsg_type == NLMSG_ERROR)
			nl_error(nh);
		if (nh->nlmsg_type != pm_family)
			continue;

		len -= NLMSG_LENGTH(GENL_HDRLEN);
		attrs = (struct rtattr *) ((char *) NLMSG_DATA(nh) +
					   GENL_HDRLEN);
		while (RTA_OK(attrs, len)) {
			if (attrs->rta_type ==
			    (MPTCP_PM_ATTR_ADDR | NLA_F_NESTED))
				print_addr((void *)RTA_DATA(attrs),
					   attrs->rta_len);
			attrs = RTA_NEXT(attrs, len);
		}
	}
}

int get_addr(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	struct rtattr *rta, *nest;
	struct nlmsghdr *nh;
	int nest_start;
	u_int8_t id;
	int off = 0;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, MPTCP_PM_CMD_GET_ADDR,
			    MPTCP_PM_VER);

	/* the only argument is the address id */
	if (argc != 3)
		syntax(argv);

	id = atoi(argv[2]);

	nest_start = off;
	nest = (void *)(data + off);
	nest->rta_type = NLA_F_NESTED | MPTCP_PM_ATTR_ADDR;
	nest->rta_len =  RTA_LENGTH(0);
	off += NLMSG_ALIGN(nest->rta_len);

	/* build a dummy addr with only the ID set */
	rta = (void *)(data + off);
	rta->rta_type = MPTCP_PM_ADDR_ATTR_ID;
	rta->rta_len = RTA_LENGTH(1);
	memcpy(RTA_DATA(rta), &id, 1);
	off += NLMSG_ALIGN(rta->rta_len);
	nest->rta_len = off - nest_start;

	print_addrs(nh, pm_family, do_nl_req(fd, nh, off, sizeof(data)));
	return 0;
}

int dump_addrs(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	pid_t pid = getpid();
	struct nlmsghdr *nh;
	int off = 0;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, MPTCP_PM_CMD_GET_ADDR,
			    MPTCP_PM_VER);
	nh->nlmsg_flags |= NLM_F_DUMP;
	nh->nlmsg_seq = 1;
	nh->nlmsg_pid = pid;
	nh->nlmsg_len = off;

	print_addrs(nh, pm_family, do_nl_req(fd, nh, off, sizeof(data)));
	return 0;
}

int flush_addrs(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	struct nlmsghdr *nh;
	int off = 0;

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, MPTCP_PM_CMD_FLUSH_ADDRS,
			    MPTCP_PM_VER);

	do_nl_req(fd, nh, off, 0);
	return 0;
}

static void print_limits(struct nlmsghdr *nh, int pm_family, int total_len)
{
	struct rtattr *attrs;
	uint32_t max;

	for (; NLMSG_OK(nh, total_len); nh = NLMSG_NEXT(nh, total_len)) {
		int len = nh->nlmsg_len;

		if (nh->nlmsg_type == NLMSG_DONE)
			break;
		if (nh->nlmsg_type == NLMSG_ERROR)
			nl_error(nh);
		if (nh->nlmsg_type != pm_family)
			continue;

		len -= NLMSG_LENGTH(GENL_HDRLEN);
		attrs = (struct rtattr *) ((char *) NLMSG_DATA(nh) +
					   GENL_HDRLEN);
		while (RTA_OK(attrs, len)) {
			int type = attrs->rta_type;

			if (type != MPTCP_PM_ATTR_RCV_ADD_ADDRS &&
			    type != MPTCP_PM_ATTR_SUBFLOWS)
				goto next;

			memcpy(&max, RTA_DATA(attrs), 4);
			printf("%s %u\n", type == MPTCP_PM_ATTR_SUBFLOWS ?
					  "subflows" : "accept", max);

next:
			attrs = RTA_NEXT(attrs, len);
		}
	}
}

int get_set_limits(int fd, int pm_family, int argc, char *argv[])
{
	char data[NLMSG_ALIGN(sizeof(struct nlmsghdr)) +
		  NLMSG_ALIGN(sizeof(struct genlmsghdr)) +
		  1024];
	uint32_t rcv_addr = 0, subflows = 0;
	int cmd, len = sizeof(data);
	struct nlmsghdr *nh;
	int off = 0;

	/* limit */
	if (argc == 4) {
		rcv_addr = atoi(argv[2]);
		subflows = atoi(argv[3]);
		cmd = MPTCP_PM_CMD_SET_LIMITS;
	} else {
		cmd = MPTCP_PM_CMD_GET_LIMITS;
	}

	memset(data, 0, sizeof(data));
	nh = (void *)data;
	off = init_genl_req(data, pm_family, cmd, MPTCP_PM_VER);

	/* limit */
	if (cmd == MPTCP_PM_CMD_SET_LIMITS) {
		struct rtattr *rta = (void *)(data + off);

		rta->rta_type = MPTCP_PM_ATTR_RCV_ADD_ADDRS;
		rta->rta_len = RTA_LENGTH(4);
		memcpy(RTA_DATA(rta), &rcv_addr, 4);
		off += NLMSG_ALIGN(rta->rta_len);

		rta = (void *)(data + off);
		rta->rta_type = MPTCP_PM_ATTR_SUBFLOWS;
		rta->rta_len = RTA_LENGTH(4);
		memcpy(RTA_DATA(rta), &subflows, 4);
		off += NLMSG_ALIGN(rta->rta_len);

		/* do not expect a reply */
		len = 0;
	}

	len = do_nl_req(fd, nh, off, len);
	if (cmd == MPTCP_PM_CMD_GET_LIMITS)
		print_limits(nh, pm_family, len);
	return 0;
}

int main(int argc, char *argv[])
{
	int fd, pm_family;

	if (argc < 2)
		syntax(argv);

	fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
	if (fd == -1)
		error(1, errno, "socket netlink");

	pm_family = resolve_mptcp_pm_netlink(fd);

	if (!strcmp(argv[1], "add"))
		return add_addr(fd, pm_family, argc, argv);
	else if (!strcmp(argv[1], "del"))
		return del_addr(fd, pm_family, argc, argv);
	else if (!strcmp(argv[1], "flush"))
		return flush_addrs(fd, pm_family, argc, argv);
	else if (!strcmp(argv[1], "get"))
		return get_addr(fd, pm_family, argc, argv);
	else if (!strcmp(argv[1], "dump"))
		return dump_addrs(fd, pm_family, argc, argv);
	else if (!strcmp(argv[1], "limits"))
		return get_set_limits(fd, pm_family, argc, argv);

	fprintf(stderr, "unknown sub-command: %s", argv[1]);
	syntax(argv);
	return 0;
}