diff --git a/docs/en/install.md b/docs/en/install.md new file mode 100644 index 0000000..8bdb16a --- /dev/null +++ b/docs/en/install.md @@ -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). diff --git a/module/compat/pfk_if.c b/module/compat/pfk_if.c index d40a811..2c0094a 100644 --- a/module/compat/pfk_if.c +++ b/module/compat/pfk_if.c @@ -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); } diff --git a/module/compat/pfk_kern.c b/module/compat/pfk_kern.c index 37a0699..bc608a8 100644 --- a/module/compat/pfk_kern.c +++ b/module/compat/pfk_kern.c @@ -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); } diff --git a/module/include/pfk.h b/module/include/pfk.h index 8252eac..7afeffb 100644 --- a/module/include/pfk.h +++ b/module/include/pfk.h @@ -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 diff --git a/module/pfsrc/pfk_hooks.c b/module/pfsrc/pfk_hooks.c index 45f6add..6f4ca1f 100644 --- a/module/pfsrc/pfk_hooks.c +++ b/module/pfsrc/pfk_hooks.c @@ -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 #include @@ -24,20 +22,110 @@ #include 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; + } } diff --git a/module/pfsrc/pfk_mod.c b/module/pfsrc/pfk_mod.c index b70b756..7ed1827 100644 --- a/module/pfsrc/pfk_mod.c +++ b/module/pfsrc/pfk_mod.c @@ -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; diff --git a/scripts/vm-run-tests.sh b/scripts/vm-run-tests.sh new file mode 100755 index 0000000..99794d7 --- /dev/null +++ b/scripts/vm-run-tests.sh @@ -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 + 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 ] diff --git a/tests/vm/01-state.sh b/tests/vm/01-state.sh new file mode 100755 index 0000000..d25d8c2 --- /dev/null +++ b/tests/vm/01-state.sh @@ -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 diff --git a/tests/vm/02-block.sh b/tests/vm/02-block.sh new file mode 100755 index 0000000..58b1df2 --- /dev/null +++ b/tests/vm/02-block.sh @@ -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 diff --git a/tests/vm/03-tables.sh b/tests/vm/03-tables.sh new file mode 100755 index 0000000..8a8edb8 --- /dev/null +++ b/tests/vm/03-tables.sh @@ -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 \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 diff --git a/tests/vm/04-anchors.sh b/tests/vm/04-anchors.sh new file mode 100755 index 0000000..0e6b87f --- /dev/null +++ b/tests/vm/04-anchors.sh @@ -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 diff --git a/tests/vm/lib.sh b/tests/vm/lib.sh new file mode 100755 index 0000000..72ef022 --- /dev/null +++ b/tests/vm/lib.sh @@ -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; }