// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-netlink.h"

#include <unistd.h>
#include <fcntl.h>

/*****************************************************************************/

#ifndef SOL_NETLINK
#define SOL_NETLINK 270
#endif

/*****************************************************************************/

#define NL_SOCK_PASSCRED        (1<<1)
#define NL_MSG_PEEK             (1<<3)
#define NL_MSG_PEEK_EXPLICIT    (1<<4)
#define NL_NO_AUTO_ACK          (1<<5)

#ifndef NETLINK_EXT_ACK
#define NETLINK_EXT_ACK         11
#endif

struct nl_msg {
	int                     nm_protocol;
	struct sockaddr_nl      nm_src;
	struct sockaddr_nl      nm_dst;
	struct ucred            nm_creds;
	struct nlmsghdr *       nm_nlh;
	size_t                  nm_size;
	bool                    nm_creds_has:1;
};

struct nl_sock {
	struct sockaddr_nl      s_local;
	struct sockaddr_nl      s_peer;
	int                     s_fd;
	int                     s_proto;
	unsigned int            s_seq_next;
	unsigned int            s_seq_expect;
	int                     s_flags;
	size_t                  s_bufsize;
};

/*****************************************************************************/

NM_UTILS_ENUM2STR_DEFINE (nl_nlmsgtype2str, int,
	NM_UTILS_ENUM2STR (NLMSG_NOOP,    "NOOP"),
	NM_UTILS_ENUM2STR (NLMSG_ERROR,   "ERROR"),
	NM_UTILS_ENUM2STR (NLMSG_DONE,    "DONE"),
	NM_UTILS_ENUM2STR (NLMSG_OVERRUN, "OVERRUN"),
);

NM_UTILS_FLAGS2STR_DEFINE (nl_nlmsg_flags2str, int,
	NM_UTILS_FLAGS2STR (NLM_F_REQUEST, "REQUEST"),
	NM_UTILS_FLAGS2STR (NLM_F_MULTI,   "MULTI"),
	NM_UTILS_FLAGS2STR (NLM_F_ACK,     "ACK"),
	NM_UTILS_FLAGS2STR (NLM_F_ECHO,    "ECHO"),
	NM_UTILS_FLAGS2STR (NLM_F_ROOT,    "ROOT"),
	NM_UTILS_FLAGS2STR (NLM_F_MATCH,   "MATCH"),
	NM_UTILS_FLAGS2STR (NLM_F_ATOMIC,  "ATOMIC"),
	NM_UTILS_FLAGS2STR (NLM_F_REPLACE, "REPLACE"),
	NM_UTILS_FLAGS2STR (NLM_F_EXCL,    "EXCL"),
	NM_UTILS_FLAGS2STR (NLM_F_CREATE,  "CREATE"),
	NM_UTILS_FLAGS2STR (NLM_F_APPEND,  "APPEND"),
);

/*****************************************************************************/

const char *
nl_nlmsghdr_to_str (const struct nlmsghdr *hdr, char *buf, gsize len)
{
	const char *b;
	const char *s;
	guint flags, flags_before;
	const char *prefix;

	if (!nm_utils_to_string_buffer_init_null (hdr, &buf, &len))
		return buf;

	b = buf;

	switch (hdr->nlmsg_type) {
	case RTM_GETLINK:    s = "RTM_GETLINK";     break;
	case RTM_NEWLINK:    s = "RTM_NEWLINK";     break;
	case RTM_DELLINK:    s = "RTM_DELLINK";     break;
	case RTM_SETLINK:    s = "RTM_SETLINK";     break;
	case RTM_GETADDR:    s = "RTM_GETADDR";     break;
	case RTM_NEWADDR:    s = "RTM_NEWADDR";     break;
	case RTM_DELADDR:    s = "RTM_DELADDR";     break;
	case RTM_GETROUTE:   s = "RTM_GETROUTE";    break;
	case RTM_NEWROUTE:   s = "RTM_NEWROUTE";    break;
	case RTM_DELROUTE:   s = "RTM_DELROUTE";    break;
	case RTM_GETRULE:    s = "RTM_GETRULE";     break;
	case RTM_NEWRULE:    s = "RTM_NEWRULE";     break;
	case RTM_DELRULE:    s = "RTM_DELRULE";     break;
	case RTM_GETQDISC:   s = "RTM_GETQDISC";    break;
	case RTM_NEWQDISC:   s = "RTM_NEWQDISC";    break;
	case RTM_DELQDISC:   s = "RTM_DELQDISC";    break;
	case RTM_GETTFILTER: s = "RTM_GETTFILTER";  break;
	case RTM_NEWTFILTER: s = "RTM_NEWTFILTER";  break;
	case RTM_DELTFILTER: s = "RTM_DELTFILTER";  break;
	case NLMSG_NOOP:     s = "NLMSG_NOOP";      break;
	case NLMSG_ERROR:    s = "NLMSG_ERROR";     break;
	case NLMSG_DONE:     s = "NLMSG_DONE";      break;
	case NLMSG_OVERRUN:  s = "NLMSG_OVERRUN";   break;
	default:             s = NULL;              break;
	}

	if (s)
		nm_utils_strbuf_append_str (&buf, &len, s);
	else
		nm_utils_strbuf_append (&buf, &len, "(%u)", (unsigned) hdr->nlmsg_type);

	flags = hdr->nlmsg_flags;

	if (!flags) {
		nm_utils_strbuf_append_str (&buf, &len, ", flags 0");
		goto flags_done;
	}

#define _F(f, n) \
	G_STMT_START { \
		if (NM_FLAGS_ALL (flags, f)) { \
			flags &= ~(f); \
			nm_utils_strbuf_append (&buf, &len, "%s%s", prefix, n); \
			if (!flags) \
				goto flags_done; \
			prefix = ","; \
		} \
	} G_STMT_END

	prefix = ", flags ";
	flags_before = flags;
	_F (NLM_F_REQUEST, "request");
	_F (NLM_F_MULTI, "multi");
	_F (NLM_F_ACK, "ack");
	_F (NLM_F_ECHO, "echo");
	_F (NLM_F_DUMP_INTR, "dump_intr");
	_F (0x20 /*NLM_F_DUMP_FILTERED*/, "dump_filtered");

	if (flags_before != flags)
		prefix = ";";

	switch (hdr->nlmsg_type) {
	case RTM_NEWLINK:
	case RTM_NEWADDR:
	case RTM_NEWROUTE:
	case RTM_NEWQDISC:
	case RTM_NEWTFILTER:
		_F (NLM_F_REPLACE, "replace");
		_F (NLM_F_EXCL, "excl");
		_F (NLM_F_CREATE, "create");
		_F (NLM_F_APPEND, "append");
		break;
	case RTM_GETLINK:
	case RTM_GETADDR:
	case RTM_GETROUTE:
	case RTM_DELQDISC:
	case RTM_DELTFILTER:
		_F (NLM_F_DUMP, "dump");
		_F (NLM_F_ROOT, "root");
		_F (NLM_F_MATCH, "match");
		_F (NLM_F_ATOMIC, "atomic");
		break;
	}

#undef _F

	if (flags_before != flags)
		prefix = ";";
	nm_utils_strbuf_append (&buf, &len, "%s0x%04x", prefix, flags);

flags_done:

	nm_utils_strbuf_append (&buf, &len, ", seq %u", (unsigned) hdr->nlmsg_seq);

	return b;
}

/*****************************************************************************/

struct nlmsghdr *
nlmsg_hdr (struct nl_msg *n)
{
	return n->nm_nlh;
}

void *
nlmsg_reserve (struct nl_msg *n, size_t len, int pad)
{
	char *buf = (char *) n->nm_nlh;
	size_t nlmsg_len = n->nm_nlh->nlmsg_len;
	size_t tlen;

	nm_assert (pad >= 0);

	if (len > n->nm_size)
		return NULL;

	tlen = pad ? ((len + (pad - 1)) & ~(pad - 1)) : len;

	if ((tlen + nlmsg_len) > n->nm_size)
		return NULL;

	buf += nlmsg_len;
	n->nm_nlh->nlmsg_len += tlen;

	if (tlen > len)
		memset (buf + len, 0, tlen - len);

	return buf;
}

/*****************************************************************************/

struct nlattr *
nla_reserve (struct nl_msg *msg, int attrtype, int attrlen)
{
	struct nlattr *nla;
	int tlen;

	if (attrlen < 0)
		return NULL;

	tlen = NLMSG_ALIGN (msg->nm_nlh->nlmsg_len) + nla_total_size (attrlen);

	if (tlen > msg->nm_size)
		return NULL;

	nla = (struct nlattr *) nlmsg_tail (msg->nm_nlh);
	nla->nla_type = attrtype;
	nla->nla_len = nla_attr_size (attrlen);

	if (attrlen)
		memset ((unsigned char *) nla + nla->nla_len, 0, nla_padlen (attrlen));
	msg->nm_nlh->nlmsg_len = tlen;

	return nla;
}

/*****************************************************************************/

struct nl_msg *
nlmsg_alloc_size (size_t len)
{
	struct nl_msg *nm;

	if (len < sizeof (struct nlmsghdr))
		len = sizeof (struct nlmsghdr);

	nm = g_slice_new (struct nl_msg);
	*nm = (struct nl_msg) {
		.nm_protocol = -1,
		.nm_size = len,
		.nm_nlh = g_malloc0 (len),
	};
	nm->nm_nlh->nlmsg_len = nlmsg_total_size (0);
	return nm;
}

/**
 * Allocate a new netlink message with the default maximum payload size.
 *
 * Allocates a new netlink message without any further payload. The
 * maximum payload size defaults to PAGESIZE or as otherwise specified
 * with nlmsg_set_default_size().
 *
 * @return Newly allocated netlink message or NULL.
 */
struct nl_msg *
nlmsg_alloc (void)
{
	return nlmsg_alloc_size (nm_utils_getpagesize ());
}

struct nl_msg *
nlmsg_alloc_convert (struct nlmsghdr *hdr)
{
	struct nl_msg *nm;

	nm = nlmsg_alloc_size (NLMSG_ALIGN (hdr->nlmsg_len));
	memcpy (nm->nm_nlh, hdr, hdr->nlmsg_len);
	return nm;
}

struct nl_msg *
nlmsg_alloc_simple (int nlmsgtype, int flags)
{
	struct nl_msg *nm;
	struct nlmsghdr *new;

	nm = nlmsg_alloc ();
	new = nm->nm_nlh;
	new->nlmsg_type = nlmsgtype;
	new->nlmsg_flags = flags;
	return nm;
}

void nlmsg_free (struct nl_msg *msg)
{
	if (!msg)
		return;

	g_free (msg->nm_nlh);
	g_slice_free (struct nl_msg, msg);
}

/*****************************************************************************/

int
nlmsg_append (struct nl_msg *n,
              const void *data,
              size_t len,
              int pad)
{
	void *tmp;

	nm_assert (n);
	nm_assert (data);
	nm_assert (len > 0);
	nm_assert (pad >= 0);

	tmp = nlmsg_reserve (n, len, pad);
	if (tmp == NULL)
		return -ENOMEM;

	memcpy (tmp, data, len);
	return 0;
}

/*****************************************************************************/

int
nlmsg_parse (struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[],
             int maxtype, const struct nla_policy *policy)
{
	if (!nlmsg_valid_hdr (nlh, hdrlen))
		return -NME_NL_MSG_TOOSHORT;

	return nla_parse (tb, maxtype, nlmsg_attrdata (nlh, hdrlen),
	                  nlmsg_attrlen (nlh, hdrlen), policy);
}

struct nlmsghdr *
nlmsg_put (struct nl_msg *n, uint32_t pid, uint32_t seq,
           int type, int payload, int flags)
{
	struct nlmsghdr *nlh;

	if (n->nm_nlh->nlmsg_len < NLMSG_HDRLEN)
		g_return_val_if_reached (NULL);

	nlh = (struct nlmsghdr *) n->nm_nlh;
	nlh->nlmsg_type = type;
	nlh->nlmsg_flags = flags;
	nlh->nlmsg_pid = pid;
	nlh->nlmsg_seq = seq;

	if (   payload > 0
	    && nlmsg_reserve (n, payload, NLMSG_ALIGNTO) == NULL)
		return NULL;

	return nlh;
}

size_t
nla_strlcpy (char *dst,
             const struct nlattr *nla,
             size_t dstsize)
{
	const char *src;
	size_t srclen;
	size_t len;

	/* - Always writes @dstsize bytes to @dst
	 * - Copies the first non-NUL characters to @dst.
	 *   Any characters after the first NUL bytes in @nla are ignored.
	 * - If the string @nla is longer than @dstsize, the string
	 *   gets truncated. @dst will always be NUL terminated. */

	if (G_UNLIKELY (dstsize <= 1)) {
		if (dstsize == 1)
			dst[0] = '\0';
		if (   nla
		    && (srclen = nla_len (nla)) > 0)
			return strnlen (nla_data (nla), srclen);
		return 0;
	}

	nm_assert (dst);

	if (nla) {
		srclen = nla_len (nla);
		if (srclen > 0) {
			src = nla_data (nla);
			srclen = strnlen (src, srclen);
			if (srclen > 0) {
				len = NM_MIN (dstsize - 1, srclen);
				memcpy (dst, src, len);
				memset (&dst[len], 0, dstsize - len);
				return srclen;
			}
		}
	}

	memset (dst, 0, dstsize);
	return 0;
}

size_t
nla_memcpy (void *dst, const struct nlattr *nla, size_t dstsize)
{
	size_t len;
	int srclen;

	if (!nla)
		return 0;

	srclen = nla_len (nla);

	if (srclen <= 0) {
		nm_assert (srclen == 0);
		return 0;
	}

	len = NM_MIN ((size_t) srclen, dstsize);
	if (len > 0) {
		/* there is a crucial difference between nla_strlcpy() and nla_memcpy().
		 * The former always write @dstsize bytes (akin to strncpy()), here, we only
		 * write the bytes that we actually have (leaving the remainder undefined). */
		memcpy (dst,
		        nla_data (nla),
		        len);
	}

	return srclen;
}

int
nla_put (struct nl_msg *msg, int attrtype, int datalen, const void *data)
{
	struct nlattr *nla;

	nla = nla_reserve (msg, attrtype, datalen);
	if (!nla) {
		if (datalen < 0)
			g_return_val_if_reached (-NME_BUG);

		return -ENOMEM;
	}

	if (datalen > 0)
		memcpy (nla_data (nla), data, datalen);

	return 0;
}

struct nlattr *
nla_find (const struct nlattr *head, int len, int attrtype)
{
	const struct nlattr *nla;
	int rem;

	nla_for_each_attr (nla, head, len, rem) {
		if (nla_type (nla) == attrtype)
			return (struct nlattr*)nla;
	}

	return NULL;
}

void
nla_nest_cancel (struct nl_msg *msg, const struct nlattr *attr)
{
	ssize_t len;

	len = (char *) nlmsg_tail (msg->nm_nlh) - (char *) attr;
	if (len < 0)
		g_return_if_reached ();
	else if (len > 0) {
		msg->nm_nlh->nlmsg_len -= len;
		memset (nlmsg_tail (msg->nm_nlh), 0, len);
	}
}

struct nlattr *
nla_nest_start (struct nl_msg *msg, int attrtype)
{
	struct nlattr *start = (struct nlattr *) nlmsg_tail (msg->nm_nlh);

	if (nla_put (msg, NLA_F_NESTED | attrtype, 0, NULL) < 0)
		return NULL;

	return start;
}

static int
_nest_end (struct nl_msg *msg, struct nlattr *start, int keep_empty)
{
	size_t pad, len;

	len = (char *) nlmsg_tail (msg->nm_nlh) - (char *) start;

	if (   len > USHRT_MAX
	    || (!keep_empty && len == NLA_HDRLEN)) {
		/*
		 * Max nlattr size exceeded or empty nested attribute, trim the
		 * attribute header again
		 */
		nla_nest_cancel (msg, start);

		/* Return error only if nlattr size was exceeded */
		return (len == NLA_HDRLEN) ? 0 : -NME_NL_ATTRSIZE;
	}

	start->nla_len = len;

	pad = NLMSG_ALIGN (msg->nm_nlh->nlmsg_len) - msg->nm_nlh->nlmsg_len;
	if (pad > 0) {
		/*
		 * Data inside attribute does not end at a alignment boundary.
		 * Pad accordingly and accoun for the additional space in
		 * the message. nlmsg_reserve() may never fail in this situation,
		 * the allocate message buffer must be a multiple of NLMSG_ALIGNTO.
		 */
		if (!nlmsg_reserve (msg, pad, 0))
			g_return_val_if_reached (-NME_BUG);
	}

	return 0;
}

int
nla_nest_end (struct nl_msg *msg, struct nlattr *start)
{
	return _nest_end (msg, start, 0);
}

static const uint16_t nla_attr_minlen[NLA_TYPE_MAX+1] = {
	[NLA_U8]        = sizeof (uint8_t),
	[NLA_U16]       = sizeof (uint16_t),
	[NLA_U32]       = sizeof (uint32_t),
	[NLA_U64]       = sizeof (uint64_t),
	[NLA_STRING]    = 1,
	[NLA_FLAG]      = 0,
};

static int
validate_nla (const struct nlattr *nla, int maxtype,
              const struct nla_policy *policy)
{
	const struct nla_policy *pt;
	unsigned int minlen = 0;
	int type = nla_type (nla);

	if (type < 0 || type > maxtype)
		return 0;

	pt = &policy[type];

	if (pt->type > NLA_TYPE_MAX)
		g_return_val_if_reached (-NME_BUG);

	if (pt->minlen)
		minlen = pt->minlen;
	else if (pt->type != NLA_UNSPEC)
		minlen = nla_attr_minlen[pt->type];

	if (nla_len (nla) < minlen)
		return -NME_UNSPEC;

	if (pt->maxlen && nla_len (nla) > pt->maxlen)
		return -NME_UNSPEC;

	if (pt->type == NLA_STRING) {
		const char *data;

		nm_assert (minlen > 0);

		data = nla_data (nla);
		if (data[nla_len (nla) - 1] != '\0')
			return -NME_UNSPEC;
	}

	return 0;
}

int
nla_parse (struct nlattr *tb[], int maxtype, struct nlattr *head, int len,
           const struct nla_policy *policy)
{
	struct nlattr *nla;
	int rem, nmerr;

	memset (tb, 0, sizeof (struct nlattr *) * (maxtype + 1));

	nla_for_each_attr (nla, head, len, rem) {
		int type = nla_type (nla);

		if (type > maxtype)
			continue;

		if (policy) {
			nmerr = validate_nla (nla, maxtype, policy);
			if (nmerr < 0)
				return nmerr;
		}

		tb[type] = nla;
	}

	return 0;
}

/*****************************************************************************/

int
nlmsg_get_proto (struct nl_msg *msg)
{
	return msg->nm_protocol;
}

void
nlmsg_set_proto (struct nl_msg *msg, int protocol)
{
	msg->nm_protocol = protocol;
}

void
nlmsg_set_src (struct nl_msg *msg, struct sockaddr_nl *addr)
{
	memcpy (&msg->nm_src, addr, sizeof (*addr));
}

struct ucred *
nlmsg_get_creds (struct nl_msg *msg)
{
	if (msg->nm_creds_has)
		return &msg->nm_creds;
	return NULL;
}

void
nlmsg_set_creds (struct nl_msg *msg, struct ucred *creds)
{
	if (creds) {
		memcpy (&msg->nm_creds, creds, sizeof (*creds));
		msg->nm_creds_has = TRUE;
	} else
		msg->nm_creds_has = FALSE;
}

/*****************************************************************************/

void *
genlmsg_put (struct nl_msg *msg, uint32_t port, uint32_t seq, int family,
             int hdrlen, int flags, uint8_t cmd, uint8_t version)
{
	struct nlmsghdr *nlh;
	struct genlmsghdr hdr = {
		.cmd = cmd,
		.version = version,
	};

	nlh = nlmsg_put (msg, port, seq, family, GENL_HDRLEN + hdrlen, flags);
	if (nlh == NULL)
		return NULL;

	memcpy (nlmsg_data (nlh), &hdr, sizeof (hdr));

	return (char *) nlmsg_data (nlh) + GENL_HDRLEN;
}

void *
genlmsg_data (const struct genlmsghdr *gnlh)
{
	return ((unsigned char *) gnlh + GENL_HDRLEN);
}

void *
genlmsg_user_hdr (const struct genlmsghdr *gnlh)
{
	return genlmsg_data (gnlh);
}

struct genlmsghdr *
genlmsg_hdr (struct nlmsghdr *nlh)
{
	return nlmsg_data (nlh);
}

void *
genlmsg_user_data (const struct genlmsghdr *gnlh, const int hdrlen)
{
	return (char *) genlmsg_user_hdr (gnlh) + NLMSG_ALIGN (hdrlen);
}

struct nlattr *
genlmsg_attrdata (const struct genlmsghdr *gnlh, int hdrlen)
{
	return genlmsg_user_data (gnlh, hdrlen);
}

int
genlmsg_len (const struct genlmsghdr *gnlh)
{
	const struct nlmsghdr *nlh;

	nlh = (const struct nlmsghdr *) ((const unsigned char *) gnlh - NLMSG_HDRLEN);
	return (nlh->nlmsg_len - GENL_HDRLEN - NLMSG_HDRLEN);
}

int
genlmsg_attrlen (const struct genlmsghdr *gnlh, int hdrlen)
{
	return genlmsg_len (gnlh) - NLMSG_ALIGN (hdrlen);
}

int
genlmsg_valid_hdr (struct nlmsghdr *nlh, int hdrlen)
{
	struct genlmsghdr *ghdr;

	if (!nlmsg_valid_hdr (nlh, GENL_HDRLEN))
		return 0;

	ghdr = nlmsg_data (nlh);
	if (genlmsg_len (ghdr) < NLMSG_ALIGN (hdrlen))
		return 0;

	return 1;
}

int
genlmsg_parse (struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[],
               int maxtype, const struct nla_policy *policy)
{
	struct genlmsghdr *ghdr;

	if (!genlmsg_valid_hdr (nlh, hdrlen))
		return -NME_NL_MSG_TOOSHORT;

	ghdr = nlmsg_data (nlh);
	return nla_parse (tb, maxtype, genlmsg_attrdata (ghdr, hdrlen),
	                  genlmsg_attrlen (ghdr, hdrlen), policy);
}

static int
_genl_parse_getfamily (struct nl_msg *msg, void *arg)
{
	static const struct nla_policy ctrl_policy[] = {
		[CTRL_ATTR_FAMILY_ID]    = { .type = NLA_U16 },
		[CTRL_ATTR_FAMILY_NAME]  = { .type = NLA_STRING,
		                             .maxlen = GENL_NAMSIZ },
		[CTRL_ATTR_VERSION]      = { .type = NLA_U32 },
		[CTRL_ATTR_HDRSIZE]      = { .type = NLA_U32 },
		[CTRL_ATTR_MAXATTR]      = { .type = NLA_U32 },
		[CTRL_ATTR_OPS]          = { .type = NLA_NESTED },
		[CTRL_ATTR_MCAST_GROUPS] = { .type = NLA_NESTED },
	};
	struct nlattr *tb[G_N_ELEMENTS (ctrl_policy)];
	struct nlmsghdr *nlh = nlmsg_hdr (msg);
	gint32 *response_data = arg;

	if (genlmsg_parse_arr (nlh, 0, tb, ctrl_policy) < 0)
		return NL_SKIP;

	if (tb[CTRL_ATTR_FAMILY_ID])
		*response_data = nla_get_u16 (tb[CTRL_ATTR_FAMILY_ID]);

	return NL_STOP;
}

int
genl_ctrl_resolve (struct nl_sock *sk, const char *name)
{
	nm_auto_nlmsg struct nl_msg *msg = NULL;
	int nmerr;
	gint32 response_data = -1;
	const struct nl_cb cb = {
		.valid_cb = _genl_parse_getfamily,
		.valid_arg = &response_data,
	};

	msg = nlmsg_alloc ();

	if (!genlmsg_put (msg, NL_AUTO_PORT, NL_AUTO_SEQ, GENL_ID_CTRL,
	                  0, 0, CTRL_CMD_GETFAMILY, 1))
		return -ENOMEM;

	nmerr = nla_put_string (msg, CTRL_ATTR_FAMILY_NAME, name);
	if (nmerr < 0)
		return nmerr;

	nmerr = nl_send_auto (sk, msg);
	if (nmerr < 0)
		return nmerr;

	nmerr = nl_recvmsgs (sk, &cb);
	if (nmerr < 0)
		return nmerr;

	/* If search was successful, request may be ACKed after data */
	nmerr = nl_wait_for_ack (sk, NULL);
	if (nmerr < 0)
		return nmerr;

	if (response_data < 0)
		return -NME_UNSPEC;

	return response_data;
}

/*****************************************************************************/

struct nl_sock *
nl_socket_alloc (void)
{
	struct nl_sock *sk;

	sk = g_slice_new0 (struct nl_sock);

	sk->s_fd = -1;
	sk->s_local.nl_family = AF_NETLINK;
	sk->s_peer.nl_family = AF_NETLINK;
	sk->s_seq_expect = sk->s_seq_next = time (NULL);

	return sk;
}

void
nl_socket_free (struct nl_sock *sk)
{
	if (!sk)
		return;

	if (sk->s_fd >= 0)
		nm_close (sk->s_fd);
	g_slice_free (struct nl_sock, sk);
}

int
nl_socket_get_fd (const struct nl_sock *sk)
{
	return sk->s_fd;
}

uint32_t
nl_socket_get_local_port (const struct nl_sock *sk)
{
	return sk->s_local.nl_pid;
}

size_t
nl_socket_get_msg_buf_size (struct nl_sock *sk)
{
	return sk->s_bufsize;
}

int
nl_socket_set_passcred (struct nl_sock *sk, int state)
{
	int err;

	if (sk->s_fd == -1)
		return -NME_NL_BAD_SOCK;

	err = setsockopt (sk->s_fd, SOL_SOCKET, SO_PASSCRED,
	                  &state, sizeof (state));
	if (err < 0)
		return -nm_errno_from_native (errno);

	if (state)
		sk->s_flags |= NL_SOCK_PASSCRED;
	else
		sk->s_flags &= ~NL_SOCK_PASSCRED;

	return 0;
}

int
nl_socket_set_msg_buf_size (struct nl_sock *sk, size_t bufsize)
{
	sk->s_bufsize = bufsize;

	return 0;
}

struct sockaddr_nl *
nlmsg_get_dst (struct nl_msg *msg)
{
	return &msg->nm_dst;
}

int
nl_socket_set_nonblocking (const struct nl_sock *sk)
{
	if (sk->s_fd == -1)
		return -NME_NL_BAD_SOCK;

	if (fcntl (sk->s_fd, F_SETFL, O_NONBLOCK) < 0)
		return -nm_errno_from_native (errno);

	return 0;
}

int
nl_socket_set_buffer_size (struct nl_sock *sk, int rxbuf, int txbuf)
{
	int err;

	if (rxbuf <= 0)
		rxbuf = 32768;

	if (txbuf <= 0)
		txbuf = 32768;

	if (sk->s_fd == -1)
		return -NME_NL_BAD_SOCK;

	err = setsockopt (sk->s_fd, SOL_SOCKET, SO_SNDBUF,
	                  &txbuf, sizeof (txbuf));
	if (err < 0) {
		return -nm_errno_from_native (errno);
	}

	err = setsockopt (sk->s_fd, SOL_SOCKET, SO_RCVBUF,
	                  &rxbuf, sizeof (rxbuf));
	if (err < 0) {
		return -nm_errno_from_native (errno);
	}

	return 0;
}

int
nl_socket_add_memberships (struct nl_sock *sk, int group, ...)
{
	int err;
	va_list ap;

	if (sk->s_fd == -1)
		return -NME_NL_BAD_SOCK;

	va_start (ap, group);

	while (group != 0) {
		if (group < 0) {
			va_end (ap);
			g_return_val_if_reached (-NME_BUG);
		}

		err = setsockopt (sk->s_fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
		                  &group, sizeof (group));
		if (err < 0) {
			int errsv = errno;

			va_end (ap);
			return -nm_errno_from_native (errsv);
		}

		group = va_arg (ap, int);
	}

	va_end (ap);

	return 0;
}

int
nl_socket_set_ext_ack (struct nl_sock *sk, gboolean enable)
{
	int err, val;

	if (sk->s_fd == -1)
		return -NME_NL_BAD_SOCK;

	val = !!enable;
	err = setsockopt (sk->s_fd, SOL_NETLINK, NETLINK_EXT_ACK, &val, sizeof (val));
	if (err < 0)
		return -nm_errno_from_native (errno);

	return 0;
}

void nl_socket_disable_msg_peek (struct nl_sock *sk)
{
	sk->s_flags |= NL_MSG_PEEK_EXPLICIT;
	sk->s_flags &= ~NL_MSG_PEEK;
}

int
nl_connect (struct nl_sock *sk, int protocol)
{
	int err, nmerr;
	socklen_t addrlen;
	struct sockaddr_nl local = { 0 };

	if (sk->s_fd != -1)
		return -NME_NL_BAD_SOCK;

	sk->s_fd = socket (AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
	if (sk->s_fd < 0) {
		nmerr = -nm_errno_from_native (errno);
		goto errout;
	}

	nmerr = nl_socket_set_buffer_size (sk, 0, 0);
	if (nmerr < 0)
		goto errout;

	nm_assert (sk->s_local.nl_pid == 0);

	err = bind (sk->s_fd, (struct sockaddr*) &sk->s_local,
	            sizeof (sk->s_local));
	if (err != 0) {
		nmerr = -nm_errno_from_native (errno);
		goto errout;
	}

	addrlen = sizeof (local);
	err = getsockname (sk->s_fd, (struct sockaddr *) &local,
	                   &addrlen);
	if (err < 0) {
		nmerr = -nm_errno_from_native (errno);
		goto errout;
	}

	if (addrlen != sizeof (local)) {
		nmerr = -NME_UNSPEC;
		goto errout;
	}

	if (local.nl_family != AF_NETLINK) {
		nmerr = -NME_UNSPEC;
		goto errout;
	}

	sk->s_local = local;
	sk->s_proto = protocol;

	return 0;

errout:
	if (sk->s_fd != -1) {
		close (sk->s_fd);
		sk->s_fd = -1;
	}
	return nmerr;
}

/*****************************************************************************/

static void
_cb_init (struct nl_cb *dst, const struct nl_cb *src)
{
	nm_assert (dst);

	if (src)
		*dst = *src;
	else
		memset (dst, 0, sizeof (*dst));
}

static int ack_wait_handler (struct nl_msg *msg, void *arg)
{
	return NL_STOP;
}

int
nl_wait_for_ack (struct nl_sock *sk,
                 const struct nl_cb *cb)
{
	struct nl_cb cb2;

	_cb_init (&cb2, cb);
	cb2.ack_cb = ack_wait_handler;
	return nl_recvmsgs (sk, &cb2);
}

#define NL_CB_CALL(cb, type, msg) \
do { \
	const struct nl_cb *_cb = (cb); \
	\
	if (_cb && _cb->type##_cb) { \
		/* the returned value here must be either a negative
		 * netlink error number, or one of NL_SKIP, NL_STOP, NL_OK. */ \
		nmerr = _cb->type##_cb ((msg), _cb->type##_arg); \
		switch (nmerr) { \
		case NL_OK: \
			nm_assert (nmerr == 0); \
			break; \
		case NL_SKIP: \
			goto skip; \
		case NL_STOP: \
			goto stop; \
		default: \
			if (nmerr >= 0) { \
				nm_assert_not_reached (); \
				nmerr = -NME_BUG; \
			} \
			goto out; \
		} \
	} \
} while (0)

int
nl_recvmsgs (struct nl_sock *sk, const struct nl_cb *cb)
{
	int n, nmerr = 0, multipart = 0, interrupted = 0, nrecv = 0;
	gs_free unsigned char *buf = NULL;
	struct nlmsghdr *hdr;
	struct sockaddr_nl nla = { 0 };
	struct ucred creds;
	gboolean creds_has;

continue_reading:
	n = nl_recv (sk, &nla, &buf, &creds, &creds_has);
	if (n <= 0)
		return n;

	hdr = (struct nlmsghdr *) buf;
	while (nlmsg_ok (hdr, n)) {
		nm_auto_nlmsg struct nl_msg *msg = NULL;

		msg = nlmsg_alloc_convert (hdr);

		nlmsg_set_proto (msg, sk->s_proto);
		nlmsg_set_src (msg, &nla);
		nlmsg_set_creds (msg, creds_has ? &creds : NULL);

		nrecv++;

		/* Only do sequence checking if auto-ack mode is enabled */
		if (! (sk->s_flags & NL_NO_AUTO_ACK)) {
			if (hdr->nlmsg_seq != sk->s_seq_expect) {
				nmerr = -NME_NL_SEQ_MISMATCH;
				goto out;
			}
		}

		if (hdr->nlmsg_type == NLMSG_DONE ||
		    hdr->nlmsg_type == NLMSG_ERROR ||
		    hdr->nlmsg_type == NLMSG_NOOP ||
		    hdr->nlmsg_type == NLMSG_OVERRUN) {
			/* We can't check for !NLM_F_MULTI since some netlink
			 * users in the kernel are broken. */
			sk->s_seq_expect++;
		}

		if (hdr->nlmsg_flags & NLM_F_MULTI)
			multipart = 1;

		if (hdr->nlmsg_flags & NLM_F_DUMP_INTR) {
			/*
			 * We have to continue reading to clear
			 * all messages until a NLMSG_DONE is
			 * received and report the inconsistency.
			 */
			interrupted = 1;
		}

		/* messages terminates a multipart message, this is
		 * usually the end of a message and therefore we slip
		 * out of the loop by default. the user may overrule
		 * this action by skipping this packet. */
		if (hdr->nlmsg_type == NLMSG_DONE) {
			multipart = 0;
			NL_CB_CALL (cb, finish, msg);
		}

		/* Message to be ignored, the default action is to
		 * skip this message if no callback is specified. The
		 * user may overrule this action by returning
		 * NL_PROCEED. */
		else if (hdr->nlmsg_type == NLMSG_NOOP)
			goto skip;

		/* Data got lost, report back to user. The default action is to
		 * quit parsing. The user may overrule this action by retuning
		 * NL_SKIP or NL_PROCEED (dangerous) */
		else if (hdr->nlmsg_type == NLMSG_OVERRUN) {
			nmerr = -NME_NL_MSG_OVERFLOW;
			goto out;
		}

		/* Message carries a nlmsgerr */
		else if (hdr->nlmsg_type == NLMSG_ERROR) {
			struct nlmsgerr *e = nlmsg_data (hdr);

			if (hdr->nlmsg_len < nlmsg_size (sizeof (*e))) {
				/* Truncated error message, the default action
				 * is to stop parsing. The user may overrule
				 * this action by returning NL_SKIP or
				 * NL_PROCEED (dangerous) */
				nmerr = -NME_NL_MSG_TRUNC;
				goto out;
			}
			if (e->error) {
				/* Error message reported back from kernel. */
				if (cb && cb->err_cb) {
					/* the returned value here must be either a negative
					 * netlink error number, or one of NL_SKIP, NL_STOP, NL_OK. */
					nmerr = cb->err_cb (&nla, e,
					                    cb->err_arg);
					if (nmerr < 0)
						goto out;
					else if (nmerr == NL_SKIP)
						goto skip;
					else if (nmerr == NL_STOP) {
						nmerr = -nm_errno_from_native (e->error);
						goto out;
					}
					nm_assert (nmerr == NL_OK);
				} else {
					nmerr = -nm_errno_from_native (e->error);
					goto out;
				}
			} else
				NL_CB_CALL (cb, ack, msg);
		} else {
			/* Valid message (not checking for MULTIPART bit to
			 * get along with broken kernels. NL_SKIP has no
			 * effect on this.  */
			NL_CB_CALL (cb, valid, msg);
		}
skip:
		nmerr = 0;
		hdr = nlmsg_next (hdr, &n);
	}

	if (multipart) {
		/* Multipart message not yet complete, continue reading */
		nm_clear_g_free (&buf);

		nmerr = 0;
		goto continue_reading;
	}

stop:
	nmerr = 0;

out:
	if (interrupted)
		nmerr = -NME_NL_DUMP_INTR;

	nm_assert (nmerr <= 0);
	return nmerr ?: nrecv;
}

int
nl_sendmsg (struct nl_sock *sk, struct nl_msg *msg, struct msghdr *hdr)
{
	int ret;

	if (sk->s_fd < 0)
		return -NME_NL_BAD_SOCK;

	nlmsg_set_src (msg, &sk->s_local);

	ret = sendmsg (sk->s_fd, hdr, 0);
	if (ret < 0)
		return -nm_errno_from_native (errno);

	return ret;
}

int
nl_send_iovec (struct nl_sock *sk, struct nl_msg *msg, struct iovec *iov, unsigned iovlen)
{
	struct sockaddr_nl *dst;
	struct ucred *creds;
	struct msghdr hdr = {
		.msg_name = (void *) &sk->s_peer,
		.msg_namelen = sizeof (struct sockaddr_nl),
		.msg_iov = iov,
		.msg_iovlen = iovlen,
	};
	char buf[CMSG_SPACE (sizeof (struct ucred))];

	/* Overwrite destination if specified in the message itself, defaults
	 * to the peer address of the socket.
	 */
	dst = nlmsg_get_dst (msg);
	if (dst->nl_family == AF_NETLINK)
		hdr.msg_name = dst;

	/* Add credentials if present. */
	creds = nlmsg_get_creds (msg);
	if (creds != NULL) {
		struct cmsghdr *cmsg;

		hdr.msg_control = buf;
		hdr.msg_controllen = sizeof (buf);

		cmsg = CMSG_FIRSTHDR (&hdr);
		cmsg->cmsg_level = SOL_SOCKET;
		cmsg->cmsg_type = SCM_CREDENTIALS;
		cmsg->cmsg_len = CMSG_LEN (sizeof (struct ucred));
		memcpy (CMSG_DATA (cmsg), creds, sizeof (struct ucred));
	}

	return nl_sendmsg (sk, msg, &hdr);
}

void
nl_complete_msg (struct nl_sock *sk, struct nl_msg *msg)
{
	struct nlmsghdr *nlh;

	nlh = nlmsg_hdr (msg);
	if (nlh->nlmsg_pid == NL_AUTO_PORT)
		nlh->nlmsg_pid = nl_socket_get_local_port (sk);

	if (nlh->nlmsg_seq == NL_AUTO_SEQ)
		nlh->nlmsg_seq = sk->s_seq_next++;

	if (msg->nm_protocol == -1)
		msg->nm_protocol = sk->s_proto;

	nlh->nlmsg_flags |= NLM_F_REQUEST;

	if (!(sk->s_flags & NL_NO_AUTO_ACK))
		nlh->nlmsg_flags |= NLM_F_ACK;
}

int
nl_send (struct nl_sock *sk, struct nl_msg *msg)
{
	struct iovec iov = {
		.iov_base = (void *) nlmsg_hdr (msg),
		.iov_len = nlmsg_hdr (msg)->nlmsg_len,
	};

	return nl_send_iovec (sk, msg, &iov, 1);
}

int nl_send_auto (struct nl_sock *sk, struct nl_msg *msg)
{
	nl_complete_msg (sk, msg);

	return nl_send (sk, msg);
}

int
nl_recv (struct nl_sock *sk,
         struct sockaddr_nl *nla,
         unsigned char **buf,
         struct ucred *out_creds,
         gboolean *out_creds_has)
{
	ssize_t n;
	int flags = 0;
	struct iovec iov;
	struct msghdr msg = {
		.msg_name = (void *) nla,
		.msg_namelen = sizeof (struct sockaddr_nl),
		.msg_iov = &iov,
		.msg_iovlen = 1,
	};
	struct ucred tmpcreds;
	gboolean tmpcreds_has = FALSE;
	int retval;
	int errsv;

	nm_assert (nla);
	nm_assert (buf && !*buf);
	nm_assert (!out_creds_has == !out_creds);

	if (   (sk->s_flags & NL_MSG_PEEK)
	    || (   !(sk->s_flags & NL_MSG_PEEK_EXPLICIT)
	        && sk->s_bufsize == 0))
		flags |= MSG_PEEK | MSG_TRUNC;

	iov.iov_len =    sk->s_bufsize
	              ?: (((size_t) nm_utils_getpagesize ()) * 4u);
	iov.iov_base = g_malloc (iov.iov_len);

	if (   out_creds
	    && (sk->s_flags & NL_SOCK_PASSCRED)) {
		msg.msg_controllen = CMSG_SPACE (sizeof (struct ucred));
		msg.msg_control = g_malloc (msg.msg_controllen);
	}

retry:
	n = recvmsg (sk->s_fd, &msg, flags);
	if (!n) {
		retval = 0;
		goto abort;
	}

	if (n < 0) {
		errsv = errno;
		if (errsv == EINTR)
			goto retry;
		retval = -nm_errno_from_native (errsv);
		goto abort;
	}

	if (msg.msg_flags & MSG_CTRUNC) {
		if (msg.msg_controllen == 0) {
			retval = -NME_NL_MSG_TRUNC;
			goto abort;
		}

		msg.msg_controllen *= 2;
		msg.msg_control = g_realloc (msg.msg_control, msg.msg_controllen);
		goto retry;
	}

	if (   iov.iov_len < n
	    || (msg.msg_flags & MSG_TRUNC)) {
		/* respond with error to an incomplete message */
		if (flags == 0) {
			retval = -NME_NL_MSG_TRUNC;
			goto abort;
		}

		/* Provided buffer is not long enough, enlarge it
		 * to size of n (which should be total length of the message)
		 * and try again. */
		iov.iov_base = g_realloc (iov.iov_base, n);
		iov.iov_len = n;
		flags = 0;
		goto retry;
	}

	if (flags != 0) {
		/* Buffer is big enough, do the actual reading */
		flags = 0;
		goto retry;
	}

	if (msg.msg_namelen != sizeof (struct sockaddr_nl)) {
		retval =  -NME_UNSPEC;
		goto abort;
	}

	if (out_creds && (sk->s_flags & NL_SOCK_PASSCRED)) {
		struct cmsghdr *cmsg;

		for (cmsg = CMSG_FIRSTHDR (&msg); cmsg; cmsg = CMSG_NXTHDR (&msg, cmsg)) {
			if (cmsg->cmsg_level != SOL_SOCKET)
				continue;
			if (cmsg->cmsg_type != SCM_CREDENTIALS)
				continue;
			memcpy (&tmpcreds, CMSG_DATA (cmsg), sizeof (tmpcreds));
			tmpcreds_has = TRUE;
			break;
		}
	}

	retval = n;

abort:
	g_free (msg.msg_control);

	if (retval <= 0) {
		g_free (iov.iov_base);
		return retval;
	}

	*buf = iov.iov_base;
	if (out_creds && tmpcreds_has)
		*out_creds = tmpcreds;
	NM_SET_OUT (out_creds_has, tmpcreds_has);
	return retval;
}
