git: 6ad73dbf6504 - main - arp: convert arp(8) to netlink.

From: Alexander V. Chernikov <melifaro_at_FreeBSD.org>
Date: Tue, 25 Apr 2023 11:28:16 UTC
The branch main has been updated by melifaro:

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

commit 6ad73dbf65048b0950a1ba6ff25607f6708c8954
Author:     Alexander V. Chernikov <melifaro@FreeBSD.org>
AuthorDate: 2023-04-25 11:24:42 +0000
Commit:     Alexander V. Chernikov <melifaro@FreeBSD.org>
CommitDate: 2023-04-25 11:26:22 +0000

    arp: convert arp(8) to netlink.
    
    The change is intended to be fully transparent to the users.
    Similarly to route(8) and netstat(8), arp can be build without
     netlink by defining WITHOUT_NETLINK in make.conf.
    
    Differential Revision:  https://reviews.freebsd.org/D39720
---
 usr.sbin/arp/Makefile      |  11 ++
 usr.sbin/arp/arp.c         | 136 +++++++++-----
 usr.sbin/arp/arp.h         |  21 +++
 usr.sbin/arp/arp_netlink.c | 433 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 555 insertions(+), 46 deletions(-)

diff --git a/usr.sbin/arp/Makefile b/usr.sbin/arp/Makefile
index b6ff7a3e253c..17575181ceec 100644
--- a/usr.sbin/arp/Makefile
+++ b/usr.sbin/arp/Makefile
@@ -1,9 +1,20 @@
 #	@(#)Makefile	8.2 (Berkeley) 4/18/94
 # $FreeBSD$
 
+.include <src.opts.mk>
+
 PROG=	arp
 MAN=	arp.4 arp.8
 
+
+SRCS=	arp.c
+
+.if ${MK_NETLINK_SUPPORT} != "no"
+SRCS+=	arp_netlink.c
+.else
+CFLAGS+=-DWITHOUT_NETLINK
+.endif
+
 LIBADD=	xo
 
 WARNS?=	3
diff --git a/usr.sbin/arp/arp.c b/usr.sbin/arp/arp.c
index e7c18b0f323f..5a6c58e99e78 100644
--- a/usr.sbin/arp/arp.c
+++ b/usr.sbin/arp/arp.c
@@ -74,6 +74,7 @@ __FBSDID("$FreeBSD$");
 #include <netdb.h>
 #include <nlist.h>
 #include <paths.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -81,13 +82,12 @@ __FBSDID("$FreeBSD$");
 #include <unistd.h>
 #include <ifaddrs.h>
 #include <libxo/xo.h>
+#include "arp.h"
 
 typedef void (action_fn)(struct sockaddr_dl *sdl, struct sockaddr_in *s_in,
     struct rt_msghdr *rtm);
-
-static int search(u_long addr, action_fn *action);
-static action_fn print_entry;
-static action_fn nuke_entry;
+static void nuke_entries(uint32_t ifindex, struct in_addr addr);
+static int print_entries(uint32_t ifindex, struct in_addr addr);
 
 static int delete(char *host);
 static void usage(void);
@@ -97,17 +97,15 @@ static int file(char *name);
 static struct rt_msghdr *rtmsg(int cmd,
     struct sockaddr_in *dst, struct sockaddr_dl *sdl);
 static int get_ether_addr(in_addr_t ipaddr, struct ether_addr *hwaddr);
-static struct sockaddr_in *getaddr(char *host);
-static int valid_type(int type);
+static int set_rtsock(struct sockaddr_in *dst, struct sockaddr_dl *sdl_m,
+    char *host);
 
-static int nflag;	/* no reverse dns lookups */
 static char *rifname;
 
-static time_t	expire_time;
-static int	flags, doing_proxy;
-
 struct if_nameindex *ifnameindex;
 
+struct arp_opts opts = {};
+
 /* which function we're supposed to do */
 #define F_GET		1
 #define F_SET		2
@@ -124,7 +122,6 @@ main(int argc, char *argv[])
 {
 	int ch, func = 0;
 	int rtn = 0;
-	int aflag = 0;	/* do it for all entries */
 
 	argc = xo_parse_args(argc, argv);
 	if (argc < 0)
@@ -133,13 +130,13 @@ main(int argc, char *argv[])
 	while ((ch = getopt(argc, argv, "andfsSi:")) != -1)
 		switch(ch) {
 		case 'a':
-			aflag = 1;
+			opts.aflag = true;
 			break;
 		case 'd':
 			SETFUNC(F_DELETE);
 			break;
 		case 'n':
-			nflag = 1;
+			opts.nflag = true;
 			break;
 		case 'S':
 			SETFUNC(F_REPLACE);
@@ -163,7 +160,7 @@ main(int argc, char *argv[])
 	if (!func)
 		func = F_GET;
 	if (rifname) {
-		if (func != F_GET && !(func == F_DELETE && aflag))
+		if (func != F_GET && !(func == F_DELETE && opts.aflag))
 			xo_errx(1, "-i not applicable to this operation");
 		if (if_nametoindex(rifname) == 0) {
 			if (errno == ENXIO)
@@ -175,7 +172,7 @@ main(int argc, char *argv[])
 	}
 	switch (func) {
 	case F_GET:
-		if (aflag) {
+		if (opts.aflag) {
 			if (argc != 0)
 				usage();
 
@@ -183,7 +180,8 @@ main(int argc, char *argv[])
 			xo_open_container("arp");
 			xo_open_list("arp-cache");
 
-			search(0, print_entry);
+			struct in_addr all_addrs = {};
+			print_entries(0, all_addrs);
 
 			xo_close_list("arp-cache");
 			xo_close_container("arp");
@@ -203,10 +201,11 @@ main(int argc, char *argv[])
 		rtn = set(argc, argv) ? 1 : 0;
 		break;
 	case F_DELETE:
-		if (aflag) {
+		if (opts.aflag) {
 			if (argc != 0)
 				usage();
-			search(0, nuke_entry);
+			struct in_addr all_addrs = {};
+			nuke_entries(0, all_addrs);
 		} else {
 			if (argc != 1)
 				usage();
@@ -269,7 +268,7 @@ file(char *name)
  * the address of the host and returns a pointer to the
  * structure.
  */
-static struct sockaddr_in *
+struct sockaddr_in *
 getaddr(char *host)
 {
 	struct hostent *hp;
@@ -290,10 +289,11 @@ getaddr(char *host)
 	return (&reply);
 }
 
+int valid_type(int type);
 /*
  * Returns true if the type is a valid one for ARP.
  */
-static int
+int
 valid_type(int type)
 {
 
@@ -318,10 +318,7 @@ valid_type(int type)
 static int
 set(int argc, char **argv)
 {
-	struct sockaddr_in *addr;
 	struct sockaddr_in *dst;	/* what are we looking for */
-	struct sockaddr_dl *sdl;
-	struct rt_msghdr *rtm;
 	struct ether_addr *ea;
 	char *host = argv[0], *eaddr = argv[1];
 	struct sockaddr_dl sdl_m;
@@ -336,21 +333,17 @@ set(int argc, char **argv)
 	dst = getaddr(host);
 	if (dst == NULL)
 		return (1);
-	doing_proxy = flags = expire_time = 0;
 	while (argc-- > 0) {
 		if (strcmp(argv[0], "temp") == 0) {
-			struct timespec tp;
 			int max_age;
 			size_t len = sizeof(max_age);
 
-			clock_gettime(CLOCK_MONOTONIC, &tp);
 			if (sysctlbyname("net.link.ether.inet.max_age",
 			    &max_age, &len, NULL, 0) != 0)
 				xo_err(1, "sysctlbyname");
-			expire_time = tp.tv_sec + max_age;
+			opts.expire_time = max_age;
 		} else if (strcmp(argv[0], "pub") == 0) {
-			flags |= RTF_ANNOUNCE;
-			doing_proxy = 1;
+			opts.flags |= RTF_ANNOUNCE;
 			if (argc && strcmp(argv[1], "only") == 0) {
 				/*
 				 * Compatibility: in pre FreeBSD 8 times
@@ -361,17 +354,17 @@ set(int argc, char **argv)
 				argc--; argv++;
 			}
 		} else if (strcmp(argv[0], "blackhole") == 0) {
-			if (flags & RTF_REJECT) {
+			if (opts.flags & RTF_REJECT) {
 				xo_errx(1, "Choose one of blackhole or reject, "
 				    "not both.");
 			}
-			flags |= RTF_BLACKHOLE;
+			opts.flags |= RTF_BLACKHOLE;
 		} else if (strcmp(argv[0], "reject") == 0) {
-			if (flags & RTF_BLACKHOLE) {
+			if (opts.flags & RTF_BLACKHOLE) {
 				xo_errx(1, "Choose one of blackhole or reject, "
 				    "not both.");
 			}
-			flags |= RTF_REJECT;
+			opts.flags |= RTF_REJECT;
 		} else {
 			xo_warnx("Invalid parameter '%s'", argv[0]);
 			usage();
@@ -379,7 +372,7 @@ set(int argc, char **argv)
 		argv++;
 	}
 	ea = (struct ether_addr *)LLADDR(&sdl_m);
-	if (doing_proxy && !strcmp(eaddr, "auto")) {
+	if ((opts.flags & RTF_ANNOUNCE) && !strcmp(eaddr, "auto")) {
 		if (!get_ether_addr(dst->sin_addr.s_addr, ea)) {
 			xo_warnx("no interface found for %s",
 			       inet_ntoa(dst->sin_addr));
@@ -397,6 +390,20 @@ set(int argc, char **argv)
 			sdl_m.sdl_alen = ETHER_ADDR_LEN;
 		}
 	}
+#ifndef WITHOUT_NETLINK
+	return (set_nl(0, dst, &sdl_m, host));
+#else
+	return (set_rtsock(dst, &sdl_m, host));
+#endif
+}
+
+#ifdef WITHOUT_NETLINK
+static int
+set_rtsock(struct sockaddr_in *dst, struct sockaddr_dl *sdl_m, char *host)
+{
+	struct sockaddr_in *addr;
+	struct sockaddr_dl *sdl;
+	struct rt_msghdr *rtm;
 
 	/*
 	 * In the case a proxy-arp entry is being added for
@@ -420,10 +427,11 @@ set(int argc, char **argv)
 		xo_warnx("cannot intuit interface index and type for %s", host);
 		return (1);
 	}
-	sdl_m.sdl_type = sdl->sdl_type;
-	sdl_m.sdl_index = sdl->sdl_index;
-	return (rtmsg(RTM_ADD, dst, &sdl_m) == NULL);
+	sdl_m->sdl_type = sdl->sdl_type;
+	sdl_m->sdl_index = sdl->sdl_index;
+	return (rtmsg(RTM_ADD, dst, sdl_m) == NULL);
 }
+#endif
 
 /*
  * Display an individual arp entry
@@ -442,7 +450,7 @@ get(char *host)
 	xo_open_container("arp");
 	xo_open_list("arp-cache");
 
-	found = search(addr->sin_addr.s_addr, print_entry);
+	found = print_entries(0, addr->sin_addr);
 
 	if (found == 0) {
 		xo_emit("{d:hostname/%s} ({d:ip-address/%s}) -- no entry",
@@ -462,8 +470,9 @@ get(char *host)
 /*
  * Delete an arp entry
  */
+#ifdef WITHOUT_NETLINK
 static int
-delete(char *host)
+delete_rtsock(char *host)
 {
 	struct sockaddr_in *addr, *dst;
 	struct rt_msghdr *rtm;
@@ -476,7 +485,7 @@ delete(char *host)
 	/*
 	 * Perform a regular entry delete first.
 	 */
-	flags &= ~RTF_ANNOUNCE;
+	opts.flags &= ~RTF_ANNOUNCE;
 
 	for (;;) {	/* try twice */
 		rtm = rtmsg(RTM_GET, dst, NULL);
@@ -506,12 +515,12 @@ delete(char *host)
 		 * Regular entry delete failed, now check if there
 		 * is a proxy-arp entry to remove.
 		 */
-		if (flags & RTF_ANNOUNCE) {
+		if (opts.flags & RTF_ANNOUNCE) {
 			xo_warnx("delete: cannot locate %s", host);
 			return (1);
 		}
 
-		flags |= RTF_ANNOUNCE;
+		opts.flags |= RTF_ANNOUNCE;
 	}
 	rtm->rtm_flags |= RTF_LLDATA;
 	if (rtmsg(RTM_DELETE, dst, NULL) != NULL) {
@@ -520,6 +529,17 @@ delete(char *host)
 	}
 	return (1);
 }
+#endif
+
+static int
+delete(char *host)
+{
+#ifdef WITHOUT_NETLINK
+	return (delete_rtsock(host));
+#else
+	return (delete_nl(0, host));
+#endif
+}
 
 
 /*
@@ -600,7 +620,7 @@ print_entry(struct sockaddr_dl *sdl,
 
 	xo_open_instance("arp-cache");
 
-	if (nflag == 0)
+	if (!opts.nflag)
 		hp = gethostbyaddr((caddr_t)&(addr->sin_addr),
 		    sizeof addr->sin_addr, AF_INET);
 	else
@@ -610,7 +630,7 @@ print_entry(struct sockaddr_dl *sdl,
 	else {
 		host = "?";
 		if (h_errno == TRY_AGAIN)
-			nflag = 1;
+			opts.nflag = true;
 	}
 	xo_emit("{:hostname/%s} ({:ip-address/%s}) at ", host,
 	    inet_ntoa(addr->sin_addr));
@@ -640,6 +660,8 @@ print_entry(struct sockaddr_dl *sdl,
 		xo_emit("{d:/ permanent}{en:permanent/true}");
 	else {
 		static struct timespec tp;
+		time_t expire_time = 0;
+
 		if (tp.tv_sec == 0)
 			clock_gettime(CLOCK_MONOTONIC, &tp);
 		if ((expire_time = rtm->rtm_rmx.rmx_expire - tp.tv_sec) > 0)
@@ -683,6 +705,17 @@ print_entry(struct sockaddr_dl *sdl,
 	xo_close_instance("arp-cache");
 }
 
+static int
+print_entries(uint32_t ifindex, struct in_addr addr)
+{
+#ifndef WITHOUT_NETLINK
+	return (print_entries_nl(ifindex, addr));
+#else
+	return (search(addr.s_addr, print_entry));
+#endif
+}
+
+
 /*
  * Nuke an arp entry
  */
@@ -699,6 +732,12 @@ nuke_entry(struct sockaddr_dl *sdl __unused,
 	delete(ip);
 }
 
+static void
+nuke_entries(uint32_t ifindex, struct in_addr addr)
+{
+	search(addr.s_addr, nuke_entry);
+}
+
 static void
 usage(void)
 {
@@ -745,7 +784,7 @@ rtmsg(int cmd, struct sockaddr_in *dst, struct sockaddr_dl *sdl)
 	if (cmd == RTM_DELETE)
 		goto doit;
 	bzero((char *)&m_rtmsg, sizeof(m_rtmsg));
-	rtm->rtm_flags = flags;
+	rtm->rtm_flags = opts.flags;
 	rtm->rtm_version = RTM_VERSION;
 
 	switch (cmd) {
@@ -753,7 +792,12 @@ rtmsg(int cmd, struct sockaddr_in *dst, struct sockaddr_dl *sdl)
 		xo_errx(1, "internal wrong cmd");
 	case RTM_ADD:
 		rtm->rtm_addrs |= RTA_GATEWAY;
-		rtm->rtm_rmx.rmx_expire = expire_time;
+		if (opts.expire_time != 0) {
+			struct timespec tp;
+
+			clock_gettime(CLOCK_MONOTONIC, &tp);
+			rtm->rtm_rmx.rmx_expire = opts.expire_time + tp.tv_sec;
+		}
 		rtm->rtm_inits = RTV_EXPIRE;
 		rtm->rtm_flags |= (RTF_HOST | RTF_STATIC | RTF_LLDATA);
 		/* FALLTHROUGH */
diff --git a/usr.sbin/arp/arp.h b/usr.sbin/arp/arp.h
new file mode 100644
index 000000000000..a7de3a1a3024
--- /dev/null
+++ b/usr.sbin/arp/arp.h
@@ -0,0 +1,21 @@
+#ifndef _USR_SBIN_ARP_ARP_H_
+#define _USR_SBIN_ARP_ARP_H_
+
+int valid_type(int type);
+struct sockaddr_in *getaddr(char *host);
+int print_entries_nl(uint32_t ifindex, struct in_addr addr);
+
+struct arp_opts {
+	bool aflag;
+	bool nflag;
+	time_t expire_time;
+	int flags;
+};
+extern struct arp_opts opts;
+
+int print_entries_nl(uint32_t ifindex, struct in_addr addr);
+int delete_nl(uint32_t ifindex, char *host);
+int set_nl(uint32_t ifindex, struct sockaddr_in *dst, struct sockaddr_dl *sdl,
+    char *host);
+
+#endif
diff --git a/usr.sbin/arp/arp_netlink.c b/usr.sbin/arp/arp_netlink.c
new file mode 100644
index 000000000000..a3481ff2d138
--- /dev/null
+++ b/usr.sbin/arp/arp_netlink.c
@@ -0,0 +1,433 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <err.h>
+#include <errno.h>
+#include <netdb.h>
+
+#include <sys/bitcount.h>
+#include <sys/param.h>
+#include <sys/linker.h>
+#include <sys/module.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <net/if_types.h>
+#include <netlink/netlink.h>
+#include <netlink/netlink_route.h>
+#include <netlink/netlink_snl.h>
+#include <netlink/netlink_snl_route.h>
+#include <netlink/netlink_snl_route_compat.h>
+#include <netlink/netlink_snl_route_parsers.h>
+
+#include <libxo/xo.h>
+#include "arp.h"
+
+#define RTF_ANNOUNCE	RTF_PROTO2
+
+static void
+nl_init_socket(struct snl_state *ss)
+{
+	if (snl_init(ss, NETLINK_ROUTE))
+		return;
+
+	if (modfind("netlink") == -1 && errno == ENOENT) {
+		/* Try to load */
+		if (kldload("netlink") == -1)
+			err(1, "netlink is not loaded and load attempt failed");
+		if (snl_init(ss, NETLINK_ROUTE))
+			return;
+	}
+
+	err(1, "unable to open netlink socket");
+}
+
+static bool
+get_link_info(struct snl_state *ss, uint32_t ifindex,
+    struct snl_parsed_link_simple *link)
+{
+	struct snl_writer nw;
+
+	snl_init_writer(ss, &nw);
+
+	struct nlmsghdr *hdr = snl_create_msg_request(&nw, RTM_GETLINK);
+	struct ifinfomsg *ifmsg = snl_reserve_msg_object(&nw, struct ifinfomsg);
+	if (ifmsg != NULL)
+		ifmsg->ifi_index = ifindex;
+	if (!snl_finalize_msg(&nw) || !snl_send_message(ss, hdr))
+		return (false);
+
+	hdr = snl_read_reply(ss, hdr->nlmsg_seq);
+
+	if (hdr == NULL || hdr->nlmsg_type != RTM_NEWLINK)
+		return (false);
+
+	if (!snl_parse_nlmsg(ss, hdr, &snl_rtm_link_parser_simple, link))
+		return (false);
+
+	return (true);
+}
+
+
+
+static bool
+has_l2(struct snl_state *ss, uint32_t ifindex)
+{
+	struct snl_parsed_link_simple link = {};
+
+	if (!get_link_info(ss, ifindex, &link))
+		return (false);
+
+	return (valid_type(link.ifi_type) != 0);
+}
+
+static uint32_t
+get_myfib()
+{
+	uint32_t fibnum = 0;
+	size_t len = sizeof(fibnum);
+
+	sysctlbyname("net.my_fibnum", (void *)&fibnum, &len, NULL, 0);
+
+	return (fibnum);
+}
+
+static int
+guess_ifindex(struct snl_state *ss, uint32_t fibnum, struct in_addr addr)
+{
+	struct snl_writer nw;
+
+	snl_init_writer(ss, &nw);
+
+	struct nlmsghdr *hdr = snl_create_msg_request(&nw, RTM_GETROUTE);
+	struct rtmsg *rtm = snl_reserve_msg_object(&nw, struct rtmsg);
+	rtm->rtm_family = AF_INET;
+
+	struct sockaddr_in dst = { .sin_family = AF_INET, .sin_addr = addr };
+	snl_add_msg_attr_ip(&nw, RTA_DST, (struct sockaddr *)&dst);
+	snl_add_msg_attr_u32(&nw, RTA_TABLE, fibnum);
+
+	if (!snl_finalize_msg(&nw) || !snl_send_message(ss, hdr))
+		return (0);
+
+	hdr = snl_read_reply(ss, hdr->nlmsg_seq);
+
+	if (hdr->nlmsg_type != NL_RTM_NEWROUTE) {
+		/* No route found, unable to guess ifindex */
+		return (0);
+	}
+
+	struct snl_parsed_route r = {};
+	if (!snl_parse_nlmsg(ss, hdr, &snl_rtm_route_parser, &r))
+		return (0);
+
+	if (r.rta_multipath || (r.rta_rtflags & RTF_GATEWAY))
+		return (0);
+
+	/* Check if the interface is of supported type */
+	if (has_l2(ss, r.rta_oif))
+		return (r.rta_oif);
+
+	/* Check the case when we matched the loopback route for P2P */
+	snl_init_writer(ss, &nw);
+	hdr = snl_create_msg_request(&nw, RTM_GETNEXTHOP);
+	snl_reserve_msg_object(&nw, struct nhmsg);
+
+	int off = snl_add_msg_attr_nested(&nw, NHA_FREEBSD);
+	snl_add_msg_attr_u32(&nw, NHAF_KID, r.rta_knh_id);
+	snl_add_msg_attr_u8(&nw, NHAF_FAMILY, AF_INET);
+	snl_add_msg_attr_u32(&nw, NHAF_TABLE, fibnum);
+	snl_end_attr_nested(&nw, off);
+
+	if (!snl_finalize_msg(&nw) || !snl_send_message(ss, hdr))
+		return (0);
+
+	hdr = snl_read_reply(ss, hdr->nlmsg_seq);
+
+	if (hdr->nlmsg_type != NL_RTM_NEWNEXTHOP) {
+		/* No nexthop found, unable to guess ifindex */
+		return (0);
+	}
+
+	struct snl_parsed_nhop nh = {};
+	if (!snl_parse_nlmsg(ss, hdr, &snl_nhmsg_parser, &nh))
+		return (0);
+
+	return (nh.nhaf_aif);
+}
+
+static uint32_t
+fix_ifindex(struct snl_state *ss, uint32_t ifindex, struct in_addr addr)
+{
+	if (ifindex == 0)
+		ifindex = guess_ifindex(ss, get_myfib(), addr);
+	return (ifindex);
+}
+
+static void
+print_entry(struct snl_parsed_neigh *neigh, struct snl_parsed_link_simple *link)
+{
+	const char *host;
+	struct hostent *hp;
+	struct sockaddr_in *addr = (struct sockaddr_in *)neigh->nda_dst;
+
+	xo_open_instance("arp-cache");
+
+	if (!opts.nflag)
+		hp = gethostbyaddr((caddr_t)&(addr->sin_addr),
+		    sizeof(addr->sin_addr), AF_INET);
+	else
+		hp = 0;
+	if (hp)
+		host = hp->h_name;
+	else {
+		host = "?";
+		if (h_errno == TRY_AGAIN)
+			opts.nflag = true;
+	}
+	xo_emit("{:hostname/%s} ({:ip-address/%s}) at ", host,
+	    inet_ntoa(addr->sin_addr));
+	if (neigh->nda_lladdr != NULL) {
+		struct sockaddr_dl sdl = {
+			.sdl_family = AF_LINK,
+			.sdl_type = link->ifi_type,
+			.sdl_len = sizeof(struct sockaddr_dl),
+			.sdl_alen = NLA_DATA_LEN(neigh->nda_lladdr),
+		};
+		memcpy(sdl.sdl_data, NLA_DATA(neigh->nda_lladdr), sdl.sdl_alen);
+
+		if ((sdl.sdl_type == IFT_ETHER ||
+		    sdl.sdl_type == IFT_L2VLAN ||
+		    sdl.sdl_type == IFT_BRIDGE) &&
+		    sdl.sdl_alen == ETHER_ADDR_LEN)
+			xo_emit("{:mac-address/%s}",
+			    ether_ntoa((struct ether_addr *)LLADDR(&sdl)));
+		else {
+
+			xo_emit("{:mac-address/%s}", link_ntoa(&sdl));
+		}
+	} else
+		xo_emit("{d:/(incomplete)}{en:incomplete/true}");
+	xo_emit(" on {:interface/%s}", link->ifla_ifname);
+
+	if (neigh->ndaf_next_ts == 0)
+		xo_emit("{d:/ permanent}{en:permanent/true}");
+	else {
+		time_t expire_time;
+		struct timeval now;
+
+		gettimeofday(&now, 0);
+		if ((expire_time = neigh->ndaf_next_ts - now.tv_sec) > 0)
+			xo_emit(" expires in {:expires/%d} seconds",
+			    (int)expire_time);
+		else
+			xo_emit("{d:/ expired}{en:expired/true}");
+	}
+
+	if (neigh->ndm_flags & NTF_PROXY)
+		xo_emit("{d:/ published}{en:published/true}");
+
+	switch(link->ifi_type) {
+	case IFT_ETHER:
+		xo_emit(" [{:type/ethernet}]");
+		break;
+	case IFT_FDDI:
+		xo_emit(" [{:type/fddi}]");
+		break;
+	case IFT_ATM:
+		xo_emit(" [{:type/atm}]");
+		break;
+	case IFT_L2VLAN:
+		xo_emit(" [{:type/vlan}]");
+		break;
+	case IFT_IEEE1394:
+		xo_emit(" [{:type/firewire}]");
+		break;
+	case IFT_BRIDGE:
+		xo_emit(" [{:type/bridge}]");
+		break;
+	case IFT_INFINIBAND:
+		xo_emit(" [{:type/infiniband}]");
+		break;
+	default:
+		break;
+	}
+
+	xo_emit("\n");
+
+	xo_close_instance("arp-cache");
+}
+
+int
+print_entries_nl(uint32_t ifindex, struct in_addr addr)
+{
+	struct snl_state ss_req = {}, ss_cmd = {};
+	struct snl_parsed_link_simple link = {};
+	struct snl_writer nw;
+
+	nl_init_socket(&ss_req);
+	snl_init_writer(&ss_req, &nw);
+
+	struct nlmsghdr *hdr = snl_create_msg_request(&nw, RTM_GETNEIGH);
+	struct ndmsg *ndmsg = snl_reserve_msg_object(&nw, struct ndmsg);
+	if (ndmsg != NULL) {
+		ndmsg->ndm_family = AF_INET;
+		ndmsg->ndm_ifindex = ifindex;
+	}
+
+	if (!snl_finalize_msg(&nw) || !snl_send_message(&ss_req, hdr)) {
+		snl_free(&ss_req);
+		return (0);
+	}
+
+	uint32_t nlmsg_seq = hdr->nlmsg_seq;
+	struct snl_errmsg_data e = {};
+	int count = 0;
+	nl_init_socket(&ss_cmd);
+
+	while ((hdr = snl_read_reply_multi(&ss_req, nlmsg_seq, &e)) != NULL) {
+		struct snl_parsed_neigh neigh = {};
+
+		if (!snl_parse_nlmsg(&ss_req, hdr, &snl_rtm_neigh_parser, &neigh))
+			continue;
+
+		if (neigh.nda_ifindex != link.ifi_index) {
+			snl_clear_lb(&ss_cmd);
+			memset(&link, 0, sizeof(link));
+			if (!get_link_info(&ss_cmd, neigh.nda_ifindex, &link))
+				continue;
+		}
+
+		print_entry(&neigh, &link);
+		count++;
+		snl_clear_lb(&ss_req);
+	}
+
+	snl_free(&ss_req);
+	snl_free(&ss_cmd);
+
+	return (count);
+}
+
+int
+delete_nl(uint32_t ifindex, char *host)
+{
+	struct snl_state ss = {};
+	struct snl_writer nw;
+	struct sockaddr_in *dst;
+
+	dst = getaddr(host);
+	if (dst == NULL)
+		return (1);
+
+	nl_init_socket(&ss);
+
+	ifindex = fix_ifindex(&ss, ifindex, dst->sin_addr);
+	if (ifindex == 0) {
+		xo_warnx("delete: cannot locate %s", host);
+		snl_free(&ss);
+		return (0);
+	}
+
+	snl_init_writer(&ss, &nw);
+	struct nlmsghdr *hdr = snl_create_msg_request(&nw, RTM_DELNEIGH);
+	struct ndmsg *ndmsg = snl_reserve_msg_object(&nw, struct ndmsg);
+	if (ndmsg != NULL) {
+		ndmsg->ndm_family = AF_INET;
+		ndmsg->ndm_ifindex = ifindex;
+	}
+	snl_add_msg_attr_ip(&nw, NDA_DST, (struct sockaddr *)dst);
+
+	if (!snl_finalize_msg(&nw) || !snl_send_message(&ss, hdr)) {
+		snl_free(&ss);
+		return (1);
+	}
+
+	struct snl_errmsg_data e = {};
+	snl_read_reply_code(&ss, hdr->nlmsg_seq, &e);
+	if (e.error != 0) {
+		if (e.error_str != NULL)
+			xo_warnx("delete %s: %s (%s)", host, strerror(e.error), e.error_str);
+		else
+			xo_warnx("delete %s: %s", host, strerror(e.error));
+	}
+	snl_free(&ss);
+
+	return (e.error != 0);
+}
+
+int
+set_nl(uint32_t ifindex, struct sockaddr_in *dst, struct sockaddr_dl *sdl, char *host)
+{
+	struct snl_state ss = {};
+	struct snl_writer nw;
+
+	nl_init_socket(&ss);
+
+	ifindex = fix_ifindex(&ss, ifindex, dst->sin_addr);
+	if (ifindex == 0) {
+		xo_warnx("delete: cannot locate %s", host);
+		snl_free(&ss);
+		return (0);
+	}
+
+	if (opts.expire_time != 0)
+		opts.flags &= ~RTF_STATIC;
+	printf("EXPIRE: %ld\n", opts.expire_time);
+
+	snl_init_writer(&ss, &nw);
+	struct nlmsghdr *hdr = snl_create_msg_request(&nw, RTM_NEWNEIGH);
+	hdr->nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE;
+	struct ndmsg *ndmsg = snl_reserve_msg_object(&nw, struct ndmsg);
+	if (ndmsg != NULL) {
+		uint8_t nl_flags = 0;
+
+		ndmsg->ndm_family = AF_INET;
+		ndmsg->ndm_ifindex = ifindex;
+		ndmsg->ndm_state = (opts.flags & RTF_STATIC) ? NUD_PERMANENT : NUD_NONE;
+
+		if (opts.flags & RTF_ANNOUNCE)
+			nl_flags |= NTF_PROXY;
+		if (opts.flags & RTF_STATIC)
+			nl_flags |= NTF_STICKY;
+		ndmsg->ndm_flags = nl_flags;
+	}
+	snl_add_msg_attr_ip(&nw, NDA_DST, (struct sockaddr *)dst);
+	snl_add_msg_attr(&nw, NDA_LLADDR, sdl->sdl_alen, LLADDR(sdl));
+	
+	if (opts.expire_time != 0) {
+		struct timeval now;
+
+		gettimeofday(&now, 0);
+		int off = snl_add_msg_attr_nested(&nw, NDA_FREEBSD);
+		snl_add_msg_attr_u32(&nw, NDAF_NEXT_STATE_TS, now.tv_sec + opts.expire_time);
+		snl_end_attr_nested(&nw, off);
+	}
+
+	if (!snl_finalize_msg(&nw) || !snl_send_message(&ss, hdr)) {
+		snl_free(&ss);
+		return (1);
+	}
+
+	struct snl_errmsg_data e = {};
+	snl_read_reply_code(&ss, hdr->nlmsg_seq, &e);
+	if (e.error != 0) {
+		if (e.error_str != NULL)
+			xo_warnx("set: %s: %s (%s)", host, strerror(e.error), e.error_str);
+		else
+			xo_warnx("set %s: %s", host, strerror(e.error));
+	}
+	snl_free(&ss);
+
+	return (e.error != 0);
+}
+