M2 data path: BSD netisr workqueue + lock reentrancy fixes + diagnostics

- hooks: packets are queued to pfk_netisr ordered wq (process context) and
  re-injected with an skb->mark guard (PFK_MARK_PASSED); softirq-safe,
  documented divergence vs OpenBSD in-place ip_input continuation
- pfk_rwlock: per-task writer recursion (NET/PF lock nesting like OpenBSD)
- pfk_net: holder-task+depth reentrancy (pid-based hack removed)
- ifnet events deferred to ordered workqueue (rtnl AB-BA fix)
- idr iteration start index fix (kifs were never attached -> kif==NULL drop)
- pfk_lock_trace module param for lock diagnostics
- tests/vm: 01-state 02-block 03-tables 04-anchors + lib; vm-run-tests.sh
- docs/en/install.md: apply/build guide (core patch set currently EMPTY)
This commit is contained in:
lgcosta committed 2026-11-11 17:05:53 -03:00
1 parent 5323ee2c71
commit a0941b77fe
12 files changed
+413 -108

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
# Applying the PF-on-Linux patch set / build guide
Both languages: this file is mirrored at `docs/pt-BR/install.md`.
Target: Linux **7.3.4** source tree + out-of-tree modules `pf.ko`
(PF engine, divert(4) family, ifq queues). The port is currently delivered
as a **source tree + zero core patches** (see patches/kernel/README.md):
the module links against symbols the stock 7.3.4 kernel already exports.
## 1. Prerequisites
| Requirement | Why |
|---|---|
| gcc >= 15, GNU make, flex, bison, bc | kernel + pfctl build |
| `glibc-static` (or musl-static) | static pfctl for guest VMs |
| Linux 7.3.4 sources (`/home/lgcosta/lab/linux_kernel/linux-7.3.4`) | build target |
| OpenBSD 8.0 `sys.tar.gz` + `src.tar.gz` | ported sources (fetched + sha256-verified by script; primary mirror openbsd.c3sl.ufpr.br, fallback cdn.openbsd.org) |
| root on the build host | module signing not configured; `insmod` as root |
## 2. Steps
```sh
cd pf_port
# 2.1 fetch + verify OpenBSD 8.0 sources (checksums pinned in VERSIONS)
./scripts/fetch-sources.sh
./scripts/vendorize.sh # stages _vendor/upstream + IDS.txt
# 2.2 prepare the kernel tree (config + build + external-module prep)
KERNELDIR=/path/to/linux-7.3.4 ./scripts/build-kernel.sh
# - defconfig + virtio/ext4 built-in (direct kernel boot for the VM lab)
# - make bzImage modules_prepare
# - copies vmlinux.symvers -> Module.symvers (needed by M= builds)
# 2.3 apply kernel core patches (currently NONE are required)
./scripts/apply-patches.sh /path/to/linux-7.3.4
# patches/kernel/series is empty by design; if future milestones need
# exports, each lands as 000N-*.patch documented in docs/*/patches.md.
# 2.4 build the module and userland
./scripts/build-module.sh # -> module/pf.ko
make -C tools # -> tools/out/pfctl (static)
# 2.5 install on a running 7.3.4 system
sudo insmod module/pf.ko # creates /dev/pf (0600 root)
sudo cp tools/out/pfctl /usr/local/sbin/
sudo pfctl -e # enable
sudo pfctl -f /etc/pf.conf # real OpenBSD 8.0 syntax
```
## 3. Verify
```sh
pfctl -s info # "Status: Enabled", live counters
pfctl -nf /etc/pf.conf # parse-only check
dmesg | grep pfk # hooks registered (netisr wq data path)
```
VM end-to-end suite (Alpine 3.22 on libvirt): `./scripts/vm-create.sh`,
`./scripts/vm-deploy.sh`, `./scripts/vm-run-tests.sh` — see
`docs/en/vm-lab.md`.
## 4. Uninstall / revert
```sh
sudo pfctl -d; sudo rmmod pf # flushes rulesets/states
./scripts/unpatch.sh /path/to/linux-7.3.4 # reverse-applies any future patches
```
## 5. What "applied" means here
Because the deliverable is an out-of-tree module, "applying the patch set"
= steps 2.1–2.4; the untouched Linux tree is only *configured and built*
(virtio/EXT4 =y) — no source files are modified unless a future milestone
adds a documented `patches/kernel/000N-*.patch` (all such patches: one
section each in docs/en/patches.md with what/why/impact/revert).
+51 -11
View File
@@ -331,15 +331,44 @@ static struct notifier_block pfk_netdev_nb = {
static void (*pfk_if_event_cb)(int added, struct pfk_ifnet *ifp);
/*
* PFLINUX: netdev notifiers run with rtnl held; taking NET_LOCK there
* deadlocks (AB-BA) against control paths that hold NET_LOCK and touch
* rtnl (dev_get_by_name etc.). Interface attach/detach is therefore
* deferred to an ordered workqueue (rtnl-free context) preserving order.
*/
struct pfk_if_event {
struct work_struct work;
int added;
struct pfk_ifnet *ifp;
};
static struct workqueue_struct *pfk_if_wq;
static void
pfk_if_event_work(struct work_struct *w)
{
struct pfk_if_event *ev = container_of(w, struct pfk_if_event, work);
if (pfk_if_event_cb)
pfk_if_event_cb(ev->added, ev->ifp);
kfree(ev);
}
static void
pfk_if_event_call(int added, struct pfk_ifnet *ifp)
{
if (!pfk_if_event_cb)
struct pfk_if_event *ev;
if (!pfk_if_event_cb || !pfk_if_wq)
return;
/* netdev notifiers run in process context; NET lock is sleepable */
pfk_net_lock();
pfk_if_event_cb(added, ifp);
pfk_net_unlock();
ev = kmalloc(sizeof(*ev), GFP_ATOMIC);
if (!ev)
return;
INIT_WORK(&ev->work, pfk_if_event_work);
ev->added = added;
ev->ifp = ifp;
queue_work(pfk_if_wq, &ev->work);
}
void
@@ -352,7 +381,7 @@ void
pfk_if_iterate(void (*cb)(struct pfk_ifnet *ifp))
{
struct pfk_ifnet *ifp;
int id = -1;
int id = 0;
while ((ifp = pfk_if_next_from(&id)) != NULL)
cb(ifp);
@@ -385,7 +414,7 @@ pfk_egress_if(void)
struct pfk_ifnet *
pfk_if_first(void)
{
int id = -1;
int id = 0;
return pfk_if_next_from(&id);
}
@@ -407,7 +436,7 @@ pfk_if_next_from(int *idp)
struct pfk_ifnet *
pfk_if_next(struct pfk_ifnet *ifp)
{
int id = ifp ? ifp->if_index : -1;
int id = ifp ? ifp->if_index : 0;
return pfk_if_next_from(&id);
}
@@ -418,9 +447,15 @@ pfk_if_init(void)
struct net_device *dev;
int err;
pfk_if_wq = alloc_ordered_workqueue("pfk_if", 0);
if (!pfk_if_wq)
return -ENOMEM;
err = register_netdevice_notifier(&pfk_netdev_nb);
if (err)
if (err) {
destroy_workqueue(pfk_if_wq);
pfk_if_wq = NULL;
return err;
}
/* populate existing devices */
rtnl_lock();
for_each_netdev(&init_net, dev) {
@@ -449,8 +484,13 @@ pfk_if_init(void)
void
pfk_if_exit(void)
{
if (pfk_if_wq) {
flush_workqueue(pfk_if_wq);
destroy_workqueue(pfk_if_wq);
pfk_if_wq = NULL;
}
struct pfk_ifnet *ifp;
int id = -1;
int id = 0;
unsigned long flags;
unregister_netdevice_notifier(&pfk_netdev_nb);
@@ -459,7 +499,7 @@ pfk_if_exit(void)
ifp->alive = 0;
idr_remove(&pfk_ifnet_idr, id);
kfree(ifp);
id = -1;
id = 0;
}
spin_unlock_irqrestore(&pfk_ifnet_lock, flags);
}
+44 -10
View File
@@ -100,27 +100,61 @@ taskq_create(const char *name, unsigned int nthreads, int ipl,
/* ---------------- NET_LOCK ---------------- */
static DECLARE_RWSEM(pfk_net_sem);
static atomic_t pfk_net_readers = ATOMIC_INIT(0);
static atomic_t pfk_net_owner = ATOMIC_INIT(0); /* task pid holding write */
/*
* PFLINUX: OpenBSD's NET_LOCK nesting is friendly for the holder (e.g.
* pfioctl takes NET_LOCK and pf_status_read() does too). Linux rw_semaphores
* are not recursive, so track the holder task + depth: nested lock/unlock by
* the same task balance without touching the rwsem. (Tracking was by task
* pointer on purpose: pids can be recycled.)
*/
static DEFINE_RAW_SPINLOCK(pfk_net_owner_lock);
static struct task_struct *pfk_net_owner;
static int pfk_net_depth;
void pfk_net_lock_init(void) { }
void
pfk_net_lock(void)
{
/* Re-entrant for the holder: OpenBSD's NET_LOCK is friendly with
* nested calls in the same path (e.g. our netdev notifier firing
* while pfioctl holds the lock). */
if (atomic_read(&pfk_net_owner) == current->pid)
unsigned long flags;
bool nested;
raw_spin_lock_irqsave(&pfk_net_owner_lock, flags);
nested = (pfk_net_owner == current);
if (nested)
pfk_net_depth++;
raw_spin_unlock_irqrestore(&pfk_net_owner_lock, flags);
if (nested)
return;
down_write(&pfk_net_sem);
atomic_set(&pfk_net_owner, current->pid);
raw_spin_lock_irqsave(&pfk_net_owner_lock, flags);
pfk_net_owner = current;
pfk_net_depth = 1;
raw_spin_unlock_irqrestore(&pfk_net_owner_lock, flags);
}
void
pfk_net_unlock(void)
{
if (atomic_read(&pfk_net_owner) != current->pid)
return; /* nested no-op for non-holders */
atomic_set(&pfk_net_owner, 0);
up_write(&pfk_net_sem);
unsigned long flags;
int depth;
raw_spin_lock_irqsave(&pfk_net_owner_lock, flags);
if (pfk_net_owner != current) {
/* not the holder: unbalanced unlock — ignore loudly */
raw_spin_unlock_irqrestore(&pfk_net_owner_lock, flags);
WARN_ON_ONCE(1);
return;
}
depth = --pfk_net_depth;
raw_spin_unlock_irqrestore(&pfk_net_owner_lock, flags);
if (depth == 0) {
raw_spin_lock_irqsave(&pfk_net_owner_lock, flags);
pfk_net_owner = NULL;
raw_spin_unlock_irqrestore(&pfk_net_owner_lock, flags);
up_write(&pfk_net_sem);
}
}
void pfk_net_read_enter(void) { down_read(&pfk_net_sem);
atomic_inc(&pfk_net_readers); }
+5
View File
@@ -294,6 +294,11 @@ struct pfk_rwlock {
const char *name;
int is_writer;
atomic_t readers;
/* PFLINUX: OpenBSD rw_enter(WRITER) recursion must not self-deadlock
* (pfioctl holds PF_LOCK and re-enters via helpers); rw_semaphores are
* non-recursive, so track writer identity + depth. */
struct task_struct *owner;
int wdepth;
};
#define rwlock pfk_rwlock
#define rwlock_s pfk_rwlock
+149 -84
View File
@@ -1,22 +1,20 @@
/*
* PF-on-Linux netfilter hook registration + M2 data path.
* PF-on-Linux netfilter hook registration + data path (ISC 2026)
*
* One pf_test() pass per BSD ip-stack call site:
* NF_INET_PRE_ROUTING → pf_test(PF_IN) (OpenBSD ip_input)
* NF_INET_LOCAL_OUT → pf_test(PF_OUT) (OpenBSD ip_output, early)
* NF_INET_POST_ROUTING → pf_test(PF_OUT) (OpenBSD ip_output, routed)
* LOCAL_IN / FORWARD are NF_ACCEPT: the packet was already evaluated on
* its PRE (input) and POST (output) passes, exactly like OpenBSD which
* runs PF once per direction inside ip_input/ip_output.
* BSD netisr pattern (PFLINUX divergence, documented):
* hooks NEVER call pf_test() in softirq context — OpenBSD runs input at
* IPL_NET with a non-sleeping NET read side, while our compat locks are
* sleepable rw_semaphores. The hook therefore steals the packet onto a
* per-CPU-free ordered workqueue and pf_test() runs in process context;
* the verdict is applied by re-injecting (netif_rx for input, the routed
* output path for transit/local-out) with an skb->mark tag so the
* survivor is not re-queued by our own hook.
*
* Replacements/deferrals (fragment reassembly, divert, refragmentation)
* re-enter the stack via netif_rx()/pfk_ip_output() — PFLINUX divergence:
* OpenBSD continues in place; re-entry re-runs PRE on the survivor, which
* for reassembled v4 datagrams yields the same state/rule outcome (the
* reassembled packet no longer matches fragment-only rules). Documented
* in docs/porting-notes.md.
*
* (c) 2026 PF-on-Linux contributors — ISC
* One pf_test pass per BSD call site:
* NF_INET_PRE_ROUTING -> PF_IN (OpenBSD ip_input)
* NF_INET_LOCAL_OUT -> PF_OUT (OpenBSD ip_output, locally generated)
* NF_INET_POST_ROUTING -> PF_OUT (OpenBSD ip_output, transit)
* LOCAL_IN / FORWARD are pass-through (already evaluated).
*/
#include <pfk.h>
#include <pfk_api.h>
@@ -24,20 +22,110 @@
#include <net/pfvar_priv.h>
extern int pf_test(sa_family_t, int, struct pfk_ifnet *, struct mbuf **);
extern struct workqueue_struct *pfk_netisr_wq;
/* shared verdict plumbing for v4/v6 (af decides header pull size) */
static unsigned int
pfk_run(int af, struct sk_buff *skb, const struct nf_hook_state *st)
/* PFLINUX: netisr work item — carries ownership of one sk_buff */
struct pfk_pkt {
struct work_struct work;
struct sk_buff *skb;
struct pfk_ifnet *ifp;
sa_family_t af;
int dir;
};
#define PFK_MARK_PASSED 0x50460001u /* skb->mark: survived pf_test already */
static void
pfk_pkt_reinject(struct pfk_pkt *pk)
{
struct pfk_ifnet *ifp;
struct mbuf *m, *m0;
int dir, action;
struct sk_buff *skb = pk->skb;
if (af == AF_INET &&
!pskb_may_pull(skb, sizeof(struct pf_ip)))
return NF_ACCEPT;
if (af == AF_INET6 &&
!pskb_may_pull(skb, sizeof(struct pf_ip6_hdr)))
skb->mark = PFK_MARK_PASSED;
if (pk->dir == PF_IN) {
if (skb->dev)
netif_rx(skb);
else {
kfree_skb(skb);
}
} else {
/* continue the output path: dst is attached at both
* LOCAL_OUT and POST_ROUTING time */
struct dst_entry *d = skb_dst(skb);
if (d && d->dev)
skb->dev = d->dev;
if (d && !IS_ERR(d) && d->output)
d->output(&init_net, skb->sk, skb);
else
kfree_skb(skb);
}
kfree(pk);
}
static void
pfk_pkt_work(struct work_struct *w)
{
struct pfk_pkt *pk = container_of(w, struct pfk_pkt, work);
struct mbuf *m, *m0;
struct sk_buff *orig;
int action;
m = pfk_m_wrap_skb(pk->skb, pk->af);
if (!m) {
pfk_pkt_reinject(pk); /* like "pf not consulted" */
return;
}
m0 = m;
/* PFLINUX: no NET read lock on the data path (M2 interim). The
* rw-semaphore read side starves softirq-fed packet flows whenever a
* control writer parks (purge/ioctl), and BSD's NET_LOCK semantics
* are being replaced by pf_lock/RCU work in M5+. Rule-set mutation
* and packet walk run concurrently from here on — documented. */
action = pf_test(pk->af, pk->dir, pk->ifp, &m);
if (m == m0) {
/* same buffer, possibly rewritten in place: our wrapper
* held one extra skb reference — release it */
m_freem(m);
if (action == PF_PASS) {
pfk_pkt_reinject(pk);
return;
}
kfree_skb(pk->skb); /* our queue reference */
kfree(pk);
return;
}
/* m != m0: pf consumed the wrapper (and its extra reference).
* Our queue reference on the original must be resolved here. */
orig = pk->skb;
if (m == NULL) {
/* buffered (fragment queue) or consumed by divert/forward:
* pf kept its own references; drop our queue reference */
kfree_skb(orig);
kfree(pk);
return;
}
/* replacement (reassembly survivor): free our original ref, then
* inject the survivor through the stack */
pk->skb = m_to_skb(m);
m->skb = NULL;
m_freem(m); /* wrapper bookkeeping only */
kfree_skb(orig);
pfk_pkt_reinject(pk);
}
/* shared: decide whether/how to queue this packet */
static unsigned int
pfk_enqueue_verdict(int af, struct sk_buff *skb,
const struct nf_hook_state *st)
{
struct pfk_pkt *pk;
struct pfk_ifnet *ifp;
int dir;
if (skb->mark == PFK_MARK_PASSED)
return NF_ACCEPT;
switch (st->hook) {
@@ -49,79 +137,46 @@ pfk_run(int af, struct sk_buff *skb, const struct nf_hook_state *st)
dir = PF_OUT;
break;
default:
return NF_ACCEPT; /* LOCAL_IN/FORWARD: seen already */
return NF_ACCEPT; /* LOCAL_IN/FORWARD */
}
/* ifp: OpenBSD passes the *input* interface on PF_IN and the
* output interface on PF_OUT (from the route). */
if (af == AF_INET && !pskb_may_pull(skb, sizeof(struct pf_ip)))
return NF_ACCEPT;
if (af == AF_INET6 && !pskb_may_pull(skb, sizeof(struct pf_ip6_hdr)))
return NF_ACCEPT;
if (dir == PF_IN)
ifp = pfk_ifp_bydev(st->in ? st->in : skb->dev);
else
ifp = pfk_ifp_bydev(skb->dev ? skb->dev : st->out);
if (!ifp)
return NF_ACCEPT; /* unknown iface: inert, like no pf */
m = pfk_m_wrap_skb(skb, af);
if (!m)
return NF_ACCEPT;
m0 = m;
pfk_net_read_enter();
action = pf_test(af, dir, ifp, &m);
pfk_net_read_release();
if (m == m0) {
/* in-place (possible header rewrite already reflected in
* the shared skb); our extra ref is released by m_freem */
m_freem(m);
return (action == PF_PASS) ? NF_ACCEPT : NF_DROP;
}
/* m != m0: either NULL (consumed: buffered fragment, divert,
* refragment-then-free, drop-after-replacement) or a *new* mbuf
* (reassembly survivor). m0 itself was consumed by pf. */
if (m == NULL) {
kfree_skb(skb); /* we own the hook ref now */
return NF_STOLEN;
}
/* survivor: drop the netfilter original, re-inject the survivor */
kfree_skb(skb);
if (dir == PF_IN) {
struct sk_buff *ns = m_to_skb(m);
ns->protocol = htons(af == AF_INET ? ETH_P_IP : ETH_P_IPV6);
if (!ns->dev)
ns->dev = st->in;
if (ns->dev)
dev_hold(ns->dev);
else {
kfree_skb(ns);
m->skb = NULL;
m_freem(m);
return NF_STOLEN;
}
netif_rx(ns);
m->skb = NULL; /* reference moved into netif_rx */
m_freem(m);
} else {
/* survivor leaves via the BSD output helper (consumes m) */
pfk_ip_output(m, NULL, NULL, 0, NULL, NULL, 0);
}
pk = kmalloc(sizeof(*pk), GFP_ATOMIC);
if (!pk)
return NF_ACCEPT; /* overload guard: bypass like full pfq */
INIT_WORK(&pk->work, pfk_pkt_work);
skb_get(skb); /* our queue reference */
pk->skb = skb;
pk->ifp = ifp;
pk->af = af;
pk->dir = dir;
queue_work(pfk_netisr_wq, &pk->work);
return NF_STOLEN;
}
struct workqueue_struct *pfk_netisr_wq;
static unsigned int
pfk_ipv4_hook(void *priv, struct sk_buff *skb,
const struct nf_hook_state *st)
pfk_ipv4_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *st)
{
return pfk_run(AF_INET, skb, st);
return pfk_enqueue_verdict(AF_INET, skb, st);
}
static unsigned int
pfk_ipv6_hook(void *priv, struct sk_buff *skb,
const struct nf_hook_state *st)
pfk_ipv6_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *st)
{
return pfk_run(AF_INET6, skb, st);
return pfk_enqueue_verdict(AF_INET6, skb, st);
}
static struct nf_hook_ops pfk_hooks[] = {
@@ -153,18 +208,23 @@ pfk_hooks_register(void)
{
int i, err;
pr_info("pfk: netfilter hooks registering (%zu, verdict path live)\n",
(size_t)PFK_NHOOKS);
pfk_netisr_wq = alloc_ordered_workqueue("pfk_netisr", WQ_MEM_RECLAIM);
if (!pfk_netisr_wq)
return -ENOMEM;
for (i = 0; i < PFK_NHOOKS; i++) {
pfk_hooks[i].priv = NULL;
err = nf_register_net_hook(&init_net, &pfk_hooks[i]);
if (err)
goto fail;
}
pr_info("pfk: netfilter hooks registered (%zu, netisr wq data path)\n",
(size_t)PFK_NHOOKS);
return 0;
fail:
while (--i >= 0)
nf_unregister_net_hook(&init_net, &pfk_hooks[i]);
destroy_workqueue(pfk_netisr_wq);
pfk_netisr_wq = NULL;
return err;
}
@@ -175,4 +235,9 @@ pfk_hooks_unregister(void)
for (i = 0; i < PFK_NHOOKS; i++)
nf_unregister_net_hook(&init_net, &pfk_hooks[i]);
if (pfk_netisr_wq) {
flush_workqueue(pfk_netisr_wq);
destroy_workqueue(pfk_netisr_wq);
pfk_netisr_wq = NULL;
}
}
+3 -3
View File
@@ -123,13 +123,13 @@ static int __init pfk_init(void)
err = pfk_if_init();
if (err)
goto err_pcb;
/* OpenBSD bring-up order: status/pools/rulesets, then kifs */
pfk_net_lock();
/* OpenBSD bring-up order: status/pools/rulesets, then kifs.
* No NET_LOCK here: pfattach/pfi take what locks they need (PF_LOCK),
* and control ioctls serialize with NET_LOCK themselves. */
pf_status_init();
pfattach(0);
pfk_if_set_event_cb(pfk_pfi_event);
pfk_if_iterate(pfk_pfi_attach_one);
pfk_net_unlock();
err = pfk_hooks_register();
if (err)
goto err_if;
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# VM lab test runner. Executes tests/vm/*.sh against the pftest-* lab via
# nested ssh (deb host → VM). Collects per-test PASS/FAIL + logs into
# out/test-logs/. Exit 0 only if every gate test passes.
set -uo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
LOGD="$REPO/out/test-logs"; mkdir -p "$LOGD"
DEB="${DEB:-root@192.168.1.242}"
source "$REPO/lab/ips.env" 2>/dev/null || true
vm() { # vm <alias> <cmd...>
local alias="$1"; shift
ssh -o ConnectTimeout=10 "$DEB" "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@${!alias:-$(grep -h "^${alias^^}=" "$REPO/lab/ips.env" 2>/dev/null | cut -d= -f2)} \"$*\""
}
PUSH=0 FAIL=0 SKIP=0
for t in "$REPO"/tests/vm/[0-9]*.sh; do
[ -e "$t" ] || continue
name="$(basename "$t")"
echo "=== $name ==="
if bash "$t" >"$LOGD/$name.log" 2>&1; then
echo "PASS $name"; PUSH=$((PUSH+1))
else
rc=$?
if [ $rc -eq 77 ]; then echo "SKIP $name"; SKIP=$((SKIP+1));
else echo "FAIL $name (rc=$rc, log $LOGD/$name.log)"; FAIL=$((FAIL+1)); fi
fi
done
echo "----------------------------------------"
echo "PASS=$PUSH FAIL=$FAIL SKIP=$SKIP"
[ $FAIL -eq 0 ]
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# M2 gate 1: default policy pass + stateful transit + state visibility
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 'rmmod pf 2>/dev/null; insmod /tmp/pf.ko; /opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null; echo READY' | grep -q READY || { bad load; finish; }
pf0 'echo "pass in quick proto icmp keep state
pass out quick proto icmp keep state" | /opt/pf/pfctl -f -' || bad rules
cli "ping -c 3 -W 2 $SRV_IP >/dev/null 2>&1" && ok "transit ping" || bad "transit ping"
N=$(pf0 '/opt/pf/pfctl -s states | grep -c icmp'); [ "${N:-0}" -ge 1 ] && ok "icmp state present ($N)" || bad "no state"
pf0 '/opt/pf/pfctl -d' >/dev/null; finish
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# M2 gate 2: block drop + block return-rst verified at packet level
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
pf0 'echo "block drop in quick proto icmp from any to any
pass out quick" | /opt/pf/pfctl -f -'
cli "ping -c 2 -W 1 $SRV_IP >/dev/null 2>&1" && bad "icmp still passes" || ok "icmp blocked"
# return-rst on tcp: cli nc to closed-filtered port expects RST back
pf0 'echo "block return-rst in quick proto tcp from any to any port 8080
pass out quick" | /opt/pf/pfctl -f -'
srv 'timeout 3 nc -l -p 8080 >/dev/null 2>&1 &' ; sleep 0
cli 'timeout 3 nc -z 10.0.2.20 8080; echo NC=$?' | grep -q 'NC=1' && ok "tcp refused (rst)" || bad "tcp not refused"
pf0 '/opt/pf/pfctl -d' >/dev/null; finish
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# M2 gate 3: pfr tables via pfctl -t
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 '/opt/pf/pfctl -e' >/dev/null
pf0 'printf "block in quick proto icmp from <blk>\npass out quick\n" | /opt/pf/pfctl -f -'
pf0 '/opt/pf/pfctl -t blk -T add 10.0.1.10' && ok "table add"
M=$(pf0 '/opt/pf/pfctl -t blk -T show | grep -c 10.0.1.10'); [ "$M" -ge 1 ] && ok "table shows entry" || bad "table empty"
cli "ping -c1 -W1 $SRV_IP >/dev/null 2>&1" && bad "blocked src passed" || ok "table block works"
pf0 "/opt/pf/pfctl -t blk -T kill 10.0.1.10" >/dev/null
cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1" && ok "unblocked passes" || bad "kill failed"
pf0 '/opt/pf/pfctl -d' >/dev/null; finish
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# M2 gate 4: anchors + quick precedence
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
pf0 'printf "pass in quick\nanchor \"x\"\nblock drop in quick proto icmp keep first\n" > /tmp/a.conf'
pf0 '/opt/pf/pfctl -f /tmp/a.conf' >/dev/null
pf0 'echo "pass in quick proto icmp from 10.0.1.10 to any" | /opt/pf/pfctl -a x -f -' && ok "anchor load"
cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1" && ok "anchor rule evaluated before block" || bad "anchor pass not honored"
pf0 '/opt/pf/pfctl -s Anchors | grep -q x' && ok "anchors listed" || bad "anchor list"
pf0 '/opt/pf/pfctl -d' >/dev/null; finish
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# helpers for tests/vm/*
DEB="${DEB:-root@192.168.1.242}"
PF0_IP="${PF0_IP:-192.168.122.8}"
CLI_IP=10.0.1.10; SRV_IP=10.0.2.20; GW2_IP=10.0.3.2; GW2_WAN=10.0.2.2
pf0() { ssh -o ConnectTimeout=10 "$DEB" "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@$PF0_IP \"$*\"" ; }
cli() { ssh -o ConnectTimeout=10 "$DEB" "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@$PF0_IP \"ssh -o StrictHostKeyChecking=accept-new root@$CLI_IP '$*'\"" ; }
srv() { ssh -o ConnectTimeout=10 "$DEB" "ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@$PF0_IP \"ssh -o StrictHostKeyChecking=accept-new root@$SRV_IP '$*'\"" ; }
ok() { echo "OK $*"; }
bad() { echo "NOT OK: $*" >&2; FAILED=1; }
finish(){ [ "${FAILED:-0}" = 1 ] && exit 1; exit 0; }