diff --git a/unit/test-rtnl.c b/unit/test-rtnl.c
index cd944af64de9..25e1859205b4 100644
--- a/unit/test-rtnl.c
+++ b/unit/test-rtnl.c
@@ -17,12 +17,282 @@
 #include <signal.h>
 #include <sys/wait.h>
 #include <asm/types.h>
+#include <arpa/inet.h>
 #include <linux/netlink.h>
 #include <linux/rtnetlink.h>
+#include <linux/neighbour.h>
+#include <linux/icmpv6.h>
+#include <linux/if.h>
 #include <sys/socket.h>
+#include <assert.h>
+#include <limits.h>
 
 #include <ell/ell.h>
-#include "ell/dbus-private.h"
+#include "ell/netlink-private.h"
+#include "ell/rtnl-private.h"
+#include "ell/useful.h"
+
+static size_t rta_add_u8(void *rta_buf, unsigned short type, uint8_t value)
+{
+	struct rtattr *rta = rta_buf;
+
+	rta->rta_len = RTA_LENGTH(sizeof(uint8_t));
+	rta->rta_type = type;
+	*((uint8_t *) RTA_DATA(rta)) = value;
+
+	return RTA_SPACE(sizeof(uint8_t));
+}
+
+static size_t rta_add_u32(void *rta_buf, unsigned short type, uint32_t value)
+{
+	struct rtattr *rta = rta_buf;
+
+	rta->rta_len = RTA_LENGTH(sizeof(uint32_t));
+	rta->rta_type = type;
+	*((uint32_t *) RTA_DATA(rta)) = value;
+
+	return RTA_SPACE(sizeof(uint32_t));
+}
+
+static size_t rta_add_data(void *rta_buf, unsigned short type, const void *data,
+								size_t data_len)
+{
+	struct rtattr *rta = rta_buf;
+
+	rta->rta_len = RTA_LENGTH(data_len);
+	rta->rta_type = type;
+	memcpy(RTA_DATA(rta), data, data_len);
+
+	return RTA_SPACE(data_len);
+}
+
+static size_t rta_add_address(void *rta_buf, unsigned short type,
+				uint8_t family,
+				const struct in6_addr *v6,
+				const struct in_addr *v4)
+{
+	struct rtattr *rta = rta_buf;
+
+	rta->rta_type = type;
+
+	switch (family) {
+	case AF_INET6:
+		rta->rta_len = RTA_LENGTH(sizeof(struct in6_addr));
+		memcpy(RTA_DATA(rta), v6, sizeof(struct in6_addr));
+		return RTA_SPACE(sizeof(struct in6_addr));
+	case AF_INET:
+		rta->rta_len = RTA_LENGTH(sizeof(struct in_addr));
+		memcpy(RTA_DATA(rta), v4, sizeof(struct in_addr));
+		return RTA_SPACE(sizeof(struct in_addr));
+	}
+
+	return 0;
+}
+
+static int address_is_null(int family, const struct in_addr *v4,
+						const struct in6_addr *v6)
+{
+	switch (family) {
+	case AF_INET:
+		return v4->s_addr == 0;
+	case AF_INET6:
+		return IN6_IS_ADDR_UNSPECIFIED(v6);
+	}
+
+	return -EAFNOSUPPORT;
+}
+
+static bool messages_equal(const struct l_netlink_message *nlm,
+				void *other_message, size_t len)
+{
+
+	if (len != nlm->hdr->nlmsg_len - NLMSG_HDRLEN)
+		return false;
+
+	return !memcmp(nlm->data + NLMSG_HDRLEN, other_message, len);
+}
+
+static const struct l_rtnl_route route = {
+	.family = AF_INET6,
+	.scope = RT_SCOPE_UNIVERSE,
+	.protocol = RTPROT_UNSPEC,
+	.gw = { .in6_addr = IN6ADDR_LOOPBACK_INIT },
+	.dst = { .in6_addr = IN6ADDR_LOOPBACK_INIT },
+	.dst_prefix_len = 32,
+	.prefsrc = { .in6_addr = IN6ADDR_LOOPBACK_INIT },
+	.lifetime = INT_MAX,
+	.expiry_time = 0, /* math performed, will cause messages to differ */
+	.mtu = 8096,
+	.priority = 300,
+	.preference = ICMPV6_ROUTER_PREF_LOW,
+};
+
+static struct rtmsg *build_rtmsg(const struct l_rtnl_route *rt, int ifindex,
+					size_t *out_len)
+{
+	struct rtmsg *rtmmsg;
+	size_t bufsize;
+	void *rta_buf;
+	uint64_t now = l_time_now();
+
+	bufsize = NLMSG_ALIGN(sizeof(struct rtmsg)) +
+			RTA_SPACE(sizeof(uint32_t)) +        /* RTA_OIF */
+			RTA_SPACE(sizeof(uint32_t)) +        /* RTA_PRIORITY */
+			RTA_SPACE(sizeof(struct in6_addr)) + /* RTA_GATEWAY */
+			RTA_SPACE(sizeof(struct in6_addr)) + /* RTA_DST */
+			RTA_SPACE(sizeof(struct in6_addr)) + /* RTA_PREFSRC */
+			256 +                                /* RTA_METRICS */
+			RTA_SPACE(sizeof(uint8_t)) +         /* RTA_PREF */
+			RTA_SPACE(sizeof(uint32_t));         /* RTA_EXPIRES */
+
+	rtmmsg = l_malloc(bufsize);
+	memset(rtmmsg, 0, bufsize);
+
+	rtmmsg->rtm_family = rt->family;
+	rtmmsg->rtm_table = RT_TABLE_MAIN;
+	rtmmsg->rtm_protocol = rt->protocol;
+	rtmmsg->rtm_type = RTN_UNICAST;
+	rtmmsg->rtm_scope = rt->scope;
+
+	rta_buf = (void *) rtmmsg + NLMSG_ALIGN(sizeof(struct rtmsg));
+	rta_buf += rta_add_u32(rta_buf, RTA_OIF, ifindex);
+
+	if (rt->priority)
+		rta_buf += rta_add_u32(rta_buf, RTA_PRIORITY,
+						rt->priority + ifindex);
+
+	if (!address_is_null(rt->family, &rt->gw.in_addr, &rt->gw.in6_addr))
+		rta_buf += rta_add_address(rta_buf, RTA_GATEWAY, rt->family,
+					&rt->gw.in6_addr, &rt->gw.in_addr);
+
+	if (rt->dst_prefix_len) {
+		rtmmsg->rtm_dst_len = rt->dst_prefix_len;
+		rta_buf += rta_add_address(rta_buf, RTA_DST, rt->family,
+					&rt->dst.in6_addr, &rt->dst.in_addr);
+	}
+
+	if (!address_is_null(rt->family, &rt->prefsrc.in_addr,
+						&rt->prefsrc.in6_addr))
+		rta_buf += rta_add_address(rta_buf, RTA_PREFSRC, rt->family,
+						&rt->prefsrc.in6_addr,
+						&rt->prefsrc.in_addr);
+
+	if (rt->mtu) {
+		uint8_t buf[256];
+		size_t written = rta_add_u32(buf, RTAX_MTU, rt->mtu);
+
+		rta_buf += rta_add_data(rta_buf, RTA_METRICS | NLA_F_NESTED,
+							buf, written);
+	}
+
+	if (rt->preference)
+		rta_buf += rta_add_u8(rta_buf, RTA_PREF, rt->preference);
+
+	if (rt->expiry_time > now)
+		rta_buf += rta_add_u32(rta_buf, RTA_EXPIRES,
+					l_time_to_secs(rt->expiry_time - now));
+
+	*out_len = rta_buf - (void *) rtmmsg;
+	return rtmmsg;
+}
+
+static void test_route(const void *data)
+{
+	static const int ifindex = 3;
+	size_t rtmsg_len;
+	_auto_(l_free) void *rtmsg = build_rtmsg(&route, ifindex, &rtmsg_len);
+	struct l_netlink_message *nlm =
+		rtnl_message_from_route(RTM_NEWROUTE,
+					NLM_F_CREATE | NLM_F_REPLACE,
+					ifindex, &route);
+
+	assert(messages_equal(nlm, rtmsg, rtmsg_len));
+	l_netlink_message_unref(nlm);
+}
+
+static const struct l_rtnl_address address = {
+	.family = AF_INET6,
+	.prefix_len = 31,
+	.scope = RT_SCOPE_UNIVERSE,
+	.in6_addr = IN6ADDR_LOOPBACK_INIT,
+	.label = { 'f', 'o', 'o', 'b', 'a', 'r', '\0' },
+	.flags = IFA_F_PERMANENT | IFA_F_NOPREFIXROUTE,
+};
+
+static struct ifaddrmsg *build_ifaddrmsg(const struct l_rtnl_address *addr,
+						int ifindex, size_t *out_len)
+{
+	struct ifaddrmsg *ifamsg;
+	void *buf;
+	size_t bufsize;
+	uint64_t now = l_time_now();
+
+	bufsize = NLMSG_ALIGN(sizeof(struct ifaddrmsg)) +
+					RTA_SPACE(sizeof(struct in6_addr)) +
+					RTA_SPACE(sizeof(struct in_addr)) +
+					RTA_SPACE(sizeof(uint32_t)) +
+					RTA_SPACE(IFNAMSIZ) +
+					RTA_SPACE(sizeof(struct ifa_cacheinfo));
+
+	ifamsg = l_malloc(bufsize);
+	memset(ifamsg, 0, bufsize);
+
+	ifamsg->ifa_index = ifindex;
+	ifamsg->ifa_family = addr->family;
+	ifamsg->ifa_scope = addr->scope;
+	ifamsg->ifa_prefixlen = addr->prefix_len;
+	/* Kernel ignores legacy flags in IFA_FLAGS, so set them here */
+	ifamsg->ifa_flags = addr->flags & 0xff;
+
+	buf = (void *) ifamsg + NLMSG_ALIGN(sizeof(struct ifaddrmsg));
+
+	if (addr->family == AF_INET) {
+		buf += rta_add_data(buf, IFA_LOCAL, &addr->in_addr,
+						sizeof(struct in_addr));
+		buf += rta_add_data(buf, IFA_BROADCAST, &addr->broadcast,
+						sizeof(struct in_addr));
+	} else
+		buf += rta_add_data(buf, IFA_LOCAL, &addr->in6_addr,
+						sizeof(struct in6_addr));
+
+	if (addr->flags & 0xffffff00)
+		buf += rta_add_u32(buf, IFA_FLAGS, addr->flags & 0xffffff00);
+
+	if (addr->label[0])
+		buf += rta_add_data(buf, IFA_LABEL,
+					addr->label, strlen(addr->label) + 1);
+
+	if (addr->preferred_expiry_time > now ||
+			addr->valid_expiry_time > now) {
+		struct ifa_cacheinfo cinfo;
+
+		memset(&cinfo, 0, sizeof(cinfo));
+		cinfo.ifa_prefered = addr->preferred_expiry_time > now ?
+			l_time_to_secs(addr->preferred_expiry_time - now) : 0;
+		cinfo.ifa_valid =  addr->valid_expiry_time > now ?
+			l_time_to_secs(addr->valid_expiry_time - now) : 0;
+
+		buf += rta_add_data(buf, IFA_CACHEINFO, &cinfo, sizeof(cinfo));
+	}
+
+	*out_len = buf - (void *) ifamsg;
+	return ifamsg;
+}
+
+static void test_address(const void *data)
+{
+	static const int ifindex = 3;
+	size_t ifamsg_len;
+	_auto_(l_free) void *ifamsg =
+				build_ifaddrmsg(&address, ifindex, &ifamsg_len);
+	struct l_netlink_message *nlm =
+		rtnl_message_from_address(RTM_NEWADDR,
+					NLM_F_CREATE | NLM_F_REPLACE,
+					ifindex, &address);
+
+	assert(messages_equal(nlm, ifamsg, ifamsg_len));
+	l_netlink_message_unref(nlm);
+}
 
 static void signal_handler(uint32_t signo, void *user_data)
 {
@@ -234,6 +504,12 @@ int main(int argc, char *argv[])
 	if (!l_main_init())
 		return -1;
 
+	/* Run the the tests not requiring the main event loop first */
+	l_test_init(&argc, &argv);
+	l_test_add("route", test_route, NULL);
+	l_test_add("address", test_address, NULL);
+	l_test_run();
+
 	test_add("Dump IPv4 routing table", test_route4_dump, NULL);
 	test_add("Dump IPv6 routing table", test_route6_dump, NULL);
 	test_add("Dump IPv4 addresses", test_ifaddr4_dump, NULL);
