Index | Thread | Search

From:
David Gwynne <david@gwynne.id.au>
Subject:
Re: LLDP daemon and display tool
To:
tech@openbsd.org
Date:
Tue, 29 Apr 2025 11:49:37 +1000

Download raw body.

Thread
  • Lyndon Nerenberg (VE7TFX/VE6BBM):

    LLDP daemon and display tool

  • On Mon, Apr 28, 2025 at 10:19:47AM +1000, David Gwynne wrote:
    > On Fri, Apr 25, 2025 at 10:10:32PM -0600, Theo de Raadt wrote:
    > > Sebastian Benoit <benno@openbsd.org> wrote:
    > > 
    > > > Claudio Jeker(cjeker@diehard.n-r-g.com) on 2025.04.25 13:31:32 +0200:
    > > > > On Fri, Apr 25, 2025 at 11:36:50AM +0100, Stuart Henderson wrote:
    > > > > > On 2025/04/25 12:04, Henning Brauer wrote:
    > > > > > > * Stuart Henderson <stu@spacehopper.org> [2025-04-25 03:25]:
    > > > > > > > On 2025/04/24 10:38, Lyndon Nerenberg (VE7TFX/VE6BBM) wrote:
    > > > > > > > > This is great!  Some quick testing shows it correctly sees
    > > > > > > > > all the Fortinet and Juniper hardware on my networks.
    > > > > > > > > 
    > > > > > > > > But I would suggest just calling it lldp[d] right from the
    > > > > > > > > start.  I don't see a conflict as it's makes no sense to 
    > > > > > > > > run both this and ports at the same time.  And if they
    > > > > > > > > are both installed, the ports cli names don't collisde
    > > > > > > > > with this one's.
    > > > > > > > 
    > > > > > > > The rc.d scripts conflict.
    > > > > > > 
    > > > > > > then the ports one needs to be adjusted.
    > > > > > > 
    > > > > > > our ntpd is ntpd, not ontpd.
    > > > > > 
    > > > > > yes and we had a problem with that around 5.0-5.1
    > > > > > 
    > > > > > > our ldapd is ldapd, not oldapd.
    > > > > > 
    > > > > > no conflict
    > > > > > 
    > > > > > > our smtpd is smtpd, not osmtpd.
    > > > > > 
    > > > > > no conflict
    > > > > > 
    > > > > > > our bgpd is bgpd, not obgpd.
    > > > > > 
    > > > > > the possibly-conflicting rc script was named quagga_bgpd from the start
    > > > > > 
    > > > > > > and so on and so on.
    > > > > > 
    > > > > > the rc-script could be renamed, but:
    > > > > > 
    > > > > > 1. what to?
    > > > > > 
    > > > > > 2. unless it's renamed in the release _before_ this is added, upgrades
    > > > > > will be broken. user updates base from a version with ports lldpd
    > > > > > installed to a version with lldpd from base, so overwriting rc.d/lldpd.
    > > > > > updating packages at that point will _remove_ the rc.d/lldpd script.
    > > > > > 
    > > > > > if we want to reuse existing names of things from ports in base we
    > > > > > could really do with a separate namespace for ports and base rc.d scripts.
    > > > > 
    > > > > Would it be acceptable to use lldpd as program name but use rc.d/lldp as
    > > > > startup script?
    > > > > 
    > > > > The only other problem point would be the file in /var/run but I think
    > > > > that implies that someone wants to run both tools at the same time which
    > > > > IMO makes little sense and people can then use an overwrite to toggle the
    > > > > control socket path.
    > > > > 
    > > > > I would really like to see dlg's tool in our tree and it seems this is the
    > > > > main pain point.
    > > > 
    > > > Does the daemon need to have a "d" at the end?
    > > > 
    > > > If not, how about
    > > > 
    > > > lldp	- the daemon binary
    > > > lldpctl	- the tool
    > > > lldp	- rc-script
    > > > _lldp	- username
    > > 
    > > I think we've been here before.  When we write our own components, eventually
    > > people stop using the ports components because ours are good enough.
    > > 
    > > Sometimes, it was an external component, which we modified iteratively until
    > > we had to throw it away, and our own system matured once we dogfooded it.
    > > 
    > > This lldp is not as feature complete, but it's privsep design is stronger,
    > > perhaps even strong enough that it can be run by default.  It probably doesn't
    > > need to be feature complete.  lldp protocol is supposed to, over time, replace
    > > all the other legacy things like cdp.  Noone wants a daemon on OpenBSD that
    > > does all the features.  Noone wants a speaker on a vendor device that speaks
    > > their custom protocol, since so many have had scary bugs, and they don't
    > > interoperate.  So can we say, bye by legacy, at the same time?  Us making such
    > > a decision also partly feeds into the internet culture and allows others to
    > > consider making the same decision (lldp, only).
    > > 
    > > With the ports lldp code, running it by default is not going to happen.
    > > 
    > > So I'm confused about why we are bending over backwards for a ports daemon.
    > > 
    > > Let's just take over the naming and keep it simple.
    > 
    > so the plan is i rename olldpd to lldpd?
    > 
    > lldp(8) stays as lldp(8)? do i use _lldp for the user the daemon runs
    > as or take over _lldpd too? what's the rc.d script called?
    > 
    > users will just have to deal with the conflict if they have the port
    > already installed?
    
    this is lldpd(8) running as _lldp, which lldp(8) can talk to via
    /var/run/lldp.sock.
    
    Index: sys/net/lldp.h
    ===================================================================
    RCS file: sys/net/lldp.h
    diff -N sys/net/lldp.h
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ sys/net/lldp.h	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,69 @@
    +/*	$OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2025 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#ifndef _NET_LLDP_H_
    +#define _NET_LLDP_H_
    +
    +#define LLDP_TLV_END			0
    +#define LLDP_TLV_CHASSIS_ID		1
    +#define LLDP_TLV_PORT_ID		2
    +#define LLDP_TLV_TTL			3
    +#define LLDP_TLV_PORT_DESCR		4
    +#define LLDP_TLV_SYSTEM_NAME		5
    +#define LLDP_TLV_SYSTEM_DESCR		6
    +#define LLDP_TLV_SYSTEM_CAP		7
    +#define LLDP_TLV_MANAGEMENT_ADDR	8
    +#define LLDP_TLV_ORG			127
    +
    +/*
    + * Chassis ID subtype enumeration
    + */
    +#define LLDP_CHASSIS_ID_CHASSIS		1	/* Chassis component */
    +#define LLDP_CHASSIS_ID_IFALIAS		2	/* Interface alias */
    +#define LLDP_CHASSIS_ID_PORT		3	/* Port component */
    +#define LLDP_CHASSIS_ID_MACADDR		4	/* MAC address */
    +#define LLDP_CHASSIS_ID_ADDR		5	/* Network address */
    +#define LLDP_CHASSIS_ID_IFNAME		6	/* Interface name */
    +#define LLDP_CHASSIS_ID_LOCAL		7	/* Locally assigned */
    +
    +/*
    + * Port ID subtype enumeration
    + */
    +
    +#define LLDP_PORT_ID_IFALIAS		1	/* Interface alias */
    +#define LLDP_PORT_ID_PORT		2	/* Port component */
    +#define LLDP_PORT_ID_MACADDR		3	/* MAC address */
    +#define LLDP_PORT_ID_ADDR		4	/* Network address */
    +#define LLDP_PORT_ID_IFNAME		5	/* Interface name */
    +#define LLDP_PORT_ID_AGENTCID		6	/* Agent circuit ID */
    +#define LLDP_PORT_ID_LOCAL		7	/* Locally assigned */
    +
    +/*
    + * System Capabilities bits
    + */
    +
    +#define LLDP_SYSTEM_CAP_OTHER		(1 << 0)
    +#define LLDP_SYSTEM_CAP_REPEATER	(1 << 1)
    +#define LLDP_SYSTEM_CAP_BRIDGE		(1 << 2)
    +#define LLDP_SYSTEM_CAP_WLAN		(1 << 3)
    +#define LLDP_SYSTEM_CAP_ROUTER		(1 << 4)
    +#define LLDP_SYSTEM_CAP_TELEPHONE	(1 << 5)
    +#define LLDP_SYSTEM_CAP_DOCSIS		(1 << 6)
    +#define LLDP_SYSTEM_CAP_STATION		(1 << 7)
    +
    +#endif /* _NET_LLDP_H_ */
    Index: usr.sbin/lldpd/Makefile
    ===================================================================
    RCS file: usr.sbin/lldpd/Makefile
    diff -N usr.sbin/lldpd/Makefile
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/Makefile	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,12 @@
    +PROG=		lldpd
    +SRCS=		lldpd.c pdu.c
    +SRCS+=		log.c
    +MAN=		lldpd.8
    +
    +CFLAGS+=	-Wall -Werror
    +DEBUG=		-g
    +
    +LDADD+=		-levent
    +DPADD+=		${LIBEVENT}
    +
    +.include <bsd.prog.mk>
    Index: usr.sbin/lldpd/lldpd.8
    ===================================================================
    RCS file: usr.sbin/lldpd/lldpd.8
    diff -N usr.sbin/lldpd/lldpd.8
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/lldpd.8	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,76 @@
    +.\" $OpenBSD$
    +.\"
    +.\" Copyright (c) 2025 David Gwynne <dlg@openbsd.org>
    +.\"
    +.\" Permission to use, copy, modify, and distribute this software for any
    +.\" purpose with or without fee is hereby granted, provided that the above
    +.\" copyright notice and this permission notice appear in all copies.
    +.\"
    +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    +.\"
    +.Dd $Mdocdate$
    +.Dt OLLDPD 8
    +.Os
    +.Sh NAME
    +.Nm lldpd
    +.Nd Link Layer Discovery Protocol (LLDP) daemon
    +.Sh SYNOPSIS
    +.Nm
    +.Op Fl d
    +.Op Fl s Ar /path/to/ctl.sock
    +.Sh DESCRIPTION
    +The
    +.Nm
    +daemon receives
    +Link Layer Discovery Protocol
    +.Pq LLDP
    +messages on Ethernet interfaces in the system and stores them for
    +display by
    +.Xr lldp 8 .
    +.Pp
    +LLDP is a link layer protocol that allows network devices to advertise
    +and discover neighbour identity and capabability with neighbour devices.
    +.Pp
    +The options are as follows:
    +.Bl -tag -width "-f fileXXX"
    +.It Fl d
    +Do not daemonize.
    +If this option is specified,
    +.Nm
    +will run in the foreground and log to stderr.
    +.It Fl s Ar path
    +Specify the filesystem
    +.Ar path
    +used for the control socket.
    +The control socket is used by
    +.Xr lldp 8
    +to communicate with the
    +.Nm
    +daemon.
    +.El
    +.Sh FILES
    +.Bl -tag -width "/var/run/lldp.sock" -compact
    +.It Pa /var/run/lldp.sock
    +Default
    +.Nm
    +control socket path.
    +.El
    +.Sh SEE ALSO
    +.\" Xr frame 4 ,
    +.Xr lldp 8
    +.Sh STANDARDS
    +.Rs
    +.%R IEEE 802.11AB
    +.%T Station and Media Access Control Connectivity Discovery
    +.Re
    +.Sh HISTORY
    +The
    +.Nm
    +program first appeared in
    +.Ox 7.8 .
    Index: usr.sbin/lldpd/lldpd.c
    ===================================================================
    RCS file: usr.sbin/lldpd/lldpd.c
    diff -N usr.sbin/lldpd/lldpd.c
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/lldpd.c	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,1316 @@
    +/*	$OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2024 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#include <sys/socket.h>
    +#include <sys/ioctl.h>
    +#include <sys/un.h>
    +#include <sys/types.h>
    +#include <sys/stat.h>
    +
    +#include <sys/queue.h>
    +#include <sys/tree.h>
    +
    +#include <net/if.h>
    +#include <net/if_dl.h>
    +#include <net/route.h>
    +#include <net/frame.h>
    +#include <net/if_types.h>
    +
    +#include <netinet/in.h>
    +#include <netinet/if_ether.h>
    +#include <net/ethertypes.h>
    +#include <net/lldp.h>
    +
    +#include <stddef.h>
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <string.h>
    +#include <fcntl.h>
    +#include <unistd.h>
    +#include <err.h>
    +#include <errno.h>
    +#include <ifaddrs.h>
    +#include <pwd.h>
    +#include <paths.h>
    +
    +#include <event.h>
    +
    +#include "pdu.h"
    +#include "lldpctl.h"
    +
    +#include <syslog.h>
    +#include "log.h"
    +
    +#ifndef nitems
    +#define nitems(_a) ((sizeof((_a)) / sizeof((_a)[0])))
    +#endif
    +
    +int rdaemon(int);
    +
    +#define LLDPD_USER		"_lldp"
    +
    +#define CMSG_FOREACH(_cmsg, _msgp) \
    +	for ((_cmsg) = CMSG_FIRSTHDR((_msgp)); \
    +	    (_cmsg) != NULL; \
    +	    (_cmsg) = CMSG_NXTHDR((_msgp), (_cmsg)))
    +
    +static const uint8_t maddr[ETHER_ADDR_LEN] =
    +    { 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e };
    +
    +static inline int
    +cmsg_match(const struct cmsghdr *cmsg, size_t len, int level, int type)
    +{
    +	return (cmsg->cmsg_len == CMSG_LEN(len) &&
    +	    cmsg->cmsg_level == level && cmsg->cmsg_type == type);
    +}
    +
    +#define CMSG_MATCH(_cmsg, _len, _level, _type) \
    +	cmsg_match((_cmsg), (_len), (_level), (_type))
    +
    +struct iface;
    +
    +struct lldp_msap {
    +	struct iface		*msap_iface;
    +	struct ether_addr	 msap_saddr;
    +	struct ether_addr	 msap_daddr;
    +	struct timespec		 msap_created;
    +	struct timespec		 msap_updated;
    +	uint64_t		 msap_packets;
    +	uint64_t		 msap_updates;
    +
    +	struct event		 msap_expiry;
    +
    +	TAILQ_ENTRY(lldp_msap)	 msap_entry;	/* iface list */
    +	TAILQ_ENTRY(lldp_msap)	 msap_aentry;	/* daemon list */
    +	unsigned int		 msap_refs;
    +
    +	void *			*msap_pdu;
    +	size_t			 msap_pdu_len;
    +
    +	/*
    +         * LLDP PDUs are required to start with chassis id, port
    +	 * id, and ttl as the first three TLVs. The MSAP id is made
    +	 * up of the chassis and port id, so rathert than extract and
    +	 * compose an id out of these TLVs, we can use the start of
    +	 * the PDU directly as the identifier.
    +	 */
    +	unsigned int		 msap_id_len;
    +};
    +
    +TAILQ_HEAD(lldp_msaps, lldp_msap);
    +
    +struct iface_key {
    +	unsigned int		 if_index;
    +	char			 if_name[IFNAMSIZ];
    +};
    +
    +struct iface {
    +	struct iface_key	 if_key; /* must be first */
    +	RBT_ENTRY(iface)	 if_entry;
    +
    +	struct lldpd		*if_lldpd;
    +	struct lldp_msaps	 if_msaps;
    +
    +	uint64_t		 if_agent_counters[AGENT_COUNTER_NCOUNTERS];
    +};
    +
    +RBT_HEAD(ifaces, iface);
    +
    +static inline int
    +
    +iface_cmp(const struct iface *a, const struct iface *b)
    +{
    +	const struct iface_key *ka = &a->if_key;
    +	const struct iface_key *kb = &b->if_key;
    +
    +	if (ka->if_index > kb->if_index)
    +		return (1);
    +	if (ka->if_index < kb->if_index)
    +		return (-1);
    +	return (0);
    +}
    +
    +RBT_PROTOTYPE(ifaces, iface, if_entry, iface_cmp);
    +
    +struct lldpd_ctl {
    +	struct lldpd		*ctl_lldpd;
    +
    +	struct event		 ctl_rd_ev;
    +	struct event		 ctl_wr_ev;
    +
    +	uid_t			 ctl_peer_uid;
    +	gid_t			 ctl_peer_gid;
    +
    +	void (*ctl_handler)(struct lldpd *, struct lldpd_ctl *, int fd);
    +	void			*ctl_ctx;
    +};
    +
    +struct lldpd {
    +	const char		*ctl_path;
    +
    +	struct event		 rt_ev;
    +	struct event		 en_ev;
    +	struct event		 ctl_ev;
    +	int			 s;
    +
    +	struct ifaces		 ifaces;
    +
    +	struct lldp_msaps	 msaps;
    +
    +	uint64_t		 agent_counters[AGENT_COUNTER_NCOUNTERS];
    +};
    +
    +static void	rtsock_open(struct lldpd *);
    +static void	rtsock_recv(int, short, void *);
    +static void	ensock_open(struct lldpd *);
    +static void	ensock_recv(int, short, void *);
    +static void	ctlsock_open(struct lldpd *);
    +static void	ctlsock_accept(int, short, void *);
    +
    +static int	getall(struct lldpd *);
    +
    +extern char *__progname;
    +
    +__dead static void
    +usage(void)
    +{
    +	fprintf(stderr, "usage: %s [-d] [-s /path/to/ctl.sock]\n", __progname);
    +	exit(1);
    +}
    +
    +int
    +main(int argc, char *argv[])
    +{
    +	struct lldpd _lldpd = {
    +		.ctl_path = LLDP_CTL_PATH,
    +		.ifaces = RBT_INITIALIZER(_lldpd.ifaces),
    +		.msaps = TAILQ_HEAD_INITIALIZER(_lldpd.msaps),
    +	};
    +	struct lldpd *lldpd = &_lldpd; /* let me use -> consistently */
    +	struct passwd *pw;
    +	int debug = 0;
    +	int devnull = -1;
    +
    +	int ch;
    +
    +	while ((ch = getopt(argc, argv, "ds:")) != -1) {
    +		switch (ch) {
    +		case 'd':
    +			debug = 1;
    +			break;
    +		case 's':
    +			lldpd->ctl_path = optarg;
    +			break;
    +		default:
    +			usage();
    +			/* NOTREACHED */
    +		}
    +	}
    +
    +	argc -= optind;
    +	argv += optind;
    +
    +	if (argc != 0)
    +		usage();
    +
    +	if (geteuid() != 0)
    +		errx(1, "need root privileges");
    +
    +	pw = getpwnam(LLDPD_USER);
    +	if (pw == NULL)
    +		errx(1, "no %s user", LLDPD_USER);
    +
    +	if (!debug) {
    +		logger_syslog(__progname, LOG_DAEMON);
    +		devnull = open(_PATH_DEVNULL, O_RDWR);
    +		if (devnull == -1)
    +			err(1, "%s", _PATH_DEVNULL);
    +	}
    +
    +	rtsock_open(lldpd);
    +	ensock_open(lldpd);
    +	ctlsock_open(lldpd);
    +
    +	printf("debug = %d\n", debug);
    +
    +	if (chroot(pw->pw_dir) == -1)
    +		err(1, "chroot %s", pw->pw_dir);
    +	if (chdir("/") == -1)
    +		err(1, "chdir %s", pw->pw_dir);
    +
    +	/* drop privs */
    +	if (setgroups(1, &pw->pw_gid) ||
    +	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
    +	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
    +		errx(1, "can't drop privileges");
    +
    +	pw = NULL;
    +	endpwent();
    +
    +	lldpd->s = socket(AF_INET, SOCK_DGRAM, 0);
    +	if (lldpd->s == -1)
    +		err(1, "inet sock");
    +
    +	if (getall(lldpd) == -1)
    +		warn("getall");
    +
    +	if (!debug && rdaemon(devnull) == -1)
    +		err(1, "unable to daemonize");
    +
    +	event_init();
    +
    +	event_set(&lldpd->rt_ev, EVENT_FD(&lldpd->rt_ev),
    +	    EV_READ|EV_PERSIST, rtsock_recv, lldpd);
    +	event_set(&lldpd->en_ev, EVENT_FD(&lldpd->en_ev),
    +	    EV_READ|EV_PERSIST, ensock_recv, lldpd);
    +	event_set(&lldpd->ctl_ev, EVENT_FD(&lldpd->ctl_ev),
    +	    EV_READ|EV_PERSIST, ctlsock_accept, lldpd);
    +
    +	event_add(&lldpd->rt_ev, NULL);
    +	event_add(&lldpd->en_ev, NULL);
    +	event_add(&lldpd->ctl_ev, NULL);
    +
    +	event_dispatch();
    +
    +	return (0);
    +}
    +
    +static void
    +agent_counter_inc(struct lldpd *lldpd, struct iface *ifp,
    +    enum agent_counter c)
    +{
    +	lldpd->agent_counters[c]++;
    +	ifp->if_agent_counters[c]++;
    +}
    +
    +static struct lldp_msap *
    +lldp_msap_take(struct lldpd *lldpd, struct lldp_msap *msap)
    +{
    +	msap->msap_refs++;
    +	return (msap);
    +}
    +
    +static void
    +lldp_msap_rele(struct lldpd *lldpd, struct lldp_msap *msap)
    +{
    +	if (--msap->msap_refs == 0) {
    +		TAILQ_REMOVE(&lldpd->msaps, msap, msap_aentry);
    +		free(msap->msap_pdu);
    +		free(msap);
    +	}
    +}
    +
    +static void
    +lldp_msap_remove(struct iface *ifp, struct lldp_msap *msap)
    +{
    +	evtimer_del(&msap->msap_expiry);
    +	TAILQ_REMOVE(&ifp->if_msaps, msap, msap_entry);
    +
    +	lldp_msap_rele(ifp->if_lldpd, msap);
    +}
    +
    +static void
    +lldp_msap_expire(int nil, short events, void *arg)
    +{
    +	struct lldp_msap *msap = arg;
    +	struct iface *ifp = msap->msap_iface;
    +	struct lldpd *lldpd = ifp->if_lldpd;
    +
    +	agent_counter_inc(lldpd, ifp, statsAgeoutsTotal);
    +
    +	ldebug("%s: entry from %s has expired", ifp->if_key.if_name,
    +	    ether_ntoa(&msap->msap_saddr));
    +
    +	lldp_msap_remove(ifp, msap);
    +}
    +
    +static void
    +rtsock_open(struct lldpd *lldpd)
    +{
    +	unsigned int rtfilter;
    +	int s;
    +
    +	s = socket(AF_ROUTE, SOCK_RAW | SOCK_NONBLOCK, AF_UNSPEC);
    +	if (s == -1)
    +		err(1, "route socket");
    +
    +	rtfilter = ROUTE_FILTER(RTM_IFINFO) | ROUTE_FILTER(RTM_IFANNOUNCE);
    +	if (setsockopt(s, AF_ROUTE, ROUTE_MSGFILTER,
    +	    &rtfilter, sizeof(rtfilter)) == -1)
    +		err(1, "route socket setsockopt msgfilter");
    +
    +	event_set(&lldpd->rt_ev, s, 0, NULL, NULL);
    +}
    +
    +static inline struct iface *
    +iface_insert(struct lldpd *lldpd, struct iface *ifp)
    +{
    +	return (RBT_INSERT(ifaces, &lldpd->ifaces, ifp));
    +}
    +
    +static struct iface *
    +iface_find(struct lldpd *lldpd, const char *ifname, int ifindex)
    +{
    +	struct iface_key key = { .if_index = ifindex };
    +
    +	return (RBT_FIND(ifaces, &lldpd->ifaces, (struct iface *)&key));
    +}
    +
    +static inline void
    +iface_remove(struct lldpd *lldpd, struct iface *ifp)
    +{
    +	RBT_REMOVE(ifaces, &lldpd->ifaces, ifp);
    +}
    +
    +static void
    +rtsock_if_attach(struct lldpd *lldpd, const struct if_announcemsghdr *ifan)
    +{
    +	struct ifreq ifr;
    +	struct if_data ifi;
    +	struct iface *ifp;
    +	struct frame_mreq fmr;
    +
    +	memset(&ifr, 0, sizeof(ifr));
    +	memcpy(ifr.ifr_name, ifan->ifan_name, sizeof(ifr.ifr_name));
    +	ifr.ifr_data = (caddr_t)&ifi;
    +
    +	if (ioctl(lldpd->s, SIOCGIFDATA, &ifr) == -1) {
    +		lwarn("%s index %u: attach get data",
    +		    ifan->ifan_name, ifan->ifan_index);
    +		return;
    +	}
    +
    +	if (ifi.ifi_type != IFT_ETHER)
    +		return;
    +
    +	ifp = malloc(sizeof(*ifp));
    +	if (ifp == NULL) {
    +		lwarn("%s index %u: allocation",
    +		    ifan->ifan_name, ifan->ifan_index);
    +		return;
    +	}
    +
    +	ifp->if_key.if_index = ifan->ifan_index;
    +	strlcpy(ifp->if_key.if_name, ifan->ifan_name,
    +	    sizeof(ifp->if_key.if_name));
    +
    +	ifp->if_lldpd = lldpd;
    +	TAILQ_INIT(&ifp->if_msaps);
    +
    +	if (iface_insert(lldpd, ifp) != NULL) {
    +		lwarnx("%s index %u: already exists",
    +		    ifan->ifan_name, ifan->ifan_index);
    +		free(ifp);
    +		return;
    +	}
    +
    +	linfo("%s index %u: attached", ifan->ifan_name, ifan->ifan_index);
    +
    +	memset(&fmr, 0, sizeof(fmr));
    +	fmr.fmr_ifindex = ifp->if_key.if_index;
    +	memcpy(fmr.fmr_addr, maddr, ETHER_ADDR_LEN);
    +
    +	if (setsockopt(EVENT_FD(&lldpd->en_ev),
    +	    IFT_ETHER, FRAME_ADD_MEMBERSHIP,
    +	    &fmr, sizeof(fmr)) == -1) {
    +		lwarn("%s index %u: add membership",
    +		    ifan->ifan_name, ifan->ifan_index);
    +	}
    +}
    +
    +static void
    +rtsock_if_detach(struct lldpd *lldpd, const struct if_announcemsghdr *ifan)
    +{
    +	struct iface *ifp;
    +	struct lldp_msap *msap, *nmsap;
    +	struct frame_mreq fmr;
    +
    +	ifp = iface_find(lldpd, ifan->ifan_name, ifan->ifan_index);
    +	if (ifp == NULL)
    +		return;
    +
    +	memset(&fmr, 0, sizeof(fmr));
    +	fmr.fmr_ifindex = ifp->if_key.if_index;
    +	memcpy(fmr.fmr_addr, maddr, ETHER_ADDR_LEN);
    +
    +	if (setsockopt(EVENT_FD(&lldpd->en_ev),
    +	    IFT_ETHER, FRAME_DEL_MEMBERSHIP,
    +	    &fmr, sizeof(fmr)) == -1) {
    +		lwarn("%s index %u: del membership",
    +		    ifp->if_key.if_name, ifp->if_key.if_index);
    +	}
    +
    +	/* don't have to leave mcast group */
    +	linfo("%s index %u: detached", ifan->ifan_name, ifan->ifan_index);
    +
    +	iface_remove(lldpd, ifp);
    +
    +	TAILQ_FOREACH_SAFE(msap, &ifp->if_msaps, msap_entry, nmsap)
    +		lldp_msap_remove(ifp, msap);
    +
    +	free(ifp);
    +}
    +
    +static void
    +rtsock_ifannounce(struct lldpd *lldpd, const struct rt_msghdr *rtm, size_t len)
    +{
    +	const struct if_announcemsghdr *ifan;
    +
    +	if (len < sizeof(*ifan)) {
    +		lwarnx("short ifannounce message: %zu < %zu", len,
    +		    sizeof(*ifan));
    +		return;
    +	}
    +
    +	ifan = (const struct if_announcemsghdr *)rtm;
    +	if (ifan->ifan_index == 0) {
    +		lwarnx("%s index %u: %s() invalid index, ignoring",
    +		    ifan->ifan_name, ifan->ifan_index, __func__);
    +		return;
    +	}
    +
    +	switch (ifan->ifan_what) {
    +	case IFAN_ARRIVAL:
    +		rtsock_if_attach(lldpd, ifan);
    +		break;
    +	case IFAN_DEPARTURE:
    +		rtsock_if_detach(lldpd, ifan);
    +		break;
    +	default:
    +		lwarnx("%s: %s index %u: unexpected ifannounce ifan %u",
    +		    __func__, ifan->ifan_name, ifan->ifan_index,
    +		    ifan->ifan_what);
    +		return;
    +	}
    +}
    +
    +static void
    +rtsock_recv(int s, short events, void *arg)
    +{
    +	struct lldpd *lldpd = arg;
    +	char buf[1024];
    +	const struct rt_msghdr *rtm = (struct rt_msghdr *)buf;
    +	ssize_t rv;
    +
    +	rv = recv(s, buf, sizeof(buf), 0);
    +	if (rv == -1) {
    +		lwarn("route message");
    +		return;
    +	}
    +
    +	linfo("route message: %zd bytes", rv);
    +	if (rtm->rtm_version != RTM_VERSION) {
    +		lwarnx("routing message version %u not understood",
    +		    rtm->rtm_version);
    +		return;
    +	}
    +
    +	switch (rtm->rtm_type) {
    +	case RTM_IFINFO:
    +		linfo("ifinfo");
    +		break;
    +	case RTM_IFANNOUNCE:
    +		rtsock_ifannounce(lldpd, rtm, rv);
    +		break;
    +	default:
    +		return;
    +	}
    +}
    +
    +static void
    +ensock_open(struct lldpd *lldpd)
    +{
    +	struct sockaddr_frame sfrm = {
    +		.sfrm_family = AF_FRAME,
    +		.sfrm_proto = htons(ETHERTYPE_LLDP),
    +	};
    +	int opt;
    +	int s;
    +
    +	s = socket(AF_FRAME, SOCK_DGRAM | SOCK_NONBLOCK, IFT_ETHER);
    +	if (s == -1)
    +		err(1, "Ethernet socket");
    +
    +	opt = 1;
    +	if (setsockopt(s, IFT_ETHER, FRAME_RECVDSTADDR,
    +	    &opt, sizeof(opt)) == -1)
    +		err(1, "Ethernet setsockopt recv dstaddr");
    +
    +	if (bind(s, (struct sockaddr *)&sfrm, sizeof(sfrm)) == -1)
    +		err(1, "Ethernet bind lldp");
    +
    +	event_set(&lldpd->en_ev, s, 0, NULL, NULL);
    +}
    +
    +static void
    +ensock_recv(int s, short events, void *arg)
    +{
    +	struct lldpd *lldpd = arg;
    +	struct sockaddr_frame sfrm;
    +	uint8_t buf[1500];
    +
    +	static const struct ether_addr naddr;
    +	struct ether_addr *saddr = NULL;
    +	struct ether_addr *daddr = (struct ether_addr *)&naddr;
    +	struct cmsghdr *cmsg;
    +	union {
    +		struct cmsghdr hdr;
    +		uint8_t buf[CMSG_SPACE(sizeof(*daddr))];
    +	} cmsgbuf;
    +	struct iovec iov[1] = {
    +		{ .iov_base = buf, .iov_len = sizeof(buf) },
    +	};
    +	struct msghdr msg = {
    +		.msg_name = &sfrm,
    +		.msg_namelen = sizeof(sfrm),
    +		.msg_control = &cmsgbuf.buf,
    +		.msg_controllen = sizeof(cmsgbuf.buf),
    +		.msg_iov = iov,
    +		.msg_iovlen = 1,
    +	};
    +	ssize_t rv;
    +	size_t len;
    +
    +	struct iface *ifp;
    +	struct iface_key key;
    +	struct lldp_msap *msap;
    +
    +	struct tlv tlv;
    +	unsigned int tlvs;
    +	int ok;
    +	unsigned int idlen;
    +	unsigned int ttl;
    +	struct timeval age;
    +	int update = 0;
    +
    +	rv = recvmsg(s, &msg, 0);
    +	if (rv == -1) {
    +		lwarn("Ethernet recv");
    +		return;
    +	}
    +
    +	CMSG_FOREACH(cmsg, &msg) {
    +		if (CMSG_MATCH(cmsg,
    +		    sizeof(*daddr), IFT_ETHER, FRAME_RECVDSTADDR)) {
    +			daddr = (struct ether_addr *)CMSG_DATA(cmsg);
    +		}
    +	}
    +	saddr = (struct ether_addr *)sfrm.sfrm_addr;
    +
    +	ldebug("%s: pdu from %s: %zd bytes", sfrm.sfrm_ifname,
    +	    ether_ntoa(saddr), rv);
    +
    +	key.if_index = sfrm.sfrm_ifindex;
    +	ifp = RBT_FIND(ifaces, &lldpd->ifaces, (struct iface *)&key);
    +	if (ifp == NULL) {
    +		/* count */
    +		return;
    +	}
    +
    +	/* XXX check if RX is enabled */
    +	agent_counter_inc(lldpd, ifp, statsFramesInTotal);
    +
    +	len = rv;
    +
    +	ok = tlv_first(&tlv, buf, len);
    +	if (ok != 1) {
    +		ldebug("%s: pdu from %s: first TLV extraction failed",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_type != LLDP_TLV_CHASSIS_ID) {
    +		ldebug("%s: pdu from %s: first TLV type is not Chassis ID",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_len < 2 || tlv.tlv_len > 256) {
    +		ldebug("%s: pdu from %s: "
    +		    "Chassis ID TLV length %u is out of range",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr),
    +		    tlv.tlv_len);
    +		goto discard;
    +	}
    +
    +	ok = tlv_next(&tlv, buf, len);
    +	if (ok != 1) {
    +		ldebug("%s: pdu from %s: second TLV extraction failed",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_type != LLDP_TLV_PORT_ID) {
    +		ldebug("%s: pdu from %s: first TLV type is not Port ID",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_len < 2 || tlv.tlv_len > 256) {
    +		ldebug("%s: pdu from %s: "
    +		    "Port ID TLV length %u is out of range",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr),
    +		    tlv.tlv_len);
    +		goto discard;
    +	}
    +
    +	ok = tlv_next(&tlv, buf, rv);
    +	if (ok != 1) {
    +		ldebug("%s: pdu from %s: third TLV extraction failed",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_type != LLDP_TLV_TTL) {
    +		ldebug("%s: pdu from %s: third TLV type is not TTL",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		goto discard;
    +	}
    +	if (tlv.tlv_len < 2) {
    +		ldebug("%s: pdu from %s: "
    +		    "TTL TLV length %u is too short",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr),
    +		    tlv.tlv_len);
    +		goto discard;
    +	}
    +
    +	ttl = pdu_u16(tlv.tlv_payload);
    +
    +        /*
    +	 * the tlv_offset points to the start of the current tlv,
    +	 * which is also the end of the previous tlv. the msap id is
    +	 * a concat of the first two tlvs, which is where the offset
    +	 * is now.
    +	 */
    +	idlen = tlv.tlv_offset;
    +
    +	tlvs = (1 << LLDP_TLV_CHASSIS_ID) | (1 << LLDP_TLV_PORT_ID) |
    +	    (1 << LLDP_TLV_TTL);
    +
    +	for (;;) {
    +		ok = tlv_next(&tlv, buf, rv);
    +		if (ok == -1) {
    +			lwarnx("TLV extraction failed");
    +			goto discard;
    +		}
    +		if (ok == 0)
    +			break;
    +
    +		switch (tlv.tlv_type) {
    +		case LLDP_TLV_END:
    +			lwarnx("end of pdu with non-zero length");
    +			goto discard;
    +
    +		case LLDP_TLV_CHASSIS_ID:
    +		case LLDP_TLV_PORT_ID:
    +		case LLDP_TLV_TTL:
    +		case LLDP_TLV_PORT_DESCR:
    +		case LLDP_TLV_SYSTEM_NAME:
    +		case LLDP_TLV_SYSTEM_DESCR:
    +		case LLDP_TLV_SYSTEM_CAP:
    +			if ((1 << tlv.tlv_type) & tlvs) {
    +				lwarnx("TLV type %u repeated", tlv.tlv_type);
    +				goto discard;
    +			}
    +			tlvs |= (1 << tlv.tlv_type);
    +			break;
    +		}
    +	}
    +
    +	TAILQ_FOREACH(msap, &ifp->if_msaps, msap_entry) {
    +		if (msap->msap_id_len == idlen &&
    +		    memcmp(msap->msap_pdu, buf, idlen) == 0)
    +			break;
    +	}
    +
    +	if (msap == NULL) {
    +		if (ttl == 0) {
    +			/* optimise DELETE_INFO below */
    +			return;
    +		}
    +
    +		msap = malloc(sizeof(*msap));
    +		if (msap == NULL) {
    +			lwarn("%s: msap alloc", ifp->if_key.if_name);
    +			agent_counter_inc(lldpd, ifp,
    +			    statsFramesDiscardedTotal);
    +			return;
    +		}
    +		msap->msap_iface = ifp;
    +		msap->msap_pdu = NULL;
    +		msap->msap_pdu_len = 0;
    +		msap->msap_id_len = idlen;
    +
    +		if (clock_gettime(CLOCK_BOOTTIME, &msap->msap_created) == -1)
    +			lerr(1, "CLOCK_BOOTTIME");
    +
    +		msap->msap_updated.tv_sec = 0;
    +		msap->msap_updated.tv_nsec = 0;
    +
    +		msap->msap_packets = 1;
    +		msap->msap_updates = 0;
    +
    +		msap->msap_refs = 1;
    +		evtimer_set(&msap->msap_expiry, lldp_msap_expire, msap);
    +		TAILQ_INSERT_TAIL(&ifp->if_msaps, msap, msap_entry);
    +		TAILQ_INSERT_TAIL(&lldpd->msaps, msap, msap_aentry);
    +	}
    +
    +	if (ttl == 0) {
    +		/* DELETE_INFO */
    +		if (msap->msap_pdu == NULL) {
    +			lwarnx("new msap DELETE_INFO");
    +			abort();
    +		}
    +
    +		evtimer_del(&msap->msap_expiry);
    +		TAILQ_REMOVE(&ifp->if_msaps, msap, msap_entry);
    +		lldp_msap_rele(lldpd, msap);
    +
    +		ldebug("%s: entry from %s deleted",
    +		    sfrm.sfrm_ifname, ether_ntoa(saddr));
    +		return;
    +	}
    +
    +	if (len != msap->msap_pdu_len) {
    +		void *pdu = realloc(msap->msap_pdu, len);
    +		if (pdu == NULL) {
    +			lwarn("%s: pdu alloc", ifp->if_key.if_name);
    +			if (msap->msap_pdu == NULL) {
    +				TAILQ_REMOVE(&lldpd->msaps, msap,
    +				    msap_aentry);
    +				TAILQ_REMOVE(&ifp->if_msaps, msap,
    +				    msap_entry);
    +				free(msap);
    +			}
    +			agent_counter_inc(lldpd, ifp,
    +			    statsFramesDiscardedTotal);
    +			return;
    +		}
    +		msap->msap_pdu = pdu;
    +		msap->msap_pdu_len = len;
    +		update = 1;
    +	} else if (memcmp(msap->msap_pdu, buf, len) != 0)
    +		update = 1;
    +
    +	if (update) {
    +		msap->msap_updates++;
    +		memcpy(msap->msap_pdu, buf, len);
    +		if (clock_gettime(CLOCK_BOOTTIME, &msap->msap_updated) == -1)
    +			lerr(1, "CLOCK_BOOTTIME");
    +	}
    +	msap->msap_saddr = *saddr;
    +	msap->msap_daddr = *daddr;
    +	msap->msap_packets++;
    +
    +	age.tv_sec = ttl;
    +	age.tv_usec = 0;
    +	evtimer_add(&msap->msap_expiry, &age);
    +
    +	return;
    +
    +discard:
    +	agent_counter_inc(lldpd, ifp, statsFramesDiscardedTotal);
    +	agent_counter_inc(lldpd, ifp, statsFramesInErrorsTotal);
    +}
    +
    +static void
    +ctlsock_open(struct lldpd *lldpd)
    +{
    +	struct sockaddr_un sun = {
    +		.sun_family = AF_UNIX,
    +	};
    +	const char *path = lldpd->ctl_path;
    +	mode_t oumask;
    +	int s;
    +
    +	if (strlcpy(sun.sun_path, path, sizeof(sun.sun_path)) >=
    +	    sizeof(sun.sun_path))
    +		errc(ENAMETOOLONG, 1, "control socket %s", path);
    +
    +	s = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK, 0);
    +	if (s == -1)
    +		err(1, "control socket");
    +
    +	/* try connect first? */
    +
    +	if (unlink(path) == -1) {
    +		if (errno != ENOENT)
    +			err(1, "control socket %s unlink", path);
    +	}
    +
    +	oumask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
    +	if (oumask == -1)
    +		err(1, "umask");
    +	if (bind(s, (struct sockaddr *)&sun, sizeof(sun)) == -1)
    +		err(1, "control socket %s bind", path);
    +	if (umask(oumask) == -1)
    +		err(1, "umask restore");
    +
    +	if (chmod(path, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) == -1)
    +		err(1, "control socket %s chmod", path);
    +
    +	if (listen(s, 5) == -1)
    +		err(1, "control socket %s listen", path);
    +
    +	event_set(&lldpd->ctl_ev, s, 0, NULL, NULL);
    +}
    +
    +static void
    +ctl_close(struct lldpd *lldpd, struct lldpd_ctl *ctl)
    +{
    +	int fd = EVENT_FD(&ctl->ctl_rd_ev);
    +
    +	event_del(&ctl->ctl_rd_ev);
    +	event_del(&ctl->ctl_wr_ev);
    +	free(ctl);
    +	close(fd);
    +}
    +
    +static ssize_t		ctl_ping(struct lldpd *, struct lldpd_ctl *,
    +			    const void *, size_t);
    +static ssize_t		ctl_msap_req(struct lldpd *, struct lldpd_ctl *,
    +			    const void *, size_t);
    +
    +static void
    +ctl_recv(int fd, short events, void *arg)
    +{
    +	struct lldpd_ctl *ctl = arg;
    +	struct lldpd *lldpd = ctl->ctl_lldpd;
    +	unsigned int msgtype;
    +	uint8_t buf[4096];
    +	struct iovec iov[2] = {
    +	    { &msgtype, sizeof(msgtype) },
    +	    { buf, sizeof(buf) },
    +	};
    +	struct msghdr msg = {
    +		.msg_iov = iov,
    +		.msg_iovlen = nitems(iov),
    +	};
    +	ssize_t rv;
    +	size_t len;
    +
    +	rv = recvmsg(fd, &msg, 0);
    +	if (rv == -1) {
    +		lwarn("ctl recv");
    +		return;
    +	}
    +	if (rv == 0) {
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	if (ctl->ctl_handler != NULL) {
    +		ldebug("ctl recv while not idle");
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	len = rv;
    +	ldebug("%s: msgtype %u, %zu bytes", __func__, msgtype, len);
    +	if (len < sizeof(msgtype)) {
    +		/* short message */
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +	len -= sizeof(msgtype);
    +
    +	switch (msgtype) {
    +	case LLDP_CTL_MSG_PING:
    +		rv = ctl_ping(lldpd, ctl, buf, len);
    +		break;
    +	case LLDP_CTL_MSG_MSAP_REQ:
    +		rv = ctl_msap_req(lldpd, ctl, buf, len);
    +		break;
    +	default:
    +		lwarnx("%s: unhandled message %u", __func__, msgtype);
    +		rv = -1;
    +		return;
    +	}
    +
    +	if (rv == -1) {
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	(*ctl->ctl_handler)(lldpd, ctl, fd);
    +}
    +
    +static void
    +ctl_done(struct lldpd *lldpd, struct lldpd_ctl *ctl)
    +{
    +	ctl->ctl_handler = NULL;
    +	ctl->ctl_ctx = NULL;
    +}
    +
    +static void
    +ctl_pong(struct lldpd *lldpd, struct lldpd_ctl *ctl, int fd)
    +{
    +	unsigned int msgtype = LLDP_CTL_MSG_PONG;
    +	struct iovec *piov = ctl->ctl_ctx;
    +	struct iovec iov[2] = {
    +	    { &msgtype, sizeof(msgtype) },
    +	    *piov,
    +	};
    +	struct msghdr msg = {
    +		.msg_iov = iov,
    +		.msg_iovlen = nitems(iov),
    +	};
    +	ssize_t rv;
    +
    +	rv = sendmsg(fd, &msg, 0);
    +	if (rv == -1) {
    +		switch (errno) {
    +		case EINTR:
    +		case EAGAIN:
    +			event_add(&ctl->ctl_wr_ev, NULL);
    +			return;
    +		default:
    +			lwarn("%s", __func__);
    +			break;
    +		}
    +
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	free(piov->iov_base);
    +	free(piov);
    +
    +	ctl_done(lldpd, ctl);
    +}
    +
    +static ssize_t
    +ctl_ping(struct lldpd *lldpd, struct lldpd_ctl *ctl,
    +    const void *buf, size_t len)
    +{
    +	struct iovec *iov;
    +
    +	iov = malloc(sizeof(*iov));
    +	if (iov == NULL) {
    +		lwarn("%s iovec", __func__);
    +		return (-1);
    +	}
    +
    +	iov->iov_base = malloc(len);
    +	if (iov->iov_base == NULL) {
    +		lwarn("%s", __func__);
    +		free(iov);
    +		return (-1);
    +	}
    +	memcpy(iov->iov_base, buf, len);
    +	iov->iov_len = len;
    +
    +	ctl->ctl_handler = ctl_pong;
    +	ctl->ctl_ctx = iov;
    +
    +	return (0);
    +}
    +
    +static void
    +ctl_send(int fd, short events, void *arg)
    +{
    +	struct lldpd_ctl *ctl = arg;
    +	struct lldpd *lldpd = ctl->ctl_lldpd;
    +
    +	ctl->ctl_handler(lldpd, ctl, fd);
    +}
    +
    +struct ctl_msap_ctx {
    +	char ifname[IFNAMSIZ];
    +	struct lldp_msap *msap;
    +};
    +
    +static void		ctl_msap(struct lldpd *, struct lldpd_ctl *, int);
    +static ssize_t		ctl_msap_req_next(struct lldpd *, struct lldpd_ctl *,
    +			    struct lldp_msap *);
    +static void		ctl_msap_req_end(struct lldpd *, struct lldpd_ctl *,
    +			    int);
    +
    +static ssize_t
    +ctl_msap_req(struct lldpd *lldpd, struct lldpd_ctl *ctl,
    +    const void *buf, size_t len)
    +{
    +	const struct lldp_ctl_msg_msap_req *req;
    +	struct ctl_msap_ctx *ctx;
    +
    +	if (len != sizeof(*req)) {
    +		lwarnx("%s req len", __func__);
    +		return (-1);
    +	}
    +	req = buf;
    +
    +	ctx = malloc(sizeof(*ctx));
    +	if (ctx == NULL) {
    +		lwarnx("%s ctx", __func__);
    +		return (-1);
    +	}
    +	memcpy(ctx->ifname, req->ifname, sizeof(ctx->ifname));
    +
    +	ctl->ctl_handler = ctl_msap;
    +	ctl->ctl_ctx = ctx;
    +
    +	return (ctl_msap_req_next(lldpd, ctl, TAILQ_FIRST(&lldpd->msaps)));
    +}
    +
    +static void
    +ctl_msap(struct lldpd *lldpd, struct lldpd_ctl *ctl, int fd)
    +{
    +	struct ctl_msap_ctx *ctx = ctl->ctl_ctx;
    +	struct lldp_msap *msap = ctx->msap;
    +	struct iface *ifp;
    +	ssize_t rv;
    +
    +	unsigned int msgtype = LLDP_CTL_MSG_MSAP;
    +	struct lldp_ctl_msg_msap msg_msap;
    +	struct iovec iov[3] = {
    +	    { &msgtype, sizeof(msgtype) },
    +	    { &msg_msap, sizeof(msg_msap) }
    +	};
    +	struct msghdr msg = {
    +		.msg_iov = iov,
    +		.msg_iovlen = nitems(iov),
    +	};
    +
    +	memset(&msg_msap, 0, sizeof(msg_msap));
    +
    +	ifp = msap->msap_iface;
    +	if (ifp != NULL) {
    +		strlcpy(msg_msap.ifname, ifp->if_key.if_name,
    +		    sizeof(msg_msap.ifname));
    +	}
    +	msg_msap.saddr = msap->msap_saddr;
    +	msg_msap.daddr = msap->msap_daddr;
    +	msg_msap.created = msap->msap_created;
    +	msg_msap.updated = msap->msap_updated;
    +	msg_msap.packets = msap->msap_packets;
    +	msg_msap.updates = msap->msap_updates;
    +
    +	iov[2].iov_base = msap->msap_pdu;
    +	iov[2].iov_len = msap->msap_pdu_len;
    +
    +	rv = sendmsg(fd, &msg, 0);
    +	if (rv == -1) {
    +		switch (errno) {
    +		case EINTR:
    +		case EAGAIN:
    +			event_add(&ctl->ctl_wr_ev, NULL);
    +			return;
    +		default:
    +			lwarn("ctl send");
    +			break;
    +		}
    +
    +		lldp_msap_rele(lldpd, msap);
    +		free(ctx);
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	rv = ctl_msap_req_next(lldpd, ctl, TAILQ_NEXT(msap, msap_aentry));
    +        lldp_msap_rele(lldpd, msap);
    +
    +	if (rv == -1) {
    +		free(ctx);
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	event_add(&ctl->ctl_wr_ev, NULL);
    +}
    +
    +static ssize_t
    +ctl_msap_req_next(struct lldpd *lldpd, struct lldpd_ctl *ctl,
    +    struct lldp_msap *msap)
    +{
    +	struct ctl_msap_ctx *ctx = ctl->ctl_ctx;
    +	struct iface *ifp;
    +
    +	for (;;) {
    +		if (msap == NULL) {
    +			ctl->ctl_handler = ctl_msap_req_end;
    +			return (0);
    +		}
    +
    +		if (ctx->ifname[0] == '\0')
    +			break;
    +		ifp = msap->msap_iface;
    +		if (ifp != NULL && strncmp(ifp->if_key.if_name, ctx->ifname,
    +		    sizeof(ifp->if_key.if_name)) == 0)
    +			break;
    +
    +		msap = TAILQ_NEXT(msap, msap_aentry);
    +	}
    +
    +	ctx->msap = lldp_msap_take(lldpd, msap);
    +	return (0);
    +}
    +
    +static void
    +ctl_msap_req_end(struct lldpd *lldpd, struct lldpd_ctl *ctl, int fd)
    +{
    +	struct ctl_msap_ctx *ctx = ctl->ctl_ctx;
    +
    +	unsigned int msgtype = LLDP_CTL_MSG_MSAP_END;
    +	ssize_t rv;
    +
    +	rv = send(fd, &msgtype, sizeof(msgtype), 0);
    +	if (rv == -1) {
    +		switch (errno) {
    +		case EINTR:
    +		case EAGAIN:
    +			event_add(&ctl->ctl_wr_ev, NULL);
    +			return;
    +		default:
    +			lwarn("%s", __func__);
    +			break;
    +		}
    +
    +		free(ctx);
    +		ctl_close(lldpd, ctl);
    +		return;
    +	}
    +
    +	free(ctx);
    +	ctl_done(lldpd, ctl);
    +}
    +
    +static void
    +ctlsock_accept(int s, short events, void *arg)
    +{
    +	struct lldpd *lldpd = arg;
    +	struct lldpd_ctl *ctl;
    +	int fd;
    +
    +	fd = accept4(s, NULL, NULL, SOCK_NONBLOCK);
    +	if (fd == -1) {
    +		lwarn("control socket %s accept", lldpd->ctl_path);
    +		return;
    +	}
    +
    +	ctl = malloc(sizeof(*ctl));
    +	if (ctl == NULL) {
    +		lwarn("ctl alloc");
    +		close(fd);
    +		return;
    +	}
    +	ctl->ctl_lldpd = lldpd;
    +	ctl->ctl_handler = NULL;
    +	ctl->ctl_ctx = NULL;
    +
    +	if (getpeereid(fd, &ctl->ctl_peer_uid, &ctl->ctl_peer_gid) == -1)
    +		err(1, "ctl getpeereid");
    +
    +	event_set(&ctl->ctl_rd_ev, fd, EV_READ|EV_PERSIST,
    +	    ctl_recv, ctl);
    +	event_set(&ctl->ctl_wr_ev, fd, EV_WRITE,
    +	    ctl_send, ctl);
    +
    +	event_add(&ctl->ctl_rd_ev, NULL);
    +}
    +
    +static int
    +getall(struct lldpd *lldpd)
    +{
    +	struct ifaddrs *ifa0, *ifa;
    +	struct sockaddr_dl *sdl;
    +	struct if_data *ifi;
    +	struct iface *ifp;
    +	struct frame_mreq fmr;
    +
    +	if (getifaddrs(&ifa0) == -1)
    +		return (-1);
    +
    +	for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) {
    +		if (ifa->ifa_addr->sa_family != AF_LINK)
    +			continue;
    +		ifi = ifa->ifa_data;
    +		if (ifi->ifi_type != IFT_ETHER)
    +			continue;
    +
    +		sdl = (struct sockaddr_dl *)ifa->ifa_addr;
    +		if (sdl->sdl_index == 0) {
    +			warnx("interface %s has index 0, skipping",
    +			    ifa->ifa_name);
    +			continue;
    +		}
    +
    +		printf("%s index %u\n", ifa->ifa_name, sdl->sdl_index);
    +
    +		ifp = malloc(sizeof(*ifp));
    +		if (ifp == NULL) {
    +			warn("interface %s allocation", ifa->ifa_name);
    +			continue;
    +		}
    +
    +		ifp->if_key.if_index = sdl->sdl_index;
    +		strlcpy(ifp->if_key.if_name, ifa->ifa_name,
    +		    sizeof(ifp->if_key.if_name));
    +
    +		ifp->if_lldpd = lldpd;
    +		TAILQ_INIT(&ifp->if_msaps);
    +
    +		if (RBT_INSERT(ifaces, &lldpd->ifaces, ifp) != NULL) {
    +			warnx("interface %s: index %u already exists",
    +			    ifa->ifa_name, ifp->if_key.if_index);
    +			free(ifp);
    +		}
    +
    +		memset(&fmr, 0, sizeof(fmr));
    +		fmr.fmr_ifindex = ifp->if_key.if_index;
    +		memcpy(fmr.fmr_addr, maddr, ETHER_ADDR_LEN);
    +
    +		if (setsockopt(EVENT_FD(&lldpd->en_ev),
    +		    IFT_ETHER, FRAME_ADD_MEMBERSHIP,
    +		    &fmr, sizeof(fmr)) == -1)
    +			warn("interface %s: add membership", ifa->ifa_name);
    +	}
    +
    +	freeifaddrs(ifa0);
    +
    +	return (0);
    +}
    +
    +RBT_GENERATE(ifaces, iface, if_entry, iface_cmp);
    +
    +/* daemon(3) clone, intended to be used in a "r"estricted environment */
    +int
    +rdaemon(int devnull)
    +{
    +	if (devnull == -1) {
    +		errno = EBADF;
    +		return (-1);
    +	} 
    +	if (fcntl(devnull, F_GETFL) == -1)
    +		return (-1);
    +
    +	switch (fork()) {
    +	case -1:
    +		return (-1);
    +	case 0:
    +		break;
    +	default:
    +		_exit(0);
    +	}
    +
    +	if (setsid() == -1)
    +		return (-1);
    +
    +	(void)dup2(devnull, STDIN_FILENO);
    +	(void)dup2(devnull, STDOUT_FILENO);
    +	(void)dup2(devnull, STDERR_FILENO);
    +	if (devnull > 2)
    +		(void)close(devnull);
    +
    +	return (0);
    +}
    Index: usr.sbin/lldpd/lldpctl.h
    ===================================================================
    RCS file: usr.sbin/lldpd/lldpctl.h
    diff -N usr.sbin/lldpd/lldpctl.h
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/lldpctl.h	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,78 @@
    +/* $OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2024 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#define LLDP_CTL_PATH		"/var/run/lldp.sock"
    +
    +enum lldp_ctl_msg {
    +	LLDP_CTL_MSG_PING,
    +	LLDP_CTL_MSG_PONG,
    +
    +	LLDP_CTL_MSG_MSAP_REQ,		/* ctl -> daemon */
    +	LLDP_CTL_MSG_MSAP,		/* daemon -> ctl */
    +	LLDP_CTL_MSG_MSAP_END,		/* daemon -> ctl */
    +
    +	LLDP_CTL_MSG_ACTR_REQ,		/* ctl -> daemon */
    +	LLDP_CTL_MSG_ACTR,		/* daemon -> ctl */
    +	LLDP_CTL_MSG_ACTR_END,		/* daemon -> ctl */
    +};
    +
    +enum agent_counter {
    +	statsAgeoutsTotal,
    +	statsFramesDiscardedTotal,
    +	statsFramesInErrorsTotal,
    +	statsFramesInTotal,
    +	statsFramesOutTotal,
    +	statsTLVsDiscardedTotal,
    +	statsTLVsUnrecognisedTotal,
    +	lldpduLengthErrors,
    +
    +	AGENT_COUNTER_NCOUNTERS
    +};
    +
    +struct lldp_ctl_msg_msap_req {
    +	char			ifname[IFNAMSIZ];
    +	unsigned int		gen;
    +};
    +
    +struct lldp_ctl_msg_msap {
    +	char			ifname[IFNAMSIZ];
    +	unsigned int		gen;
    +	struct ether_addr	saddr;
    +	struct ether_addr	daddr;
    +
    +	struct timespec		created;
    +	struct timespec		updated;
    +
    +	uint64_t		packets;
    +	uint64_t		updates;
    +
    +	/* followed by the pdu */
    +};
    +
    +struct lldp_ctl_msg_actrs_req {
    +	unsigned int		which;
    +#define LLDP_ACTRS_ALL			0
    +#define LLDP_ACTRS_DAEMON		1
    +#define LLDP_ACTRS_IFACE		2
    +	char			ifname[IFNAMSIZ];
    +};
    +
    +struct lldp_ctl_msg_actrs {
    +	char			ifname[IFNAMSIZ];
    +	uint64_t		ctrs[AGENT_COUNTER_NCOUNTERS];
    +};
    Index: usr.sbin/lldpd/log.c
    ===================================================================
    RCS file: usr.sbin/lldpd/log.c
    diff -N usr.sbin/lldpd/log.c
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/log.c	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,150 @@
    +
    +/*
    + * Copyright (c) 2008 David Gwynne <loki@animata.net>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <string.h>
    +#include <errno.h>
    +#include <err.h>
    +#include <time.h>
    +#include <syslog.h>
    +#include <stdarg.h>
    +
    +#include "log.h"
    +
    +static const struct __logger conslogger = {
    +	err,
    +	errx,
    +	warn,
    +	warnx,
    +	warnx, /* info */
    +	warnx /* debug */
    +};
    +
    +__dead static void	syslog_err(int, const char *, ...)
    +			    __attribute__((__format__ (printf, 2, 3)));
    +__dead static void	syslog_errx(int, const char *, ...)
    +			    __attribute__((__format__ (printf, 2, 3)));
    +static void		syslog_warn(const char *, ...)
    +			    __attribute__((__format__ (printf, 1, 2)));
    +static void		syslog_warnx(const char *, ...)
    +			    __attribute__((__format__ (printf, 1, 2)));
    +static void		syslog_info(const char *, ...)
    +			    __attribute__((__format__ (printf, 1, 2)));
    +static void		syslog_debug(const char *, ...)
    +			    __attribute__((__format__ (printf, 1, 2)));
    +static void		syslog_vstrerror(int, int, const char *, va_list)
    +			    __attribute__((__format__ (printf, 3, 0)));
    +
    +static const struct __logger syslogger = {
    +	syslog_err,
    +	syslog_errx,
    +	syslog_warn,
    +	syslog_warnx,
    +	syslog_info,
    +	syslog_debug
    +};
    +
    +const struct __logger *__logger = &conslogger;
    +
    +void
    +logger_syslog(const char *progname, int facility)
    +{
    +	openlog(progname, LOG_PID | LOG_NDELAY, facility);
    +	tzset();
    +
    +	__logger = &syslogger;
    +}
    +
    +static void
    +syslog_vstrerror(int e, int priority, const char *fmt, va_list ap)
    +{
    +	char *s;
    +
    +	if (vasprintf(&s, fmt, ap) == -1) {
    +		syslog(LOG_EMERG, "unable to alloc in syslog_vstrerror");
    +		exit(1);
    +	}
    +
    +	syslog(priority, "%s: %s", s, strerror(e));
    +
    +	free(s);
    +}
    +
    +static void
    +syslog_err(int ecode, const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	syslog_vstrerror(errno, LOG_CRIT, fmt, ap);
    +	va_end(ap);
    +
    +	exit(ecode);
    +}
    +
    +static void
    +syslog_errx(int ecode, const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	vsyslog(LOG_CRIT, fmt, ap);
    +	va_end(ap);
    +
    +	exit(ecode);
    +}
    +
    +static void
    +syslog_warn(const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	syslog_vstrerror(errno, LOG_ERR, fmt, ap);
    +	va_end(ap);
    +}
    +
    +static void
    +syslog_warnx(const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	vsyslog(LOG_ERR, fmt, ap);
    +	va_end(ap);
    +}
    +
    +static void
    +syslog_info(const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	vsyslog(LOG_INFO, fmt, ap);
    +	va_end(ap);
    +}
    +
    +static void
    +syslog_debug(const char *fmt, ...)
    +{
    +	va_list ap;
    +
    +	va_start(ap, fmt);
    +	vsyslog(LOG_DEBUG, fmt, ap);
    +	va_end(ap);
    +}
    Index: usr.sbin/lldpd/log.h
    ===================================================================
    RCS file: usr.sbin/lldpd/log.h
    diff -N usr.sbin/lldpd/log.h
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/log.h	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,47 @@
    +
    +/*
    + * Copyright (c) 2008 David Gwynne <loki@animata.net>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#ifndef _LOG_H_
    +#define _LOG_H_
    +
    +struct __logger {
    +	__dead void (*err)(int, const char *, ...)
    +	    __attribute__((__format__ (printf, 2, 3)));
    +	__dead void (*errx)(int, const char *, ...)
    +	    __attribute__((__format__ (printf, 2, 3)));
    +	void (*warn)(const char *, ...)
    +	    __attribute__((__format__ (printf, 1, 2)));
    +	void (*warnx)(const char *, ...)
    +	    __attribute__((__format__ (printf, 1, 2)));
    +	void (*info)(const char *, ...)
    +	    __attribute__((__format__ (printf, 1, 2)));
    +	void (*debug)(const char *, ...)
    +	    __attribute__((__format__ (printf, 1, 2)));
    +};
    +
    +extern const struct __logger *__logger;
    +
    +#define lerr(_e, _f...) __logger->err((_e), _f)
    +#define lerrx(_e, _f...) __logger->errx((_e), _f)
    +#define lwarn(_f...) __logger->warn(_f)
    +#define lwarnx(_f...) __logger->warnx(_f)
    +#define linfo(_f...) __logger->info(_f)
    +#define ldebug(_f...) __logger->debug(_f)
    +
    +void	logger_syslog(const char *, int);
    +
    +#endif /* _LOG_H_ */
    Index: usr.sbin/lldpd/pdu.c
    ===================================================================
    RCS file: usr.sbin/lldpd/pdu.c
    diff -N usr.sbin/lldpd/pdu.c
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/pdu.c	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,93 @@
    +/*	$OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2024 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#include <stdlib.h>
    +#include <stdint.h>
    +
    +#include "pdu.h"
    +
    +uint16_t
    +pdu_u16(const uint8_t *buf)
    +{
    +	uint16_t u16;
    +
    +	u16 = (uint16_t)buf[0] << 8;
    +	u16 |= (uint16_t)buf[1];
    +
    +	return (u16);
    +}
    +
    +uint32_t
    +pdu_u32(const uint8_t *buf)
    +{
    +	uint32_t u32;
    +
    +	u32 = (uint32_t)buf[0] << 24;
    +	u32 |= (uint32_t)buf[1] << 16;
    +	u32 |= (uint32_t)buf[2] << 8;
    +	u32 |= (uint32_t)buf[3];
    +
    +	return (u32);
    +}
    +
    +static int
    +tlv_read(struct tlv *tlv, const uint8_t *buf, size_t len, unsigned int offset)
    +{
    +	unsigned int hdr;
    +	unsigned int plen;
    +
    +	len -= offset;
    +	if (len == 0)
    +		return (0);
    +	if (len < 2)
    +		return (-1);
    +
    +	buf += offset;
    +	hdr = pdu_u16(buf);
    +
    +	/* End of LLDPDU TLV */
    +	if (hdr == 0)
    +		return (0);
    +
    +	plen = hdr & 0x1ff;
    +
    +	len -= 2;
    +	if (len < plen)
    +		return (-1);
    +
    +	tlv->tlv_payload = buf + 2;
    +	tlv->tlv_type = hdr >> 9;
    +	tlv->tlv_len = plen;
    +	tlv->tlv_offset = offset;
    +
    +	return (1);
    +}
    +
    +int
    +tlv_first(struct tlv *tlv, const void *pdu, size_t len)
    +{
    +	return (tlv_read(tlv, pdu, len, 0));
    +}
    +
    +int
    +tlv_next(struct tlv *tlv, const void *pdu, size_t len)
    +{
    +	unsigned int offset = tlv->tlv_offset + 2 + tlv->tlv_len;
    +
    +	return (tlv_read(tlv, pdu, len, offset));
    +}
    Index: usr.sbin/lldpd/pdu.h
    ===================================================================
    RCS file: usr.sbin/lldpd/pdu.h
    diff -N usr.sbin/lldpd/pdu.h
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldpd/pdu.h	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,32 @@
    +/*	$OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2024 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +struct tlv {
    +	const void		*tlv_payload;
    +
    +	unsigned int		 tlv_type;
    +	unsigned int		 tlv_len;
    +
    +	unsigned int		 tlv_offset;
    +};
    +
    +uint16_t	pdu_u16(const uint8_t *);
    +uint32_t	pdu_u32(const uint8_t *);
    +
    +int		tlv_first(struct tlv *, const void *, size_t);
    +int		tlv_next(struct tlv *, const void *, size_t);
    Index: usr.sbin/lldp/Makefile
    ===================================================================
    RCS file: usr.sbin/lldp/Makefile
    diff -N usr.sbin/lldp/Makefile
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldp/Makefile	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,14 @@
    +
    +OLLDPD_DIR=	${.CURDIR}/../lldpd
    +
    +.PATH:		${OLLDPD_DIR}
    +
    +PROG=		lldp
    +SRCS=		lldp.c pdu.c
    +MAN=		lldp.8
    +
    +CFLAGS+=	-I${.CURDIR} -I${OLLDPD_DIR}
    +CFLAGS+=	-Wall -Werror
    +DEBUG=		-g
    +
    +.include <bsd.prog.mk>
    Index: usr.sbin/lldp/lldp.8
    ===================================================================
    RCS file: usr.sbin/lldp/lldp.8
    diff -N usr.sbin/lldp/lldp.8
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldp/lldp.8	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,56 @@
    +.\" $OpenBSD$
    +.\"
    +.\" Copyright (c) 2025 David Gwynne<dlg@openbsd.org>
    +.\"
    +.\" Permission to use, copy, modify, and distribute this software for any
    +.\" purpose with or without fee is hereby granted, provided that the above
    +.\" copyright notice and this permission notice appear in all copies.
    +.\"
    +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    +.\"
    +.Dd $Mdocdate$
    +.Dt LLDP 8
    +.Os
    +.Sh NAME
    +.Nm lldp
    +.Nd control the lldpd agent 
    +.Sh SYNOPSIS
    +.Nm lldp
    +.Op Fl v
    +.Op Fl i Ar interface
    +.Sh DESCRIPTION
    +The
    +.Nm
    +program communicates with the
    +.Xr lldpd 8
    +Link Layer Discovery Protocol (LLDP)
    +agent to fetch and display LLDP entries received by the agent.
    +.Pp
    +The options are as follows:
    +.Bl -tag -width Ds
    +.It Fl i Ar interface
    +Limit the LLDP entries to those received on the specified
    +.Ar interface .
    +.It Fl v
    +Increase the verbosity of output.
    +.El
    +.Sh FILES
    +.Bl -tag -width "/var/run/lldp.sock" -compact
    +.It Pa /var/run/lldp.sock
    +default
    +.Xr lldpd 8
    +control socket
    +.El
    +.Sh SEE ALSO
    +.Xr lldpd 8
    +.Sh HISTORY
    +The
    +.Nm
    +program first appeared in
    +.Ox 7.8 .
    Index: usr.sbin/lldp/lldp.c
    ===================================================================
    RCS file: usr.sbin/lldp/lldp.c
    diff -N usr.sbin/lldp/lldp.c
    --- /dev/null	1 Jan 1970 00:00:00 -0000
    +++ usr.sbin/lldp/lldp.c	29 Apr 2025 01:47:08 -0000
    @@ -0,0 +1,869 @@
    +/* $OpenBSD$ */
    +
    +/*
    + * Copyright (c) 2024 David Gwynne <dlg@openbsd.org>
    + *
    + * Permission to use, copy, modify, and distribute this software for any
    + * purpose with or without fee is hereby granted, provided that the above
    + * copyright notice and this permission notice appear in all copies.
    + *
    + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    + */
    +
    +#include <sys/socket.h>
    +#include <sys/types.h>
    +#include <sys/un.h>
    +
    +#include <net/if.h> /* IFNAMSIZ */
    +#include <net/frame.h>
    +#include <net/lldp.h>
    +
    +#include <netinet/in.h>
    +#include <netinet/if_ether.h> /* ether_ntoa */
    +
    +#include <arpa/inet.h> /* inet_ntop */
    +
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <string.h>
    +#include <stddef.h>
    +#include <stdint.h>
    +#include <unistd.h>
    +#include <vis.h>
    +#include <assert.h>
    +#include <ctype.h>
    +#include <err.h>
    +
    +#include "pdu.h"
    +#include "lldpctl.h"
    +
    +#ifndef nitems
    +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
    +#endif
    +
    +#ifndef ISSET
    +#define ISSET(_a, _b) ((_a) & (_b))
    +#endif
    +
    +struct slice {
    +	const void	*sl_base;
    +	size_t		 sl_len;
    +};
    +
    +struct string {
    +	char		*str_base;
    +	size_t		 str_len;
    +};
    +
    +void hexdump(const void *, size_t);
    +
    +enum lldp_tlv_idx {
    +	tlv_chassis_id,
    +	tlv_port_id,
    +	tlv_ttl,
    +	tlv_port_descr,
    +	tlv_system_name,
    +	tlv_system_descr,
    +	tlv_system_cap,
    +	tlv_management_addr,
    +
    +	tlv_org,
    +
    +	tlv_count
    +};
    +
    +struct lldp_tlv {
    +	uint8_t			 type;
    +	const char		*name;
    +
    +	void (*toscratch)(const void *, size_t, int);
    +};
    +
    +static void	lldp_bytes_to_scratch(const void *, size_t, int);
    +static void	lldp_string_to_scratch(const void *, size_t, int);
    +
    +static void	lldp_chassis_id_to_scratch(const void *, size_t, int);
    +static void	lldp_port_id_to_scratch(const void *, size_t, int);
    +static void	lldp_ttl_to_scratch(const void *, size_t, int);
    +static void	lldp_system_cap_to_scratch(const void *, size_t, int);
    +static void	lldp_system_descr_to_scratch(const void *, size_t, int);
    +static void	lldp_mgmt_addr_to_scratch(const void *, size_t, int);
    +static void	lldp_org_to_scratch(const void *, size_t, int);
    +
    +static const struct lldp_tlv lldp_tlv_map[] = {
    +	[tlv_chassis_id] = {
    +		.type =		LLDP_TLV_CHASSIS_ID,
    +		.name =		"Chassis ID",
    +		.toscratch = 	lldp_chassis_id_to_scratch,
    +	},
    +	[tlv_port_id] = {
    +		.type =		LLDP_TLV_PORT_ID,
    +		.name = 	"Port ID",
    +		.toscratch = 	lldp_port_id_to_scratch,
    +	},
    +	[tlv_ttl] = {
    +		.type =		LLDP_TLV_TTL,
    +		.name = 	"Time-To-Live",
    +		.toscratch = 	lldp_ttl_to_scratch,
    +	},
    +	[tlv_port_descr] = {
    +		.type =		LLDP_TLV_PORT_DESCR,
    +		.name = 	"Port Description",
    +		.toscratch = 	lldp_string_to_scratch,
    +	},
    +	[tlv_system_name] = {
    +		.type =		LLDP_TLV_SYSTEM_NAME,
    +		.name = 	"System Name",
    +		.toscratch = 	lldp_string_to_scratch,
    +	},
    +	[tlv_system_descr] = {
    +		.type =		LLDP_TLV_SYSTEM_DESCR,
    +		.name = 	"System Description",
    +		.toscratch = 	lldp_system_descr_to_scratch,
    +	},
    +	[tlv_system_cap] = {
    +		.type =		LLDP_TLV_SYSTEM_CAP,
    +		.name = 	"System Capabilities",
    +		.toscratch = 	lldp_system_cap_to_scratch,
    +	},
    +	[tlv_management_addr] = {
    +		.type =		LLDP_TLV_MANAGEMENT_ADDR,
    +		.name = 	"Management Address",
    +		.toscratch = 	lldp_mgmt_addr_to_scratch,
    +	},
    +
    +	[tlv_org] = {
    +		.type =		LLDP_TLV_ORG,
    +		.name = 	NULL,
    +		.toscratch = 	lldp_org_to_scratch,
    +	},
    +};
    +
    +struct lldp_pdu {
    +	struct slice	slices[tlv_count];
    +	struct string	strings[tlv_count];
    +};
    +
    +static void	lldpctl_req_msaps(int, const char *);
    +static void	lldp_dump(const struct lldp_ctl_msg_msap *,
    +		    const void *, size_t);
    +static void	lldp_dump_verbose(const struct lldp_ctl_msg_msap *,
    +		    const void *, size_t, int);
    +
    +static char scratch_mem[8192];
    +static FILE *scratch;
    +
    +static void	scratch_reset(void);
    +static size_t	scratch_len(void);
    +static size_t	scratch_end(void);
    +
    +static void	reltime_to_scratch(time_t);
    +
    +__dead static void
    +usage(void)
    +{
    +	extern char *__progname;
    +
    +	fprintf(stderr, "usage: %s [-v] [-i ifname]\n", __progname);
    +
    +	exit(1);
    +}
    +
    +int
    +main(int argc, char *argv[])
    +{
    +	const char *ifname = NULL;
    +	int verbose = 0;
    +	struct sockaddr_un sun = {
    +		.sun_family = AF_UNIX,
    +		.sun_path = LLDP_CTL_PATH,
    +	};
    +	int ch;
    +	int s;
    +
    +	scratch = fmemopen(scratch_mem, sizeof(scratch_mem), "w");
    +	if (scratch == NULL)
    +		err(1, "fmemopen scratch");
    +
    +	while ((ch = getopt(argc, argv, "i:v")) != -1) {
    +		switch (ch) {
    +		case 'i':
    +			ifname = optarg;
    +			break;
    +		case 'v':
    +			verbose++;
    +			break;
    +		default:
    +			usage();
    +			/* NOTREACHED */
    +		}
    +	}
    +
    +	s = socket(AF_UNIX, SOCK_SEQPACKET, 0);
    +	if (s == -1)
    +		err(1, "socket");
    +
    +	if (connect(s, (struct sockaddr *)&sun, sizeof(sun)) == -1)
    +		err(1, "%s connect", sun.sun_path);
    +
    +	lldpctl_req_msaps(s, ifname);
    +
    +	if (!verbose) {
    +		printf("%-8s %-24s %-24s %s\n", "IFACE",
    +		    "SYSTEM", "PORTID", "CHASSISID");
    +	}
    +
    +	for (;;) {
    +		unsigned int msgtype;
    +		struct lldp_ctl_msg_msap msg_msap;
    +		char buf[2048];
    +		struct iovec iov[3] = {
    +		    { &msgtype, sizeof(msgtype) },
    +		    { &msg_msap, sizeof(msg_msap) },
    +		    { buf, sizeof(buf) },
    +		};
    +		struct msghdr msg = {
    +			.msg_iov = iov,
    +			.msg_iovlen = nitems(iov),
    +		};
    +		ssize_t rv;
    +		size_t len;
    +
    +		rv = recvmsg(s, &msg, 0);
    +		if (rv == -1)
    +			err(1, "recv");
    +		if (rv == 0)
    +			break;
    +		len = rv;
    +
    +		if (len < sizeof(msgtype)) {
    +			warnx("too short for msgtype\n");
    +			continue;
    +		}
    +		if (msgtype == LLDP_CTL_MSG_MSAP_END)
    +			break;
    +		if (msgtype != LLDP_CTL_MSG_MSAP) {
    +			warnx("unexpected msgtype %u", msgtype);
    +			continue;
    +		}
    +		len -= sizeof(msgtype);
    +
    +		if (len < sizeof(msg_msap)) {
    +			warnx("too short for msg_msap\n");
    +			continue;
    +		}
    +		len -= sizeof(msg_msap);
    +#if 0
    +		printf("%s %s\n", msg_msap.ifname,
    +		    ether_ntoa(&msg_msap.saddr));
    +		hexdump(buf, len);
    +#endif
    +
    +		if (!verbose)
    +			lldp_dump(&msg_msap, buf, len);
    +		else
    +			lldp_dump_verbose(&msg_msap, buf, len, verbose);
    +	}
    +
    +	return (0);
    +}
    +
    +static void
    +lldpctl_req_msaps(int fd, const char *ifname)
    +{
    +	unsigned int msgtype = LLDP_CTL_MSG_MSAP_REQ;
    +	struct lldp_ctl_msg_msap_req msg_msap_req;
    +	struct iovec iov[2] = {
    +	    { &msgtype, sizeof(msgtype) },
    +	    { &msg_msap_req, sizeof(msg_msap_req) },
    +	};
    +	struct msghdr msg = {
    +		.msg_iov = iov,
    +		.msg_iovlen = nitems(iov),
    +	};
    +	ssize_t rv;
    +
    +	memset(&msg_msap_req, 0, sizeof(msg_msap_req));
    +
    +	if (ifname != NULL) {
    +		if (strlcpy(msg_msap_req.ifname, ifname,
    +		    sizeof(msg_msap_req.ifname)) >=
    +		    sizeof(msg_msap_req.ifname))
    +			errx(1, "interface name too long");
    +	}
    +
    +	rv = sendmsg(fd, &msg, 0);
    +	if (rv == -1)
    +		err(1, "send msap msg req");
    +}
    +
    +static const struct lldp_tlv *
    +lldp_tlv_lookup(unsigned int type)
    +{
    +	const struct lldp_tlv *ltlv;
    +	size_t i;
    +
    +	for (i = 0; i < nitems(lldp_tlv_map); i++) {
    +		ltlv = &lldp_tlv_map[i];
    +		if (ltlv->type == type)
    +			return (ltlv);
    +	}
    +
    +	return (NULL);
    +}
    +
    +static size_t
    +lldp_tlv_to_idx(const struct lldp_tlv *ltlv)
    +{
    +	return (ltlv - lldp_tlv_map);
    +}
    +
    +static void
    +lldp_dump(const struct lldp_ctl_msg_msap *msg_msap,
    +    const void *pdu, size_t len)
    +{
    +	struct lldp_pdu lpdu;
    +	struct tlv tlv;
    +	const struct lldp_tlv *ltlv;
    +	struct slice *sl;
    +	size_t i;
    +
    +	for (i = 0; i < tlv_count; i++) {
    +		sl = &lpdu.slices[i];
    +		sl->sl_base = NULL;
    +		sl->sl_len = 0;
    +	}
    +
    +	/* olldpd only gives us well formed PDUs */
    +	tlv_first(&tlv, pdu, len);
    +	do {
    +		struct slice *sl;
    +
    +		ltlv = lldp_tlv_lookup(tlv.tlv_type);
    +		if (ltlv == NULL)
    +			continue;
    +		i = lldp_tlv_to_idx(ltlv);
    +
    +		sl = &lpdu.slices[i];
    +		sl->sl_base = tlv.tlv_payload;
    +		sl->sl_len = tlv.tlv_len;
    +	} while (tlv_next(&tlv, pdu, len));
    +
    +	printf("%-8s", msg_msap->ifname);
    +
    +	scratch_reset();
    +	sl = &lpdu.slices[tlv_system_name];
    +	if (sl->sl_base == NULL) {
    +		fprintf(scratch, "-");
    +	} else {
    +		ltlv = &lldp_tlv_map[tlv_system_name];
    +		ltlv->toscratch(sl->sl_base, sl->sl_len, 0);
    +	}
    +	scratch_end();
    +	printf(" %-24s", scratch_mem);
    +
    +	scratch_reset();
    +	sl = &lpdu.slices[tlv_port_id];
    +	if (sl->sl_base == NULL) {
    +		fprintf(scratch, "-");
    +	} else {
    +		ltlv = &lldp_tlv_map[tlv_port_id];
    +		ltlv->toscratch(sl->sl_base, sl->sl_len, 0);
    +	}
    +	scratch_end();
    +	printf(" %-24s", scratch_mem);
    +
    +	scratch_reset();
    +	sl = &lpdu.slices[tlv_chassis_id];
    +	if (sl->sl_base == NULL) {
    +		fprintf(scratch, "-");
    +	} else {
    +		ltlv = &lldp_tlv_map[tlv_chassis_id];
    +		ltlv->toscratch(sl->sl_base, sl->sl_len, 0);
    +	}
    +	scratch_end();
    +	printf(" %s", scratch_mem);
    +
    +	printf("\n");
    +}
    +
    +static void
    +lldp_dump_verbose(const struct lldp_ctl_msg_msap *msg_msap,
    +    const void *pdu, size_t len, int verbose)
    +{
    +	struct tlv tlv;
    +	const struct lldp_tlv *ltlv;
    +
    +	printf("Local interface: %s, Source address: %s\n", msg_msap->ifname,
    +	    ether_ntoa((struct ether_addr *)&msg_msap->saddr));
    +
    +	if (verbose > 1) {
    +		struct timespec now, diff;
    +		if (clock_gettime(CLOCK_BOOTTIME, &now) == -1)
    +			err(1, "clock_gettime(CLOCK_BOOTTIME)");
    +
    +		scratch_reset();
    +		timespecsub(&now, &msg_msap->created, &diff);
    +		reltime_to_scratch(diff.tv_sec);
    +		scratch_end();
    +		printf("LLDP entry created: %s\n", scratch_mem);
    +
    +		scratch_reset();
    +		timespecsub(&now, &msg_msap->updated, &diff);
    +		reltime_to_scratch(diff.tv_sec);
    +		scratch_end();
    +		printf("LLDP entry updated: %s\n", scratch_mem);
    +
    +		printf("LLDP packets: %llu\n", msg_msap->packets);
    +		printf("LLDP updates: %llu\n", msg_msap->updates);
    +	}
    +
    +	/* olldpd only gives us well formed PDUs */
    +	tlv_first(&tlv, pdu, len);
    +	do {
    +		void (*toscratch)(const void *, size_t, int) =
    +		    lldp_bytes_to_scratch;
    +
    +		ltlv = lldp_tlv_lookup(tlv.tlv_type);
    +		if (ltlv == NULL) {
    +			printf("tlv type %u: ", tlv.tlv_type);
    +		} else {
    +			if (ltlv->name)
    +				printf("%s: ", ltlv->name);
    +			toscratch = ltlv->toscratch;
    +		}
    +
    +		scratch_reset();
    +		toscratch(tlv.tlv_payload, tlv.tlv_len, 1);
    +		scratch_end();
    +		printf("%s\n", scratch_mem);
    +
    +	} while (tlv_next(&tlv, pdu, len));
    +
    +	printf("---\n");
    +}
    +
    +static void
    +scratch_reset(void)
    +{
    +	if (fseeko(scratch, 0, SEEK_SET) == -1)
    +		err(1, "scratch reset");
    +}
    +
    +static size_t
    +scratch_len(void)
    +{
    +	off_t len = ftello(scratch);
    +	assert(len < sizeof(scratch_mem));
    +	return (len);
    +}
    +
    +static size_t
    +scratch_end(void)
    +{
    +	off_t len = scratch_len();
    +	scratch_mem[len] = '\0';
    +	return (len);
    +}
    +
    +struct interval {
    +	const char	p;
    +	time_t		s;
    +};
    +
    +static const struct interval intervals[] = {
    +	{ 'w',	60 * 60 * 24 * 7 },
    +	{ 'd',	60 * 60 * 24 },
    +	{ 'h',	60 * 60 },
    +	{ 'm',	60 },
    +	{ 's',	1 },
    +};
    +
    +static void
    +reltime_to_scratch(time_t sec)
    +{
    +	size_t i;
    +
    +	for (i = 0; i < nitems(intervals); i++) {
    +		const struct interval *ival = &intervals[i];
    +
    +		if (sec >= ival->s) {
    +			time_t d = sec / ival->s;
    +			fprintf(scratch, "%lld%c", d, ival->p);
    +			sec -= d * ival->s;
    +		}
    +	}
    +}
    +
    +static void
    +lldp_bytes_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	size_t i;
    +
    +	for (i = 0; i < len; i++)
    +		fprintf(scratch, "%02x", buf[i]);
    +}
    +
    +static void
    +lldp_string_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	size_t i;
    +	char dst[8];
    +
    +	for (i = 0; i < len; i++) {
    +		vis(dst, buf[i], VIS_NL, 0);
    +		fprintf(scratch, "%s", dst);
    +	}
    +}
    +
    +static void
    +lldp_system_descr_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	size_t i;
    +	char dst[8];
    +
    +	for (i = 0; i < len; i++) {
    +		int ch = buf[i];
    +		switch (ch) {
    +		case '\r':
    +			break;
    +		case '\n':
    +			fprintf(scratch, "\n\t");
    +			break;
    +		default:
    +			vis(dst, ch, 0, 0);
    +			fprintf(scratch, "%s", dst);
    +			break;
    +		}
    +	}
    +}
    +
    +static void
    +lldp_macaddr_to_scratch(const void *base, size_t len, int flags)
    +{
    +	struct ether_addr *ea;
    +
    +	if (len < sizeof(*ea)) {
    +		lldp_bytes_to_scratch(base, len, flags);
    +		return;
    +	}
    +
    +	ea = (struct ether_addr *)base;
    +	fprintf(scratch, "%s", ether_ntoa(ea));
    +}
    +
    +static void
    +lldp_chassis_id_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	uint8_t subtype;
    +
    +	assert(len >= 2);
    +
    +	subtype = buf[0];
    +
    +	buf++;
    +	len--;
    +
    +	switch (subtype) {
    +	case LLDP_CHASSIS_ID_MACADDR:
    +		lldp_macaddr_to_scratch(buf, len, flags);
    +		break;
    +	case LLDP_CHASSIS_ID_CHASSIS:
    +	case LLDP_CHASSIS_ID_IFALIAS:
    +	case LLDP_CHASSIS_ID_PORT:
    +	case LLDP_CHASSIS_ID_ADDR:
    +	case LLDP_CHASSIS_ID_IFNAME:
    +	case LLDP_CHASSIS_ID_LOCAL:
    +		lldp_string_to_scratch(buf, len, flags);
    +		break;
    +	default:
    +		fprintf(scratch, "reserved (subtype %u) ", subtype);
    +		lldp_bytes_to_scratch(buf, len, flags);
    +		break;
    +	}
    +}
    +
    +static void
    +lldp_port_id_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	uint8_t subtype;
    +
    +	assert(len >= 2);
    +
    +	subtype = buf[0];
    +
    +	buf++;
    +	len--;
    +
    +	switch (subtype) {
    +	case LLDP_PORT_ID_MACADDR:
    +		lldp_macaddr_to_scratch(base, len, flags);
    +		break;
    +	case LLDP_PORT_ID_IFALIAS:
    +	case LLDP_PORT_ID_PORT:
    +	case LLDP_PORT_ID_ADDR:
    +	case LLDP_PORT_ID_IFNAME:
    +	case LLDP_PORT_ID_AGENTCID:
    +	case LLDP_PORT_ID_LOCAL:
    +		lldp_string_to_scratch(buf, len, flags);
    +		break;
    +	default:
    +		fprintf(scratch, "reserved (subtype %u) ", subtype);
    +		lldp_bytes_to_scratch(buf, len, flags);
    +		break;
    +	}
    +}
    +
    +static void
    +lldp_ttl_to_scratch(const void *base, size_t len, int flags)
    +{
    +	uint16_t ttl;
    +
    +	assert(len >= sizeof(ttl));
    +	ttl = pdu_u16(base);
    +
    +	reltime_to_scratch(ttl);
    +}
    +
    +struct lldp_system_cap {
    +	uint16_t	 bit;
    +	const char	*name;
    +};
    +
    +static const struct lldp_system_cap lldp_system_caps[] = {
    +	{ LLDP_SYSTEM_CAP_OTHER,	"Other" },
    +	{ LLDP_SYSTEM_CAP_REPEATER,	"Repeater" },
    +	{ LLDP_SYSTEM_CAP_BRIDGE,	"Bridge" },
    +	{ LLDP_SYSTEM_CAP_WLAN,		"WLAN" },
    +	{ LLDP_SYSTEM_CAP_ROUTER,	"Router" },
    +	{ LLDP_SYSTEM_CAP_TELEPHONE,	"Telephone" },
    +	{ LLDP_SYSTEM_CAP_DOCSIS,	"DOCSIS" },
    +	{ LLDP_SYSTEM_CAP_STATION,	"Station" },
    +};
    +
    +static const char *
    +lldp_system_cap_name(uint16_t bit)
    +{
    +	size_t i;
    +
    +	for (i = 0; i < nitems(lldp_system_caps); i++) {
    +		const struct lldp_system_cap *e = &lldp_system_caps[i];
    +		if (e->bit == bit)
    +			return (e->name);
    +	}
    +
    +	return (NULL);
    +}
    +
    +static void
    +lldp_system_cap_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf;
    +	struct lldp_system_cap {
    +		uint16_t	available;
    +		uint16_t	enabled;
    +	} cap;
    +	const char *sep = "";
    +	unsigned int i;
    +
    +	if (len < sizeof(cap) ){
    +		fprintf(scratch, "[|system cap]");
    +		return;
    +	}
    +
    +	buf = base;
    +
    +	cap.available = pdu_u16(buf +
    +	    offsetof(struct lldp_system_cap, available));
    +	cap.enabled = pdu_u16(buf +
    +	    offsetof(struct lldp_system_cap, enabled));
    +
    +	for (i = 0; i < NBBY * sizeof(cap.available); i++) {
    +		const char *name;
    +		uint16_t bit = (1 << i);
    +
    +		if (!ISSET(cap.available, bit))
    +			continue;
    +
    +		fprintf(scratch, "%s", sep);
    +		name = lldp_system_cap_name(bit);
    +		if (name == NULL)
    +			fprintf(scratch, "Bit%u", i + 1);
    +		else
    +			fprintf(scratch, "%s", name);
    +
    +		fprintf(scratch, ": %s",
    +		    ISSET(cap.enabled, bit) ? "enabled" : "disabled");
    +
    +		sep = ", ";
    +	}
    +}
    +
    +static void
    +lldp_mgmt_addr_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf = base;
    +	uint8_t aftype;
    +	size_t alen;
    +	const uint8_t *abuf;
    +	char ipbuf[64];
    +	uint32_t ifnum;
    +
    +	if (len < 1) {
    +		fprintf(scratch, "too short (%zu bytes)", len);
    +		return;
    +	}
    +	alen = buf[0];
    +	len--;
    +	buf++;
    +
    +	if (len < alen) {
    +		fprintf(scratch, "address len %zu is longer than tlv", alen);
    +		return;
    +	}
    +	abuf = buf;
    +	len -= alen;
    +	buf += alen;
    +
    +	if (alen < 1) {
    +		fprintf(scratch, "address len %zu is too short", alen);
    +		return;
    +	}
    +	aftype = abuf[0];
    +	alen--;
    +	abuf++;
    +
    +	switch (aftype) {
    +	case 1:
    +		if (alen != 4) {
    +			fprintf(scratch, "IPv4? ");
    +			goto abytes;
    +		}
    +		inet_ntop(AF_INET, abuf, ipbuf, sizeof(ipbuf));
    +		fprintf(scratch, "%s", ipbuf);
    +		break;
    +	case 2:
    +		if (alen != 16) {
    +			fprintf(scratch, "IPv7? ");
    +			goto abytes;
    +		}
    +		inet_ntop(AF_INET6, abuf, ipbuf, sizeof(ipbuf));
    +		fprintf(scratch, "%s", ipbuf);
    +		break;
    +
    +	case 6:
    +		if (alen != 6) {
    +			fprintf(scratch, "802? ");
    +			goto abytes;
    +		}
    +		fprintf(scratch, "%s", ether_ntoa((struct ether_addr *)abuf));
    +		break;
    +
    +	default:
    +		fprintf(scratch, "af %u ", aftype);
    +abytes:
    +		lldp_bytes_to_scratch(abuf, alen, 0);
    +		break;
    +	}
    +
    +	if (len < 5) {
    +		fprintf(scratch, ", [|interface number]");
    +		return;
    +	}
    +	ifnum = pdu_u32(buf + 1);
    +
    +	switch (buf[0]) {
    +	case 0:
    +		break;
    +	case 1:
    +		fprintf(scratch, ", ifIndex %u", ifnum);
    +		break;
    +	case 2:
    +		fprintf(scratch, ", port %u", ifnum);
    +		break;
    +	default:
    +		fprintf(scratch, ", if type %u num %u", buf[0], ifnum);
    +		break;
    +	}
    +
    +	len -= 5;
    +	buf += 5;
    +
    +	if (len < 1) {
    +		fprintf(scratch, ", [|object identifier]");
    +		return;
    +	}
    +	alen = buf[0];
    +	len--;
    +	buf++;
    +
    +	if (alen == 0)
    +		return;
    +	if (len < alen) {
    +		fprintf(scratch, ", oid %zu is longer than tlv", alen);
    +		return;
    +	}
    +	fprintf(scratch, ", oid ");
    +	lldp_bytes_to_scratch(buf, len, 0);
    +}
    +
    +static void
    +lldp_org_to_scratch(const void *base, size_t len, int flags)
    +{
    +	const uint8_t *buf;
    +
    +	if (len < 4) {
    +		fprintf(scratch, "[|org]");
    +		return;
    +	}
    +
    +	buf = base;
    +	fprintf(scratch, "%02X-%02X-%02X subtype %u: ",
    +	    buf[0], buf[1], buf[2], buf[3]);
    +
    +	buf += 4;
    +	len -= 4;
    +
    +	lldp_bytes_to_scratch(buf, len, flags);
    +}
    +
    +static int
    +printable(int ch)
    +{
    +	if (ch == '\0')
    +		return ('_');
    +	if (!isprint(ch))
    +		return ('~');
    +
    +	return (ch);
    +}
    +
    +void
    +hexdump(const void *d, size_t datalen)
    +{
    +	const uint8_t *data = d;
    +	size_t i, j = 0;
    +
    +	for (i = 0; i < datalen; i += j) {
    +		printf("%4zu: ", i);
    +		for (j = 0; j < 16 && i+j < datalen; j++)
    +			printf("%02x ", data[i + j]);
    +		while (j++ < 16)
    +			printf("   ");
    +		printf("|");
    +		for (j = 0; j < 16 && i+j < datalen; j++)
    +			putchar(printable(data[i + j]));
    +		printf("|\n");
    +	}
    +}
    Index: sys/conf/GENERIC
    ===================================================================
    RCS file: /cvs/src/sys/conf/GENERIC,v
    diff -u -p -r1.299 GENERIC
    --- sys/conf/GENERIC	3 Oct 2024 04:39:09 -0000	1.299
    +++ sys/conf/GENERIC	29 Apr 2025 01:47:08 -0000
    @@ -81,6 +81,7 @@ pseudo-device	endrun	1	# EndRun line dis
     pseudo-device	vnd	4	# vnode disk devices
     pseudo-device	ksyms	1	# kernel symbols device
     pseudo-device	kstat		# kernel statistics device
    +pseudo-device	llt		# low-level tracing device
     
     # clonable devices
     pseudo-device	bpfilter	# packet filter
    @@ -114,5 +115,7 @@ pseudo-device	wg		# WireGuard
     pseudo-device	bio	1	# ioctl multiplexing device
     
     pseudo-device	fuse		# fuse device
    +
    +pseudo-device	af_frame	# (Ethernet) frame sockets
     
     option		BOOT_CONFIG	# add support for boot -c
    
    
  • Lyndon Nerenberg (VE7TFX/VE6BBM):

    LLDP daemon and display tool