commit ba81284bd3b0161500a371c529385b51bdae9506 Author: lgcosta Date: Wed Nov 11 11:26:43 2026 -0300 chore: repo scaffold, READMEs (EN/pt-BR), version pins, source fetch+vendorize scripts - pins OpenBSD 8.0 tarballs (sha256) and Linux 7.3.4 as the port targets - scripts/fetch-sources.sh (C3SL primary, cdn fallback), scripts/vendorize.sh - scripts/{kernel-config,build-kernel}.sh for the VM-lab kernel diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08cc293 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +_vendor/ +out/ +*.o +*.ko +*.mod +*.mod.c +*.o.d +*.symvers +.tmp_versions/ +modules.order +Module.symvers +tools/out/ +lab/seeds/ +*.pcap +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58feab0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +ISC License (new Linux-side code and glue) + +Copyright (c) 2026 PF-on-Linux contributors + +Permission to use, copy, modify, and/or 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. + +--- + +Kernel ported files under module/ originate from The OpenBSD Project +(sys.tar.gz / src.tar.gz, OpenBSD 8.0) and retain their original +copyright and license (ISC-style, see the header of each file). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..959c555 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# PF-on-Linux top-level make +KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4 + +.PHONY: module tools kernel test clean vm vm-destroy docs + +module: + $(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules + +tools: + $(MAKE) -C tools KERNELDIR=$(KERNELDIR) + +kernel: + ./scripts/build-kernel.sh + +vm: + ./scripts/vm-create.sh + +vm-destroy: + ./scripts/vm-destroy.sh + +test: + ./scripts/vm-run-tests.sh + +docs: + @for f in docs/en/*.md; do echo "EN $$f"; done + @for f in docs/pt-BR/*.md; do echo "PT $$f"; done + +clean: + $(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true + $(MAKE) -C tools clean || true + rm -rf out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..25e701b --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# PF on Linux + +Port of the OpenBSD **PF** (packet filter) to **Linux 7.3.4** as out-of-tree +kernel modules — the real PF engine (`pf.c`, `pf_norm.c`, `pf_table.c`, +`pf_lb.c`, … ported from OpenBSD 8.0 sources) driven by the real OpenBSD +`pfctl` over the `/dev/pf` ioctl ABI. + +**Status:** implementation in progress — see +[`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md) +for task progress and +[`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.md) +for the design. + +## What you get (planned) + +| Area | Feature | +|---|---| +| Filtering | rules, anchors, quick, `match`/`block`(drop/return/return-rst)/`pass`, `keep state`, syncookies | +| State/Tables | stateful firewall engine, `pfr` tables (radix + hash, `pfctl -t`) | +| NAT | `nat-to` (port pools, sticky-port, static-port), `rdr-to`, `binat-to` | +| Scrub | fragment reassembly, overlap drop, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` | +| QoS (ALTQ successor) | `queue`/`queue on` with OpenBSD 8.0 `ifq` schedulers: **default (priq), HFSC, FQ-CoDel**, attached via the `pfq` qdisc; `pfctl -s queue -v` stats | +| Divert | `divert-to` / `divert` tag + `pdivert` userspace channel (netlink; Linux has no `AF_DIVERT`) | +| Route | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` | +| Logging | `pflogd` via netlink pflog channel (DLT_PFLOG pcap) | +| Control | `/dev/pf` (0600), OpenBSD 8.0 ioctl ABI, Linux build of `pfctl` (single binary) | + +Not in scope: `pfsync`/CARP failover, IPsec/pfkey, `ftp-proxy`, per-netns PF +instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before +OpenBSD 8.0). + +## Quick start (after M1+) + +```sh +./scripts/fetch-sources.sh # OpenBSD 8.0 sources (C3SL mirror, checksummed) +./scripts/apply-patches.sh ../linux-7.3.4 # core export patch(es) +make module tools # pf.ko, pfq.ko, pfctl, pdivert, pflogd +sudo insmod module/pf.ko module/pfq.ko +sudo pfctl -f /etc/pf.conf && sudo pfctl -e +``` + +VM validation lab (Alpine on libvirt): see `docs/en/vm-lab.md`. +Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e `docs/pt-BR/`. + +## Layout + +- `module/` — kernel sources (ported PF + compat layer + pfq qdisc) +- `tools/` — pfctl/pdivert/pflogd Linux builds +- `patches/kernel/` — documented Linux core patches (export symbols only) +- `scripts/` — fetch/build/patch/VM automation +- `tests/` — VM suite (`tests/vm/NN-*.sh`, scapy helpers) +- `docs/en/` + `docs/pt-BR/` — full bilingual documentation +- `docs/superpowers/` — design spec + implementation plan + +## License + +Ported OpenBSD code retains its original [ISC license and copyright notices](LICENSE). +New Linux-side code: ISC. diff --git a/README.pt-BR.md b/README.pt-BR.md new file mode 100644 index 0000000..9d99aa0 --- /dev/null +++ b/README.pt-BR.md @@ -0,0 +1,56 @@ +# PF no Linux + +Port do **PF** (packet filter) do OpenBSD para o **Linux 7.3.4** como módulos +out-of-tree — o engine PF real (`pf.c`, `pf_norm.c`, `pf_table.c`, `pf_lb.c`, … +portados dos fontes do OpenBSD 8.0) controlado pelo `pfctl` real do OpenBSD via +a ABI ioctl de `/dev/pf`. + +**Status:** implementação em andamento — veja o +[plano de implementação](docs/superpowers/plans/2026-11-11-pf-linux-port.md) e o +[design doc](docs/superpowers/specs/2026-11-11-pf-linux-port-design.md). + +## O que você obtém (planejado) + +| Área | Recurso | +|---|---| +| Filtragem | regras, anchors, quick, `match`/`block`(drop/return/return-rst)/`pass`, `keep state`, syncookies | +| Estado/Tables | firewall stateful, tables `pfr` (radix + hash, `pfctl -t`) | +| NAT | `nat-to` (pools de portas, sticky-port, static-port), `rdr-to`, `binat-to` | +| Scrub | remonta­gmento de fragmentos, drop por overlap, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` | +| QoS (sucessor do ALTQ) | `queue`/`queue on` com os agendadores `ifq` do OpenBSD 8.0: **default (priq), HFSC, FQ-CoDel**, anexados via qdisc `pfq`; estatísticas `pfctl -s queue -v` | +| Divert | `divert-to` / tag `divert` + canal userspace `pdivert` (netlink; Linux não tem `AF_DIVERT`) | +| Roteamento | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` | +| Logging | `pflogd` via canal netlink pflog (pcap DLT_PFLOG) | +| Controle | `/dev/pf` (0600), ABI ioctl do OpenBSD 8.0, `pfctl` compilado para Linux (binário único) | + +Fora de escopo: failover `pfsync`/CARP, IPsec/pfkey, `ftp-proxy`, instâncias PF +por netns, agendadores exclusivos do ALTQ clássico (cbq/wfq/red — removidos do +OpenBSD antes da 8.0). + +## Início rápido (após M1+) + +```sh +./scripts/fetch-sources.sh # fontes OpenBSD 8.0 (mirror C3SL, com checksum) +./scripts/apply-patches.sh ../linux-7.3.4 # patch(es) de core (exports) +make module tools # pf.ko, pfq.ko, pfctl, pdivert, pflogd +sudo insmod module/pf.ko module/pfq.ko +sudo pfctl -f /etc/pf.conf && sudo pfctl -e +``` + +Lab de validação em VMs (Alpine via libvirt): `docs/pt-BR/vm-lab.md`. +Documentation in English: [`README.md`](README.md) and `docs/en/`. + +## Estrutura + +- `module/` — fontes de kernel (PF portado + camada de compat + qdisc pfq) +- `tools/` — builds Linux de pfctl/pdivert/pflogd +- `patches/kernel/` — patches documentados no core do Linux (apenas exports) +- `scripts/` — automação de fetch/build/patch/VM +- `tests/` — suíte em VM (`tests/vm/NN-*.sh`, helpers scapy) +- `docs/en/` + `docs/pt-BR/` — documentação completa bilíngue +- `docs/superpowers/` — design + plano de implementação + +## Licença + +O código portado do OpenBSD mantém as licenças e avisos originais +([ISC](LICENSE)). Código novo do lado Linux: ISC. diff --git a/VERSIONS b/VERSIONS new file mode 100644 index 0000000..b16893a --- /dev/null +++ b/VERSIONS @@ -0,0 +1,8 @@ +Linux kernel: 7.3.4 (vermagic recorded at build; see VERSIONS) +OpenBSD base: 8.0 +Source tarballs (sha256): + sys.tar.gz 964b1aed7839b86f4e010a0fdd03dcd409e6f569e69fe85f95e5fceb740c2aaf + src.tar.gz 9984c74ff875b5ff041e4676a7f3e4e1ead2cc47a284d705eb9f37cbec4fe795 +Primary mirror: https://openbsd.c3sl.ufpr.br/pub/OpenBSD/8.0/ +Fallback: https://cdn.openbsd.org/pub/OpenBSD/8.0/ +Date pinned: 2026-11-11 diff --git a/docs/superpowers/plans/2026-11-11-pf-linux-port.md b/docs/superpowers/plans/2026-11-11-pf-linux-port.md new file mode 100644 index 0000000..19608b8 --- /dev/null +++ b/docs/superpowers/plans/2026-11-11-pf-linux-port.md @@ -0,0 +1,160 @@ +# PF-on-Linux Port Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship a faithful OpenBSD 8.0 PF port for Linux 7.3.4 as out-of-tree modules (`pf.ko`, `pfq.ko`) + one documented core export patch + Linux `pfctl`, validated by a PASS/FAIL suite across a 5-VM Alpine libvirt lab. + +**Architecture:** Ported 8.0 `sys/net/pf*.c`, `ifq/hfsc/fq_codel` files sit under an sk_buff-backed mbuf compatibility layer; netfilter hooks run PF at PREROUTING/FORWARD/OUTPUT/POSTROUTING; `/dev/pf` serves the OpenBSD ioctl ABI; queueing bridges to a classful `pfq` qdisc; divert/pflog use kernel netlink channels; userland `pfctl/pdivert/pflogd` built from 8.0 sources against a shared `pfvar.h`. See spec. + +**Tech Stack:** OpenBSD 8.0 sources (C3SL mirror), Linux 7.3.4 build tree, gcc-16/bison/flex, libvirt/qemu on 192.168.1.242 ("deb"), Alpine 3.22 cloud qcow2 + cloud-init NoCloud, bash/scapy/iperf3 tests in-guest. + +**Spec:** `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` (read it first; this plan argues from it). + +## Global Constraints + +- Kernel tree: `/home/lgcosta/lab/linux_kernel/linux-7.3.4` (Linux 7.3.4, vermagic `7.3.4`). +- PF sources base: **OpenBSD 8.0** release (`sys.tar.gz`, `src.tar.gz` from `https://openbsd.c3sl.ufpr.br/pub/OpenBSD/8.0/`, fallback `cdn.openbsd.org`), sha256-pinned in `scripts/fetch-sources.sh` (sys: `$(sha256)` — pin at T2; src: pin at T2). +- Module builds out-of-tree (`make -C M=$PWD/module`); kernel core changes ONLY via `patches/kernel/000N-*.patch`, each documented in `docs/{en,pt-BR}/patches.md`. +- Every ported kernel file keeps the OpenBSD header block verbatim + our ` * Copyright (c) 2026 PF-on-Linux contributors (compat changes under ISC)` line. +- License of new code: ISC. Docs bilingual: EN primary + full pt-BR counterpart (`docs/en/`↔`docs/pt-BR/`, `README.md`↔`README.pt-BR.md`). +- `/dev/pf` mode 0600 root-only; all ioctl in-blobs length-validated as upstream (never relax upstream checks). +- Single global PF instance bound to `init_net` (faithful; netns = out of scope). PF hooks MUST run before conntrack (`NF_IP_PRI_CONNTRACK-1`-equivalent ordering, documented). +- Test gates are v4; v6: state/block/nat66/scrub smoke. No numeric perf gate (`bench-basic.sh` informational). +- Commit per task; branch `main`, push to `https://git.labexposed.com/lgcosta/pf-linux-port` each task. +- VM lab lives on **deb** (ssh root@192.168.1.242); never touch the laptop's `sms-vm`; laptop = build host only. Artifacts sync laptop→deb via rsync. +- pf0 VM boots the self-built kernel by direct kernel boot with virtio/ext4 **built-in** (no initrd). + +## Review Focus + +- Non-IPv4/IPv6 EtherTypes passing hooks unchanged (ARP etc. — expect `NF_ACCEPT`, tested). +- Fragment/extension-header packets into the mbuf shim (linearize path; crafted scapy inputs must not leak skbs or panic; tested via scrub suite). +- Multi-segment GSO/GRO skbs entering PF (must be handled: linearize fallback; tested by iperf through a passing ruleset). +- Malformed/oversized `pfioc_*` blobs from local root (length fields must be bounded-checked exactly as upstream; fuzz test T9). +- Module unload (`rmmod pf pfq`) under live traffic and with states present (must drain, no oops; tested by soak T23). +- NAT port-pool reuse after state expiry (no stale bindings; tested in T15 via rapid reconnect churn). + +--- + +### Task 1: Repo scaffold + remote +**Files:** Create `pf_port/.gitignore` (`_vendor/`, `out/`, `*.ko`), `README.md`, `README.pt-BR.md` (project one-pager each), `LICENSE` (ISC + OpenBSD copyright notice block), `VERSIONS` (kernel 7.3.4; OpenBSD 8.0; date), `Makefile` (targets: module tools kernel-vm docs test). +**Interfaces:** none produced beyond layout in spec §6. +- [ ] Step 1: `git init` in `pf_port/`, identity lgcosta, remote origin = Gitea repo. +- [ ] Step 2: write the files above (READMEs mirror: what/why/status/features-matrix-table-empty/build-once-commands). +- [ ] Step 3: first commit, push. Verify `git ls-remote origin` shows `main`. + +### Task 2: Source acquisition + vendorization +**Files:** Create `scripts/fetch-sources.sh`, `scripts/vendorize.sh`; `scripts/sha256-pin.txt`. +**Interfaces:** Produces `_vendor/openbsd/8.0-sys.tar.gz`, `8.0-src.tar.gz` (verified), and staged pristine copies at `_vendor/upstream/sys/net/*`, `_vendor/upstream/sbin/pfctl/*` used by all port tasks; prints per-file `$OpenBSD: id` lines to `_vendor/upstream/IDS.txt`. +- [ ] Step 1: fetch script with mirror→cdn fallback; verify against pin file populated from the two tarballs already downloaded + SHA256 from `.../8.0/SHA256`. +- [ ] Step 2: vendorize extracts exactly: sys: `pf.c pf_ioctl.c pf_norm.c pf_if.c pf_lb.c pf_ruleset.c pf_table.c pf_osfp.c pf_syncookies.c pfvar.h pfvar_priv.h if_pflog.c if_pflog.h ifq.c ifq.h hfsc.c hfsc.h fq_codel.c fq_codel.h ip_divert.c ip_divert.h tree.h queue.h` (sys/sys/) `radix.c radix.h` (sys/net/); src: `sbin/pfctl/*` (all .c/.h/.y/.l + Makefile), `include/`. +- [ ] Step 3: run both; `IDS.txt` non-empty; commit scripts only. + +### Task 3: Kernel build pipeline (laptop) +**Files:** Create `scripts/build-kernel.sh`, `scripts/kernel-config.sh`. +**Interfaces:** Produces `out/linux-7.3.4-bzImage`, `out/System.map`, `out/.config`, module tree build-ready at `linux-7.3.4` (Module.symvers). +- [ ] Step 1: config = defconfig + `VIRTIO_BLK=y VIRTIO_PCI=y VIRTIO_NET=y EXT4_FS=y` + `NET_SCHED` `NET_EMATCH` off-safe, `KPROBES` off, `DEBUG_INFO_REDUCED` optional; script idempotent, diff-checked (`scripts/kernel-config.sh diff`). +- [ ] Step 2: full build `make -j$(nproc-2) bzImage modules`; verify `file bzImage`, `modpost` artifacts exist; record kernel `vermagic` string into `VERSIONS`. +- [ ] Step 3: commit (scripts + VERSIONS update only; out/ gitignored). + +### Task 4: Core export patch + patch tooling +**Files:** Create `patches/kernel/0001-net-export-symbols-needed-by-pf.patch`, `patches/kernel/series`, `scripts/apply-patches.sh`, `scripts/unpatch.sh`; docs stubs `docs/en/patches.md`, `docs/pt-BR/patches.md`. +**Interfaces:** `apply-patches.sh ` → applies all series patches (git-am style `git apply`), prints per-patch rationale from `docs/*/patches.md` anchors; build tasks 3/6 consume it. +- [ ] Step 1: after M1 first build reveals the exact unexported list (start: `neigh_output`, `ip_route_output_key`, `ip6_route_output`, `netlink_kernel_create`, plus any `ip_neigh_for_gw`, `fib6_lookup_*` discovered by build/link errors), write 0001 adding `EXPORT_SYMBOL_GPL(...)` lines adjacent to each existing export cluster. +- [ ] Step 2: apply to `linux-7.3.4` (keep `git` clean via `unpatch.sh` = reverse apply; the tree is NOT a git repo → patches applied/reverted textually, never left dirty on final artifact builds without record), rebuild, verify `Module.symvers` contains them. +- [ ] Step 3: docs table entry for 0001 (what/why/impact/revert); commit. + +### Task 5: VM lab provisioning on deb +**Files:** Create `scripts/vm-create.sh`, `scripts/vm-destroy.sh`, `scripts/lab-lib.sh`, `lab/{cloud-init/*.yaml.tmpl,domains/*.xml.tmpl}`. +**Interfaces:** `vm-create.sh` idempotent → domains `pftest-pf0` (3 NICs `pf-lan/pf-wan/pf-wan2` + mgmt default-net NIC), `pftest-cli`, `pftest-srv` (mgmt+data NICs), `pftest-gw2` (pf-wan2+pf-wan); static IPs per `lab/ips.env` (sourced by ALL test scripts): PF0_LAN=10.0.1.1 PF0_WAN=10.0.2.1 PF0_WAN2=10.0.3.1 CLI=10.0.1.10 SRV=10.0.2.20 GW2=10.0.3.2 GW2_WAN=10.0.2.2; ssh aliases `pf0/cli/srv/gw2` via `~/.ssh/config.d/pflab` (generated), root key auth from deb. Non-pf VMs boot Alpine stock kernel; pf0 boots direct kernel from `out/` later (T6). +- [ ] Step 1: seed ISO/dir per VM (NoCloud meta-data+user-data incl. `packages: [iperf3, tcpdump, python3, py3-scapy, bash]` — verify pkgs install needs mgmt NIC up: enable `virsh net-start default` on deb). +- [ ] Step 2: create networks XMLs (forward=none for pf-*); domains XMLs (q35, virtio disks, host passthrough? plain64 OK, 2 vCPU/1G each). +- [ ] Step 3: run; loop `ssh pf0 true` until OK (≤3min); `vm-destroy.sh` tears down cleanly both directions; commit + rsync verified. + +### Task 6: Direct-boot pipeline for pf0 (custom kernel) +**Files:** Modify `scripts/vm-create.sh` (pf0 domain uses `--boot kernel=... `); Create `scripts/vm-deploy.sh` (rsync out/bzImage + module .kos + tools to deb `/var/lib/pflab/pf0/`; copies into pf0 over ssh to `/opt/pf/`). +**Interfaces:** `vm-deploy.sh pf0` leaves: `uname -r` == 7.3.4 in pf0. +- [ ] Step 1: define pf0 with direct kernel boot (`/var/lib/pflab/pf0/bzImage`, cmdline `root=/dev/vda1 ro console=ttyS0`, no initrd); serial console log capture file for tests. +- [ ] Step 2: deploy, boot, verify `uname -r`+`lsmod` clean; commit. + +### Task 7: Compat layer core (`module/compat/`) +**Files:** Create `module/Makefile` (obj-m: `pf.o` `pfq.o`), `module/include/` (our `pfvar.h` staged: upstream + Linux compat block: OpenBSD-shaped `_IO/_IOR/_IOW/_IOWR`, `u_intXX_t` typedefs, `sockaddr`/`in_addr` shims), `module/compat/pfk.h` (master header), `compat/pfk_mbuf.{c,h}` (`struct mbuf{next,packet,len,cap_data,flags,type,sk_buff *skb}` — the skb wrapper + `m_copym/m_adj/m_pull/m_copydata/m_cat/m_clump/m_free/m_freem/m_dup/m_copypacket/m_devget/m_get/m_getptr` + `pfk_to_skb()/pfk_from_skb()`), `compat/pfk_kern.c` (malloc/free M_*, pools, `pfk_time` (jiffies→sec), timeout/evtimer→timer_list+work, rw/mutex wrappers, per-cpu counters, arc4random→get_random_u32), `compat/tree.h` (BSD RB via upstream with `container_of` swap), `compat/radix.{c,h}` (OpenBSD radix ported), `compat/pfk_mod.c` first skeleton: misc `/dev/pf` 0600, `DIOCGETSTATUS` only, netfilter hooks NOT yet registered. +**Interfaces (produced, used by every port task):** `struct pf_buf` ops above; `#include ` resolves to our staged header; `pfk_ifname2dev()`, `pfk_addrofv6/v4`, per-file pattern `#ifdef PFLINUX` minimal. +- [ ] Step 1: headers + skeleton `.c` files compile clean: `make -C linux-7.3.4 M=pf_port/module` produces `pf.ko` (modpost warnings = 0). +- [ ] Step 2: deploy to pf0 VM, `insmod pf.ko` → `ls /dev/pf`, dmesg clean; commit. + +### Task 8: pfctl Linux build +**Files:** Create `tools/Makefile`, `tools/compat-user/{err.c,strlcpy.c,strtonum.c,imsg-buffer.c?,pledge.c,sys-queue.h}`, stage 8.0 `sbin/pfctl/*.c|parse.y` into `tools/pfctl/` (+`pfctl_linux.c` for path/ioctl glue: `/dev/pf`, `pfctl.conf` optional), `tools/pdivert/pdivert.c` (reads pkts from NETLINK_USERSOCK proto channel: bind port=N ↔ divert id; reply write reinjects via kernel; modeled on OpenBSD `divert(8)`), `tools/pflogd/pflogd.c` (consumes NETLINK_PFLOG multicast → pcap file, rotation flags). +**Interfaces:** `tools/out/pfctl` (static-ish linked, glibc fine): supports `-n -f -s rules|states|tables|queue|info|src -t -T -sS -i -d` — same flags (same code). `pdivert -p ` used by test 11; `pflogd -f /var/log/pflog` used by test 08. +- [ ] Step 1: build on laptop against staged headers: `make -C tools` → flex/bison parse.y compiles without errors (only compat edits inside `#if defined(__linux__)` or small sed-style upstream changes recorded in `tools/UPSTREAM.md` diff-notes). +- [ ] Step 2: `pfctl -n -f tests/confs/min.conf` succeeds OFFLINE (parse-only). +- [ ] Step 3: deploy binary to pf0; `pfctl -s info` vs our T7 skeleton status → prints Status: Enabled; commit. + +### Task 9: ioctl ABI parity harness +**Files:** Create `tools/abitest/{abitest.c,gen-sizecheck.sh}`, `tests/confs/fuzz-ioctls.py`. +**Interfaces:** `abitest` prints PASS/FAIL for every DIOC* struct: sizeof/offsetof computed by kernel (`DIOCGSTRUCTSIZE`-style custom ioctl we add under our own vendor-safe id? NO — via `DIOCGETSTATUS` roundtrip + userspace `sizeof` table generated from SAME `pfvar.h` → assert both sides) — concrete: kernel exposes `pfk_abi_hash` module param (CRC32 of packed struct sizes table); abitest recomputes from header → mismatch = FAIL. fuzz script throws oversize/garbage `pfioc_*` blobs. +- [ ] Step 1: implement, run on pf0: ABI CRC match; fuzz: no oops/leak (dmesg+kmem checks). +- [ ] Step 2: negative case: corrupt table → abitest FAILs (self-test of the harness); commit. + +### Task 10: Ruleset engine port (pf_ruleset + pf_table) +**Files:** `module/pf/pf_ruleset.c`, `module/pf/pf_table.c` staged+ported; `module/pf/hooks.c` (first real netfilter registration: IPv4+IPv6 PRE/FWD/OUT/POST, verdict plumbing skeleton, pfk_mbuf ingress), `module/pf/pfk_if.c` (net_device/in_ifaddr walks replacing OpenBSD ifnet/ifa lists; interface anchor list). +**Interfaces:** `pfctl -f min.conf` with a `table { 10.0.1.99 }` + `block drop quick from ` loads; `pfctl -s tables`, `pfctl -t blk -T add/test/del/kill` roundtrip; **no forwarding decisions yet** (match results not applied until T12). +- [ ] Step 1: port+compile the two files (their OpenBSD deps satisfied by T7). +- [ ] Step 2: VM: load ruleset, table ops incl. bulk add from file, `-T show`; `rmmod` after: kmemleak clean; commit. + +### Task 11: pf_norm / scrub +**Files:** `module/pf/pf_norm.c` (+`pf_osfp.c` staged), `tests/vm/08-scrub.py` (scapy crafted: overlapping frags → drop, fragment TTL, `max-mss 1380`, `df-bit` set, `no-df` fragmentation, random-id reassembly correctness). +**Interfaces:** `pf_scrub()`/`pf_normalise()` wired into hooks ingress; `set reassemble yes|no` honored; OS-fp stored in states (`pass ... set os ...` matching via `pfctl -s states -v` shows osname if configured). +- [ ] Step 1: port; compile; wire. +- [ ] Step 2: run 08-scrub on lab (cli scapy → pf0 via srv sink): all asserts green incl. no skb leak; commit. + +### Task 12: pf.c core (matching + states) + verdicts +**Files:** `module/pf/pf.c` staged+ported (state hash: `pfk_hashkey()` = Linux `jhash2()` over the `pf_state_key` bytes; state table = OpenBSD 8.0 open-hashing layout kept upstream-shaped via pfk wrappers — no rhashtable), `module/pf/pfk_state_timer.c` (purger→delayed_work), syncookies (`pf_syncookies.c`). +**Interfaces:** default policy pass + rules; `keep state` (icmp/tcp/udp), state insert direction (INBOUND/OUTBOUND semantics as 8.0), `block drop|return|return-rst|drop-rst` actual verdicts + return packet generation via `icmp_send`/`tcp_v4_send_reset`-like in-pf path `pfk_send_icmp_error()`; `pfctl -s states -v` full fields; `match-to state`/`sync` no-op fields fine (documented NG). +- [ ] Step 1: port pf.c in passes: (a) structs/globals, (b) `pf_test`/`pf_match_key_all`/`pf_state_insert`, (c) verdict application at hooks, (d) returns, (e) syncookies. +- [ ] Step 2: VM tests `01-state.sh` (icmp: reply accepted only with state; `block drop in` before state creation kills ping; tcp 3-way via nc; udp pseudo state; `keep state` interfaces), `02-block.sh` (return-rst payload/TCP seq asserts via tcpdump on cli), `03-anchors.sh` (include anchor quick precedence), `04-table-rules.sh` (block from table live-updated), **non-IPv4 EtherTypes (ARP frame) pass hooks untouched (tcpdump asserts ARP still served on pf0)**; all green; commit. + +### Task 13: NAT (nat-to / binat-to) + pf_lb sticky/bitmap +**Files:** `module/pf/pf_lb.c` ported; NAT branches in pf.c wired at POSTROUTING (outbound) + reverse xlate on inbound; port-pool allocator using pfk (random port from range + bitmap state-attached, 8.0 semantics incl. `portrange high` bitmap persisting on rule? sticky-port yes, static-port yes). +**Interfaces:** transit `nat-to` rewrites seen on wan with correct checksums + reply xlate + state tracking; `binat-to` static 1:1; `rdr-to` (inbound DNAT incl. `route-to` combo basic); `pfctl -s states -v` shows translated keys; port exhaustion behavior = `block-rst`+ICMP as upstream? (match 8.0: no nat rule → normal block path; test asserts drop). +- [ ] Step 1: port+wire; tests `05-nat.sh` (cli→srv with SNAT: srv sees PF0_WAN; reply flows; no state → reply dropped), `06-rdr.sh` (srv httpd:80 via PF0_LAN:8080; sticky-port: 2nd client gets distinct PF0 src port? sticky: same), `07-binat.sh`; churn test rapid reconnect ×50 → no stale port reuse before timeout (Review Focus #6); commit. + +### Task 14: route-to / dup-to / fail-to / reply-to +**Files:** `module/pf/pf_route.c` (new: Linux implementation of `pf_route()/pf_route6()` bodies: gw resolve `ip_route_output_key`+`neigh_lookup(&arp_tbl/nd_tbl)`+`neigh_output`, TTL-- , loop guard via `skb->mark` magic + 8.0 `pf_rt_tblid` equivalent — DECISION: loop guard = per-cpu re-entry counter like OpenBSD's `pfroute` depth check; dup via `skb_clone`+original-path re-entry), pf_lb pool selection wired (rr/round-robin-source/hash-state as upstream). +**Interfaces:** `pass out route-to (GW2 10.0.3.2) ...` → traffic lands on wan2 and gw2 delivers; pools with 2 gw rotate (rr) / hash-source stable per client; `reply-to` uses state's answer route; `dup-to` copy seen on other leg; `fail-to` when primary gw down (gw2 arp-stopped test); IPv6 route-to smoke. +- [ ] Step 1: implement + port `pf_route/pf_route6` glue; compile. +- [ ] Step 2: tests `09-routeto.sh` (tcpdump counts wan vs wan2: rr alternation exact, hash-source deterministic per src; loop-guard: pf0->pf0 route-to with gw=pf0 must not livelock: bounded pkts then block `block loop drop`? use upstream `pf_route` loop detection → drop; assert), `10-dup-fail.sh`; commit. + +### Task 15: divert +**Files:** `module/pf/pf_divert_nl.c` (kernel: `netlink_kernel_create(NETLINK_USERSOCK, proto=31? use NETLINK_USERSOCK group id per divert port)`, table port→socket, `divert-to` path: copy skb → nl msg `{cookie, divport, af, ip hdr+payload}`; reinjection path on user `send`: parse → route per original semantics `ip_divert.c` reference (divert-to: re-route to original dst via gw if needed; plain divert tag: re-enter `netif_receive_skb`/local_xmit per 8.0 `pf_send` behavior; drop-after-timeout via per-packet timer default 5s? match upstream: no accept → packet never delivered (copy consumed? original held until accept/timeout then free — mirror OpenBSD: keep, timeout drop). +**Interfaces:** `pdivert -p 10000` + rule `pass in divert-to 127.0.0.1 10000`? upstream model: `divert-to` sends to divert socket at **host:port**; ours: netlink port id; document mapping in `docs/en/divert.md`; stats `DIOCBUSYSTATS` unchanged. +- [ ] Step 1: implement + tool link already from T8. +- [ ] Step 2: test `11-divert.sh`: pdivert on pf0 flips a byte of ping payload → cli receives modified echo-replies; kill pdivert → traffic stops (timeout) not leaks; commit. + +### Task 16: ifq + pfq queueing +**Files:** `module/ifq/ifq.c ifq.h hfsc.c fq_codel.c` ported (skb-backed via pfk_mbuf), `module/pfq/pfq_qdisc.c` (classful qdisc `pfq`: root=ifq attach; ops: enqueue→`ifq_enqueue(ifq_for_class)`, dequeue loop; classid = queue index from pf rule queue tag), `module/pf/pf_ifq_bridge.c` (DIOCADDQUEUE/GETQUEUES/GETQUEUE/QSTATS + `pf_test` queue assignment + `set queue` default-class + ifq per-iface registry via `pfq_q_enter`), scripts `pf-qdisc.sh` auto-attach on `queue on` detection at DIOCADDQUEUE. +**Interfaces:** `pfctl -s queue -v` counters (pkts/bytes/drops per queue + per scheduler), `queue on { ... }` semantics: default priq (link share/delay), `hfsc` (realtime/upperlimit/linkshare), `fq_codel` (flows; ECN mark? upstream `ifq_fq_codel` flags: ecn,quantum). Verdict: packets matching queued rules get `skb->priority`-tag (pfq) and are served under `pfq` root qdisc. +- [ ] Step 1: port files; qdisc registers (visible `tc qdisc show` as `pfq` when attached). +- [ ] Step 2: test `12-queues.sh`: hfsc 10M upperlimit → iperf cli→srv ≈10M ±15% while unqueued passthrough ≈ line-rate; counters grow + `-s queue -v` stable; detaching leaves net clean (`tc del qdisc` + rmmod); fq_codel smoke: 1 flow flows, drops counted; commit. + +### Task 17: pflog +**Files:** `module/pf/pflog_nl.c` (kernel netlink socket on `NETLINK_USERSOCK` +protocol slot `PFLOG_PROTO=1` (divert uses slot `PFDIVERT_PROTO=2` — both slots +documented in `docs/en/divert-wireformat.md`): pass/block/drop/log-rule copies per +`log`/`log all`/`block log` rules incl. `if_pflog.c` wrapper header bytes exactly as +upstream DLT_PFLOG), `tools/pflogd` from T8. +**Interfaces:** `pflogd` writes `/var/log/pflog` pcap with linktype DLT_PFLOG(117) → readable by `tcpdump -r`; test asserts NAT'd/scrub'd headers logged per rules; `pfctl -s log` config. +- [ ] Step 1: implement + wire `pf_klog` call sites in pf.c port. +- [ ] Step 2: test `13-pflog.sh` (tcpdump parse: expected pkts/ifs/priority bits present, block-with-log entries); commit. + +### Task 18: Soak, leak, reload-under-traffic + bench info +**Files:** `tests/vm/14-soak.sh` (30-min: continuous iperf cli→srv + ruleset `pfctl -f` reload loop every 2s + `rmmod`/`insmod` cycle at end), `scripts/bench-basic.sh` (pps via scapy TRAFFICGEN one-pkt/sec rules, iptables-empty vs pf-empty — informational CSV only). +**Interfaces:** PASS = zero oops in dmesg, kmemleak (config DEBUG_KMEMLEAK on for test kernel build variant via `kernel-config.sh test`), conntrack-coexist run (`iptables -A ... ACCEPT; iptables -F` before each run: no rule bleed; Review Focus #5/#3: GSO path exercised by iperf). +- [ ] Step 1: build test-config kernel with kmemleak; run soak; commit artifacts. + +### Task 19: Docs EN/pt-BR + release +**Files:** `docs/en/{index,install,apply-patches,build,vm-lab,pf-guide,feature-matrix,divert-wireformat,patches}.md` + full `docs/pt-BR/` counterparts; READMEs final (build-once recipe `make all`, features + matrix with **all NG/FAIL rows stated**, OpenBSD→Linux behavioral differences: no `divert(4)` sockets, queues via pfq attach, `/dev/pf` root-only, coexist notes); `Makefile` targets wired; GitHub-release notes style; git tag `v0.1.0`. +**Interfaces:** `scripts/quickstart-check.sh`: fresh clone on deb → fetch/build/deploy/run tests → exit 0 (the release gate). +- [ ] Step 1: write docs from actual artifacts (no placeholders); run quickstart-check end-to-end from clean tree on a scratch dir; tag + push + `create_release` with suite logs; commit. + +--- +**Execution notes:** Tasks 7→12 are the serialized critical path (same `pf.c` file; one owner). 8/9 parallelize with 7 after interfaces freeze; 13→17 follow 12; 5/6 (lab) fully parallel from the start; 19 per-milestone increments + final pass. Every ported upstream file: changes grouped with `/* PFLINUX: reason */` markers for future rebases. diff --git a/docs/superpowers/specs/2026-11-11-pf-linux-port-design.md b/docs/superpowers/specs/2026-11-11-pf-linux-port-design.md new file mode 100644 index 0000000..467d40f --- /dev/null +++ b/docs/superpowers/specs/2026-11-11-pf-linux-port-design.md @@ -0,0 +1,268 @@ +# PF on Linux — Port Design / Design do Port + +Date/2026-11-11 · Status: awaiting approval/aguardando aprovação · Repo: https://git.labexposed.com/lgcosta/pf-linux-port (public) + +## 1. Intent / Intenção + +Run OpenBSD's PF packet filter inside Linux 7.3.4 — the real PF engine, driven by the +real `pfctl` speaking the real `/dev/pf` ioctl ABI — as an out-of-tree module with a +documented, minimal set of kernel core patches, validated end-to-end in a libvirt +Alpine VM. Docs bilingual (EN + pt-BR). + +Rodar o PF do OpenBSD no Linux 7.3.4 — engine PF real, controlado pelo `pfctl` real +falando a ABI ioctl `/dev/pf` — como módulo out-of-tree com um conjunto mínimo e +documentado de patches no core, validado numa VM Alpine via libvirt. + +## 2. Facts found / Fatos levantados + +- OpenBSD **replaced classic ALTQ by ≥7.7** (verified: no `sys/net/altq` dir in + 7.7/7.8/8.0) — **but PF queueing lives on in 8.0**, rearchitected into the core + `ifq` framework: `sys/net/ifq.c` (priq ops), `sys/net/hfsc.c`, + `sys/net/fq_codel.c`, ioctls `DIOCADDQUEUE/DIOCGETQUEUES/DIOCGETQUEUE/ + DIOCGETQSTATS` (`pfvar.h:1707+`), `pf_rule.queue` glue all present; `pfctl` + still parses/installs `queue on … hfsc|fq_codel|default`. Older ALTQ releases + (≤7.6) are no longer mirrored anywhere (cdn/ftp/mirrors/GitHub-raw/Wayback all + 404), making a 7.x base infeasible/undesirable anyway. + ⇒ **port base = OpenBSD 8.0** (exactly matches the user's provided userland + tree, pfctl.c v1.406 2026-08-30; sources fetched from the user's C3SL mirror). + The ALTQ requirement is met as **OpenBSD-8.0 queueing (priq + HFSC + fq_codel)** + — the framework modern OpenBSD itself ships; classic cbq/wfq/red are + non-goals *because they don't exist in the base*, honestly matrixed. +- Kernel side ≈ 24.6k LOC (`pf.c` 9.2k, `pf_ioctl.c` 4.6k, `pf_norm/if/lb/table/ + ruleset/osfp/syncookies`, `pfvar(_priv).h`) + queueing core (`ifq/hfsc/fq_codel` + ≈ 2.4k LOC) + `ip_divert.c` reference (~300 LOC). +- Userland: single `pfctl` (~20k LOC incl. `parse.y`) — **libpfctl was folded back + into pfctl** in this era (Makefile `LDADD+= -lm` only; no libpfctl in tree). +- Linux 7.3.4 tree clean; toolchain gcc 16/flex/bison/bc; libvirt 12 + KVM usable + (existing `sms-vm` untouched); sudo works; Alpine cloud images available on dl-cdn. +- Module-visible APIs confirmed exported: `neigh_lookup`, `pskb_expand_head`, + `skb_clone`, `register_qdisc`, `qdisc_tree_reduce_backlog`, `tcf_classify`, + `__dev_queue_xmit`, `netif_rx`, `inet_addr_type`, `fib_table_lookup` (GPL). +- Confirmed **not exported** in 7.3.4: `neigh_output`, `dev_queue_xmit` (inline + wrapper over `__dev_queue_xmit`), `ip_route_output_key`, `ip6_route_output`, + `netlink_kernel_create` ⇒ core patch **P1 (mandatory): export symbols required + by the PF module** (additive `EXPORT_SYMBOL_*`, zero behavior change) — allowed + by user, fully documented in `patches/kernel/` + `docs/*/patches.md`. + +## 3. Goals / Non-goals + +**Goals (G)** +1. PF engine in-tree-equivalent quality, delivered as module `pf.ko` built from + `pf_port/` sources: rules (anchors, quick, match/block/pass/drop, return variants, + keep state), states + syncookies, tables (pfr: radix/hash, `pfctl -t`), + NAT (nat-to, rdr-to incl. sticky-port/bitmap basics, binat-to, route-to), + **scrub/normalize** (fragment reassembly, DF/TTL actions, min-ttl, max-mss), + **PF queues/ALTQ semantics of modern OpenBSD 8.0** (`queue`, `queue on` with + `default`(priq), `hfsc`, `fq_codel` schedulers from `sys/net/ifq.c`, + `hfsc.c`, `fq_codel.c`; `pfctl -s queue -v` stats), **divert** (`divert-to`, + `divert` tag; userspace channel modeled on `sys/netinet/ip_divert.c`), + **route-to** (pools + rr/rr-source/rr-any, hash-source/state; + `reply-to`, `dup-to`, `fail-to`). +2. Control plane = OpenBSD ioctl ABI on `/dev/pf`; Linux-built `pfctl` (8.0, + single binary, no libpfctl) + `pflogd`-lite + `pdivert` tool. +3. Patchset + scripts in the repo; kernel core patches only when unavoidable, each + documented (what/why/where/impact). +4. VM validation: kernel 7.3.4 built with patches, module + tools deployed into an + Alpine VM booted with that kernel; a scripted suite exercises every goal-1 area + with real traffic across a 4-VM libvirt lab (§7) and checks PASS/FAIL. +5. Full docs EN + pt-BR: apply patches, build, install, feature guide, VM validation + how-to, feature matrix incl. exclusions. + +**Non-goals (NG, explicit to user)**: pfsync/carp (no failover), full IPsec/pfkey, +classic-ALTQ-only schedulers (cbq/wfq/red — **do not exist in the OpenBSD 8.0 base**, +matrixed as "removed upstream"), queues on interfaces whose root qdisc is not ours, +per-netns PF instances (faithful port = single global instance like OpenBSD; netns +isolation documented as out of scope), OS-fingerprinting beyond `pfctl -o` (osfp is +bonus if cheap), `ftp-proxy` and other userland daemons. + +**Anti-goals**: silently faking a feature (stub verdicts), nftables-as-substitute +claims, tests that only check exit codes without inspecting packets/state. + +## 4. Approach (chosen) / Abordagem + +**B′ — faithful port with a compat shim** (alternative C rejected: user wants PF in +kernel; A rejected: infeasible). Port the actual OpenBSD 8.0 `pf*.c` + `ifq/hfsc/ +fq_codel` + `ip_divert`-referenced divert under a compatibility layer, hook into +Linux via netfilter, keep upstream ABI for userland. +(Rejected variant: rewrite of PF semantics on nf APIs = not a port.) + +### 4.1 Compat layer (`module/compat/`) +- **Packet object**: `struct mbuf` shim over `sk_buff` (single-segment: linearize on + entry to PF; `m_copym/m_adj/m_pull/m_copydata/m_cat/m_clump/m_free` → skb ops; + `m_pkthdr` ↔ `skb->protocol/tc/index`, address family, ifindex via + `skb->dev`). This mirrors how OpenBSD's own pf code treats mbufs and keeps diffs + localized to packet I/O, not to matching logic. +- **APIs**: `kern_*` glue: M_* malloc→`kmalloc/kfree` (pf's malloc flags → GFP), + `timeout()`→timer/workqueue (state purger), `rwlock_*`→RW-lock, + `pool`→kmem_cache, epoch→RCU read side + refcounts on rulesets (matching how + conntrack already models this), `time_second`→`ktime_get_seconds`, + `syncache` untouched, `radix.h`/`tree.h` (BSD) vendored with Linux tweaks, + `imsg` not needed in kernel. +- **Netlink/ioctl**: `misc_register` `/dev/pf`, all DIOC* handled exactly as + `pf_ioctl.c` (port file with shim includes). **ioctl number encoding**: define + `_IOR/_IOW/_IOWR(PFRDR,'n',type)` OpenBSD-style in our shared `pfvar.h` so kernel + and Linux-`pfctl` agree bit-for-bit. +- **IPv4/IPv6**: `in_proto`/`in6_proto` glue for checksums (`ip_sum_eq`, + pseudo-header sum via `csum_tcpudp_magic`, `tcp_input.c`'s in_cksum ported to + `pf_compat_cksum.c`); raw socket `sosend` paths replaced by + `icmp_send`/`icmp6_send` (RST via `tcp_v4_send_reset` equivalents kept in-pf to + preserve PF's own return logic). +- **Interface/addresses**: `ifnet`→`net_device` + `in_ifaddr`→`net_ifa` walks via + `net_if_FOREACH`/`in_ifa_lookup` helpers over `if_addr_list` + `fib` (replaces + `ifa_ifwithroute` for route-to); `pf_getifaddr` ported with Linux address lists. + +### 4.2 Hooking +Register netfilter hooks per family: +- `PREROUTING (NF_IP_PRI_FIRST+)`: pf input path = scrub/normalize → state lookup → + nat/rdr (pf's own NAT, **independent of conntrack**; deployment docs warn about + overlap zones), filter INPUT. +- `FORWARD`: filter + route-to/dup-to execution. +- `LOCAL_OUT`: filter (OUTPUT) + nat-to (locally generated). +- `POSTROUTING (pri low)`: state insert, queue classify (skb→pfq class id), + route-to final xmit. +Verdict mapping: pass→`NF_ACCEPT`, block/normalization-drop→`NF_DROP` (with PF's +return-RST/ICMP generated by pf itself), `divert`→`NF_STOLEN` + divert channel, +`nat/rdr` rewrite → `NF_ACCEPT` after `skb_cow` + checksum fix. Coexistence with +iptables/nft: PF hooks run before conntrack (unregister-safe ordering); documented. + +### 4.3 Queues (OpenBSD 8.0 `ifq`) ⇒ `pfq` qdisc shim +Port `sys/net/ifq.c/ifq.h` + `sys/net/hfsc.c` + `sys/net/fq_codel.c` (the real 8.0 +queueing framework: priq glue, HFSC, FQ-CoDel as `ifq_ops`) into `module/ifq/`. +`struct ifqueue` instances are backed by a classful Linux qdisc **`pfq`** +(`register_qdisc`): `pfq_enqueue` → `ifq_enqueue` (matched class via pf tags), +`pfq_dequeue` → `ifq_deq_begin/commit` → `__dev_queue_xmit`. `tc qdisc replace dev X +root pfq` attaches; `scripts/pf-qdisc.sh` auto-attaches when a loaded ruleset +declares `queue on` (pfq handles both schedulers + default priq via `pf_qblocks` +priority bits). `DIOCADDQUEUE/DIOCGETQUEUES/DIOCGETQUEUE/DIOCGETQSTATS` (the real +8.0 names) served against the pfq/ifq registry so `pfctl -s queue -v` and +`pfctl -s ifq -v` work. Tested mandatory schedulers: **default(priq), hfsc, +fq_codel** with rate gates (§7); class visibility via `tc -s qdisc show` is a +stretch goal, counters via `pfctl` are the gate. + +### 4.4 divert +- In-pf action `divert-to host:port` / tag `divert port`: packet copied to + userspace divert channel; **channel = kernel netlink** (`NETLINK_USERSOCK` proto) + because Linux has no `AF_DIVERT` (in-tree reference logic ported from + `sys/netinet/ip_divert.c`; userland `pdivert` tool implements `divert(8)`-style + rewrite-loop over the netlink channel; wire format documented; `DIOCBUSYSTATS` + etc. unchanged). Diverted flow re-enters via the channel write → + `__dev_queue_xmit`/`netif_receive_skb` following ip_divert `divert_output` + semantics (re-route via gw neighbor for `divert-to`). +- Test parity anchored by upstream `regress/sys/net/pf_divert` semantics. + +### 4.5 route-to / dup-to / fail-to +`pf_route6`/`pf_route` port: pool selection via `pf_lb.c` (as-is), next-hop +resolution `ifa_ifwithroute`→`pf_route4gw()`: build `flowi4/flowi6`, +`ip_route_output_key` to **gw**, then L2-redirect via `neigh_lookup(arp_tbl/nd_tbl, +&gw, dev)` + `neigh_output` (or `dev_queue_xmit` after `dev_hard_header`); TTL-- +before xmit, state key = translated, `reply-to` via reverse state's stored answer +route. `dup-to` = `skb_clone` to original path. If a needed route/neigh helper +isn't exported → kernel core export patch, documented. + +### 4.6 pflog (small, included) +`if_pflog.c` → netlink multicast channel (`NETLINK_PF` group) + `pflogd` userland +that writes pcap file (OpenBSD pflogd logic), so `tcpdump -r` verifies passes; +enables state/queue tests to assert headers (e.g., NAT rewrites, scrub). + +### 4.7 Userland build (`tools/`) +Linux build of OpenBSD 8.0 `sbin/pfctl` (single binary — libpfctl is folded in), +`parse.y` via flex/bison, plus `pdivert` + `pflogd`, with a `compat-user/` shim +(err/warn, strlcpy, pledge stub, arc4random, sysctl subset, OpenBSD-shaped +`_IOR/_IOW/_IOWR`). Shared ABI header `include/net/pfvar.h` (ours, identical to +the kernel's) guarantees ioctl bit-parity; static asserts on every ioctl struct. + +## 5. Kernel core patches +Target **zero behavior changes** to Linux; the patchset ships: +- **P1 (mandatory)** `net: export symbols needed by the PF module` + (`neigh_output`, `ip_route_output_key`, `ip6_route_output`, + `netlink_kernel_create`, + any added during M1–M5 builds) — additive + `EXPORT_SYMBOL*()` lines, no logic touched. Confirmed missing in 7.3.4. +- No other core changes by default: module builds out-of-tree (`make -C + linux-7.3.4 M=pf_port/module`), tree otherwise untouched; if a future step + needs more (e.g., a netlink protocol reservation), each lands as its own + `patches/kernel/000N-*.patch` appended by `scripts/apply-patches.sh`, with a + per-patch entry in `docs/{en,pt-BR}/patches.md`: what/why/impact/how to revert. + +## 6. Repo layout +``` +pf_port/ (= repo root, git → pf-linux-port) + README.md / README.pt-BR.md + docs/{en,pt-BR}/ guide, install, apply-patches, feature-matrix, validation, + divert-wireformat, patches/*.md + docs/superpowers/specs/ this file + patches/kernel/ 000N-*.patch + series + README + module/ pf.ko + pfq.ko sources: pf/, ifq/, compat/, include/ + tools/ pfctl/, pdivert/, pflogd/, compat-user/ + scripts/ fetch-sources.sh apply-patches.sh unpatch.sh + build-module.sh build-tools.sh vm-create.sh + vm-deploy.sh vm-run-tests.sh bench-basic.sh + tests/vm/ 01-state.sh … 14-altq.sh, scapy/python helpers, expect-files + _vendor/ OpenBSD 7.8/8.0 tarballs (gitignored; refetch script) +``` + +## 7. Validation (libvirt on the 192.168.1.242 host, Alpine) — "done" definition +`scripts/vm-create.sh` clones the Alpine 3.22 cloud qcow2 (verified: 840 MiB, +sha256 e83786b1…), NoCloud seed (user/network config), and defines **4 libvirt +domains on an isolated 192.168.122.x-free bridge lab (`pf-lan`, `pf-wan`, `pf-wan2` +networks, no NAT to the outside needed for gates)**: +`pftest-pf0` (**boots the self-built 7.3.4 kernel + initrd via direct kernel boot**; +3 NICs: lan/wan/wan2), `pftest-cli`, `pftest-srv`, `pftest-gw2`. +Decision (builder-check #1): PF stays a **single global instance bound to +`init_net`**, exactly like OpenBSD — no per-netns rulesets; hence endpoints must be +real hosts (VMs), not namespaces. The `deb` host (12 cores, libvirt 11.3 validated) +runs the lab; the laptop keeps building sources. +Suite (`vm-run-tests.sh`, ssh-driven, PASS/FAIL per feature): stateful icmp/tcp/udp +(+IPv6 smoke), block drop/return, tables (`pfctl -t -T add/del/test`), +anchors+quick, nat-to (asserted on srv: src=pf0's addr), rdr-to (srv httpd reached +via pf0 DNAT), binat-to, scrub (crafted frags via scapy on cli: overlap drop, +DF-set, max-mss clamp), **route-to pools rr + hash-source (asserted: wan vs wan2 +tcpdump counts; fail-to when gw2 down; reply-to symmetry; dup-to capture)**, +**divert (`pdivert` rewrites payload end-to-end; timeout drop without server)**, +**queues: `queue on … { hfsc … }` and `fq_codel` and default priq — iperf +throughput within ±15% of rate on the bottleneck link + `pfctl -s queue -v` +counter growth + pfq attaches/detaches cleanly**, pflog capture sanity (passes +appear in pcap), ruleset reload under traffic (soak loop), rmmod/load leak check +(kmemleak + module refcounts). IPv6 policy (builder-check #4): engine is v6-complete +by construction (single pf.c); **gates are v4 for everything + v6 smoke for +state/block/nat66/scrub**; v6-only landmines (route-to6 pool failover) matrixed +"ported, smoke-validated". Performance (builder-check #2): **functional gates are +the bar; `bench-basic.sh` is informational** (reports pps vs iptables baseline, no +numeric gate) — a perf contract would force a non-faithful rewrite-first shim. +Artifacts: run log + per-test pcap attached to the Gitea release. **All gates must +pass**; anything failing ships as explicit FAIL in the feature matrix, not silently. + +## 8. Milestones & delivery +M0 infra: repo+CI-ish scripts, kernel baseline build OK, VM boots custom kernel. +M1 engine-boot: compat layer + module loads, `/dev/pf`, pfctl `-f` empty + `-s info`. +M2 filter core: rules/states/tables/anchors pass VM tests 1–4. +M3 NAT suite (nat/rdr/binat) + scrub + pflog. +M4 route-to/dup-to/fail-to + divert. +M5 queues: ifq/hfsc/fq_codel port + pfq qdisc + `queue` semantics + counters. +M6 docs bilingual complete, patchset tidy, full suite re-run green, tags v0.x in repo. +Commits per milestone to Gitea (public), final release with validation artifacts. + +## 9. Builder-decisions (my calls, reversible) +- Base **OpenBSD 8.0** (matches the provided userland tree exactly; ALTQ delivered + as 8.0's real queue framework ifq{priq,hfsc,fq_codel}; older ALTQ releases are + unrecoverable from public mirrors anyway). +- **Single global PF instance on `init_net`** (faithful to OpenBSD); validation via + multi-VM lab, not netns (builder-check #1). +- sk_buff-backed mbuf shim instead of rewriting pf.c to skb (keeps upstream logic + line-proximate; enables future rebase to newer OpenBSD). +- divert via netlink channel instead of inventing AF_DIVERT (no core socket-family + patch). +- pflog via netlink not pseudo-if tap (keeps "no extra interface" promise to users). +- Module name `pf.ko` (no Linux collision). +- **No numeric perf gate** (bench informational) and **v4-gated + v6-smoked** suite + (builder-checks #2/#4). + +## 10. Risks +- `pf.c` porting surface largest; queue scheduler coverage limited to what exists + upstream in 8.0 (mitigation: 3 schedulers gated, matrix explicit). +- ioctl-ABI bit parity between two toolchains (mitigation: single shared header, + struct-size static asserts both sides). +- Kernel 7.3 symbol drift (mitigation: P1 export patches allowed+documented). +- Netfilter ordering vs conntrack for transit flows (mitigation: docs + test with + conntrack module loaded/unloaded). +- Multi-segment/GSO packets through mbuf shim (mitigation: linearize-on-entry for + v1, document perf cost; bench script quantifies). diff --git a/scripts/build-kernel.sh b/scripts/build-kernel.sh new file mode 100755 index 0000000..eba6ba2 --- /dev/null +++ b/scripts/build-kernel.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Build Linux 7.3.4 (VM-lab config) + collect boot artifacts into out/. +set -euo pipefail +REPO="$(cd "$(dirname "$0")/.." && pwd)" +K="${KERNELDIR:-$(cd "$REPO/../linux-7.3.4" && pwd)}" +cd "$K" +"$REPO/scripts/kernel-config.sh" "${1:-}" +JOBS="${JOBS:-$(($(nproc)-2))}" +make -j"$JOBS" -s bzImage modules +mkdir -p "$REPO/out" +KVER="$(make -s kernelversion)" +cp -f arch/x86/boot/bzImage "$REPO/out/linux-${KVER}-bzImage" +cp -f System.map "$REPO/out/System.map" +cp -f .config "$REPO/out/.config" +make -s kernelrelease > "$REPO/out/kernelversion" +sha256sum "$REPO/out/linux-${KVER}-bzImage" > "$REPO/out/bzImage.sha256" +echo "KERNEL_BUILD_OK $(cat "$REPO/out/kernelversion")" diff --git a/scripts/fetch-sources.sh b/scripts/fetch-sources.sh new file mode 100755 index 0000000..70c4b91 --- /dev/null +++ b/scripts/fetch-sources.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Fetch OpenBSD 8.0 source tarballs (primary mirror: user-provided C3SL; +# fallback: cdn.openbsd.org) and verify against VERSIONS pins. +set -euo pipefail +REPO="$(cd "$(dirname "$0")/.." && pwd)" +V="$REPO/_vendor/openbsd"; mkdir -p "$V" +MIRROR_PRIMARY="https://openbsd.c3sl.ufpr.br/pub/OpenBSD/8.0" +MIRROR_FALLBACK="https://cdn.openbsd.org/pub/OpenBSD/8.0" +declare -A WANT +while read -r f sha; do WANT[$f]=$sha; done < <(grep -E '^ (sys|src)\.tar\.gz' "$REPO/VERSIONS" | awk '{print $1, $2}') + +verify() { # file expected-sha + local f="$V/$1" + [ -s "$f" ] && [ "$(sha256sum "$f" | cut -d' ' -f1)" = "$2" ] +} + +for pair in "sys.tar.gz ${WANT[sys.tar.gz]}" "src.tar.gz ${WANT[src.tar.gz]}"; do + set -- $pair; f=$1; sha=$2 + if verify "$f" "$sha"; then echo "OK $f (cached, sha matches)"; continue; fi + ok="" + for base in "$MIRROR_PRIMARY" "$MIRROR_FALLBACK"; do + echo "FETCH $f from $base" + if curl -fL --retry 3 -sS -o "$V/$f.part" "$base/$f" && [ "$(sha256sum "$V/$f.part" | cut -d' ' -f1)" = "$sha" ]; then + mv "$V/$f.part" "$V/$f"; ok=1; break + fi + done + [ -n "$ok" ] || { echo "FAIL $f sha256 mismatch vs $sha (see VERSIONS)"; exit 1; } + echo "OK $f" +done diff --git a/scripts/kernel-config.sh b/scripts/kernel-config.sh new file mode 100755 index 0000000..7f62884 --- /dev/null +++ b/scripts/kernel-config.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Configure the Linux 7.3.4 tree for PF-on-Linux VM lab use: +# defconfig baseline + direct-kernel-boot deps. Idempotent. +# `kernel-config.sh test` additionally enables DEBUG_KMEMLEAK (soak builds). +set -euo pipefail +K="${KERNELDIR:-$(cd "$(dirname "$0")/../../linux_kernel/linux-7.3.4" && pwd)}" +cd "$K" +[ -f .config ] || make defconfig +scripts/config --file .config \ + --set-val CONFIG_VIRTIO_BLK y \ + --set-val CONFIG_VIRTIO_PCI y \ + --set-val CONFIG_VIRTIO_NET y \ + --set-val CONFIG_EXT4_FS y \ + --set-val CONFIG_NETFILTER y \ + --enable CONFIG_NETFILTER_ADVANCED \ + --set-val CONFIG_NET_SCHED y \ + --set-val CONFIG_INET y \ + --set-val CONFIG_IPV6 y \ + --set-val CONFIG_SERIAL_8250 y \ + --enable CONFIG_SERIAL_8250_CONSOLE +if [ "${1:-}" = "test" ]; then + scripts/config --file .config --set-val CONFIG_DEBUG_INFO y --enable CONFIG_DEBUG_KMEMLEAK +fi +make olddefconfig +echo "--- effective ---" +grep -E '^(CONFIG_VIRTIO_(BLK|PCI|NET)|CONFIG_EXT4_FS|CONFIG_NETFILTER|CONFIG_NET_SCHED|CONFIG_INET|CONFIG_IPV6|CONFIG_DEBUG_KMEMLEAK)(=| is not)' .config || true diff --git a/scripts/vendorize.sh b/scripts/vendorize.sh new file mode 100755 index 0000000..7d74ba9 --- /dev/null +++ b/scripts/vendorize.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Stage pristine OpenBSD 8.0 sources used by port tasks into _vendor/upstream. +# Run after fetch-sources.sh. Output tree is gitignored; IDS.txt records each +# file's $OpenBSD: tag for rebase forensics. +set -euo pipefail +REPO="$(cd "$(dirname "$0")/.." && pwd)" +V="$REPO/_vendor/openbsd"; U="$REPO/_vendor/upstream" +SYS_K=(net/pf.c net/pf_ioctl.c net/pf_norm.c net/pf_if.c net/pf_lb.c net/pf_ruleset.c + net/pf_table.c net/pf_osfp.c net/pf_syncookies.c net/pfvar.h net/pfvar_priv.h + net/if_pflog.c net/if_pflog.h net/ifq.c net/ifq.h net/hfsc.c net/hfsc.h + net/fq_codel.c net/fq_codel.h netinet/ip_divert.c netinet/ip_divert.h + net/radix.c net/radix.h net/radix_node.h net/rtable.c net/rtable.h + sys/tree.h sys/queue.h sys/param.h sys/hash.h) +SRC_K=(sbin/pfctl/parse.y sbin/pfctl/pfctl.c sbin/pfctl/pfctl.h sbin/pfctl/pfctl_parser.c + sbin/pfctl/pfctl_parser.h sbin/pfctl/pfctl_optimize.c sbin/pfctl/pfctl_print.c + sbin/pfctl/pfctl_osfp.c sbin/pfctl/pfctl_queue.c sbin/pfctl/pfctl_radix.c + sbin/pfctl/pfctl_table.c sbin/pfctl/pfctl_snmp.c sbin/pfctl/pf_print_state.c + sbin/pfctl/pfctl.8 include/net/pfvar.h include/net/if_pflog.h) + +rm -rf "$U"; mkdir -p "$U/sys/net" "$U/sys/sys" "$U/sys/netinet" "$U/sbin/pfctl" "$U/include/net" +for f in "${SYS_K[@]}"; do + if ! tar xzf "$V/sys.tar.gz" -C "$U" "./sys/$f" "sys/$f" 2>/dev/null; then + echo "WARN: sys/$f not found in 8.0-sys" >&2 + fi +done +for f in "${SRC_K[@]}"; do + tar xzf "$V/src.tar.gz" -C "$U" "./$f" "$f" 2>/dev/null || echo "WARN: $f not found in 8.0-src" >&2 +done +find "$U" -type f \( -name '*.c' -o -name '*.h' -o -name '*.y' -o -name '*.8' \) | sort | while read -r p; do + tag=$(grep -m1 -oE '\$OpenBSD: [^$]*\$' "$p" | head -1 | sed 's/\$OpenBSD: //;s/ \$//') + echo "${p#$U/} :: ${tag:-NO-ID}" +done > "$U/IDS.txt" +n=$(find "$U" -type f \( -name '*.c' -o -name '*.h' -o -name '*.y' \) | wc -l) +echo "staged $n files -> $U (IDS.txt)" +[ "$n" -ge 30 ]