git: f2c8381fce9b - main - netlink: add snl(3) - simple netlink library

From: Alexander V. Chernikov <melifaro_at_FreeBSD.org>
Date: Fri, 23 Dec 2022 15:03:19 UTC
The branch main has been updated by melifaro:

URL: https://cgit.FreeBSD.org/src/commit/?id=f2c8381fce9b87695ea448591e4412cbed38aa77

commit f2c8381fce9b87695ea448591e4412cbed38aa77
Author:     Alexander V. Chernikov <melifaro@FreeBSD.org>
AuthorDate: 2022-12-18 17:34:41 +0000
Commit:     Alexander V. Chernikov <melifaro@FreeBSD.org>
CommitDate: 2022-12-23 15:03:13 +0000

    netlink: add snl(3) - simple netlink library
    
    Reviewed by:    bapt, pauamma
    Differential Revision: https://reviews.freebsd.org/D37736
---
 share/man/man3/snl.3            | 303 ++++++++++++++++++++++++++++
 sys/netlink/netlink_snl.h       | 435 ++++++++++++++++++++++++++++++++++++++++
 sys/netlink/netlink_snl_route.h | 128 ++++++++++++
 tests/sys/netlink/Makefile      |   3 +-
 tests/sys/netlink/test_snl.c    |  92 +++++++++
 5 files changed, 959 insertions(+), 2 deletions(-)

diff --git a/share/man/man3/snl.3 b/share/man/man3/snl.3
new file mode 100644
index 000000000000..9fdeeaf56178
--- /dev/null
+++ b/share/man/man3/snl.3
@@ -0,0 +1,303 @@
+.\"
+.\" Copyright (C) 2022 Alexander Chernikov <melifaro@FreeBSD.org>.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\" $FreeBSD$
+.Dd December 16, 2022
+.Dt SNL 3
+.Os
+.Sh NAME
+.Nm snl_init ,
+.Nm snl_free ,
+.Nm snl_read_message ,
+.Nm snl_send ,
+.Nm snl_get_seq ,
+.Nm snl_allocz ,
+.Nm snl_clear_lb ,
+.Nm snl_parse_nlmsg ,
+.Nm snl_parse_header ,
+.Nm snl_parse_attrs ,
+.Nm snl_parse_attrs_raw ,
+.Nm snl_attr_get_flag ,
+.Nm snl_attr_get_ip ,
+.Nm snl_attr_get_uint16 ,
+.Nm snl_attr_get_uint32 ,
+.Nm snl_attr_get_string ,
+.Nm snl_attr_get_stringn ,
+.Nm snl_attr_get_nla ,
+.Nm snl_field_get_uint8 ,
+.Nm snl_field_get_uint16 ,
+.Nm snl_field_get_uint32
+.Nd "simple netlink library"
+.Sh SYNOPSIS
+.In netlink/netlink_snl.h
+.In netlink/netlink_snl_route.h
+.Ft "bool"
+.Fn snl_init "struct snl_state *ss" "int netlink_family"
+.Fn snl_free "struct snl_state *ss"
+.Ft "struct nlmsghdr *"
+.Fn snl_read_message "struct snl_state *ss"
+.Ft "bool"
+.Fn snl_send "struct snl_state *ss" "void *data" "int sz"
+.Ft "uint32_t"
+.Fn snl_get_seq "struct snl_state *ss"
+.Ft "void *"
+.Fn snl_allocz "struct snl_state *ss" "int len"
+.Fn snl_clear_lb "struct snl_state *ss"
+.Ft "bool"
+.Fn snl_parse_nlmsg "struct snl_state *ss" "struct nlmsghdr *hdr" "const struct snl_hdr_parser *ps" "void *target"
+.Ft "bool"
+.Fn snl_parse_header "struct snl_state *ss" "void *hdr" "int len" "const struct snl_hdr_parser *ps" "int pslen" "void *target"
+.Ft "bool"
+.Fn snl_parse_attrs "struct snl_state *ss" "struct nlmsghdr *hdr" "int hdrlen" "const struct snl_attr_parser *ps" "int pslen" "void *target"
+.Ft "bool"
+.Fn snl_parse_attrs_raw "struct snl_state *ss" "struct nlattr *nla_head" "int len" "const struct snl_attr_parser *ps" "int pslen" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_flag "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_uint16 "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_uint32 "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_string "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_stringn "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_nla "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_ip "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Ft "bool"
+.Fn snl_attr_get_ipvia "struct snl_state *ss" "struct nlattr *nla" "void *target"
+.Sh DESCRIPTION
+The
+.Xr snl 3
+library provides an easy way of sending and receiving Netlink messages,
+taking care of serialisation and deserialisation.
+.Ss INITIALISATION
+Call
+.Fn snl_init
+with a pointer to the
+.Dv struct snl_state
+and the desired Netlink family to initialise the library instance.
+To free the library instance, call
+.Fn snl_free .
+.Pp
+The library functions are NOT multithread-safe.
+If multithreading is desired, consider initializing an instance
+per thread.
+.Ss MEMORY ALLOCATION
+The library uses pre-allocated extendable memory buffers to handle message parsing.
+The typical usage pattern is to allocate the necessary data structures during the
+message parsing or writing process via
+.Fn snl_allocz
+and free all allocated data at once using
+.Fn snl_clear_lb
+after handling the message.
+.Ss COMPOSING AND SENDING MESSAGES
+The library does not currently offer any wrappers for writing netlink messages.
+Simple request messages can be composed by filling in all needed fields directly.
+Example for constructing an interface dump request:
+.Bd -literal
+	struct {
+		struct nlmsghdr hdr;
+		struct ifinfomsg ifmsg;
+	} msg = {
+		.hdr.nlmsg_type = RTM_GETLINK,
+		.hdr.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST,
+		.hdr.nlmsg_seq = snl_get_seq(ss),
+	};
+	msg.hdr.nlmsg_len = sizeof(msg);
+.Ed
+.Fn snl_get_seq
+can be used to generate a unique message number.
+To send the resulting message,
+.Fn snl_send
+can be used.
+.Ss RECEIVING AND PARSING MESSAGES
+To receive a message, use
+.Fn snl_read_message .
+Currently, this call is blocking.
+.Pp
+The library provides an easy way to convert the message to the pre-defined C
+structure.
+For each message type, one needs to define rules, converting the protocol header
+fields and the desired attributes to the specified structure.
+It can be accomplished by using message parsers.
+Each message parser consists of an array of attribute getters and an array of
+header field getters.
+The former array needs to be sorted by the attribute type.
+There is a
+.Fn SNL_VERIFY_PARSERS
+macro to check if the order is correct.
+.Fn SNL_DECLARE_PARSER "parser_name" "family header type" "struct snl_field_parser[]" "struct snl_attr_parser[]"
+can be used to create a new parser.
+.Fn SNL_DECLARE_ATTR_PARSER "parser_name" "struct snl_field_parser[]"
+can be used to create an attribute-only message parser.
+.Pp
+Each attribute getter needs to be embedded in the following structure:
+.Bd -literal
+typedef bool snl_parse_attr_f(struct snl_state *ss, struct nlattr *attr, const void *arg, void *target);
+struct snl_attr_parser {
+	uint16_t		type;	/* Attribute type */
+	uint16_t		off;	/* field offset in the target structure */
+	snl_parse_attr_f	*cb;	/* getter function to call */
+	const void		*arg;	/* getter function custom argument */
+};
+.Ed
+The generic attribute getter has the following signature:
+.Ft "bool"
+.Fn snl_attr_get_<type> "struct snl_state *ss" "struct nlattr *nla" "const void *arg" "void *target" .
+nla contains the pointer of the attribute to use as the datasource.
+The target field is the pointer to the field in the target structure.
+It is up to the getter to know the type of the target field.
+The getter must check the input attribute and return
+false if the attribute is not formed correctly.
+Otherwise, the getter fetches the attribute value and stores it in the target,
+then returns true.
+It is possible to use
+.Fn snl_allocz
+to create the desired data structure .
+A number of predefined getters for the common data types exist.
+.Fn snl_attr_get_flag
+converts a flag-type attribute to an uint8_t value of 1 or 0, depending on the
+attribute presence.
+.Fn snl_attr_get_uint16
+stores a uint16_t type attribute into the uint16_t target field.
+.Fn snl_attr_get_uint32
+stores a uint32_t type attribute into the uint32_t target field.
+.Fn snl_attr_get_ip
+and
+.Fn snl_attr_get_ipvia
+stores a pointer to the sockaddr structure with the IPv4/IPv6 address contained
+in the attribute.
+Sockaddr is allocated using
+.Fn snl_allocz .
+.Fn snl_attr_get_string
+stores a pointer to the NULL-terminated string.
+The string itself is allocated using
+.Fn snl_allocz .
+.Fn snl_attr_get_nla
+stores a pointer to the specified attribute.
+.Fn snl_attr_get_stringn
+stores a pointer to the non-NULL-terminated string.
+.Pp
+Similarly, each family header getter needs to be embedded in the following structure:
+.Bd -literal
+typedef void snl_parse_field_f(struct snl_state *ss, void *hdr, void *target);
+struct snl_field_parser {
+	uint16_t		off_in;	/* field offset in the input structure */
+	uint16_t                off_out;/* field offset in the target structure */
+	snl_parse_field_f       *cb;	/* getter function to call */
+};
+.Ed
+The generic field getter has the following signature:
+.Ft "void"
+snl_field_get_<type> "struct snl_state *ss" "void *src" "void *target" .
+A number of pre-defined getters for the common data types exist.
+.Fn "snl_field_get_uint8"
+fetches an uint8_t value and stores it in the target.
+.Fn "snl_field_get_uint16"
+fetches an uint8_t value and stores it in the target.
+.Fn "snl_field_get_uint32"
+fetches an uint32_t value and stores it in the target.
+.Sh EXAMPLES
+The following example demonstrates how to list all system interfaces
+using netlink.
+.Bd -literal
+#include <stdio.h>
+
+#include <netlink/netlink.h>
+#include <netlink/netlink_route.h>
+#include "netlink/netlink_snl.h"
+#include "netlink/netlink_snl_route.h"
+
+struct nl_parsed_link {
+	uint32_t		ifi_index;
+	uint32_t		ifla_mtu;
+	char			*ifla_ifname;
+};
+
+#define	_IN(_field)	offsetof(struct ifinfomsg, _field)
+#define	_OUT(_field)	offsetof(struct nl_parsed_link, _field)
+static const struct snl_attr_parser ap_link[] = {
+	{ .type = IFLA_IFNAME, .off = _OUT(ifla_ifname), .cb = snl_attr_get_string },
+	{ .type = IFLA_MTU, .off = _OUT(ifla_mtu), .cb = snl_attr_get_uint32 },
+};
+static const struct snl_field_parser fp_link[] = {
+	{.off_in = _IN(ifi_index), .off_out = _OUT(ifi_index), .cb = snl_field_get_uint32 },
+};
+#undef _IN
+#undef _OUT
+SNL_DECLARE_PARSER(link_parser, struct ifinfomsg, fp_link, ap_link);
+
+
+int
+main(int ac, char *argv[])
+{
+	struct snl_state ss;
+
+	if (!snl_init(&ss, NETLINK_ROUTE))
+		return (1);
+
+	struct {
+		struct nlmsghdr hdr;
+		struct ifinfomsg ifmsg;
+	} msg = {
+		.hdr.nlmsg_type = RTM_GETLINK,
+		.hdr.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST,
+		.hdr.nlmsg_seq = snl_get_seq(&ss),
+	};
+	msg.hdr.nlmsg_len = sizeof(msg);
+
+	if (!snl_send(&ss, &msg, sizeof(msg))) {
+		snl_free(&ss);
+		return (1);
+	}
+
+	struct nlmsghdr *hdr;
+	while ((hdr = snl_read_message(&ss)) != NULL && hdr->nlmsg_type != NLMSG_DONE) {
+		if (hdr->nlmsg_seq != msg.hdr.nlmsg_seq)
+			break;
+
+		struct nl_parsed_link link = {};
+		if (!snl_parse_nlmsg(&ss, hdr, &link_parser, &link))
+			continue;
+		printf("Link#%u %s mtu %u\n", link.ifi_index, link.ifla_ifname, link.ifla_mtu);
+	}
+
+	return (0);
+}
+.Ed
+.Sh SEE ALSO
+.Xr genetlink 4 ,
+.Xr netlink 4 ,
+and
+.Xr rtnetlink 4
+.Sh HISTORY
+The
+.Dv SNL
+library appeared in
+.Fx 14.0 .
+.Sh AUTHORS
+This library was implemented by
+.An Alexander Chernikov Aq Mt melifaro@FreeBSD.org .
diff --git a/sys/netlink/netlink_snl.h b/sys/netlink/netlink_snl.h
new file mode 100644
index 000000000000..4a137b4e5d08
--- /dev/null
+++ b/sys/netlink/netlink_snl.h
@@ -0,0 +1,435 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2022 Alexander V. Chernikov <melifaro@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#ifndef	_NETLINK_NETLINK_SNL_H_
+#define	_NETLINK_NETLINK_SNL_H_
+
+/*
+ * Simple Netlink Library
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+
+#define _roundup2(x, y)         (((x)+((y)-1))&(~((y)-1)))
+
+#define NETLINK_ALIGN_SIZE      sizeof(uint32_t)
+#define NETLINK_ALIGN(_len)     _roundup2(_len, NETLINK_ALIGN_SIZE)
+
+#define NLA_ALIGN_SIZE          sizeof(uint32_t)
+#define	NLA_HDRLEN		((int)sizeof(struct nlattr))
+#define	NLA_DATA_LEN(_nla)	((int)((_nla)->nla_len - NLA_HDRLEN))
+#define	NLA_DATA(_nla)		NL_ITEM_DATA(_nla, NLA_HDRLEN)
+#define	NLA_DATA_CONST(_nla)	NL_ITEM_DATA_CONST(_nla, NLA_HDRLEN)
+
+#define	NLA_TYPE(_nla)		((_nla)->nla_type & 0x3FFF)
+
+#define NLA_NEXT(_attr) (struct nlattr *)((char *)_attr + NLA_ALIGN(_attr->nla_len))
+
+#define	_NLA_END(_start, _len)	((char *)(_start) + (_len))
+#define NLA_FOREACH(_attr, _start, _len)      \
+        for (_attr = (_start);		\
+		((char *)_attr < _NLA_END(_start, _len)) && \
+		((char *)NLA_NEXT(_attr) <= _NLA_END(_start, _len));	\
+		_attr =  NLA_NEXT(_attr))
+
+#define	NL_ARRAY_LEN(_a)	(sizeof(_a) / sizeof((_a)[0]))
+
+struct linear_buffer {
+	char		*base;	/* Base allocated memory pointer */
+	uint32_t	offset;	/* Currently used offset */
+	uint32_t	size;	/* Total buffer size */
+};
+
+static inline char *
+lb_allocz(struct linear_buffer *lb, int len)
+{
+	len = roundup2(len, sizeof(uint64_t));
+	if (lb->offset + len > lb->size)
+		return (NULL);
+	void *data = (void *)(lb->base + lb->offset);
+	lb->offset += len;
+	return (data);
+}
+
+static inline void
+lb_clear(struct linear_buffer *lb)
+{
+	memset(lb->base, 0, lb->offset);
+	lb->offset = 0;
+}
+
+struct snl_state {
+	int fd;
+	char *buf;
+	size_t off;
+	size_t bufsize;
+	size_t datalen;
+	uint32_t seq;
+	bool init_done;
+	struct linear_buffer lb;
+};
+#define	SCRATCH_BUFFER_SIZE	1024
+
+typedef void snl_parse_field_f(struct snl_state *ss, void *hdr, void *target);
+struct snl_field_parser {
+	uint16_t		off_in;
+	uint16_t		off_out;
+	snl_parse_field_f	*cb;
+};
+
+typedef bool snl_parse_attr_f(struct snl_state *ss, struct nlattr *attr,
+    const void *arg, void *target);
+struct snl_attr_parser {
+	uint16_t		type;	/* Attribute type */
+	uint16_t		off;	/* field offset in the target structure */
+	snl_parse_attr_f	*cb;	/* parser function to call */
+	const void		*arg;	/* Optional argument parser */
+};
+
+struct snl_hdr_parser {
+	int			hdr_off; /* aligned header size */
+	int			fp_size;
+	int			np_size;
+	const struct snl_field_parser	*fp; /* array of header field parsers */
+	const struct snl_attr_parser	*np; /* array of attribute parsers */
+};
+
+#define	SNL_DECLARE_PARSER(_name, _t, _fp, _np)		\
+static const struct snl_hdr_parser _name = {		\
+	.hdr_off = sizeof(_t),				\
+	.fp = &((_fp)[0]),				\
+	.np = &((_np)[0]),				\
+	.fp_size = NL_ARRAY_LEN(_fp),			\
+	.np_size = NL_ARRAY_LEN(_np),			\
+}
+
+#define	SNL_DECLARE_ATTR_PARSER(_name, _np)		\
+static const struct snl_hdr_parser _name = {		\
+	.np = &((_np)[0]),				\
+	.np_size = NL_ARRAY_LEN(_np),			\
+}
+
+
+static void
+snl_free(struct snl_state *ss)
+{
+	if (ss->init_done) {
+		close(ss->fd);
+		if (ss->buf != NULL)
+			free(ss->buf);
+		if (ss->lb.base != NULL)
+			free(ss->lb.base);
+	}
+}
+
+static inline bool
+snl_init(struct snl_state *ss, int netlink_family)
+{
+	memset(ss, 0, sizeof(*ss));
+
+	ss->fd = socket(AF_NETLINK, SOCK_RAW, netlink_family);
+	if (ss->fd == -1)
+		return (false);
+	ss->init_done = true;
+
+	int rcvbuf;
+	socklen_t optlen = sizeof(rcvbuf);
+	if (getsockopt(ss->fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen) == -1) {
+		snl_free(ss);
+		return (false);
+	}
+
+	ss->bufsize = rcvbuf;
+	ss->buf = malloc(ss->bufsize);
+	if (ss->buf == NULL) {
+		snl_free(ss);
+		return (false);
+	}
+
+	ss->lb.size = SCRATCH_BUFFER_SIZE;
+	ss->lb.base = calloc(1, ss->lb.size);
+	if (ss->lb.base == NULL) {
+		snl_free(ss);
+		return (false);
+	}
+
+	return (true);
+}
+
+static inline void *
+snl_allocz(struct snl_state *ss, int len)
+{
+	return (lb_allocz(&ss->lb, len));
+}
+
+static inline void
+snl_clear_lb(struct snl_state *ss)
+{
+	lb_clear(&ss->lb);
+}
+
+static inline bool
+snl_send(struct snl_state *ss, void *data, int sz)
+{
+	return (send(ss->fd, data, sz, 0) == sz);
+}
+
+static inline uint32_t
+snl_get_seq(struct snl_state *ss)
+{
+	return (++ss->seq);
+}
+
+static inline struct nlmsghdr *
+snl_read_message(struct snl_state *ss)
+{
+	if (ss->off == ss->datalen) {
+		struct sockaddr_nl nladdr;
+		struct iovec iov = {
+			.iov_base = ss->buf,
+			.iov_len = ss->bufsize,
+		};
+		struct msghdr msg = {
+			.msg_name = &nladdr,
+			.msg_namelen = sizeof(nladdr),
+			.msg_iov = &iov,
+			.msg_iovlen = 1,
+		};
+		ss->off = 0;
+		ss->datalen = 0;
+		for (;;) {
+			ssize_t datalen = recvmsg(ss->fd, &msg, 0);
+			if (datalen > 0) {
+				ss->datalen = datalen;
+				break;
+			} else if (errno != EINTR)
+				return (NULL);
+		}
+	}
+	struct nlmsghdr *hdr = (struct nlmsghdr *)&ss->buf[ss->off];
+	ss->off += NLMSG_ALIGN(hdr->nlmsg_len);
+	return (hdr);
+}
+
+/*
+ * Checks that attributes are sorted by attribute type.
+ */
+static inline void
+snl_verify_parsers(const struct snl_hdr_parser **parser, int count)
+{
+	for (int i = 0; i < count; i++) {
+		const struct snl_hdr_parser *p = parser[i];
+		int attr_type = 0;
+		for (int j = 0; j < p->np_size; j++) {
+			assert(p->np[j].type > attr_type);
+			attr_type = p->np[j].type;
+		}
+	}
+}
+#define	SNL_VERIFY_PARSERS(_p)	snl_verify_parsers((_p), NL_ARRAY_LEN(_p))
+
+static const struct snl_attr_parser *
+find_parser(const struct snl_attr_parser *ps, int pslen, int key)
+{
+	int left_i = 0, right_i = pslen - 1;
+
+	if (key < ps[0].type || key > ps[pslen - 1].type)
+		return (NULL);
+
+	while (left_i + 1 < right_i) {
+		int mid_i = (left_i + right_i) / 2;
+		if (key < ps[mid_i].type)
+			right_i = mid_i;
+		else if (key > ps[mid_i].type)
+			left_i = mid_i + 1;
+		else
+			return (&ps[mid_i]);
+	}
+	if (ps[left_i].type == key)
+		return (&ps[left_i]);
+	else if (ps[right_i].type == key)
+		return (&ps[right_i]);
+	return (NULL);
+}
+
+static inline bool
+snl_parse_attrs_raw(struct snl_state *ss, struct nlattr *nla_head, int len,
+    const struct snl_attr_parser *ps, int pslen, void *target)
+{
+	struct nlattr *nla;
+
+	NLA_FOREACH(nla, nla_head, len) {
+		if (nla->nla_len < sizeof(struct nlattr))
+			return (false);
+		int nla_type = nla->nla_type & NLA_TYPE_MASK;
+		const struct snl_attr_parser *s = find_parser(ps, pslen, nla_type);
+		if (s != NULL) {
+			void *ptr = (void *)((char *)target + s->off);
+			if (!s->cb(ss, nla, s->arg, ptr))
+				return (false);
+		}
+	}
+	return (true);
+}
+
+static inline bool
+snl_parse_attrs(struct snl_state *ss, struct nlmsghdr *hdr, int hdrlen,
+    const struct snl_attr_parser *ps, int pslen, void *target)
+{
+	int off = NLMSG_HDRLEN + NETLINK_ALIGN(hdrlen);
+	int len = hdr->nlmsg_len - off;
+	struct nlattr *nla_head = (struct nlattr *)((char *)hdr + off);
+
+	return (snl_parse_attrs_raw(ss, nla_head, len, ps, pslen, target));
+}
+
+static inline bool
+snl_parse_header(struct snl_state *ss, void *hdr, int len,
+    const struct snl_hdr_parser *parser, void *target)
+{
+	/* Extract fields first (if any) */
+	for (int i = 0; i < parser->fp_size; i++) {
+		const struct snl_field_parser *fp = &parser->fp[i];
+		void *src = (char *)hdr + fp->off_in;
+		void *dst = (char *)target + fp->off_out;
+
+		fp->cb(ss, src, dst);
+	}
+
+	struct nlattr *nla_head = (struct nlattr *)((char *)hdr + parser->hdr_off);
+	bool result = snl_parse_attrs_raw(ss, nla_head, len - parser->hdr_off,
+	    parser->np, parser->np_size, target);
+
+	return (result);
+}
+
+static inline bool
+snl_parse_nlmsg(struct snl_state *ss, struct nlmsghdr *hdr,
+    const struct snl_hdr_parser *parser, void *target)
+{
+	return (snl_parse_header(ss, hdr + 1, hdr->nlmsg_len - sizeof(*hdr), parser, target));
+}
+
+static inline bool
+snl_attr_get_flag(struct snl_state *ss, struct nlattr *nla, void *target)
+{
+	if (NLA_DATA_LEN(nla) == 0) {
+		*((uint8_t *)target) = 1;
+		return (true);
+	}
+	return (false);
+}
+
+static inline bool
+snl_attr_get_uint16(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	if (NLA_DATA_LEN(nla) == sizeof(uint16_t)) {
+		*((uint16_t *)target) = *((const uint16_t *)NL_RTA_DATA_CONST(nla));
+		return (true);
+	}
+	return (false);
+}
+
+static inline bool
+snl_attr_get_uint32(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	if (NLA_DATA_LEN(nla) == sizeof(uint32_t)) {
+		*((uint32_t *)target) = *((const uint32_t *)NL_RTA_DATA_CONST(nla));
+		return (true);
+	}
+	return (false);
+}
+
+static inline bool
+snl_attr_get_string(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	size_t maxlen = NLA_DATA_LEN(nla);
+
+	if (strnlen((char *)NLA_DATA(nla), maxlen) < maxlen) {
+		*((char **)target) = (char *)NLA_DATA(nla);
+		return (true);
+	}
+	return (false);
+}
+
+static inline bool
+snl_attr_get_stringn(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	int maxlen = NLA_DATA_LEN(nla);
+
+	char *buf = snl_allocz(ss, maxlen + 1);
+	if (buf == NULL)
+		return (false);
+	buf[maxlen] = '\0';
+	memcpy(buf, NLA_DATA(nla), maxlen);
+
+	*((char **)target) = buf;
+	return (true);
+}
+
+static inline bool
+snl_attr_get_nested(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	const struct snl_hdr_parser *p = (const struct snl_hdr_parser *)arg;
+
+	/* Assumes target points to the beginning of the structure */
+	return (snl_parse_header(ss, NLA_DATA(nla), NLA_DATA_LEN(nla), p, target));
+}
+
+static inline bool
+snl_attr_get_nla(struct snl_state *ss, struct nlattr *nla, void *target)
+{
+	*((struct nlattr **)target) = nla;
+	return (true);
+}
+
+static inline void
+snl_field_get_uint8(struct snl_state *ss, void *src, void *target)
+{
+	*((uint8_t *)target) = *((uint8_t *)src);
+}
+
+static inline void
+snl_field_get_uint16(struct snl_state *ss, void *src, void *target)
+{
+	*((uint16_t *)target) = *((uint16_t *)src);
+}
+
+static inline void
+snl_field_get_uint32(struct snl_state *ss, void *src, void *target)
+{
+	*((uint32_t *)target) = *((uint32_t *)src);
+}
+
+#endif
diff --git a/sys/netlink/netlink_snl_route.h b/sys/netlink/netlink_snl_route.h
new file mode 100644
index 000000000000..d281ec051513
--- /dev/null
+++ b/sys/netlink/netlink_snl_route.h
@@ -0,0 +1,128 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2022 Alexander V. Chernikov <melifaro@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#ifndef	_NETLINK_NETLINK_SNL_ROUTE_H_
+#define	_NETLINK_NETLINK_SNL_ROUTE_H_
+
+#include <netinet/in.h>
+
+/*
+ * Simple Netlink Library - NETLINK_ROUTE helpers
+ */
+
+#define snl_alloc_sockaddr(_ss, _len)  ((struct sockaddr *)(snl_allocz(_ss, _len)))
+
+static inline struct sockaddr *
+parse_rta_ip4(struct snl_state *ss, void *rta_data, int *perror)
+{
+	struct sockaddr_in *sin;
+
+	sin = (struct sockaddr_in *)snl_alloc_sockaddr(ss, sizeof(struct sockaddr_in));
+	if (sin == NULL) {
+		*perror = ENOBUFS;
+		return (NULL);
+	}
+	sin->sin_len = sizeof(struct sockaddr_in);
+	sin->sin_family = AF_INET;
+	memcpy(&sin->sin_addr, rta_data, sizeof(struct in_addr));
+	return ((struct sockaddr *)sin);
+}
+
+static inline struct sockaddr *
+parse_rta_ip6(struct snl_state *ss, void *rta_data, int *perror)
+{
+	struct sockaddr_in6 *sin6;
+
+	sin6 = (struct sockaddr_in6 *)snl_alloc_sockaddr(ss, sizeof(struct sockaddr_in6));
+	if (sin6 == NULL) {
+		*perror = ENOBUFS;
+		return (NULL);
+	}
+	sin6->sin6_len = sizeof(struct sockaddr_in6);
+	sin6->sin6_family = AF_INET6;
+	memcpy(&sin6->sin6_addr, rta_data, sizeof(struct in6_addr));
+	return ((struct sockaddr *)sin6);
+}
+
+static inline struct sockaddr *
+parse_rta_ip(struct snl_state *ss, struct rtattr *rta, int *perror)
+{
+	void *rta_data = NL_RTA_DATA(rta);
+	int rta_len = NL_RTA_DATA_LEN(rta);
+
+	if (rta_len == sizeof(struct in_addr)) {
+		return (parse_rta_ip4(ss, rta_data, perror));
+	} else if (rta_len == sizeof(struct in6_addr)) {
+		return (parse_rta_ip6(ss, rta_data, perror));
+	} else {
+		*perror = ENOTSUP;
+		return (NULL);
+	}
+	return (NULL);
+}
+
+static inline bool
+snl_attr_get_ip(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	int error = 0;
+	struct sockaddr *sa = parse_rta_ip(ss, (struct rtattr *)nla, &error);
+	if (error == 0) {
+		*((struct sockaddr **)target) = sa;
+		return (true);
+	}
+	return (false);
+}
+
+static inline struct sockaddr *
+parse_rta_via(struct snl_state *ss, struct rtattr *rta, int *perror)
+{
+	struct rtvia *via = NL_RTA_DATA(rta);
+
+	switch (via->rtvia_family) {
+	case AF_INET:
+		return (parse_rta_ip4(ss, via->rtvia_addr, perror));
+	case AF_INET6:
+		return (parse_rta_ip6(ss, via->rtvia_addr, perror));
+	default:
+		*perror = ENOTSUP;
+		return (NULL);
+	}
+}
+
+static inline bool
+snl_attr_get_ipvia(struct snl_state *ss, struct nlattr *nla, const void *arg, void *target)
+{
+	int error = 0;
+
+	struct sockaddr *sa = parse_rta_via(ss, (struct rtattr *)nla, &error);
+	if (error == 0) {
+		*((struct sockaddr **)target) = sa;
+		return (true);
+	}
+	return (false);
+}
+
+#endif
diff --git a/tests/sys/netlink/Makefile b/tests/sys/netlink/Makefile
index d05965761f62..c112a9079cd2 100644
--- a/tests/sys/netlink/Makefile
+++ b/tests/sys/netlink/Makefile
@@ -5,8 +5,7 @@ WARNS?=		1
 
 TESTSDIR=       ${TESTSBASE}/sys/netlink
 
-#ATF_TESTS_C +=	test_rtsock_l3
-#ATF_TESTS_C +=	test_rtsock_lladdr
+ATF_TESTS_C +=	test_snl
 ATF_TESTS_PYTEST +=	test_rtnl_iface.py
 
 CFLAGS+=	-I${.CURDIR:H:H:H}
diff --git a/tests/sys/netlink/test_snl.c b/tests/sys/netlink/test_snl.c
new file mode 100644
index 000000000000..d917d81d967d
--- /dev/null
+++ b/tests/sys/netlink/test_snl.c
@@ -0,0 +1,92 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <sys/param.h>
+#include <sys/module.h>
+
+#include <netlink/netlink.h>
+#include <netlink/netlink_route.h>
+#include "netlink/netlink_snl.h"
+#include "netlink/netlink_snl_route.h"
+
+#include <atf-c.h>
+
+static void
+require_netlink(void)
+{
+	if (modfind("netlink") == -1)
+		atf_tc_skip("netlink module not loaded");
+}
+
+
+ATF_TC(snl_list_ifaces);
+ATF_TC_HEAD(snl_list_ifaces, tc)
+{
+	atf_tc_set_md_var(tc, "descr", "Tests snl(3) listing interfaces");
+}
+
+struct nl_parsed_link {
+	uint32_t		ifi_index;
+	uint32_t		ifla_mtu;
+	char			*ifla_ifname;
+};
+
+#define	_IN(_field)	offsetof(struct ifinfomsg, _field)
+#define	_OUT(_field)	offsetof(struct nl_parsed_link, _field)
+static struct snl_attr_parser ap_link[] = {
+	{ .type = IFLA_IFNAME, .off = _OUT(ifla_ifname), .cb = snl_attr_get_string },
+	{ .type = IFLA_MTU, .off = _OUT(ifla_mtu), .cb = snl_attr_get_uint32 },
+};
+static struct snl_field_parser fp_link[] = {
+	{.off_in = _IN(ifi_index), .off_out = _OUT(ifi_index), .cb = snl_field_get_uint32 },
+};
+#undef _IN
+#undef _OUT
+SNL_DECLARE_PARSER(link_parser, struct ifinfomsg, fp_link, ap_link);
+
+
+ATF_TC_BODY(snl_list_ifaces, tc)
+{
+	struct snl_state ss;
+
+	require_netlink();
+
+	if (!snl_init(&ss, NETLINK_ROUTE))
+		atf_tc_fail("snl_init() failed");
+
+	struct {
+		struct nlmsghdr hdr;
+		struct ifinfomsg ifmsg;
+	} msg = {
*** 31 LINES SKIPPED ***