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
This commit is contained in:
lgcosta committed 2026-11-11 11:26:43 -03:00
commit ba81284bd3
12 files changed
+724

No files matched your search

+15
View File
@@ -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
+21
View File
@@ -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).
+31
View File
@@ -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/
+58
View File
@@ -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.
+56
View File
@@ -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.
+8
View File
@@ -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
@@ -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 <kernel> 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 <kerneldir>` → 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 <net/pfvar.h>` 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 <id>` 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 <blk> { 10.0.1.99 }` + `block drop quick from <blk>` 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 <dev> { ... }` 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.
@@ -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).
+17
View File
@@ -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")"
+29
View File
@@ -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
+26
View File
@@ -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
+35
View File
@@ -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 ]