pflog: /dev/pflog chardev backend (BSD pflog_hdr layout, DLT_PFLOG) + tools/pflogd + gate 12
This commit is contained in:
1 parent
dbb21048e4
commit
7a81076c6a
11 files changed
+2401
-17
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
savedcmd_pf.mod := printf '%s\n' pfsrc/pfk_mod.o pfsrc/pfk_hooks.o compat/pfk_mbuf.o compat/pfk_kern.o compat/pfk_if.o compat/pfk_xmit.o compat/pfk_pcb.o compat/pfk_ip4.o compat/siphash.o compat/toeplitz.o compat/md5.o compat/sha2.o compat/radix.o compat/pfk_rtable.o compat/pfk_ip6.o compat/subr_tree.o pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o compat/pfk_ifq.o | awk '!x[$$0]++ { print("./"$$0) }' > pf.mod
|
||||
savedcmd_pf.mod := printf '%s\n' pfsrc/pfk_mod.o pfsrc/pfk_hooks.o compat/pfk_mbuf.o compat/pfk_kern.o compat/pfk_if.o compat/pfk_xmit.o compat/pfk_pcb.o compat/pfk_ip4.o compat/siphash.o compat/toeplitz.o compat/md5.o compat/sha2.o compat/radix.o compat/pfk_rtable.o compat/pfk_ip6.o compat/subr_tree.o pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.o compat/pfk_ifq.o | awk '!x[$$0]++ { print("./"$$0) }' > pf.mod
|
||||
+1
-1
@@ -20,7 +20,7 @@ pf-objs := pfsrc/pfk_mod.o pfsrc/pfk_hooks.o \
|
||||
# ported OpenBSD 8.0 engine
|
||||
pf-objs += pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o \
|
||||
pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o \
|
||||
pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o
|
||||
pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.o
|
||||
|
||||
# queueing framework (compat/pfk_ifq.c implements the ifq API with priq
|
||||
# semantics; the OpenBSD hfsc/fq_codel back ends land in milestone M5)
|
||||
|
||||
@@ -619,16 +619,5 @@ if_congested(void)
|
||||
}
|
||||
|
||||
|
||||
/* ---------------- pflog (netlink backend, milestone M4) ---------------- */
|
||||
/* NPFLOG=1: the staged if_pflog.c needs the OpenBSD ifpf layer; until the
|
||||
* netlink-backed pflog lands (M4), pflog_packet() consumes nothing and
|
||||
* reports the missing backend at least once so logged packets are never
|
||||
* silently vanished (see docs/porting-notes.md). */
|
||||
int
|
||||
pflog_packet(struct pf_pdesc *pd, u_int8_t reason, struct pf_rule *rm,
|
||||
struct pf_rule *am, struct pf_ruleset *ruleset, struct pf_rule *cm)
|
||||
{
|
||||
(void)pd; (void)reason; (void)rm; (void)am; (void)ruleset; (void)cm;
|
||||
pr_warn_once("pfk: pflog backend pending M4; packet not logged\n");
|
||||
return (0);
|
||||
}
|
||||
/* pflog_packet() lives in pfsrc/pfk_pflog.c (the /dev/pflog backend). */
|
||||
|
||||
@@ -1203,4 +1203,14 @@ int pf_anchor_stack_pop(struct pf_ruleset **, struct pf_rule **,
|
||||
struct inpcb;
|
||||
void pf_remove_divert_state(struct inpcb *);
|
||||
|
||||
/* pflog backend (/dev/pflog) - 'L' keeps the namespace clear of DIOC* ('P') */
|
||||
struct pfk_pflog_stat {
|
||||
u_int64_t queued; /* records currently queued */
|
||||
u_int64_t bytes; /* bytes queued */
|
||||
u_int64_t dropped; /* records dropped (queue full) */
|
||||
u_int64_t seq; /* records produced since load */
|
||||
};
|
||||
#define PFKIOCGETPFLOGSTAT _IOR('L', 1, struct pfk_pflog_stat)
|
||||
|
||||
#endif /* _PFK_H */
|
||||
|
||||
@@ -12,6 +12,8 @@ void pfk_status(struct pf_status *s);
|
||||
int pfk_hooks_register(void);
|
||||
void pfk_hooks_unregister(void);
|
||||
/* AF_DIVERT family (module-internal socket protocol, PF_DIVERT=32) */
|
||||
int pfk_pflog_register(void);
|
||||
void pfk_pflog_unregister(void);
|
||||
int pfk_divert_sock_register(void);
|
||||
void pfk_divert_sock_unregister(void);
|
||||
void divert_packet(struct mbuf *, int, u_int16_t);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -94,6 +94,8 @@ static struct miscdevice pfk_dev = {
|
||||
extern void pf_status_init(void);
|
||||
extern int pfattach(int);
|
||||
extern int pfk_divert_sock_register(void);
|
||||
extern int pfk_pflog_register(void);
|
||||
extern void pfk_pflog_unregister(void);
|
||||
extern void pfk_divert_sock_unregister(void);
|
||||
|
||||
static void
|
||||
@@ -133,9 +135,12 @@ static int __init pfk_init(void)
|
||||
pfattach(0);
|
||||
pfk_if_set_event_cb(pfk_pfi_event);
|
||||
pfk_if_iterate(pfk_pfi_attach_one);
|
||||
err = pfk_divert_sock_register();
|
||||
err = pfk_pflog_register();
|
||||
if (err)
|
||||
goto err_if;
|
||||
err = pfk_divert_sock_register();
|
||||
if (err)
|
||||
goto err_pflog;
|
||||
err = pfk_hooks_register();
|
||||
if (err)
|
||||
goto err_divert;
|
||||
@@ -147,6 +152,8 @@ static int __init pfk_init(void)
|
||||
err_hooks:
|
||||
pfk_divert_sock_unregister();
|
||||
err_divert:
|
||||
pfk_pflog_unregister();
|
||||
err_pflog:
|
||||
pfk_hooks_unregister();
|
||||
err_if:
|
||||
pfk_if_exit();
|
||||
@@ -166,6 +173,7 @@ static void __exit pfk_exit(void)
|
||||
* workqueues non-empty forever (observed rmmod hang). */
|
||||
misc_deregister(&pfk_dev);
|
||||
pfk_divert_sock_unregister();
|
||||
pfk_pflog_unregister();
|
||||
pfk_net_lock();
|
||||
pf_status.running = 0;
|
||||
pfk_net_unlock();
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* PF-on-Linux: pflog backend (ISC 2026)
|
||||
*
|
||||
* OpenBSD logs matched packets to the pflog(4) *network interface*, which
|
||||
* pflogd(8) reads through bpf(4). Linux has neither if_pflog-style virtual
|
||||
* interfaces exposed to bpf nor a DLT_PFLOG consumer, so this port offers the
|
||||
* same record format over a character device:
|
||||
*
|
||||
* /dev/pflog (0600) - read() returns one record at a time:
|
||||
* struct pflog_hdr followed by the captured
|
||||
* packet bytes (truncated to PFLOG_COPY_SIZE).
|
||||
*
|
||||
* poll() reports readable when a record is queued. A single consumer is the
|
||||
* real use case (pflogd), which is why a chardev - not netlink multicast -
|
||||
* was chosen; the deviation is documented in docs/porting-notes.md.
|
||||
*
|
||||
* The records are byte-compatible with OpenBSD's pflog(4) so tcpdump/wireshark
|
||||
* read the pcap with linktype DLT_PFLOG (117).
|
||||
*/
|
||||
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/miscdevice.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/mm.h>
|
||||
#include <linux/poll.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/spinlock.h>
|
||||
#include <linux/wait.h>
|
||||
#include <linux/timekeeping.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/netdevice.h>
|
||||
|
||||
#include <pfk.h>
|
||||
#include <pfk_api.h>
|
||||
|
||||
/* OpenBSD sys/net/pflog.h */
|
||||
#define PFLOG_IFNAMSIZ 16
|
||||
#define PFLOG_RULESET_NAME_SIZE 16
|
||||
#define PFLOG_HDRLEN 64 /* sizeof(struct pfk_pflog_hdr) */
|
||||
#define PFLOG_COPY_SIZE 160
|
||||
|
||||
/* byte-compatible with OpenBSD's <net/pflog.h> (sizeof == 64) */
|
||||
struct pfk_pflog_hdr {
|
||||
struct {
|
||||
s64 tv_sec;
|
||||
s64 tv_usec;
|
||||
} pflog_ts;
|
||||
u8 pflog_af;
|
||||
u8 pflog_reason;
|
||||
u8 pflog_ifname[PFLOG_IFNAMSIZ];
|
||||
u8 pflog_ruleset[PFLOG_RULESET_NAME_SIZE];
|
||||
u8 pflog_nr;
|
||||
u8 pflog_anc;
|
||||
u8 pflog_flags;
|
||||
u8 pflog_pad[9];
|
||||
u16 pflog_len; /* captured packet length */
|
||||
} __packed;
|
||||
|
||||
struct pfk_pflog_rec {
|
||||
struct list_head list;
|
||||
size_t len; /* header + payload */
|
||||
char data[]; /* pfk_pflog_hdr, then the packet */
|
||||
};
|
||||
|
||||
#define PFK_PFLOG_MAX_QUEUE 256u
|
||||
#define PFK_PFLOG_MAX_BYTES (1u << 20)
|
||||
|
||||
static LIST_HEAD(pflog_list);
|
||||
static DEFINE_SPINLOCK(pflog_lock);
|
||||
static DECLARE_WAIT_QUEUE_HEAD(pflog_wq);
|
||||
static unsigned long pflog_queued, pflog_bytes, pflog_dropped;
|
||||
static u64 pflog_seq;
|
||||
|
||||
/*
|
||||
* Queue one record. Called from the packet path (process context here: the
|
||||
* data path runs in pfk_netisr_wq), so a GFP_ATOMIC allocation and a bounded
|
||||
* queue keep it safe even if a future caller appears in softirq context.
|
||||
*/
|
||||
static void
|
||||
pfk_pflog_put(struct pfk_pflog_hdr *hdr, const void *pkt, size_t pktlen)
|
||||
{
|
||||
struct pfk_pflog_rec *rec;
|
||||
size_t len = sizeof(*hdr) + pktlen;
|
||||
unsigned long flags;
|
||||
|
||||
if (READ_ONCE(pflog_queued) >= PFK_PFLOG_MAX_QUEUE ||
|
||||
READ_ONCE(pflog_bytes) + len > PFK_PFLOG_MAX_BYTES) {
|
||||
pflog_dropped++;
|
||||
return;
|
||||
}
|
||||
|
||||
rec = kmalloc(struct_size(rec, data, len), GFP_ATOMIC);
|
||||
if (rec == NULL) {
|
||||
pflog_dropped++;
|
||||
return;
|
||||
}
|
||||
rec->len = len;
|
||||
memcpy(rec->data, hdr, sizeof(*hdr));
|
||||
memcpy(rec->data + sizeof(*hdr), pkt, pktlen);
|
||||
|
||||
spin_lock_irqsave(&pflog_lock, flags);
|
||||
list_add_tail(&rec->list, &pflog_list);
|
||||
pflog_queued++;
|
||||
pflog_bytes += len;
|
||||
pflog_seq++;
|
||||
spin_unlock_irqrestore(&pflog_lock, flags);
|
||||
|
||||
wake_up_interruptible(&pflog_wq);
|
||||
}
|
||||
|
||||
/* copy the leading bytes of the packet (the mbuf chain may be a wrapper) */
|
||||
static size_t
|
||||
pfk_pflog_pull(struct mbuf *m, char *buf, size_t want)
|
||||
{
|
||||
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
|
||||
size_t n;
|
||||
|
||||
if (skb == NULL)
|
||||
return 0;
|
||||
n = min_t(size_t, want, skb->len);
|
||||
if (skb_copy_bits(skb, 0, buf, n))
|
||||
return 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------------- pflog_packet() (called by the ported pf.c) ---------------- */
|
||||
|
||||
int
|
||||
pflog_packet(struct pf_pdesc *pd, u_int8_t reason, struct pf_rule *rm,
|
||||
struct pf_rule *am, struct pf_ruleset *ruleset, struct pf_rule *cm)
|
||||
{
|
||||
struct pfk_pflog_hdr hdr;
|
||||
char pkt[PFLOG_COPY_SIZE];
|
||||
struct pf_rule *r;
|
||||
size_t n;
|
||||
|
||||
if (pd == NULL || pd->m == NULL)
|
||||
return (0);
|
||||
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
{
|
||||
struct timespec64 ts;
|
||||
|
||||
ktime_get_real_ts64(&ts);
|
||||
hdr.pflog_ts.tv_sec = ts.tv_sec;
|
||||
hdr.pflog_ts.tv_usec = ts.tv_nsec / 1000;
|
||||
}
|
||||
hdr.pflog_af = pd->af;
|
||||
hdr.pflog_reason = reason;
|
||||
|
||||
r = cm ? cm : (am ? am : rm);
|
||||
if (r) {
|
||||
strscpy((char *)hdr.pflog_ifname,
|
||||
pd->kif ? pd->kif->pfik_name : "",
|
||||
sizeof(hdr.pflog_ifname));
|
||||
strscpy((char *)hdr.pflog_ruleset,
|
||||
(ruleset && ruleset->anchor) ? ruleset->anchor->name : "",
|
||||
sizeof(hdr.pflog_ruleset));
|
||||
hdr.pflog_nr = r->nr;
|
||||
hdr.pflog_anc = 0;
|
||||
hdr.pflog_flags = r->log;
|
||||
}
|
||||
|
||||
n = pfk_pflog_pull(pd->m, pkt, sizeof(pkt));
|
||||
hdr.pflog_len = cpu_to_be16(n); /* BSD stores it in network order */
|
||||
|
||||
pfk_pflog_put(&hdr, pkt, n);
|
||||
return (0);
|
||||
}
|
||||
|
||||
/* ---------------- /dev/pflog ---------------- */
|
||||
|
||||
static int
|
||||
pfk_pflog_open(struct inode *inode, struct file *filp)
|
||||
{
|
||||
filp->private_data = NULL;
|
||||
return nonseekable_open(inode, filp);
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
pfk_pflog_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
|
||||
{
|
||||
struct pfk_pflog_rec *rec = filp->private_data;
|
||||
unsigned long flags;
|
||||
size_t off, n;
|
||||
|
||||
spin_lock_irqsave(&pflog_lock, flags);
|
||||
if (rec == NULL) {
|
||||
if (list_empty(&pflog_list)) {
|
||||
spin_unlock_irqrestore(&pflog_lock, flags);
|
||||
if (filp->f_flags & O_NONBLOCK)
|
||||
return -EAGAIN;
|
||||
if (wait_event_interruptible(pflog_wq,
|
||||
!list_empty(&pflog_list)))
|
||||
return -ERESTARTSYS;
|
||||
spin_lock_irqsave(&pflog_lock, flags);
|
||||
if (list_empty(&pflog_list)) {
|
||||
spin_unlock_irqrestore(&pflog_lock, flags);
|
||||
return -EAGAIN;
|
||||
}
|
||||
}
|
||||
rec = list_first_entry(&pflog_list, struct pfk_pflog_rec, list);
|
||||
list_del(&rec->list);
|
||||
pflog_queued--;
|
||||
pflog_bytes -= rec->len;
|
||||
filp->private_data = rec;
|
||||
}
|
||||
spin_unlock_irqrestore(&pflog_lock, flags);
|
||||
|
||||
/* partial reads resume through *ppos (record is pinned to the file) */
|
||||
off = *ppos;
|
||||
if (off >= rec->len) {
|
||||
kfree(rec);
|
||||
filp->private_data = NULL;
|
||||
*ppos = 0;
|
||||
return 0;
|
||||
}
|
||||
n = min(count, rec->len - off);
|
||||
if (copy_to_user(buf, rec->data + off, n))
|
||||
return -EFAULT;
|
||||
*ppos += n;
|
||||
if (*ppos == rec->len) {
|
||||
kfree(rec);
|
||||
filp->private_data = NULL;
|
||||
*ppos = 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static __poll_t
|
||||
pfk_pflog_poll(struct file *filp, poll_table *wait)
|
||||
{
|
||||
__poll_t mask = 0;
|
||||
|
||||
poll_wait(filp, &pflog_wq, wait);
|
||||
if (filp->private_data != NULL)
|
||||
mask |= EPOLLIN | EPOLLRDNORM;
|
||||
else if (!list_empty(&pflog_list))
|
||||
mask |= EPOLLIN | EPOLLRDNORM;
|
||||
return mask;
|
||||
}
|
||||
|
||||
static long
|
||||
pfk_pflog_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
|
||||
{
|
||||
struct pfk_pflog_stat st;
|
||||
|
||||
switch (cmd) {
|
||||
case PFKIOCGETPFLOGSTAT:
|
||||
st.queued = pflog_queued;
|
||||
st.bytes = pflog_bytes;
|
||||
st.dropped = pflog_dropped;
|
||||
st.seq = pflog_seq;
|
||||
if (copy_to_user((void __user *)arg, &st, sizeof(st)))
|
||||
return -EFAULT;
|
||||
return 0;
|
||||
default:
|
||||
return -ENOTTY;
|
||||
}
|
||||
}
|
||||
|
||||
static const struct file_operations pfk_pflog_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.open = pfk_pflog_open,
|
||||
.read = pfk_pflog_read,
|
||||
.poll = pfk_pflog_poll,
|
||||
.unlocked_ioctl = pfk_pflog_ioctl,
|
||||
.compat_ioctl = compat_ptr_ioctl,
|
||||
};
|
||||
|
||||
static struct miscdevice pfk_pflog_dev = {
|
||||
.minor = MISC_DYNAMIC_MINOR,
|
||||
.name = "pflog",
|
||||
.fops = &pfk_pflog_fops,
|
||||
.mode = 0600,
|
||||
};
|
||||
|
||||
int
|
||||
pfk_pflog_register(void)
|
||||
{
|
||||
return misc_register(&pfk_pflog_dev);
|
||||
}
|
||||
|
||||
void
|
||||
pfk_pflog_unregister(void)
|
||||
{
|
||||
struct pfk_pflog_rec *rec, *tmp;
|
||||
|
||||
misc_deregister(&pfk_pflog_dev);
|
||||
spin_lock(&pflog_lock);
|
||||
list_for_each_entry_safe(rec, tmp, &pflog_list, list) {
|
||||
list_del(&rec->list);
|
||||
kfree(rec);
|
||||
}
|
||||
pflog_queued = 0;
|
||||
pflog_bytes = 0;
|
||||
spin_unlock(&pflog_lock);
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# M4 gate: pflog — logged packets must reach userspace.
|
||||
#
|
||||
# pflogd reads /dev/pflog (the port's chardev backend) and writes a pcap with
|
||||
# linktype DLT_PFLOG (117). A `log` rule + ICMP traffic must produce records
|
||||
# that contain the real packet bytes (checked by searching the client address
|
||||
# in the capture) and a valid pcap header.
|
||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||
|
||||
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
|
||||
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||
pf0 'pkill -f "[p]flogd" 2>/dev/null; rm -f /tmp/pflog.pcap; true'
|
||||
|
||||
LOGD=$(pfput ../../tools/out/pflogd) || { bad "pflogd deploy"; finish; }
|
||||
pf0 "test -c /dev/pflog && echo DEV_OK" | grep -q DEV_OK || { bad "/dev/pflog missing"; finish; }
|
||||
pf0 "chmod +x $LOGD; (setsid $LOGD -c 4 -f /dev/pflog /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; pgrep pflogd >/dev/null && echo DAEMON_UP" \
|
||||
| grep -q DAEMON_UP || { bad "pflogd start"; finish; }
|
||||
|
||||
cat > /tmp/g12.conf <<C
|
||||
pass in quick log proto icmp from $CLI_IP keep state
|
||||
pass out quick proto icmp keep state
|
||||
C
|
||||
pfload /tmp/g12.conf || { bad "log rule load"; finish; }
|
||||
|
||||
cli "ping -c3 -W2 $SRV_IP >/dev/null 2>&1"
|
||||
sleep 2
|
||||
|
||||
# pcap header: magic d4c3b2a1 (LE) and network = 117 at offset 20
|
||||
HDR=$(pf0 "od -An -tx1 -N24 /tmp/pflog.pcap 2>/dev/null | tr -d '[[:space:]]'")
|
||||
if [ "${HDR:0:8}" = "d4c3b2a1" ]; then ok "pcap magic ok"; else bad "pcap header: $HDR"; fi
|
||||
if [ "${HDR:40:4}" = "7500" ] || [ "${HDR:40:8}" = "75000000" ]; then
|
||||
ok "linktype DLT_PFLOG (117)"
|
||||
else
|
||||
bad "linktype: ${HDR:40:8}"
|
||||
fi
|
||||
|
||||
SZ=$(pf0 'wc -c < /tmp/pflog.pcap' | tr -d '[:space:]')
|
||||
if [ "${SZ:-0}" -gt 24 ]; then ok "records captured ($SZ bytes)"; else bad "no records ($SZ)"; fi
|
||||
|
||||
# the client address must appear inside a captured packet
|
||||
if pf0 "od -An -tx1 /tmp/pflog.pcap | tr -d '[[:space:]]' | grep -q 0a00010a"; then
|
||||
ok "captured payload contains $CLI_IP"
|
||||
else
|
||||
bad "client address not in capture"
|
||||
fi
|
||||
|
||||
pf0 'pkill pflogd 2>/dev/null; /opt/pf/pfctl -d' >/dev/null
|
||||
finish
|
||||
+10
-1
@@ -57,7 +57,7 @@ GOODTESTS := tests/01_basic.conf tests/02_translations.conf \
|
||||
tests/03_queues_scrub.conf
|
||||
|
||||
.PHONY: all pfctl abitest test abi clean
|
||||
all: pfctl abitest pdivert
|
||||
all: pfctl abitest pdivert pflogd
|
||||
|
||||
pfctl: $(OUT)/pfctl
|
||||
|
||||
@@ -190,3 +190,12 @@ $(PD): pdivert/pdivert.c
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ pdivert/pdivert.c
|
||||
|
||||
clean:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# pflogd - reads /dev/pflog and writes DLT_PFLOG pcap (self-contained)
|
||||
LOGD := $(OUT)/pflogd
|
||||
pflogd: $(LOGD)
|
||||
|
||||
$(LOGD): pflogd/pflogd.c
|
||||
@mkdir -p $(OUT)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ pflogd/pflogd.c
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* pflogd for PF-on-Linux (ISC 2026)
|
||||
*
|
||||
* Reads records from /dev/pflog (see module/pfsrc/pfk_pflog.c) and writes a
|
||||
* pcap file with linktype DLT_PFLOG (117) - the same format OpenBSD's pflogd
|
||||
* produces, so tcpdump -r / wireshark decode it.
|
||||
*
|
||||
* pflogd [-i iface] [-f /dev/pflog] [-p pidfile] [-c count] <file>
|
||||
*
|
||||
* A "-" as <file> writes to stdout. Omitting <file> prints a human-readable
|
||||
* one-line summary per record (useful in the lab gates).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
#define PFLOG_IFNAMSIZ 16
|
||||
#define PFLOG_RULESET_NAME 16
|
||||
#define DLT_PFLOG 117
|
||||
|
||||
struct pflog_hdr {
|
||||
int64_t ts_sec;
|
||||
int64_t ts_usec;
|
||||
uint8_t af;
|
||||
uint8_t reason;
|
||||
uint8_t ifname[PFLOG_IFNAMSIZ];
|
||||
uint8_t ruleset[PFLOG_RULESET_NAME];
|
||||
uint8_t nr;
|
||||
uint8_t anc;
|
||||
uint8_t flags;
|
||||
uint8_t pad[9];
|
||||
uint16_t pktlen; /* network order, as in BSD */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct pcap_file_header {
|
||||
uint32_t magic;
|
||||
uint16_t major, minor;
|
||||
int32_t thiszone;
|
||||
uint32_t sigfigs, snaplen, network;
|
||||
};
|
||||
|
||||
struct pcap_pkthdr {
|
||||
uint32_t ts_sec, ts_usec, caplen, len;
|
||||
};
|
||||
|
||||
static volatile int running = 1;
|
||||
static void onterm(int s) { (void)s; running = 0; }
|
||||
|
||||
static const char *reason_name(uint8_t r)
|
||||
{
|
||||
static const char *n[] = { "pass", "drop", "scrub", "noscrub",
|
||||
"nat", "nonat", "rdr", "nordr" };
|
||||
return r < 8 ? n[r] : "?";
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
const char *dev = "/dev/pflog", *file = NULL, *pidfile = NULL;
|
||||
long want = -1;
|
||||
int fd, out = 1, ch, wrote_header = 0, nrec = 0;
|
||||
unsigned char buf[sizeof(struct pflog_hdr) + 2048];
|
||||
|
||||
while ((ch = getopt(argc, argv, "f:c:p:")) != -1)
|
||||
switch (ch) {
|
||||
case 'f': dev = optarg; break;
|
||||
case 'c': want = atol(optarg); break;
|
||||
case 'p': pidfile = optarg; break;
|
||||
default: goto usage;
|
||||
}
|
||||
if (optind < argc)
|
||||
file = argv[optind];
|
||||
|
||||
signal(SIGINT, onterm);
|
||||
signal(SIGTERM, onterm);
|
||||
|
||||
fd = open(dev, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "pflogd: open %s: %s\n", dev, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
if (file && strcmp(file, "-") != 0) {
|
||||
out = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
if (out < 0) {
|
||||
fprintf(stderr, "pflogd: open %s: %s\n", file,
|
||||
strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (pidfile) {
|
||||
FILE *p = fopen(pidfile, "w");
|
||||
if (p) { fprintf(p, "%d\n", (int)getpid()); fclose(p); }
|
||||
}
|
||||
|
||||
while (running) {
|
||||
ssize_t n, off = 0;
|
||||
struct pflog_hdr *h;
|
||||
|
||||
/* read one full record: the chardev returns header+payload
|
||||
* consecutively; a short read just continues where we left */
|
||||
while (off < (ssize_t)sizeof(*h)) {
|
||||
n = read(fd, buf + off, sizeof(buf) - off);
|
||||
if (n <= 0) {
|
||||
if (n < 0 && errno == EINTR)
|
||||
goto cont;
|
||||
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
fd_set r;
|
||||
struct timeval tv = { 0, 500000 };
|
||||
FD_ZERO(&r); FD_SET(fd, &r);
|
||||
if (select(fd + 1, &r, NULL, NULL, &tv) <= 0)
|
||||
goto cont;
|
||||
continue;
|
||||
}
|
||||
goto cont;
|
||||
}
|
||||
off += n;
|
||||
}
|
||||
h = (struct pflog_hdr *)buf;
|
||||
{
|
||||
uint16_t plen = ntohs(h->pktlen);
|
||||
while (off < (ssize_t)(sizeof(*h) + plen)) {
|
||||
n = read(fd, buf + off, sizeof(buf) - off);
|
||||
if (n <= 0)
|
||||
break;
|
||||
off += n;
|
||||
}
|
||||
if (off < (ssize_t)(sizeof(*h) + plen))
|
||||
goto cont;
|
||||
|
||||
if (file) {
|
||||
struct pcap_file_header g = {
|
||||
.magic = 0xa1b2c3d4, .major = 2,
|
||||
.minor = 4, .thiszone = 0,
|
||||
.sigfigs = 0, .snaplen = 65535,
|
||||
.network = DLT_PFLOG,
|
||||
};
|
||||
struct pcap_pkthdr ph = {
|
||||
.ts_sec = (uint32_t)h->ts_sec,
|
||||
.ts_usec = (uint32_t)h->ts_usec,
|
||||
.caplen = sizeof(*h) + plen,
|
||||
.len = sizeof(*h) + plen,
|
||||
};
|
||||
if (!wrote_header) {
|
||||
if (write(out, &g, sizeof(g)) != sizeof(g))
|
||||
break;
|
||||
wrote_header = 1;
|
||||
}
|
||||
if (write(out, &ph, sizeof(ph)) != sizeof(ph) ||
|
||||
write(out, buf, ph.caplen) != (ssize_t)ph.caplen)
|
||||
break;
|
||||
} else {
|
||||
printf("%lld.%06lld %s if=%s ruleset=%s nr=%u "
|
||||
"len=%u\n", (long long)h->ts_sec,
|
||||
(long long)h->ts_usec, reason_name(h->reason),
|
||||
(char *)h->ifname, (char *)h->ruleset,
|
||||
h->nr, plen);
|
||||
fflush(stdout);
|
||||
}
|
||||
nrec++;
|
||||
if (want > 0 && nrec >= want)
|
||||
break;
|
||||
}
|
||||
cont:
|
||||
if (want > 0 && nrec >= want)
|
||||
break;
|
||||
}
|
||||
if (file && strcmp(file, "-") != 0)
|
||||
close(out);
|
||||
close(fd);
|
||||
fprintf(stderr, "pflogd: %d records\n", nrec);
|
||||
return 0;
|
||||
usage:
|
||||
fprintf(stderr, "usage: pflogd [-f dev] [-c count] [-p pidfile] [file]\n");
|
||||
return 2;
|
||||
}
|
||||
Reference in new issue
Block a user