Index | Thread | Search

From:
David Gwynne <david@gwynne.id.au>
Subject:
bpflogd(8): capture packets via BPF to log files
To:
tech@openbsd.org
Date:
Thu, 24 Apr 2025 15:44:53 +1000

Download raw body.

Thread
this is basically pflogd(8), but different.

the reason it exists is because i needed to continously log some packets
from span ports coming from multiple switches to try and help debug a
network issue that only seems to occur every couple of months. pflogd
provides that for a single pflog interface, but i needed it on multiple
ethernet interfaces.

im sending this out in case anyone else is interested in it.

the main differences are:

- it can log on any type of BPF interface and DLT, not just pflog(4)
- a single bpflogd can log packets from multiple BPF interfaces to the
  one log file
- it uses libevent and non-blocking FDs instead of a blocking on
  pcap_dispatch in a loop with crazy signal handling
  - this also avoids restartable syscalls
- it uses unveil and drops privs instead of chroot+privsep
  - this means the log file has to be writable by the user bpflogd
    runs as so it can reopen it after rotation.
- it captures full packets by default, not just 160 bytes
- you can provide a pcap-filter expression in a file
  - this makes the pexp handling in rc.subr a lot more robust
- the .c files are about half the number of lines
  - but it uses libevent so maybe this doesnt matter

the network issue i was dealing with was dhcp related, so i was
running it like this:

xdlg@sundew etc$ cat rc.d/bpflog_dhcp
#!/bin/ksh

daemon="/opt/local/sbin/bpflogd"
daemon_flags="-f /var/log/dhcp.pcap -F /etc/bpflog_dhcp.bpfilter -i mcx1 -i mcx2"

. /etc/rc.d/rc.subr

rc_cmd $1
xdlg@sundew etc$ cat /etc/bpflog_dhcp.bpfilter
vlan and udp and ((ip and (port bootpc or port bootps)) or (ip6 and (port dhcpv6-client or port dhcpv6-server)))
xdlg@sundew etc$ ls -l /var/log/dhcp.pcap
-rw-r-----  1 _pflogd  sysadm  41027133 Apr 24 15:01 /var/log/dhcp.pcap
xdlg@sundew etc$ grep bpflog_dhcp /etc/newsyslog.conf
/var/log/dhcp.pcap			640  7     100000 *   ZB "rcctl reload bpflog_dhcp > /dev/null"

rcctl reload sends HUP by default, which is what bpflogd (and pflogd)
use to trigger a reopen of their logfiles.

for fun i replaced pflogd with bpflogd by changing /etc/rc.d/pflogd on a
few boxes:

xdlg@sundew rc.d$ diff -u pflogd /etc/rc.d/pflogd
--- pflogd	Fri Jan 12 05:52:12 2018
+++ /etc/rc.d/pflogd	Thu Feb 13 11:59:50 2025
@@ -2,11 +2,10 @@
 #
 # $OpenBSD: pflogd,v 1.3 2018/01/11 19:52:12 rpe Exp $
 
-daemon="/sbin/pflogd"
+daemon="/opt/local/sbin/bpflogd"
+daemon_flags="-f /var/log/pflog -i pflog0 -s 160"
 
 . /etc/rc.d/rc.subr
-
-pexp="pflogd: \[priv\]"
 
 rc_pre() {
 	if pfctl -si | grep -q Enabled; then

Index: bpflogd/Makefile
===================================================================
RCS file: bpflogd/Makefile
diff -N bpflogd/Makefile
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ bpflogd/Makefile	24 Apr 2025 03:56:54 -0000
@@ -0,0 +1,13 @@
+
+PROG=	bpflogd
+SRCS=	bpflogd.c
+SRCS+=	log.c
+MAN=	bpflogd.8
+
+LDADD=-lpcap -levent
+DPADD=${LIBPCAP} ${LIBEVENT}
+
+DEBUG=-g
+WARNINGS=yes
+
+.include <bsd.prog.mk>
Index: bpflogd/bpflogd.8
===================================================================
RCS file: bpflogd/bpflogd.8
diff -N bpflogd/bpflogd.8
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ bpflogd/bpflogd.8	24 Apr 2025 03:56:54 -0000
@@ -0,0 +1,129 @@
+.\"	$OpenBSD: pflogd.8,v 1.51 2019/08/30 17:51:47 jmc Exp $
+.\"
+.\" Copyright (c) 2001 Can Erkin Acar.  All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+.\" 3. The name of the author may not be used to endorse or promote products
+.\"    derived from this software without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+.\" IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+.\" OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+.\" IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+.\" INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+.\" NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+.\" THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+.\"
+.Dd $Mdocdate: August 30 2019 $
+.Dt BPFLOGD 8
+.Os
+.Sh NAME
+.Nm bpflogd
+.Nd Berkely Packet Filter logging daemon
+.Sh SYNOPSIS
+.Nm bpflogd
+.Op Fl dpP
+.Op Fl F Ar filterfile
+.Op Fl s Ar snaplen
+.Op Fl w Ar waitms
+.Op Fl y Ar datalinktype
+.Fl f Ar filename
+.Fl i Ar interface
+.Op Ar expression
+.Sh DESCRIPTION
+.Nm
+is a daemon which captures packets using
+.Xr bpf 4
+and writes the packets to a logfile
+in
+.Xr tcpdump 8
+binary format.
+These logs can be reviewed later using the
+.Fl r
+option of
+.Xr tcpdump 8 .
+.Pp
+.Nm
+closes and then re-opens the log file when it receives
+.Dv SIGHUP ,
+permitting
+.Xr newsyslog 8
+to rotate logfiles automatically.
+.Pp
+If the log file contains data when starting or a
+.Dv SIGHUP ,
+the PCAP header is checked before new logs are appended to the existing file.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl d
+Debugging mode.
+.Nm
+does not daemonise and logs to the terminal.
+.It Fl f Ar filename
+Log output filename.
+The file must already exist, and be readable and writable by the
+_pflogd user.
+.It Fl F Ar filterfile
+Specify a file containing a filter expression as per
+.Xr pcap-filter 5 .
+.It Fl i Ar interface
+Specifies the interface to capture packets on using
+.Xr bpf 4 .
+This can be specified multiple times to capture packets from multiple
+interfaces, but all the interfaces must support the same datalink type.
+.It Fl p
+Do not put the interfaces into promiscuous mode.
+This is the default.
+.It Fl P
+Put the interfaces into promiscuous mode.
+.It Fl s Ar snaplen
+Capture at most the first
+.Ar snaplen
+bytes of data from each packet.
+By default
+.Nm
+captures whole packets.
+.It Fl w Ar waitms
+Specify the maximum amount of time in milliseconds between when a
+packet is captured and when it will be written to the log file.
+The default
+.Ar waitms
+value is 2000 milliseconds.
+.It Fl y Ar datalinktype
+Specify the datalink type when capturing packets.
+If this is not specified then the default datalink type on the first
+interface is used.
+.It Ar expression
+Specify a filter expression for matching packets as per
+.Xr pcap-filter 5 .
+.El
+.Pp
+A filter expression may only be specified by a file with
+.Ar -F
+or as arguments on the command line, specifying both is unsupported.
+If a filter is not provided then all packets are captured.
+.Sh SEE ALSO
+.Xr pcap_open_live 3 ,
+.Xr pcap-filter 5 ,
+.Xr newsyslog 8 ,
+.Xr tcpdump 8
+.Sh HISTORY
+The
+.Nm
+command appeared in
+.Ox 7.8 .
+.\" .Sh AUTHORS
+.\" .Nm
+.\" was written by
+.\" .An David Gwynne Aq Mt dlg@uq.edu.au .
Index: bpflogd/bpflogd.c
===================================================================
RCS file: bpflogd/bpflogd.c
diff -N bpflogd/bpflogd.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ bpflogd/bpflogd.c	24 Apr 2025 03:56:54 -0000
@@ -0,0 +1,686 @@
+/* */
+
+/*
+ * Copyright (c) 2025 The University of Queensland
+ *
+ * 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/types.h>
+#include <sys/sysctl.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <paths.h>
+#include <signal.h>
+#include <pwd.h>
+#include <errno.h>
+#include <err.h>
+
+#include <sys/queue.h>
+
+#include <net/if.h>
+#include <net/bpf.h>
+
+#include <event.h>
+#include <pcap.h>
+
+#include "log.h"
+
+#ifndef nitems
+#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+int  rdaemon(int);
+
+#define BPFLOGD_USER "_pflogd"
+
+#define _DEV_BPF "/dev/bpf"
+
+struct bpflogd;
+
+struct bpfif {
+	struct bpflogd		*bif_bd;
+	const char		*bif_name;
+	int			 bif_bpf;
+
+	struct event		 bif_ev;
+
+	TAILQ_ENTRY(bpfif)	 bif_entry;
+};
+
+TAILQ_HEAD(bpfif_list, bpfif);
+
+struct bpflogd {
+	const char		*bd_user;
+	const char		*bd_fname;
+	int			 bd_fd;
+	unsigned int		 bd_snaplen;
+	int			 bd_dlt;
+	const char		*bd_dlt_name;
+
+	struct bpfif_list	 bd_bif_list;
+
+	int			 bd_buflen;
+	void			*bd_buf;
+
+	struct event		 bd_sighup;
+};
+
+static int		bpf_maxbufsize(void);
+
+static void		bpflogd_hup(int, short, void *);
+
+static void		bpfif_open(struct bpfif *);
+static void		bpfif_read(int, short, void *);
+
+static int		bpflog_open(struct bpflogd *);
+
+__dead static void
+usage(void)
+{
+	extern char *__progname;
+
+	fprintf(stderr, "usage: %s [-dpP] [-F expression] [-s snaplen] "
+	    "[-w delay] [-y datalinktype]" "\n"
+	    "\t" "-f filename -i interface [expression]\n", __progname);
+
+	exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+	struct bpflogd _bd = {
+		.bd_user = BPFLOGD_USER,
+		.bd_fname = NULL,
+		.bd_fd = -1,
+		.bd_buflen = bpf_maxbufsize(),
+		.bd_snaplen = BPF_MAXBUFSIZE,
+		.bd_dlt = -1,
+		.bd_bif_list = TAILQ_HEAD_INITIALIZER(_bd.bd_bif_list),
+	};
+	struct bpflogd *bd = &_bd;
+	struct bpfif *bif, *bif0;
+	struct bpf_version bv;
+	char *filter = NULL;
+	struct bpf_program bf;
+	struct bpf_insn insns[] = { { BPF_RET, 0, 0, -1 } };
+	int devnull = -1;
+	int debug = 0;
+	int promisc = 0;
+	int waitms = 1000;
+	struct timeval waittv;
+	struct passwd *pw;
+
+	int ch;
+	const char *errstr;
+
+	while ((ch = getopt(argc, argv, "df:F:i:pPs:u:w:y:")) != -1) {
+		switch (ch) {
+		case 'd':
+			debug = 1;
+			break;
+		case 'f':
+			bd->bd_fname = optarg;
+			break;
+		case 'F':
+			filter = optarg;
+			break;
+		case 'i':
+			TAILQ_FOREACH(bif, &bd->bd_bif_list, bif_entry) {
+				if (strcmp(bif->bif_name, optarg) == 0) {
+					errx(1, "interface %s already exists",
+					    optarg);
+				}
+			}
+
+			bif = malloc(sizeof(*bif));
+			if (bif == NULL)
+				err(1, "bpf interface alloc");
+			bif->bif_bd = bd;
+			bif->bif_name = optarg;
+			TAILQ_INSERT_TAIL(&bd->bd_bif_list, bif, bif_entry);
+			break;
+		case 'p':
+			promisc = 0;
+			break;
+		case 'P':
+			promisc = 1;
+			break;
+		case 's':
+			bd->bd_snaplen = strtonum(optarg, 60, BPF_MAXBUFSIZE,
+			    &errstr);
+			if (errstr != NULL)
+				errx(1, "snaplen: %s", errstr);
+			break;
+		case 'u':
+			bd->bd_user = optarg;
+			break;
+		case 'w':
+			waitms = strtonum(optarg, 10, 300000, &errstr);
+			if (errstr != NULL)
+				errx(1, "wait ms: %s", errstr);
+			break;
+		case 'y':
+			bd->bd_dlt = pcap_datalink_name_to_val(optarg);
+			if (bd->bd_dlt == -1) {
+				errx(1, "%s: unknown datalink type name",
+				    optarg);
+			}
+			bd->bd_dlt_name = optarg;
+			break;
+		default:
+			usage();
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (bd->bd_fname == NULL) {
+		warnx("output file not specified");
+		usage();
+	}
+
+	bif0 = TAILQ_FIRST(&bd->bd_bif_list);
+	if (bif0 == NULL) {
+		warnx("no interfaces specified");
+		usage();
+	}
+
+	if (filter != NULL && argc > 0) {
+		warnx("use either -F or extra arguments, not both");
+		usage();
+	}
+
+	signal(SIGPIPE, SIG_IGN);
+
+	if (geteuid())
+		errx(1, "need root privileges");
+
+	pw = getpwnam(bd->bd_user);
+	if (pw == NULL)
+		errx(1, "%s: unknown user", bd->bd_user);
+
+	bd->bd_buf = malloc(bd->bd_buflen);
+	if (bd->bd_buf == NULL)
+		err(1, "bpf read buffer");
+
+	bpfif_open(bif0); /* err on failure */
+
+	if (ioctl(bif0->bif_bpf, BIOCVERSION, &bv) == -1)
+		err(1, "%s: get filter language version", bif0->bif_name);
+
+	if (bv.bv_major != BPF_MAJOR_VERSION) {
+		errx(1, "bpf major %u, expected %u",
+		    bv.bv_major, BPF_MAJOR_VERSION);
+	}
+	if (bv.bv_minor < BPF_MINOR_VERSION) {
+		errx(1, "bpf minor %u, expected >= %u",
+		    bv.bv_minor, BPF_MINOR_VERSION);
+	}
+
+	if (bd->bd_dlt != -1) {
+		if (ioctl(bif0->bif_bpf, BIOCSDLT, &bd->bd_dlt) == -1) {
+			err(1, "%s: unsupported datalink type %s",
+			    bif0->bif_name, bd->bd_dlt_name);
+		}
+	} else {
+		if (ioctl(bif0->bif_bpf, BIOCGDLT, &bd->bd_dlt) == -1)
+			err(1, "%s: get datalink type", bif0->bif_name);
+
+		bd->bd_dlt_name = pcap_datalink_val_to_name(bd->bd_dlt);
+		if (bd->bd_dlt_name == NULL) {
+			errx(1, "%s: datalink type %d is unknown to libpcap",
+			    bif0->bif_name, bd->bd_dlt);
+		}
+	}
+
+	if (filter != NULL || argc > 0) {
+		pcap_t *ph;
+		char *expr = NULL;
+		int i;
+
+		if (filter != NULL) {
+			int fd;
+			ssize_t rv;
+
+			fd = open(filter, O_RDONLY);
+			if (fd == -1)
+				err(1, "%s", filter);
+
+#define BPFLOG_FILTER_MAX	8192
+
+			expr = malloc(BPFLOG_FILTER_MAX);
+			if (expr == NULL)
+				err(1, NULL);
+
+			rv = read(fd, expr, BPFLOG_FILTER_MAX);
+			if (rv == -1)
+				err(1, "%s read", filter);
+			if (rv == 0)
+				errx(1, "%s is empty", filter);
+			if (rv >= BPFLOG_FILTER_MAX - 1)
+				errx(1, "%s is too long", filter);
+
+			close(fd);
+		} else if (argc == 1)
+			expr = argv[0];
+		else {
+			size_t alen = strlen(argv[0]);
+			size_t len = alen;
+
+			expr = malloc(len + 1);
+			if (expr == NULL)
+				err(1, "bpf expression buffer");
+
+			memcpy(expr, argv[0], alen);
+
+			for (i = 1; i < argc; i++) {
+				size_t nlen;
+
+				alen = strlen(argv[i]);
+				if (alen == 0)
+					continue;
+
+				nlen = len + 1 + alen;
+
+				expr = realloc(expr, nlen + 1);
+				if (expr == NULL)
+					err(1, "bpf expression buffer");
+
+				expr[len] = ' ';
+				memcpy(expr + len + 1, argv[i], alen);
+
+				len = nlen;
+			}
+			expr[len] = '\0';
+		}
+
+		ph = pcap_open_dead(bd->bd_dlt, bd->bd_snaplen);
+		if (ph == NULL)
+			err(1, "pcap_open_dead");
+
+		if (pcap_compile(ph, &bf, expr, 1, PCAP_NETMASK_UNKNOWN) == -1)
+			errx(1, "%s", pcap_geterr(ph));
+
+		pcap_close(ph);
+
+		if (argc != 1)
+			free(expr);
+	} else {
+		insns[0].k = bd->bd_snaplen;
+		bf.bf_insns = insns;
+		bf.bf_len = nitems(insns);
+	}
+
+	bif = bif0;
+	while ((bif = TAILQ_NEXT(bif, bif_entry)) != NULL) {
+		bpfif_open(bif); /* err on failure */
+
+		if (ioctl(bif->bif_bpf, BIOCSDLT, &bd->bd_dlt) == -1) {
+			err(1, "%s: unsupported datalink type %s",
+			    bif->bif_name, bd->bd_dlt_name);
+		}
+	}
+
+	waittv.tv_sec = waitms / 1000;
+	waittv.tv_usec = (waitms % 1000) * 1000;
+
+	TAILQ_FOREACH(bif, &bd->bd_bif_list, bif_entry) {
+		if (ioctl(bif->bif_bpf, BIOCSETF, &bf) == -1)
+			err(1, "%s: set filter", bif0->bif_name);
+
+		if (promisc) {
+			if (ioctl(bif->bif_bpf, BIOCPROMISC, NULL) == -1)
+				err(1, "%s: enable promisc", bif0->bif_name);
+		}
+
+		if (ioctl(bif->bif_bpf, BIOCSWTIMEOUT, &waittv) == -1)
+			err(1, "%s: wait ms %d", bif0->bif_name, waitms);
+	}
+
+	if (bf.bf_insns != insns)
+		pcap_freecode(&bf);
+
+	if (!debug) {
+		devnull = open(_PATH_DEVNULL, O_RDWR);
+		if (devnull == -1)
+			err(1, "%s", _PATH_DEVNULL);
+	}
+
+	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");
+
+	endpwent();
+
+	if (unveil(bd->bd_fname, "rwc") == -1)
+		err(1, "unveil %s", bd->bd_fname);
+
+	bd->bd_fd = bpflog_open(bd);
+	if (bd->bd_fd == -1) {
+		/* error has already been printed */
+		exit(1);
+	}
+
+	if (!debug) {
+		extern char *__progname;
+
+		if (rdaemon(devnull) == -1)
+			err(1, "unable to daemonize");
+
+		logger_syslog(__progname);
+	}
+
+	event_init();
+
+	signal_set(&bd->bd_sighup, SIGHUP, bpflogd_hup, bd);
+	signal_add(&bd->bd_sighup, NULL);
+
+	TAILQ_FOREACH(bif, &bd->bd_bif_list, bif_entry) {
+		event_set(&bif->bif_ev, bif->bif_bpf, EV_READ | EV_PERSIST,
+		    bpfif_read, bif);
+		event_add(&bif->bif_ev, NULL);
+	}
+
+	event_dispatch();
+
+	return (0);
+}
+
+static int
+bpf_maxbufsize(void)
+{
+	int mib[] = { CTL_NET, PF_BPF, NET_BPF_MAXBUFSIZE };
+	int maxbuf;
+	size_t maxbufsize = sizeof(maxbuf);
+
+	if (sysctl(mib, nitems(mib), &maxbuf, &maxbufsize, NULL, 0) == -1)
+		return (-1);
+
+	if (maxbuf > 1 << 20)
+		maxbuf = 1 << 20;
+
+	return (maxbuf);
+}
+
+static void
+bpflogd_hup(int nil, short events, void *arg)
+{
+	struct bpflogd *bd = arg;
+	struct bpfif *bif;
+	int fd;
+
+	fd = bpflog_open(bd);
+
+	TAILQ_FOREACH(bif, &bd->bd_bif_list, bif_entry)
+		bpfif_read(bif->bif_bpf, 0, bif);
+
+	close(bd->bd_fd);
+
+	if (fd == -1)
+		lerrx(1, "exiting");
+
+	bd->bd_fd = fd;
+
+	linfo("%s turned over", bd->bd_fname);
+}
+
+static int
+bpflog_open(struct bpflogd *bd)
+{
+	const struct pcap_file_header pfh = {
+		.magic = 0xa1b2c3d4,
+		.version_major = PCAP_VERSION_MAJOR,
+		.version_minor = PCAP_VERSION_MINOR,
+		.thiszone = 0, /* we work in UTC */
+		.sigfigs = 0,
+		.snaplen = BPF_MAXBUFSIZE,
+		.linktype = bd->bd_dlt,
+	};
+	struct pcap_file_header epfh;
+	struct stat st;
+	ssize_t rv;
+	int fd;
+
+	fd = open(bd->bd_fname, O_RDWR|O_APPEND);
+	if (fd == -1) {
+		lwarn("%s", bd->bd_fname);
+		return (-1);
+	}
+
+	if (fstat(fd, &st) == -1) {
+		lwarn("%s stat", bd->bd_fname);
+		goto close;
+	}
+
+	if (st.st_size == 0) {
+		rv = write(fd, &pfh, sizeof(pfh));
+		if (rv == -1) {
+			lwarn("%s pcap file header write", bd->bd_fname);
+			goto close;
+		}
+		if ((size_t)rv < sizeof(pfh)) {
+			lwarnx("%s pcap file header short write",
+			    bd->bd_fname);
+			goto close;
+		}
+
+		return (fd);
+	}
+
+	rv = pread(fd, &epfh, sizeof(epfh), 0);
+	if (rv == -1) {
+		lwarn("%s pcap file header read", bd->bd_fname);
+		goto close;
+	}
+	if ((size_t)rv < sizeof(epfh)) {
+		lwarn("%s pcap file header is short", bd->bd_fname);
+		goto close;
+	}
+
+	if (epfh.magic != pfh.magic) {
+		lwarnx("%s pcap file header magic is wrong",
+		    bd->bd_fname);
+		goto close;
+	}
+	if (epfh.version_major != pfh.version_major ||
+	    epfh.version_minor < pfh.version_minor) {
+		lwarnx("%s pcap file header version is unsupported",
+		    bd->bd_fname);
+		goto close;
+	}
+	if (epfh.thiszone != pfh.thiszone) {
+		lwarnx("%s pcap file timezone is different", bd->bd_fname);
+		goto close;
+	}
+	if (epfh.snaplen < bd->bd_snaplen) {
+		lwarnx("%s pcap file snaplen is too short", bd->bd_fname);
+		goto close;
+	}
+	if (epfh.linktype != pfh.linktype) {
+		lwarnx("%s pcap file linktype is different", bd->bd_fname);
+		goto close;
+	}
+
+	return (fd);
+
+close:
+	close(fd);
+	return (-1);
+}
+
+static void
+bpfif_open(struct bpfif *bif)
+{
+	struct bpflogd *bd = bif->bif_bd;
+	struct ifreq ifr;
+
+	bif->bif_bpf = open(_DEV_BPF, O_RDWR | O_NONBLOCK);
+	if (bif->bif_bpf == -1)
+		err(1, "%s: open %s", bif->bif_name, _DEV_BPF);
+
+	memset(&ifr, 0, sizeof(ifr));
+	if (strlcpy(ifr.ifr_name, bif->bif_name, sizeof(ifr.ifr_name)) >=
+	    sizeof(ifr.ifr_name))
+		errx(1, "%s: interface name is too long", bif->bif_name);
+
+	if (ioctl(bif->bif_bpf, BIOCSBLEN, &bd->bd_buflen) == -1) {
+		err(1, "%s: set buffer length %d", bif->bif_name,
+		    bd->bd_buflen);
+	}
+
+	if (ioctl(bif->bif_bpf, BIOCSETIF, &ifr) == -1)
+		err(1, "%s: set bpf interface", bif->bif_name);
+}
+
+#define PCAP_PKTS	64
+
+static void
+bpfif_read(int fd, short events, void *arg)
+{
+	struct bpfif *bif = arg;
+	struct bpflogd *bd = bif->bif_bd;
+	ssize_t rv;
+	size_t len, bpflen, caplen;
+	uint8_t *buf;
+
+	struct pcap_pkthdr pps[PCAP_PKTS], *pp;
+	struct iovec iovs[PCAP_PKTS * 2], *iov = iovs;
+	size_t np = 0;
+
+	rv = read(fd, bd->bd_buf, bd->bd_buflen);
+	switch (rv) {
+	case -1:
+		switch (errno) {
+		case EINTR:
+		case EAGAIN:
+			break;
+		default:
+			lerr(1, "%s bpf read", bif->bif_name);
+			/* NOTREACHED */
+		}
+		return;
+	case 0:
+		/* bpf buffer is empty */
+		return;
+	default:
+		break;
+	}
+
+	buf = bd->bd_buf;
+	len = rv;
+
+	for (;;) {
+		const struct bpf_hdr *bh;
+
+		/* the kernel lied to us.  */
+		if (len < sizeof(*bh))
+			lerrx(1, "%s: short bpf header", bif->bif_name);
+
+		bh = (const struct bpf_hdr *)buf;
+		bpflen = bh->bh_hdrlen + bh->bh_caplen;
+
+		/*
+		 * If the bpf header plus data doesn't fit in what's
+		 * left of the buffer, we've got a problem...
+		 */
+		if (bpflen > len)
+			lerrx(1, "%s: short bpf read", bif->bif_name);
+
+		if (np >= PCAP_PKTS) {
+			rv = writev(bd->bd_fd, iovs, iov - iovs);
+			if (rv == -1)
+				lwarn("%s write", bd->bd_fname);
+			iov = iovs;
+			np = 0;
+		}
+
+		caplen = bh->bh_caplen;
+		if (caplen > BPF_MAXBUFSIZE)
+			caplen = BPF_MAXBUFSIZE;
+
+		pp = &pps[np++];
+
+		pp->ts = bh->bh_tstamp;
+		pp->caplen = caplen;
+		pp->len = bh->bh_datalen;
+
+		iov->iov_base = pp;
+		iov->iov_len = sizeof(*pp);
+		iov++;
+		iov->iov_base = buf + bh->bh_hdrlen;
+		iov->iov_len = caplen;
+		iov++;
+
+		bpflen = BPF_WORDALIGN(bpflen);
+		if (len <= bpflen) {
+			/* everything is consumed */
+			break;
+		}
+
+		/* Move the lop to the next packet */
+		buf += bpflen;
+		len -= bpflen;
+	}
+
+	if (np > 0) {
+		rv = writev(bd->bd_fd, iovs, iov - iovs);
+		if (rv == -1)
+			lwarn("%s write", bd->bd_fname);
+	}
+
+	fsync(bd->bd_fd);
+}
+
+/* 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: bpflogd/log.c
===================================================================
RCS file: bpflogd/log.c
diff -N bpflogd/log.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ bpflogd/log.c	24 Apr 2025 03:56:54 -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)
+{
+	openlog(progname, LOG_PID | LOG_NDELAY, LOG_DAEMON);
+	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: bpflogd/log.h
===================================================================
RCS file: bpflogd/log.h
diff -N bpflogd/log.h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ bpflogd/log.h	24 Apr 2025 03:56:54 -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 *);
+
+#endif /* _LOG_H_ */