diff --git a/README.md b/README.md index 25e701b..88da7b5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,29 @@ # 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. +Port of the OpenBSD **PF** (packet filter) to **Linux 7.3.4** as an +out-of-tree kernel module — 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. (A second module for the +`pfq` qdisc arrives with M5.) -**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. +**Status:** early milestones. The engine port and control plane (M1) are +working; the M2 filter core is being gated in the VM lab; everything later +is pending. Honest per-feature status: +[`docs/en/feature-matrix.md`](docs/en/feature-matrix.md). +Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.md) +· plan: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md). + +## Milestones + +| Milestone | Scope | Status (2026-11-11) | +|---|---|---| +| M0 | repo, scripts, kernel build, VM boots the custom kernel | done | +| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f` / `-s info` | done | +| M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **in progress** — data path implemented, gates not all green yet | +| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending | +| M4 | route-to/dup-to/fail-to, divert e2e | pending | +| M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | pending | +| M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs) | ## What you get (planned) @@ -20,7 +34,7 @@ for the design. | 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`) | +| Divert | `divert-to` / `divert` tag + `pdivert` relay over an in-module `PF_DIVERT` socket family (pending — see `docs/en/feature-matrix.md`) | | 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) | @@ -29,24 +43,35 @@ 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+) +## Quick start + +Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version +(commands are read from the scripts; a fresh-clone end-to-end run is still +pending): ```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 +./scripts/fetch-sources.sh # OpenBSD 8.0 sources (C3SL mirror, sha256-pinned) +./scripts/vendorize.sh # stage the pristine files we port +./scripts/build-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (export patch: none needed) +./scripts/build-module.sh # -> module/pf.ko +make -C tools # -> tools/out/pfctl (static), ABI harness +sudo insmod module/pf.ko # /dev/pf (0600) + netfilter hooks 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/`. +Read [`docs/en/pf-guide.md`](docs/en/pf-guide.md) **before** enabling PF on a +path you need: only the M2 filter core is gated today; NAT/scrub/queues/ +divert/pflog are not usable yet ([`docs/en/feature-matrix.md`](docs/en/feature-matrix.md)). + +VM validation lab (Alpine on libvirt): [`docs/en/vm-lab.md`](docs/en/vm-lab.md). +Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e +[`docs/pt-BR/`](docs/pt-BR/README.md). ## 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) +- `module/` — kernel sources (ported PF + compat layer; the `pfq` qdisc lands with M5) +- `tools/` — pfctl (static Linux build), pdivert relay source, ABI harness (`pflogd` not ported yet) +- `patches/kernel/` — Linux core patch set (currently empty; evidence in `p1-needs.txt`, see `docs/en/patches.md`) - `scripts/` — fetch/build/patch/VM automation - `tests/` — VM suite (`tests/vm/NN-*.sh`, scapy helpers) - `docs/en/` + `docs/pt-BR/` — full bilingual documentation diff --git a/README.pt-BR.md b/README.pt-BR.md index 9d99aa0..d187dfa 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -1,13 +1,29 @@ # 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`. +Port do **PF** (packet filter) do OpenBSD para o **Linux 7.3.4** como um +módulo de kernel 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`. (Um segundo +módulo para o qdisc `pfq` chega com o M5.) -**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). +**Status:** milestones iniciais. O engine portado e o plano de controle (M1) +funcionam; o núcleo de filtragem do M2 está em validação na bancada de VMs; +todo o resto está pendente. Status honesto por feature: +[`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md). +Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.md) +· plano: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md). + +## Milestones + +| Milestone | Escopo | Status (2026-11-11) | +|---|---|---| +| M0 | repo, scripts, build do kernel, VM sobe com kernel próprio | concluído | +| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f` / `-s info` | concluído | +| M2 | núcleo de filtragem: regras/estados/tabelas/anchors, gates VM 01–04 | **em andamento** — caminho de dados implementado, gates ainda não todos verdes | +| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente | +| M4 | route-to/dup-to/fail-to, divert e2e | pendente | +| M5 | filas: back ends HFSC/FQ-CoDel + qdisc `pfq` | pendente | +| M6 | docs completas, patchset arrumado, suíte inteira verde, release com tag | em andamento (docs) | ## O que você obtém (planejado) @@ -18,7 +34,7 @@ a ABI ioctl de `/dev/pf`. | 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`) | +| Divert | `divert-to` / tag `divert` + relay `pdivert` sobre uma família de sockets `PF_DIVERT` dentro do módulo (pendente — ver `docs/pt-BR/feature-matrix.md`) | | 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) | @@ -27,24 +43,36 @@ 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+) +## Início rápido + +Receita completa: [`docs/pt-BR/install.md`](docs/pt-BR/install.md). Versão +curta (comandos lidos dos scripts; a execução de ponta a ponta a partir de +clone limpo ainda está pendente): ```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 +./scripts/fetch-sources.sh # fontes OpenBSD 8.0 (mirror C3SL, sha256 fixado) +./scripts/vendorize.sh # staging dos arquivos upstream portados +./scripts/build-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (patch de export: nenhum necessário) +./scripts/build-module.sh # -> module/pf.ko +make -C tools # -> tools/out/pfctl (estático), verificador de ABI +sudo insmod module/pf.ko # /dev/pf (0600) + hooks netfilter 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/`. +Leia [`docs/pt-BR/pf-guide.md`](docs/pt-BR/pf-guide.md) **antes** de habilitar +o PF num caminho que você precisa: só o núcleo de filtragem do M2 tem gate +hoje; NAT/scrub/filas/divert/pflog ainda não são utilizáveis +([`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md)). + +Lab de validação em VMs (Alpine via libvirt): [`docs/pt-BR/vm-lab.md`](docs/pt-BR/vm-lab.md). +English documentation: [`README.md`](README.md) e +[`docs/en/`](docs/en/README.md). ## 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) +- `module/` — fontes de kernel (PF portado + camada de compat; o qdisc `pfq` chega com o M5) +- `tools/` — pfctl (build estático Linux), fonte do relay pdivert, verificador de ABI (`pflogd` ainda não portado) +- `patches/kernel/` — conjunto de patches de core do Linux (hoje vazio; evidência em `p1-needs.txt`, ver `docs/pt-BR/patches.md`) - `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 diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..e8f2a96 --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,36 @@ +# PF-on-Linux documentation (English) + +Documentação em português: [`docs/pt-BR/README.md`](../pt-BR/README.md). + +This is the user documentation set for the port of the **OpenBSD 8.0 PF +packet filter** to **Linux 7.3.4** — the real PF engine (`pf.c`, `pf_norm.c`, +`pf_table.c`, `pf_lb.c`, …) running as the out-of-tree module `pf.ko`, driven +by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. + +## Index + +| Doc | What it covers | +|---|---| +| [guide.md](guide.md) | One-page architecture: components, data path, control plane, milestones | +| [install.md](install.md) | Apply/build/install recipe (kernel, module, tools) | +| [build.md](build.md) | Build pipeline in detail: fetch → vendorize → kernel → module → pfctl | +| [vm-lab.md](vm-lab.md) | The libvirt validation lab: topology, recreate/run, what each gate proves | +| [feature-matrix.md](feature-matrix.md) | Per-feature status: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx | +| [pf-guide.md](pf-guide.md) | Daily use: what works today in `/etc/pf.conf`, what to avoid until M3/M4 | +| [patches.md](patches.md) | Linux core patch set (currently empty) + rules and template for future `000N-*.patch` | +| [caveats.md](caveats.md) | Known divergences from OpenBSD behavior and current limitations | + +## Sources of truth + +The docs describe the code as it exists in this tree. When in doubt, the +authoritative references are: + +- `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` — design +- `docs/superpowers/plans/2026-11-11-pf-linux-port.md` — milestone plan (tasks T1–T19) +- `docs/porting-notes.md` — staging log + M1 deviation record (the truth about compat-layer divergences) + +Feature-status vocabulary used across these docs: + +- **VERIFIED-in-VM** — exercised end-to-end in the libvirt lab (see vm-lab.md). +- **COMPILES-untested** — the code is built into `pf.ko` but no gate has proven the behavior yet. +- **PENDING-Mx** — lands (or lands fully) in milestone Mx; current observable behavior is documented. diff --git a/docs/en/build.md b/docs/en/build.md new file mode 100644 index 0000000..10f1091 --- /dev/null +++ b/docs/en/build.md @@ -0,0 +1,116 @@ +# Build pipeline in detail + +Português: [docs/pt-BR/build.md](../pt-BR/build.md). +Quick recipe: [install.md](install.md). This page explains *what each stage +does* and *what has actually been exercised*. + +Everything below runs on the build host (the laptop); the VM lab (separate +host "deb") only receives artifacts — see [vm-lab.md](vm-lab.md). + +## 0. Prerequisites + +From `docs/en/install.md`: gcc ≥ 15, GNU make, bison (flex is not needed — +OpenBSD `parse.y` carries a hand-written `yylex`), `bc`, rsync, `glibc-static` +for the static pfctl, root for `insmod`. Kernel tree expected at +`../linux-7.3.4` (override with `KERNELDIR=`; the root `Makefile` defaults to +`$(HOME)/lab/linux_kernel/linux-7.3.4`). + +## 1. Fetch + verify OpenBSD sources — `scripts/fetch-sources.sh` + +- Downloads `sys.tar.gz` and `src.tar.gz` from the primary mirror + `https://openbsd.c3sl.ufpr.br/pub/OpenBSD/8.0/`, falling back to + `cdn.openbsd.org`. +- Verifies sha256 against the pins in `VERSIONS` (sys: `964b1aed…`, src: + `9984c74f…`); an already-cached tarball is skipped when its hash matches. +- Landing zone: `_vendor/openbsd/` (gitignored). + +## 2. Vendorize — `scripts/vendorize.sh` + +Extracts **only the files the port consumes** into `_vendor/upstream/` +(gitignored): the `sys/net/pf*.c|h`, `ifq/hfsc/fq_codel/ip_pflog/ip_divert`, +`radix/rtable/toeplitz`, `crypto/{md5,sha2,siphash}`, `sys/sys/*.h` list and +`sbin/pfctl/*` + headers (see the `SYS_K`/`SRC_K` arrays). It then records +each file's `$OpenBSD: …$` tag into `_vendor/upstream/IDS.txt` — the forensic +baseline for every ported line and for future rebases. + +## 3. Staging into the port — `scripts/stage-port.sh` (deterministic porting) + +`stage-port.sh` turns pristine upstream files into the files under `module/`: + +1. every `#include` is replaced by `pfk.h` + the staged `pfvar` headers + (kernel branches selected with `-D_KERNEL=1`); +2. the shared rename program (`pf_sed`) resolves BSD↔Linux name collisions + (`struct ip` → `struct pf_ip`, `ifnet` → `pfk_ifnet`, `ip_output` → + `pfk_ip_output`, `sa_len` assignments stripped, …); +3. **each run is appended to `docs/porting-notes.md`** — that file is the + staging log, not prose. + +Ported files are then committed and hand-edited in-tree; re-staging is only +for forensics (`stage-port.sh --diff ` compares our copy against the +pristine upstream text). Radix/rtable/siphash/toeplitz are deliberately not +auto-staged (hand-ported). + +## 4. Kernel — `scripts/build-kernel.sh` (+ `kernel-config.sh`) + +- `kernel-config.sh`: starts from `defconfig` if there is no `.config`, then + sets (idempotently, via `scripts/config` + `make olddefconfig`): + `VIRTIO_BLK/PCI/NET=y`, `EXT4_FS=y`, `NETFILTER=y`, `NETFILTER_ADVANCED`, + `NET_SCHED=y`, `INET=y`, `IPV6=y`, `SERIAL_8250(+CONSOLE)=y` — i.e. + everything needed to direct-boot the pf0 VM with **no initrd**. + `kernel-config.sh test` additionally enables `DEBUG_INFO` + + `DEBUG_KMEMLEAK` for soak builds. +- `build-kernel.sh`: `make -j(nproc-2) bzImage modules_prepare`, ensures + `Module.symvers` (copies `vmlinux.symvers` — kbuild does not write the + former) and `scripts/module.lds` exist, then drops boot artifacts in + `out/`: `linux--bzImage`, `System.map`, `.config`, `kernelversion`, + `bzImage.sha256`. Prints `KERNEL_BUILD_OK `. +- **Kernel core patches are currently none**: `patches/kernel/` holds no + `000N-*.patch` (see [patches.md](patches.md)). + +## 5. Module — `scripts/build-module.sh` + +`make -C "$KERNELDIR" M=module` → `module/pf.ko` (single module today; the +`pfq` qdisc arrives with M5). The script self-prepares `Module.symvers` / +`modules_prepare` first. Object list (`module/Makefile`): ported engine +(`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`, `divert_sock.c`) + compat layer +(mbuf shim, kern glue, if/ip4/ip6/pcb/xmit/ifq, crypto, radix, subr_tree) + +`pfk_mod.c`/`pfk_hooks.c`. Compile flags define `PFLINUX=1` and `_KERNEL=1`. +Prints `MODULE_BUILD_OK`. + +`insmod module/pf.ko` creates `/dev/pf` (0600) and registers the netfilter +hooks; `dmesg` shows `pfk: …` lines (see [caveats.md](caveats.md) for what +the data path does). + +## 6. Userland — `make -C tools` + +Targets (from `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`. + +- `pfctl` — the OpenBSD 8.0 sources compiled against the copied ABI headers; + result is a **static binary** at `tools/out/pfctl`. Every deviation from + upstream is listed in `tools/UPSTREAM.md` (7 marked edits; the rest is the + compat header/source set). +- `abi` — builds `abitest` (userland) and `abitest-module` (same table under + kernel compat headers), dumps both struct-size/offset tables and `diff`s + them; a mismatch means the ioctl ABI drifted. `out/abi-*.tbl` are the + reference inputs. +- `test` — offline parse checks: runs `pfctl -n -f` over `tools/tests/*.conf` + (basic / translations / queues+scrub / anchor / negative `broken.conf`). + This is pure userland; it needs no `/dev/pf`. +- `pdivert` — divert relay source under `tools/pdivert/`; see + [feature-matrix.md](feature-matrix.md) for its (pending) kernel side. + +## 7. Root `Makefile` + +`make module | tools | kernel | vm | vm-destroy | test | docs | clean` — thin +wrappers over the scripts above. `make test` runs the VM suite +([vm-lab.md](vm-lab.md)); `make docs` just lists the bilingual doc set. + +## What has been exercised vs. not + +- Stages 1–6 have each been run during M0–M2 development on the build host + (artifacts exist in-tree: `out/`, `module/pf.ko`, `tools/out/pfctl`), and + the scripts above were verified line-by-line for this document. +- A **fresh-clone end-to-end run** (`quickstart-check.sh`, plan task T19) has + *not* been executed yet; treat the exact command sequence in install.md as + "as designed, last-run state per milestone" rather than CI-verified. diff --git a/docs/en/caveats.md b/docs/en/caveats.md new file mode 100644 index 0000000..14c6c84 --- /dev/null +++ b/docs/en/caveats.md @@ -0,0 +1,137 @@ +# Caveats — divergences from OpenBSD behavior + +Português: [docs/pt-BR/caveats.md](../pt-BR/caveats.md). + +These are the ways this port differs from running the same ruleset on +OpenBSD 8.0, or is not yet production-shaped. Everything here is grounded in +the code (`module/pfsrc/pfk_hooks.c`, `module/pfsrc/pfk_mod.c`, +`docs/porting-notes.md`) and in lab observations; nothing is speculative. + +## Data path + +### 1. PF runs in process context via a workqueue (netisr model), not in the packet fast path + +OpenBSD evaluates `pf_test()` on the CPU that received the packet, at +`IPL_NET`, with a non-sleeping NET read lock. Linux gives us softirq context +and *sleepable* compat locks (rw-semaphores), so the hook never calls +`pf_test()` inline: it steals the skb (`NF_STOLEN`) onto an **ordered +workqueue** (`pfk_netisr`, `WQ_MEM_RECLAIM`) and runs the engine in process +context, then re-injects (`netif_rx()` for input, `dst->output()` for +output/transit). + +Consequences, all observable: + +- **Verdicts are asynchronous.** Anything chained after our hook on the same + netfilter chain (other iptables/nft rules at higher priorities) does not + see the packet until the reinjected pass comes through. Counters and log + interleaving differ accordingly. +- **Latency + throughput**: every packet takes a work-queue round trip + serialized on one ordered worker. Functional gates are the current bar; + the planned `bench-basic.sh` is informational only. +- **Reordering**: the ordered queue preserves arrival order of *queued* + work items, but the stack around it (RPS, multi-NAPI) can still hand us + packets whose original completion order differs from reinjection order. + +### 2. Re-entry protection is a per-direction tag in `skb->mark` + +The reinjected survivor carries `skb->mark |= PFK_DONE_IN (0x50460010)` or +`PFK_DONE_OUT (0x50460020)`; our hooks accept any packet already tagged for +that direction instead of re-queuing it (otherwise PF_IN → netif_rx → +PF_IN… would loop). `skb->mark` is a **shared, writable 32-bit field**: any +subsystem that *overwrites* it — nft `meta mark set`, `CONNMARK --restore`, +tc filters, some VPN stacks — can either (a) clear our tag and cause the +packet to be **re-queued through PF again**, or (b) coincidentally set those +bits and let a packet **skip** a PF pass. Practical rule on a PF-on-Linux +host: do not use fwmark mangling on the same traffic; if you must, mask the +PF tag bits out of your mark space. + +### 3. No NET-lock on the data path (M2 interim) + +`pf_test()` currently runs **without** the compat NET read lock (comment in +`pfk_hooks.c`): with the workqueue model, a parked control writer (ruleset +change, purge ioctl) starved the packet side badly enough to be +counter-productive. Rule-set mutation and packet walk therefore run +concurrently, protected by what upstream `pf.c` itself uses (PF_LOCK/RCU-ish +patterns) — a deliberate interim deviation being reworked with the lock +model in M5+. Until then: expect correctness under steady state, not under +aggressive concurrent reload; the reload-under-traffic soak is an open gate. + +### 4. Fail-open under memory pressure + +The queue item is `kmalloc(GFP_ATOMIC)` in softirq. If it fails, the hook +returns `NF_ACCEPT` — the packet **bypasses PF**. This is a deliberate +availability choice (mirrors dropping into a full queue bypassing the qdisc) +but it is a security trade-off worth knowing: PF verdicts are best-effort +exactly when the host is out of atomic memory. + +### 5. Hook ordering vs conntrack differs by direction + +- Ingress (`PREROUTING`, `NF_IP_PRI_FIRST`): PF runs **before** defrag and + conntrack — as designed (spec §4.2). +- Output (`LOCAL_OUT` at `NF_IP_PRI_CONNTRACK+1`): PF sees locally generated + traffic **after** conntrack has already committed the entry. +- Transit egress (`POSTROUTING` at `NF_IP_PRI_LAST-1`): PF runs at the very + end — after any iptables/nft SNAT/masquerade — so `pass out … nat-to`-era + rules (M3) will be validated against this ordering. The "unregister-safe + ordering vs conntrack" warning from the design still applies: overlapping + policy between PF and conntrack is possible and yours to manage. + +## Control plane / lifecycle + +### 6. Unload is hardened; the soak is still pending + +`rmmod pf` deregisters `/dev/pf`, zeroes `pf_status.running` under the NET +lock, deletes **and syncs** both purge timers (they self-rearm — a real rmmod +hang was observed and fixed by stopping sources before flushing the +workqueue), unregisters hooks and flushes `pfk_netisr`. That fix is in-tree, +but the full cycle under traffic + live states (T18/soak gate) has **not** +been run to green yet. For now treat unload as admin-safe, automation- +unproven. + +## Behavioral / rendering issues open today + +### 7. `pfctl -s states` visibility anomaly (M2 gate work) + +In the most recent captured run (gate 01): transit ping **passed** under +`keep state`, but no icmp state was listed afterwards. Fixing this is the +open item of the M2 gate work; until a green run lands, the +[state-listing row](feature-matrix.md) is the honest status. Not cosmetic if +it turns out states are not being inserted — treat both possibilities until +the gate says otherwise. + +### 8. Silent-but-warned stubs + +`divert` verdicts drop with a rate-limited warning; `log` rules log nothing +with a one-time warning. Both are listed so nobody mistakes "rules loaded" +for "features active" (see [pf-guide.md](pf-guide.md)). + +## Compat-layer deviations (from `docs/porting-notes.md`, M1 record) + +These are deliberate, marked-in-code divergences of the compat layer; they +matter when reasoning about rule semantics: + +- **Socket-owner matching** (`user`/`pid` on rules): uid only — Linux `struct + sock` carries no egid/cpid, so gid degrades to `(gid_t)-1`, pid to + `NO_PID`. +- **Checksums**: driver offload is not trusted on ingress (M1 decision) — + TCP checksums are recomputed in software; output pseudo-checksums are done + in the port's own path. +- **IP options / record-route / timestamps**: not processed on the PF output + path; **ICMP redirects are not generated** by PF's route code (the Linux + FIB emits those). +- **Routing labels**: non-goal — the ioctl layer rejects labeled rules; + `rtable_iterate()` always returns NULL (no multipath uRPF iteration). +- **Interface groups**: Linux has none; `group …` matches behave like a + missing group on OpenBSD. +- **GSO/multi-segment skbs**: handled by linearization on entry to the mbuf + shim (perf cost documented in spec §10; gates will quantify). +- **Single instance**: one PF world in `init_net`, like OpenBSD has one + kernel. Containers/namespaces are not visible to PF and cannot be filtered + with it in this design. + +## What is *not* a caveat + +ARP and other non-IPv4/IPv6 EtherTypes never enter the netfilter IPv4/IPv6 +hooks — they pass through the box untouched, exactly as PF on OpenBSD ignores +non-IP. `/dev/pf` is 0600 root-only by design (matches OpenBSD's +`securelevel`-era default for `pfctl` write access being root). diff --git a/docs/en/feature-matrix.md b/docs/en/feature-matrix.md new file mode 100644 index 0000000..e59c09f --- /dev/null +++ b/docs/en/feature-matrix.md @@ -0,0 +1,78 @@ +# Feature matrix + +Português: [docs/pt-BR/feature-matrix.md](../pt-BR/feature-matrix.md). + +Status vocabulary (used identically in both languages): + +- **VERIFIED-in-VM** — passed end-to-end in the libvirt lab (see [vm-lab.md](vm-lab.md)). +- **M2-in-progress** — implemented; VM gates 01–04 exist but the suite is **not all green yet**; no VERIFIED claim until a clean full run. +- **COMPILES-untested** — built into `pf.ko`/`pfctl`; no gate has exercised it. +- **PENDING-Mx** — lands in milestone Mx; today's observable behavior is stated. +- **OUT-OF-SCOPE** — non-goal by design (spec §3). + +As of 2026-11-11; source of per-row evidence: `out/test-logs/`, +`docs/porting-notes.md`, milestone commits. + +## Control plane & tools + +| Feature | Upstream syntax | Milestone | Status | +|---|---|---|---| +| Module load, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM | +| Enable/disable | `pfctl -e`, `pfctl -d` | M1 | VERIFIED-in-VM | +| Ruleset load | `pfctl -f file` | M2 | VERIFIED-in-VM (loads succeeded in every captured gate run; run status itself is mixed — see [vm-lab.md](vm-lab.md) §6) | +| Parse-only check | `pfctl -n -f file` | M1 | VERIFIED (offline fixture suite, `make -C tools test`) | +| Status/counters | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM | +| Rules listing | `pfctl -s rules [-v]` | M2 | COMPILES-untested | +| States listing | `pfctl -s states [-v]` | M2 | M2-in-progress — gate 01 shows a listing anomaly (transit ping OK, no icmp state displayed) while state machinery is being fixed | +| Tables listing/ops | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) | +| Anchors | `pfctl -a anchor -f file`, `-s Anchors` | M2 | M2-in-progress (gate 04) | +| Queue listing | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (parses fine; counters not wired to real back ends yet) | +| ioctl ABI parity | `make -C tools abi` (abitest tables) | M1 | VERIFIED build-side (userland table); kernel-side diff is an open integrator step (`tools/UPSTREAM.md`) | + +## Filtering core + +| Feature | Upstream syntax | Milestone | Status | +|---|---|---|---| +| `pass`/`match`, quick, on-interface/proto/addr/port | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | M2-in-progress (data path runs the real `pf_test`; latest partial run: transit ping OK, icmp block OK) | +| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) | +| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) | +| `keep state` (icmp/tcp/udp), state table, purger | `… keep state` | M2 | M2-in-progress | +| `set state-policy`, `allow-opts`, etc. | `set …` options | M2 | COMPILES-untested | +| Syncookies (state overflow path) | `set syncookies always` | M2 | COMPILES-untested | +| pfr tables (radix/hash) | `table persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) | +| Anchors + include + quick precedence | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) | +| IPv6 for all of the above | same syntax, v6 | M2+ | COMPILES-untested (engine is v6-complete by construction; v6 smoke gates planned) | +| Non-IPv4/IPv6 traffic (ARP…) | — | M2 | PFLINUX by construction: netfilter IPv4/IPv6 hooks only — ARP never enters PF path (no gate yet) | + +## NAT / normalization (M3) + +| Feature | Upstream syntax | Milestone | Status | +|---|---|---|---| +| `nat-to` (port pools, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | PENDING-M3 (`pf.c`/`pf_lb.c` compiled; wiring + VM gate in M3) | +| `rdr-to` | `rdr pass on wan proto tcp from any to any port 8080 -> srv port 80` | M3 | PENDING-M3 | +| `binat-to` | `binat-to …` | M3 | PENDING-M3 | +| Scrub/normalize: reassembly, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match on all scrub (max-mss 1380 …)` | M3 | COMPILES-untested (`pf_norm.c` compiled into pf.ko and reached from `pf_test`; scapy gate 08 pending) | +| OS fingerprinting in states | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compiled; no fingerprint packaging yet — missing `/etc/pf.os` is tolerated) | + +## Routing / divert / queues / logging + +| Feature | Upstream syntax | Milestone | Status | +|---|---|---|---| +| `route-to` pools (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to`, `fail-to` | `pass out route-to (GW 10.0.3.2)` | M4 | PENDING-M4 (Linux route/neigh glue `pf_route` not implemented yet) | +| `divert-to` / `divert` tag | `pass in … divert-to 10.0.99.9 10000`, `divert tag X` | M4 e2e; socket family per code comments M6 | PENDING — today's observable behavior: PF's `PF_DIVERT` verdict calls `divert_packet()`, which **drops the packet and logs a rate-limited `pr_warn`**. The userspace relay `pdivert` exists and expects an in-module `AF_DIVERT` family (`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`), which is not instantiated yet. **Do not ship rules using divert on a live host.** | +| Queues, default scheduler (priq) | `queue root on wan`, `pfctl -s queue -v` | M5 | COMPILES-untested — `compat/pfk_ifq.c` implements the ifq API with **priq fallback semantics**; `queue` rules parse and load, but packets are not actually shunted through a classful qdisc until M5 | +| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **priq fallback until M5**; `pfsrc/hfsc.c` staged (real OpenBSD 8.0 code) but **not in the module object list** | +| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — same as HFSC: staged, not built | +| `pfq` qdisc (`tc qdisc … root pfq`) | n/a (Linux glue) | M5 | PENDING-M5 (does not exist in the tree yet) | +| pflog (`log`, `log all`, `block log`, `pfctl -s log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` | M4 | PENDING-M4 — `pflog_packet()` returns success **without logging** (one-time `pr_warn "packet not logged"`); `if_pflog.c` staged, not built; `pflogd` not ported yet | + +## Non-goals (explicit) + +| Item | Reason | +|---|---| +| `pfsync` / CARP failover | non-goal (spec §3) | +| IPsec / pfkey | non-goal | +| `ftp-proxy` and userland daemons | non-goal | +| Per-netns PF instances | faithful port = single global instance on `init_net` | +| Classic-ALTQ-only schedulers (cbq/wfq/red…) | removed upstream before OpenBSD 8.0 — cannot be ported from the base | +| Routing labels (`route-label`) | non-goal; ioctl layer rejects labeled rules | diff --git a/docs/en/guide.md b/docs/en/guide.md new file mode 100644 index 0000000..4c29686 --- /dev/null +++ b/docs/en/guide.md @@ -0,0 +1,101 @@ +# Architecture guide — one page + +Português: [docs/pt-BR/guide.md](../pt-BR/guide.md). + +## What this port is + +A faithful port of the **OpenBSD 8.0 PF engine** running inside **Linux +7.3.4** as one out-of-tree module, `pf.ko`. Nothing is faked: the matching, +state, table, normalization and queueing code *is* OpenBSD's, compiled under a +compatibility layer; control comes from the real `pfctl` speaking the real +OpenBSD ioctl ABI through `/dev/pf`. There is **one global PF instance bound +to `init_net`** — exactly like OpenBSD has one kernel; per-netns instances are +out of scope. + +Build targets: OpenBSD 8.0 sources (sha256-pinned, `VERSIONS`), Linux 7.3.4 +(`linux-7.3.4`), `gcc >= 15`, bison. `pfvar.h` v1.548 is shared bit-for-bit +between kernel and userland (ioctl encoding per `module/include/pfk.h`). + +## Components + +| Path | Role | +|---|---| +| `module/pfsrc/` | Ported OpenBSD files: `pf.c` (engine), `pf_ioctl.c` (DIOC* ABI), `pf_norm.c` (scrub/fragments), `pf_table.c`, `pf_ruleset.c`, `pf_if.c`, `pf_lb.c` (pools/NAT ports), `pf_osfp.c`, `pf_syncookies.c`, `ifq.c`/`hfsc.c`/`fq_codel.c` (staged for M5), `if_pflog.c` + `ip_divert.c` (staged references), `divert_sock.c` (divert entry stub), `pfk_mod.c` (load/exit, `/dev/pf`), `pfk_hooks.c` (netfilter data path) | +| `module/compat/` | Linux plumbing under the BSD-shaped sources: mbuf-over-`sk_buff` shim (`pfk_mbuf`), kernel glue (malloc/pools/timeouts/locks, `pfk_kern`), interface+address views (`pfk_if`), BSD-semantics IP output/forward (`pfk_ip4`, `pfk_ip6`), socket lookups (`pfk_pcb`), ifq API with priq semantics (`pfk_ifq`), radix/route-table/crypto ports | +| `module/include/` | `pfk.h` (master compat header + OpenBSD-style `_IO*` encoding), staged `net/pfvar.h`, `pfk_mbuf.h`, `pfk_net.h`, `pfk_if.h` | +| `tools/` | Linux build of OpenBSD 8.0 `pfctl` (single binary), `pdivert` relay, ABI parity harness (`abitest` + `make abi`), offline parse tests (`make test`). Deltas vs upstream: `tools/UPSTREAM.md` | +| `scripts/` | `fetch-sources.sh`, `vendorize.sh`, `stage-port.sh` (deterministic staging), `build-kernel.sh`, `build-module.sh`, `vm-*.sh` lab automation | +| `tests/vm/` | VM suite gates `01-state.sh … 04-anchors.sh` + `lib.sh`; `tests/confs/` rulesets deployed to the lab | +| `patches/kernel/` | Core patch set — currently **empty** (`p1-needs.txt` records why no exports were needed); see [patches.md](patches.md) | + +## Data path (M2, as implemented today) + +OpenBSD runs PF at `IPL_NET` in process-ish context; our compat locks are +sleepable, so the hook **never calls `pf_test()` in softirq**. Packets are +stolen onto an ordered workqueue (`pfk_netisr`) and the verdict is applied by +re-injection, tagged per direction via `skb->mark` so the survivor is not +re-queued by our own hook. + +``` + Linux 7.3.4, module pf.ko + NIC ──► netfilter (softirq — enqueue only) + PRE_ROUTING PF_IN ─┐ ┌─ LOCAL_OUT PF_OUT + POST_ROUTING PF_OUT ─┤ NF_STOLEN ├─ (runs AFTER conntrack) + LOCAL_IN/FORWARD: pass-through ┘ POST_ROUTING: NF_IP_PRI_LAST-1 + │ (skb ref + kmalloc pfk_pkt; + │ alloc failure ⇒ NF_ACCEPT = fail-open) + ▼ + pfk_netisr workqueue (process context) + │ + pfk_m_wrap_skb(skb)──► struct mbuf shim + │ + pf_test(af, dir, ifp, &m) ◄── the real OpenBSD 8.0 engine: + │ scrub/normalize → state lookup + │ → rules/anchors/tables/quick + │ → state insert → verdict + ▼ + pass ──► mark |= PFK_DONE_IN/OUT ──► reinject: PF_IN: netif_rx() (survivor + of reassembly allowed through) + PF_OUT: skb_dst()->output() + block/drop ──► kfree_skb() (+ PF's own return-rst/ICMP generation) + fragment-buffered ──► our reference dropped; pf keeps the survivor (m == NULL) +``` + +Control plane: `/dev/pf` (miscdev, mode 0600, write gated on `CAP_NET_ADMIN`) +→ `pfk_ioctl_io()` copies the parameter struct sized from the OpenBSD `_IOR/ +_IOW` encoding, dispatches into the ported `pfioctl()` (`pf_ioctl.c`) — the +full DIOC* ABI as upstream. `pfctl -e/-f/-s/-t` are that same OpenBSD binary, +rebuilt. + +## Queues, divert, pflog — today's observable behavior + +- **Queues**: `pfctl` parses `queue`/`queue on` (8.0 syntax); kernel-side the + ifq API is implemented by `compat/pfk_ifq.c` with **priq semantics only**; + the `pfq` classful qdisc and the HFSC/FQ-CoDel back ends land in **M5**. + `ifq.c`/`hfsc.c`/`fq_codel.c` are staged and compile-ready but are not in + the module object list yet. +- **Divert**: `divert-to`/`divert` tag evaluate to PF's `PF_DIVERT` verdict; + `divert_packet()` (`pfsrc/divert_sock.c`) currently **drops the packet with + a rate-limited kernel log** because the AF_DIVERT socket family is not yet + instantiated. The `pdivert` relay (userspace) exists and expects + `socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`. +- **pflog**: `pflog_packet()` returns success but **logs nothing** (one-time + `pr_warn "packet not logged"`); `if_pflog.c` is staged, netlink back end is + M4/M5. + +## Milestones (see feature-matrix.md for per-feature status) + +| Milestone | Scope | Status (2026-11-11) | +|---|---|---| +| M0 | repo, scripts, kernel build, VM boots custom kernel | done | +| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f`/`-s info` | done | +| M2 | filter core: rules/states/tables/anchors VM gates | **in progress** — data path implemented, gates 01–04 not all green yet | +| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending | +| M4 | route-to/dup-to/fail-to, divert e2e | pending | +| M5 | queues: ifq back ends (hfsc/fq_codel) + `pfq` qdisc | pending | +| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress (this doc set) | + +## Next reads + +[build.md](build.md) → [vm-lab.md](vm-lab.md) → [pf-guide.md](pf-guide.md) → +[feature-matrix.md](feature-matrix.md) → [caveats.md](caveats.md). diff --git a/docs/en/install.md b/docs/en/install.md index 8bdb16a..1779547 100644 --- a/docs/en/install.md +++ b/docs/en/install.md @@ -74,3 +74,29 @@ Because the deliverable is an out-of-tree module, "applying the patch set" (virtio/EXT4 =y) — no source files are modified unless a future milestone adds a documented `patches/kernel/000N-*.patch` (all such patches: one section each in docs/en/patches.md with what/why/impact/revert). + +## 6. Notes on the steps above (2026-11-11) + +- Step **2.3 is a documented no-op today**: the patch set is empty and + `scripts/apply-patches.sh` / `scripts/unpatch.sh` / `patches/kernel/series` + are not yet in the tree (they land with the first real core patch — see + [patches.md](patches.md)). You can safely skip 2.3. +- Step 2.4 builds `pfctl` and the ABI harness only (`make -C tools` targets: + `pfctl abitest abi test clean`). `pdivert` is present as a source file with + no Makefile target yet; `pflogd` is not ported. +- After 2.5, what you can actually *use* is the M2 filter core (pass/block/ + quick/keep state/tables/anchors); NAT/scrub/queues/divert/pflog are gated + by later milestones — see the [feature matrix](feature-matrix.md) and read + [pf-guide.md](pf-guide.md) before enabling PF on a path you need. + +## 7. Where to go next + +| | | +|---|---| +| Architecture, one page | [guide.md](guide.md) | +| Build stages in detail | [build.md](build.md) | +| VM validation lab | [vm-lab.md](vm-lab.md) | +| Daily rulesets & warnings | [pf-guide.md](pf-guide.md) | +| Known divergences | [caveats.md](caveats.md) | +| Core patch mechanism | [patches.md](patches.md) | +| Doc index | [README.md](README.md) | diff --git a/docs/en/patches.md b/docs/en/patches.md new file mode 100644 index 0000000..8475e0c --- /dev/null +++ b/docs/en/patches.md @@ -0,0 +1,73 @@ +# Linux core patch set + +Português: [docs/pt-BR/patches.md](../pt-BR/patches.md). + +## Current status: EMPTY + +The port requires **zero modifications to the Linux 7.3.4 core**. The design +(spec §5) anticipated one mandatory patch — **P1: export symbols needed by +the PF module** — but the M1 compile-port proved it unnecessary: modpost +reported **no undefined symbols** for `pf.ko`. The compat layer was written +against APIs the stock kernel already exports (`__inet_lookup_established`, +`__inet_lookup_listener`, `__inet6_lookup_established`, +`inet6_lookup_listener`, `__udp4_lib_lookup`, `__udp6_lib_lookup`, +`ip_route_output_flow`/`ip6_route_output_flags` via inline wrappers, +`icmp_send`, …). The evidence and the exact list live in +`patches/kernel/p1-needs.txt`, which also carries a **watch list**: e.g. +`tcp_v4_send_reset` would need an export only if userland RST helpers are +used. + +`patches/kernel/` therefore contains no `000N-*.patch` today, and applying +the (empty) set is a no-op. + +> Honest note: install.md and the design mention `scripts/apply-patches.sh`, +> `scripts/unpatch.sh` and `patches/kernel/series`. Those helpers are **not +> in the tree yet**; until they land, "apply the patch set" is by definition +> nothing to do. When a real patch is needed, the first commit that adds it +> MUST also add the two scripts and the `series` file (see rules below). + +## The mechanism (how the set is meant to work) + +- Every kernel-core change is a standalone file + `patches/kernel/000N-short-description.patch`, listed in order in + `patches/kernel/series`, applied to the pristine kernel tree with + `git apply`-style tooling and reverted the same way + (`unpatch.sh` reverse-applies). +- The tree must never be left dirty silently: builds that must be + reproducible apply, build, and record the state (`out/.config`, + `out/kernelversion`). +- Hard rules for any future patch: + 1. **Additive, zero behavior change** (e.g. `EXPORT_SYMBOL_GPL()` lines + adjacent to existing export clusters). Anything that alters logic needs + a design-spec amendment first. + 2. One concern per patch, numbered sequentially — never edit a landed + patch; supersede with a new number. + 3. A section in this file (both languages) **before** the patch lands: + what/why/impact/revert + the VM evidence. + 4. The patch must survive `make -C bzImage modules` from a + clean tree (CI-of-the-lab: `vm-run-tests.sh` after a fresh boot). + +## Template for a future `000N` entry + +Copy this block into both `docs/en/patches.md` and `docs/pt-BR/patches.md` +(EN above the pt-BR file's same anchor) when adding `000N-*.patch`: + +``` +### 000N — + +- File: `patches/kernel/000N-.patch` +- Status: landed (kernel 7.3.4) / superseded by 000M +- Touches: , added lines, 0 removed +- What: +- Why: +- Impact: +- Revert: `./scripts/unpatch.sh ` (reverse-apply); module + then fails to link with the missing symbol named in modpost output. +- Evidence: +``` + +## Change log of this directory + +| Date | Change | +|---|---| +| 2026-11-11 | P1 evaluated after M1 compile-port → **not needed**; `p1-needs.txt` records the evidence and the watch list. Set remains empty. | diff --git a/docs/en/pf-guide.md b/docs/en/pf-guide.md new file mode 100644 index 0000000..0a0d952 --- /dev/null +++ b/docs/en/pf-guide.md @@ -0,0 +1,93 @@ +# Using PF-on-Linux day to day + +Português: [docs/pt-BR/pf-guide.md](../pt-BR/pf-guide.md). + +This is the "what can I actually run today" guide. It follows the +[feature matrix](feature-matrix.md): **M2 scope (filtering core: pass/block/ +quick/keep state/tables/anchors + enable/disable)** has VM evidence behind it; +everything beyond that is either COMPILES-untested or PENDING. Install/build +is covered by [install.md](install.md) and [build.md](build.md). + +## Daily loop + +```sh +sudo insmod module/pf.ko # creates /dev/pf +sudo pfctl -nf /etc/pf.conf # parse check (userland only — always safe) +sudo pfctl -f /etc/pf.conf # load ruleset +sudo pfctl -e # ENABLE — traffic starts hitting the ruleset NOW +sudo pfctl -s info # status + counters +sudo pfctl -s states # states listing (see caveat below) +sudo pfctl -t blk -T add 10.0.1.10 # live table update +sudo pfctl -a admin -f /etc/pf.d/admin.conf +sudo pfctl -d # disable +sudo rmmod pf # unload (rule/state flush happens here) +``` + +> **Warning — order matters**: `-f` then `-e`. Enabling with no matching `pass` rule +> for your admin path blocks it — this port follows OpenBSD's default-block +> policy, and the M2 gate runs have *actually cut the test VM's ssh* that +> way (the lab keeps a serial console for exactly this). On a remote host, +> pass your management interface before you enable anything. + +## A ruleset that works in this port today + +```pf +# /etc/pf.conf — M2 scope only. Interface names here match the VM lab +# (mgmt/lan/wan); substitute your own (eth0, enp1s0, …). + +# 1) keep yourself reachable (do this first, always) +pass in quick on mgmt +pass out quick + +# 2) filter core — proven today: quick, proto/port matching, state, drop +table persist # live blocklist (gate 03 pattern) +block in quick proto tcp from to any + +pass in quick on lan proto tcp from 10.0.1.0/24 to any port { 80 443 } keep state +block drop in quick on wan proto icmp from any to any + +# 3) anchors with precedence (gate 04 pattern) +anchor "admin" +``` + +Macros (`xif = "lan"`), nested `anchor "in/*"`, `label`, `tag`, `set +timeout/limit/state-policy/block-policy/skip` and the rest of the OpenBSD 8.0 +grammar **parse** fine — `make -C tools test` exercises them offline — but +the *kernel behavior* of most `set` options has not been gate-verified yet; +treat them as load-and-pray until the matrix moves them. + +## What to avoid until the milestones land + +| Syntax | Until | What happens today | +|---|---|---| +| `nat-to`, `rdr-to`, `binat-to` | M3 | Rules load (engine code compiled) but translation is unwired/ungated — **do not rely on NAT**; outbound masquerade still comes from iptables/nft during the lab. | +| `scrub (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. | +| `divert-to`, `divert` tag | M4 (+ family per code comments) | Matching packets are **dropped** with a rate-limited kernel warning. Never ship this on a live host. | +| `route-to`, `dup-to`, `fail-to`, `reply-to` | M4 | Not implemented as Linux routing glue yet; rules may load but the behavior is undefined — avoid. | +| `queue` / `queue on` / `pfctl -s queue` | M5 | Parse and load. Queuing falls back to the priq-shaped compat `ifq` layer; **no bandwidth enforcement happens until the `pfq` qdisc lands**. | +| `log`, `log all`, `block log`, `pflogd` | M4 | Nothing is logged (stub warns once at load: `pfk: … packet not logged`). | +| `route-label …` | never (non-goal) | Rejected at the ioctl layer. | +| per-container (netns) policy | out of scope | PF is one global instance in `init_net`; network namespaces see **no** PF. | + +## Reading state correctly + +`pfctl -s states` currently has a listing/visibility anomaly being fixed in +the M2 gate work (transit flows can pass while no state is displayed). Until +then, use counters (`pfctl -s info`) and external observation (ping/nc +results, `tcpdump`) as the primary evidence, as the gates do. + +## Kernel log signals you should know + +``` +pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok +pfk: netfilter hooks registered (10, netisr wq data path) +pfk: divert v4 port N: no divert socket layer (M6 pending), dropping +pfk: … packet not logged ← pflog stub +``` + +Unloading stops the purge timers and drains the work queue first (an rmmod +hang observed in the lab was fixed that way) — but the *soak* run +(rmmod/insmod cycle under traffic) is still an open gate, so expect unload to +be correct for admin use and unproven for hot-reload automation. + +Full list of behavioral divergences: [caveats.md](caveats.md). diff --git a/docs/en/vm-lab.md b/docs/en/vm-lab.md new file mode 100644 index 0000000..e3ad77d --- /dev/null +++ b/docs/en/vm-lab.md @@ -0,0 +1,116 @@ +# VM validation lab (libvirt / Alpine) + +Português: [docs/pt-BR/vm-lab.md](../pt-BR/vm-lab.md). + +All commands below were read from the scripts in `scripts/` (they are the +source of truth); the lab has been created and run during M0–M2 — a full-suite +green run is the M2+ gate and is still in progress (see §6). + +## 1. Where it lives + +| Host | Role | +|---|---| +| laptop (this repo) | builds kernel/module/tools; drives everything over ssh | +| **deb** — `root@192.168.1.242` | libvirt hypervisor; runs the 4 `pftest-*` Alpine VMs | + +The lab never needs NAT to the outside; the only external access is the +libvirt `default` network (192.168.122.0/24, mgmt+DHCP) used for cloud-init +package installs and test ssh. The existing `sms-vm` on deb is never touched. + +## 2. Topology (single source: `lab/ips.env`) + +| VM | Role | NICs | Kernel | +|---|---|---|---| +| `pftest-pf0` | filter under test (runs `pf.ko`) | `mgmt` (dhcp, default net), `lan` 10.0.1.1/24, `wan` 10.0.2.1/24, `wan2` 10.0.3.1/24 | stock Alpine via image-extracted direct boot; **or** the self-built 7.3.4 `bzImage` with `--pf0-kernel=` (`root=/dev/vda2 ro console=ttyS0,115200n8`, no initrd) | +| `pftest-cli` | client | `mgmt` dhcp (routes disabled), `lan` 10.0.1.10/24, gw 10.0.1.1 (pf0) | Alpine stock | +| `pftest-srv` | server | `mgmt` dhcp (routes disabled), `wan` 10.0.2.20/24, gw 10.0.2.1 (pf0), 10.0.3.0/24 via gw2 | Alpine stock | +| `pftest-gw2` | second-path gateway (ip_forward=1) | `wan2` 10.0.3.2/24, `wan` 10.0.2.2/24 (no mgmt NIC) | Alpine stock | + +All VMs get `net.ipv4.ip_forward=1` and `rp_filter=0` via cloud-init, root +password `pftest` plus an injected lab keypair (`/root/.ssh/pflab_ed` on deb, +copy on the laptop for ProxyJump), and packages +`iperf3 tcpdump python3 scapy bash iptables` (Alpine name is `scapy`; the +image also needs `iptables` for pf0 masquerade experiments). + +libvirt networks: `default` (kept as-is, started+autostarted for packages) and +isolated bridges `pf-lan` 10.0.1.0/24, `pf-wan` 10.0.2.0/24, `pf-wan2` +10.0.3.0/24 — `forward="none"`, no DHCP, plus host-side bridge IPs +`10.0.x.254` on deb so deb can ssh into every guest's data NIC. VM sizing: +2 vCPU / 1 GB each. Disks are per-VM qcow2 **backing** a shared immutable +`/var/lib/libvirt/images/alpine-base.qcow2` (synced from +`_vendor/images/alpine-3.22-x86_64-cloud.qcow2` by sha256). + +> Boot note: the Alpine cloud image is not BIOS/UEFI bootable in QEMU as +> delivered (empty ESP), so *all* VMs use direct kernel boot from the image's +> own extracted kernel; `--pf0-kernel=` only swaps pf0's kernel for the +> built one. + +## 3. Recreate / deploy / destroy + +```sh +./scripts/vm-create.sh # idempotent: tears down and rebuilds pftest-* domains + seeds +./scripts/vm-create.sh --pf0-kernel=out/linux-7.3.4-bzImage # pf0 boots the built kernel (T6) +./scripts/vm-create.sh --recreate-base # force base-image re-sync +./scripts/vm-deploy.sh # laptop→deb ($DEPLOY_DIR)→pf0:/opt/pf: + # out/*bzImage, module/*.ko, tools/out/* +./scripts/vm-destroy.sh # domains, disks, seeds, pf-* nets, ssh fragments +./scripts/vm-destroy.sh --purge-base --purge-keys # + base image + lab keypair +``` + +`vm-create.sh` ends by validating `ssh pf0 'ip -br a'` and leaves all VMs +running. Laptop ssh aliases (`pf0|cli|srv|gw2`, ProxyJump via deb) are +generated into `~/.ssh/config.d/pflab`. + +## 4. Running the suite + +```sh +./scripts/vm-run-tests.sh # or: make test +``` + +Deterministic by design: **before every gate** the runner destroys and +restarts `pftest-pf0` (fresh disk delta + no leftover module), waits for ssh +on the mgmt DHCP address `192.168.122.8`, re-deploys `pf.ko`/`pfctl`/`*.conf` +into the guest, then runs the gate script from the **laptop** (gates talk to +the VMs through deb with nested ssh — `tests/vm/lib.sh`). Per-gate output is +captured to `out/test-logs/.sh.log`; the runner prints +`PASS ` / `FAIL (rc=...)` per test, a summary `PASS=n FAIL=m`, +and exits non-zero if anything failed. + +## 5. What each gate proves today + +| Gate | Scenario (asserted with real traffic, not exit codes) | +|---|---| +| `01-state.sh` | `pass in/out quick proto icmp keep state` → cli pings srv **through** pf0 (transit); pf0 then lists an icmp state via `pfctl -s states` | +| `02-block.sh` | `block drop in quick proto icmp` → cli ping fails; `block return-rst in quick proto tcp … port 8080` → cli's `nc` to srv:8080 is refused (RST), checked via nc exit | +| `03-tables.sh` | `table { 10.0.1.10 }` + `block in quick from ` → `pfctl -t blk -T show` lists it; cli ping blocked; live `-T delete` unblocks the same flow | +| `04-anchors.sh` | anchor rule (`-a restricted -f`) passing icmp is evaluated **before** a later main-ruleset `block` (quick/top-placement); `pfctl -s Anchors` lists the anchor | + +Planned gates per the plan document (T11–T18): 05-nat, 06-rdr, 07-binat, +08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (iperf rate +tolerance ±15%), 13-pflog, 14-soak (reload under traffic + rmmod cycle + +kmemleak). IPv6 is smoke-only for state/block/nat66/scrub. `bench-basic.sh` +(planned, not in the tree yet) is informational; there is no numeric perf gate. + +## 6. Current suite status (2026-11-11, honestly) + +The M2 gates 01–04 exist and run, but **are not all green yet** — the data +path (netisr workqueue model) and state visibility are being fixed in the M2 +gate work. The latest captured run (`out/test-logs/`) shows the mixed state: +transit ping OK / state listing failing on 01, icmp-blocked OK on 02, partial +results on 03, 04 aborted. Treat "VERIFIED-in-VM" in the +[feature matrix](feature-matrix.md) as reserved for gates that pass in a +full clean run; nothing beyond M1 is claimed VERIFIED until then. + +## 7. Manual poking + +```sh +ssh pf0 # via laptop alias (ProxyJump deb) +insmod /opt/pf/pf.ko +/opt/pf/pfctl -e +/opt/pf/pfctl -f /tmp/g1.conf +/opt/pf/pfctl -s info ; /opt/pf/pfctl -s states -v +dmesg | grep pfk +``` + +Artifacts of future full runs (log + pcaps) are meant to ship with the Gitea +release (spec §7). diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md new file mode 100644 index 0000000..129eda6 --- /dev/null +++ b/docs/pt-BR/README.md @@ -0,0 +1,36 @@ +# Documentação PF-on-Linux (português) + +English documentation: [`docs/en/README.md`](../en/README.md). + +Este é o conjunto de documentação de usuário do port do **PF do OpenBSD 8.0** +para o **Linux 7.3.4** — o engine PF de verdade (`pf.c`, `pf_norm.c`, +`pf_table.c`, `pf_lb.c`, …) rodando como módulo out-of-tree `pf.ko`, +controlado pelo `pfctl` real do OpenBSD pela ABI ioctl de `/dev/pf`. + +## Índice + +| Documento | Conteúdo | +|---|---| +| [guide.md](guide.md) | Arquitetura em uma página: componentes, caminho de dados, plano de controle, milestones | +| [install.md](install.md) | Receita de aplicação/build/instalação (kernel, módulo, ferramentas) | +| [build.md](build.md) | Pipeline de build em detalhe: fetch → vendorize → kernel → módulo → pfctl | +| [vm-lab.md](vm-lab.md) | Bancada de validação em libvirt: topologia, recriar/rodar, o que cada gate prova | +| [feature-matrix.md](feature-matrix.md) | Status por feature: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx | +| [pf-guide.md](pf-guide.md) | Uso diário: o que funciona hoje em `/etc/pf.conf`, o que evitar até M3/M4 | +| [patches.md](patches.md) | Patches de core do Linux (hoje vazio) + regras e template para futuros `000N-*.patch` | +| [caveats.md](caveats.md) | Divergências conhecidas de comportamento e limitações atuais | + +## Fontes de verdade + +A documentação descreve o código como ele existe na árvore. Em caso de dúvida, +as referências autoritativas são: + +- `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` — design +- `docs/superpowers/plans/2026-11-11-pf-linux-port.md` — plano de milestones (tarefas T1–T19) +- `docs/porting-notes.md` — log de staging + registro de desvios da camada de compat (M1) + +Vocabulário de status usado em todos os documentos: + +- **VERIFIED-in-VM** — exercitado de ponta a ponta na bancada libvirt (ver vm-lab.md). +- **COMPILES-untested** — o código compila no `pf.ko`, mas nenhum gate provou o comportamento ainda. +- **PENDING-Mx** — entra (ou entra por completo) no milestone Mx; o comportamento observável hoje está documentado. diff --git a/docs/pt-BR/build.md b/docs/pt-BR/build.md new file mode 100644 index 0000000..267cf64 --- /dev/null +++ b/docs/pt-BR/build.md @@ -0,0 +1,120 @@ +# Pipeline de build em detalhe + +English: [docs/en/build.md](../en/build.md). +Receita rápida: [install.md](install.md). Esta página explica *o que cada +etapa faz* e *o que já foi realmente exercitado*. + +Tudo abaixo roda no host de build (o laptop); a bancada de VMs (host +separado, "deb") só recebe artefatos — ver [vm-lab.md](vm-lab.md). + +## 0. Pré-requisitos + +De `docs/pt-BR/install.md`: gcc ≥ 15, GNU make, bison (flex não é +necessário — o `parse.y` do OpenBSD traz um `yylex` escrito à mão), `bc`, +rsync, `glibc-static` para o pfctl estático, root para `insmod`. A árvore do +kernel é esperada em `../linux-7.3.4` (troque com `KERNELDIR=`; o `Makefile` +raiz usa `$(HOME)/lab/linux_kernel/linux-7.3.4`). + +## 1. Baixar + verificar as fontes do OpenBSD — `scripts/fetch-sources.sh` + +- Baixa `sys.tar.gz` e `src.tar.gz` do mirror primário + `https://openbsd.c3sl.ufpr.br/pub/OpenBSD/8.0/`, com fallback para + `cdn.openbsd.org`. +- Confere o sha256 contra os valores de `VERSIONS` (sys: `964b1aed…`, src: + `9984c74f…`); tarball já em cache com hash correto é pulado. +- Destino: `_vendor/openbsd/` (fora do git). + +## 2. Vendorize — `scripts/vendorize.sh` + +Extrai **só os arquivos que o port consome** para `_vendor/upstream/` +(fora do git): a lista `sys/net/pf*.c|h`, `ifq/hfsc/fq_codel/ip_pflog/ +ip_divert`, `radix/rtable/toeplitz`, `crypto/{md5,sha2,siphash}`, +`sys/sys/*.h` e `sbin/pfctl/*` + headers (arrays `SYS_K`/`SRC_K`). Em +seguida registra o tag `$OpenBSD: …$` de cada arquivo em +`_vendor/upstream/IDS.txt` — a base forense de toda linha portada e de +futuros rebases. + +## 3. Staging para o port — `scripts/stage-port.sh` (porting determinístico) + +O `stage-port.sh` transforma os arquivos upstream pristinos nos arquivos em +`module/`: + +1. todo `#include` é substituído por `pfk.h` + os headers `pfvar` staged + (os ramos de kernel são selecionados com `-D_KERNEL=1`); +2. o programa de renomeação compartilhado (`pf_sed`) resolve colisões de + nomes BSD↔Linux (`struct ip` → `struct pf_ip`, `ifnet` → `pfk_ifnet`, + `ip_output` → `pfk_ip_output`, atribuições a `sa_len` removidas, …); +3. **cada execução é anexada a `docs/porting-notes.md`** — esse arquivo é o + log de staging, não prosa. + +Os arquivos portados são então commitados e editados à mão na árvore; o +re-staging serve só para perícia (`stage-port.sh --diff ` compara a +nossa cópia com o texto upstream pristino). Radix/rtable/siphash/toeplitz +ficam de fora do staging automático de propósito (portados à mão). + +## 4. Kernel — `scripts/build-kernel.sh` (+ `kernel-config.sh`) + +- `kernel-config.sh`: parte de `defconfig` quando não há `.config` e então + ajusta (idempotente, via `scripts/config` + `make olddefconfig`): + `VIRTIO_BLK/PCI/NET=y`, `EXT4_FS=y`, `NETFILTER=y`, + `NETFILTER_ADVANCED`, `NET_SCHED=y`, `INET=y`, `IPV6=y`, + `SERIAL_8250(+CONSOLE)=y` — ou seja, tudo que o boot direto da VM pf0 + precisa **sem initrd**. `kernel-config.sh test` habilita também + `DEBUG_INFO` + `DEBUG_KMEMLEAK` para builds de soak. +- `build-kernel.sh`: `make -j(nproc-2) bzImage modules_prepare`, garante que + `Module.symvers` exista (copia `vmlinux.symvers` — o kbuild não escreve o + primeiro) e que `scripts/module.lds` exista; depois deposita os artefatos + de boot em `out/`: `linux--bzImage`, `System.map`, `.config`, + `kernelversion`, `bzImage.sha256`. Termina com + `KERNEL_BUILD_OK `. +- **Hoje não há patch de core**: `patches/kernel/` não tem nenhum + `000N-*.patch` (ver [patches.md](patches.md)). + +## 5. Módulo — `scripts/build-module.sh` + +`make -C "$KERNELDIR" M=module` → `module/pf.ko` (um único módulo hoje; o +qdisc `pfq` chega com o M5). O script prepara sozinho `Module.symvers` / +`modules_prepare`. Lista de objetos (`module/Makefile`): engine portado +(`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`, `divert_sock.c`) + camada de +compat (shim mbuf, glue de kernel, if/ip4/ip6/pcb/xmit/ifq, crypto, radix, +subr_tree) + `pfk_mod.c`/`pfk_hooks.c`. As flags de compilação definem +`PFLINUX=1` e `_KERNEL=1`. Termina com `MODULE_BUILD_OK`. + +`insmod module/pf.ko` cria `/dev/pf` (0600) e registra os hooks netfilter; o +`dmesg` mostra linhas `pfk: …` (ver [caveats.md](caveats.md) para o que o +caminho de dados faz). + +## 6. Userland — `make -C tools` + +Alvos (de `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`. + +- `pfctl` — as fontes do OpenBSD 8.0 compiladas contra os headers de ABI + copiados; o resultado é um **binário estático** em `tools/out/pfctl`. + Cada desvio do upstream está listado em `tools/UPSTREAM.md` (7 edições + marcadas; o resto é o conjunto de headers/fontes de compat). +- `abi` — compila `abitest` (userland) e `abitest-module` (mesma tabela com + os headers de compat de kernel), despeja as tabelas de tamanho/offset de + cada struct e as compara com `diff`; divergência = ABI de ioctl quebrada. + `out/abi-*.tbl` são as entradas de referência. +- `test` — checagens de parse offline: roda `pfctl -n -f` sobre + `tools/tests/*.conf` (básico / traduções / filas+scrub / anchor / negativa + `broken.conf`). É userland puro; não precisa de `/dev/pf`. +- `pdivert` — o fonte do relay de divert fica em `tools/pdivert/`; o lado + kernel (pendente) está na [matriz de features](feature-matrix.md). + +## 7. `Makefile` raiz + +`make module | tools | kernel | vm | vm-destroy | test | docs | clean` — +wrappers finos sobre os scripts acima. `make test` roda a suíte em VM +([vm-lab.md](vm-lab.md)); `make docs` apenas lista o conjunto bilíngue. + +## O que já foi exercitado vs. não + +- As etapas 1–6 foram rodadas ao longo do desenvolvimento M0–M2 no host de + build (há artefatos na árvore: `out/`, `module/pf.ko`, `tools/out/pfctl`), + e os scripts acima foram verificados linha a linha para este documento. +- Uma execução **de ponta a ponta a partir de clone limpo** + (`quickstart-check.sh`, tarefa T19 do plano) **ainda não** foi feita; + trate a sequência exata do install.md como "conforme projetada e conforme + o último estado por milestone", não como verificada em CI. diff --git a/docs/pt-BR/caveats.md b/docs/pt-BR/caveats.md new file mode 100644 index 0000000..29fc389 --- /dev/null +++ b/docs/pt-BR/caveats.md @@ -0,0 +1,141 @@ +# Ressalvas — divergências do comportamento do OpenBSD + +English: [docs/en/caveats.md](../en/caveats.md). + +Estas são as formas pelas quais este port difere de rodar o mesmo ruleset no +OpenBSD 8.0, ou ainda não tem forma de produção. Tudo aqui tem base no +código (`module/pfsrc/pfk_hooks.c`, `module/pfsrc/pfk_mod.c`, +`docs/porting-notes.md`) e em observações da bancada; nada é especulação. + +## Caminho de dados + +### 1. O PF roda em contexto de processo via workqueue (modelo netisr), não no caminho rápido do pacote + +O OpenBSD avalia o `pf_test()` na CPU que recebeu o pacote, em `IPL_NET`, +com uma lock de leitura NET que não dorme. O Linux nos dá contexto de +softirq e locks da compat *sleepáveis* (rw-semaphores), então o hook nunca +chama o `pf_test()` inline: ele furta a skb (`NF_STOLEN`) para uma +**workqueue ordenada** (`pfk_netisr`, `WQ_MEM_RECLAIM`), roda o engine em +contexto de processo e então reinjeta (`netif_rx()` na entrada, +`dst->output()` na saída/trânsito). + +Consequências, todas observáveis: + +- **Vereditos são assíncronos.** Qualquer regra encadeada depois do nosso + hook na mesma chain netfilter (outras regras iptables/nft em prioridades + maiores) não vê o pacote até o passe reinjetado chegar. Contadores e + intercalação de log mudam na mesma medida. +- **Latência + vazão**: cada pacote paga um ida-e-volta pela workqueue, + serializado num único worker ordenado. Os gates funcionais são a régua + atual; o `bench-basic.sh` planejado (ainda não está na árvore) é só informativo. +- **Reordenação**: a fila ordenada preserva a ordem de chegada dos itens + *enfileirados*, mas o stack em volta (RPS, multi-NAPI) ainda pode nos + entregar pacotes cuja ordem original de conclusão difere da ordem de + reinjeção. + +### 2. A proteção contra reentrada é um tag por direção no `skb->mark` + +O sobrevivente reinjetado carrega `skb->mark |= PFK_DONE_IN (0x50460010)` ou +`PFK_DONE_OUT (0x50460020)`; nossos hooks aceitam qualquer pacote já marcado +para aquela direção em vez de re-enfileirar (senão PF_IN → netif_rx → PF_IN… +entraria em loop). O `skb->mark` é um **campo compartilhado e gravável de +32 bits**: qualquer subsistema que o *sobrescreva* — `meta mark set` do nft, +`CONNMARK --restore`, filtros de tc, alguns stacks de VPN — pode (a) limpar o +nosso tag e fazer o pacote ser **re-enfileirado pelo PF de novo**, ou (b) +acidentalmente ligar esses bits e deixar o pacote **pular** um passe do PF. +Regra prática num host com PF-on-Linux: não use mangling de fwmark no mesmo +tráfego; se precisar, mascare os bits do tag do PF no seu espaço de mark. + +### 3. Sem lock NET no caminho de dados (interino do M2) + +O `pf_test()` roda hoje **sem** a lock de leitura NET da compat (comentário +em `pfk_hooks.c`): com o modelo de workqueue, um escritor de controle +parqueado (mudança de ruleset, ioctl de purge) esfomeava o lado dos pacotes +a ponto de ser contraproducente. A mutação do ruleset e a caminhada de +pacotes rodam, portanto, concorrentemente, protegidas pelo que o próprio +`pf.c` do upstream usa (PF_LOCK/padrões tipo RCU) — um desvio interino +deliberado, sendo retrabalhado junto com o modelo de locks no M5+. Até lá: +espere correção em regime estável, não sob reload concorrente agressivo; o +soak de reload sob tráfego é um gate aberto. + +### 4. Fail-open sob pressão de memória + +O item da fila é um `kmalloc(GFP_ATOMIC)` em softirq. Se falhar, o hook +retorna `NF_ACCEPT` — o pacote **passa por fora do PF**. É uma escolha +deliberada de disponibilidade (espelha "cai numa fila cheia = bypass do +qdisc"), mas é um trade-off de segurança que vale conhecer: os vereditos do +PF são best-effort exatamente quando o host está sem memória atômica. + +### 5. A ordem dos hooks vs conntrack varia por direção + +- Entrada (`PREROUTING`, `NF_IP_PRI_FIRST`): o PF roda **antes** do defrag + e do conntrack — como projetado (spec §4.2). +- Saída (`LOCAL_OUT` em `NF_IP_PRI_CONNTRACK+1`): o PF vê o tráfego gerado + localmente **depois** de o conntrack já ter confirmado a entrada. +- Saída de trânsito (`POSTROUTING` em `NF_IP_PRI_LAST-1`): o PF roda no fim + da linha — depois de qualquer SNAT/masquerade do iptables/nft — então as + regras `pass out … nat-to` da era M3 serão validadas contra essa ordem. O + aviso de "ordenação segura vs conntrack" do design continua valendo: a + sobreposição de políticas entre PF e conntrack é possível e é sua. + +## Plano de controle / ciclo de vida + +### 6. O unload está endurecido; o soak ainda está pendente + +O `rmmod pf` desregistra `/dev/pf`, zera `pf_status.running` sob a lock NET, +deleta **e sincroniza** os dois timers de purge (eles se rearmam sozinhos — +um travamento real de rmmod foi observado e corrigido parando as fontes +antes de drenar a workqueue), desregistra os hooks e drena o `pfk_netisr`. +A correção está na árvore, mas o ciclo completo sob tráfego + estados vivos +(gate de soak T18) **ainda não** rodou verde. Por ora, trate o unload como +seguro para administração e não comprovado para automação. + +## Problemas de comportamento/renderização abertos hoje + +### 7. Anomalia de visibilidade do `pfctl -s states` (trabalho do gate M2) + +Na rodada capturada mais recente (gate 01): o ping de trânsito **passou** com +`keep state`, mas nenhum estado icmp foi listado depois. Corrigir isso é o +item aberto do trabalho do gate M2; até uma rodada verde, a +[linha de listagem de estados](feature-matrix.md) é o status honesto. Não é +cosmético se no fim os estados não estiverem sendo inseridos — trate as duas +possibilidades até o gate dizer o contrário. + +### 8. Stubs silenciosos-mas-avisados + +Vereditos `divert` descartam com aviso rate-limited; regras de `log` não +logam nada e avisam uma única vez. Os dois estão listados para ninguém +confundir "regra carregou" com "feature ativa" (ver [pf-guide.md](pf-guide.md)). + +## Desvios da camada de compat (de `docs/porting-notes.md`, registro do M1) + +Estes são desvios deliberados e marcados no código; importam ao raciocinar +sobre a semântica das regras: + +- **Match pelo dono do socket** (`user`/`pid` em regras): só uid — o + `struct sock` do Linux não guarda egid/cpid, então o gid degrada para + `(gid_t)-1` e o pid para `NO_PID`. +- **Checksums**: o offload do driver não é confiável na entrada (decisão do + M1) — checksums TCP são recalculados em software; os pseudo-checksums de + saída são feitos no caminho próprio do port. +- **Opções IP / record-route / timestamps**: não processadas no caminho de + saída do PF; **redirecionamentos ICMP não são gerados** pelo código de + rota do PF (o FIB do Linux emite). +- **Labels de rota**: não-objetivo — a camada de ioctl rejeita regras com + label; `rtable_iterate()` sempre retorna NULL (sem iteração de uRPF + multipath). +- **Grupos de interface**: o Linux não tem; matches `group …` se comportam + como um grupo inexistente no OpenBSD. +- **Skbs GSO/multi-segmento**: tratadas por linearização na entrada do shim + mbuf (custo de performance documentado no spec §10; os gates vão + quantificar). +- **Instância única**: um mundo PF em `init_net`, como o OpenBSD tem um + kernel. Containers/namespaces não são visíveis ao PF e não podem ser + filtrados por ele neste design. + +## O que *não* é ressalva + +ARP e outros EtherTypes não-IPv4/IPv6 nunca entram nos hooks netfilter de +IPv4/IPv6 — passam pela máquina intactos, exatamente como o PF no OpenBSD +ignora não-IP. O `/dev/pf` é 0600 somente-root por projeto (equivalente ao +padrão do `pfctl` no OpenBSD da era `securelevel`). diff --git a/docs/pt-BR/feature-matrix.md b/docs/pt-BR/feature-matrix.md new file mode 100644 index 0000000..35d1353 --- /dev/null +++ b/docs/pt-BR/feature-matrix.md @@ -0,0 +1,78 @@ +# Matriz de features + +English: [docs/en/feature-matrix.md](../en/feature-matrix.md). + +Vocabulário de status (idêntico nos dois idiomas): + +- **VERIFIED-in-VM** — passou de ponta a ponta na bancada libvirt (ver [vm-lab.md](vm-lab.md)). +- **M2-in-progress** — implementado; os gates VM 01–04 existem, mas a suíte **ainda não está toda verde**; sem alegação de VERIFIED até uma rodada limpa. +- **COMPILES-untested** — compila em `pf.ko`/`pfctl`; nenhum gate exercitou. +- **PENDING-Mx** — entra no milestone Mx; o comportamento observável hoje está declarado. +- **OUT-OF-SCOPE** — não-objetivo por projeto (spec §3). + +Estado em 2026-11-11; evidência por linha: `out/test-logs/`, +`docs/porting-notes.md`, commits de milestone. + +## Plano de controle e ferramentas + +| Feature | Sintaxe de upstream | Milestone | Status | +|---|---|---|---| +| Carga do módulo, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM | +| Habilitar/desabilitar | `pfctl -e`, `pfctl -d` | M1 | VERIFIED-in-VM | +| Carga de ruleset | `pfctl -f arquivo` | M2 | VERIFIED-in-VM (cargas bem-sucedidas em toda rodada capturada; o status geral da rodada é misto — ver [vm-lab.md](vm-lab.md) §6) | +| Checagem só de parse | `pfctl -n -f arquivo` | M1 | VERIFIED (suíte de fixtures offline, `make -C tools test`) | +| Status/contadores | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM | +| Listagem de regras | `pfctl -s rules [-v]` | M2 | COMPILES-untested | +| Listagem de estados | `pfctl -s states [-v]` | M2 | M2-in-progress — o gate 01 mostra anomalia de listagem (ping passa, nenhum estado icmp exibido) enquanto a máquina de estados está sendo corrigida | +| Tabelas (listagem/ops) | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) | +| Anchors | `pfctl -a anchor -f arquivo`, `-s Anchors` | M2 | M2-in-progress (gate 04) | +| Listagem de filas | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (o parse funciona; contadores ainda não conectados aos back ends reais) | +| Paridade de ABI ioctl | `make -C tools abi` (tabelas do abitest) | M1 | VERIFIED no lado do build (tabela userland); o diff do lado kernel é um passo aberto para o integrador (`tools/UPSTREAM.md`) | + +## Núcleo de filtragem + +| Feature | Sintaxe de upstream | Milestone | Status | +|---|---|---|---| +| `pass`/`match`, quick, match por interface/proto/endereço/porta | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | M2-in-progress (o caminho de dados roda o `pf_test` real; rodada parcial recente: ping de trânsito OK, icmp bloqueado OK) | +| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) | +| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) | +| `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | M2-in-progress | +| `set state-policy`, `allow-opts` etc. | opções `set …` | M2 | COMPILES-untested | +| Syncookies (caminho de estouro de estado) | `set syncookies always` | M2 | COMPILES-untested | +| Tabelas pfr (radix/hash) | `table persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) | +| Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) | +| IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested (o engine é completo para v6 por construção; gates v6 smoke planejados) | +| Tráfego não-IPv4/IPv6 (ARP…) | — | M2 | PFLINUX por construção: hooks netfilter só de IPv4/IPv6 — ARP nunca entra no caminho do PF (sem gate ainda) | + +## NAT / normalização (M3) + +| Feature | Sintaxe de upstream | Milestone | Status | +|---|---|---|---| +| `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | PENDING-M3 (`pf.c`/`pf_lb.c` compilados; fiação + gate VM no M3) | +| `rdr-to` | `rdr pass on wan proto tcp from any to any port 8080 -> srv port 80` | M3 | PENDING-M3 | +| `binat-to` | `binat-to …` | M3 | PENDING-M3 | +| Scrub/normalize: remontagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match on all scrub (max-mss 1380 …)` | M3 | COMPILES-untested (`pf_norm.c` compilado no pf.ko e alcançado pelo `pf_test`; gate scapy 08 pendente) | +| Fingerprint de SO em estados | `pass … set os A` + `/etc/pf.os` | bônus | COMPILES-untested (`pf_osfp.c` compilado; sem empacotamento de fingerprints ainda — ausência de `/etc/pf.os` é tolerada) | + +## Roteamento / divert / filas / logging + +| Feature | Sintaxe de upstream | Milestone | Status | +|---|---|---|---| +| Pools de `route-to` (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to`, `fail-to` | `pass out route-to (GW 10.0.3.2)` | M4 | PENDING-M4 (o glue Linux de rota/neigh `pf_route` ainda não existe) | +| `divert-to` / tag `divert` | `pass in … divert-to 10.0.99.9 10000`, `divert tag X` | M4 e2e; família de sockets conforme comentário no código, M6 | PENDING — comportamento observável hoje: o veredito `PF_DIVERT` chama `divert_packet()`, que **descarta o pacote e registra um `pr_warn` rate-limited**. O relay userspace `pdivert` existe e espera a família `AF_DIVERT` dentro do módulo (`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`), que ainda não foi instanciada. **Não coloque regras com divert num host em produção.** | +| Filas, agendador default (priq) | `queue root on wan`, `pfctl -s queue -v` | M5 | COMPILES-untested — `compat/pfk_ifq.c` implementa a API ifq com **semântica de fallback priq**; regras `queue` fazem parse e carregam, mas pacotes ainda não são desviados por um qdisc classful até o M5 | +| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **fallback priq até o M5**; `pfsrc/hfsc.c` staged (código real do OpenBSD 8.0) mas **fora da lista de objetos** do módulo | +| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado | +| Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) | +| pflog (`log`, `log all`, `block log`, `pfctl -s log`) + pcap do `pflogd` (DLT_PFLOG) | `pass out quick log` | M4 | PENDING-M4 — `pflog_packet()` retorna sucesso **sem logar** (`pr_warn "packet not logged"` uma única vez); `if_pflog.c` staged, não compilado; `pflogd` ainda não portado | + +## Não-objetivos (explícitos) + +| Item | Motivo | +|---|---| +| Failover `pfsync` / CARP | não-objetivo (spec §3) | +| IPsec / pfkey | não-objetivo | +| `ftp-proxy` e daemons de userland | não-objetivo | +| Instâncias PF por netns | port fiel = instância global única em `init_net` | +| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos do upstream antes do OpenBSD 8.0 — impossível portar da base | +| Labels de rota (`route-label`) | não-objetivo; a camada de ioctl rejeita regras com label | diff --git a/docs/pt-BR/guide.md b/docs/pt-BR/guide.md new file mode 100644 index 0000000..9dc2c44 --- /dev/null +++ b/docs/pt-BR/guide.md @@ -0,0 +1,103 @@ +# Guia de arquitetura — uma página + +English: [docs/en/guide.md](../en/guide.md). + +## O que este port é + +Um port fiel do **engine PF do OpenBSD 8.0** rodando dentro do **Linux +7.3.4** como um único módulo out-of-tree, `pf.ko`. Nada é simulado: o +casamento de regras, estado, tabelas, normalização e filas *é* o código do +OpenBSD, compilado sob uma camada de compatibilidade; o controle vem do +`pfctl` real, falando a ABI ioctl OpenBSD de verdade por `/dev/pf`. Existe +**uma instância global do PF, presa a `init_net`** — exatamente como o +OpenBSD tem um único kernel; instâncias por netns estão fora de escopo. + +Alvos de build: fontes OpenBSD 8.0 (sha256 fixado em `VERSIONS`), Linux +7.3.4 (`linux-7.3.4`), `gcc >= 15`, bison. O `pfvar.h` v1.548 é compartilhado +bit a bit entre kernel e userland (codificação de ioctl em +`module/include/pfk.h`). + +## Componentes + +| Caminho | Papel | +|---|---| +| `module/pfsrc/` | Arquivos portados do OpenBSD: `pf.c` (engine), `pf_ioctl.c` (ABI DIOC*), `pf_norm.c` (scrub/fragmentos), `pf_table.c`, `pf_ruleset.c`, `pf_if.c`, `pf_lb.c` (pools/portas NAT), `pf_osfp.c`, `pf_syncookies.c`, `ifq.c`/`hfsc.c`/`fq_codel.c` (staged para M5), `if_pflog.c` + `ip_divert.c` (referências staged), `divert_sock.c` (stub de entrada do divert), `pfk_mod.c` (load/exit, `/dev/pf`), `pfk_hooks.c` (caminho de dados netfilter) | +| `module/compat/` | Infra Linux sob os fontes em forma BSD: shim mbuf-sobre-`sk_buff` (`pfk_mbuf`), glue de kernel (malloc/pools/timeouts/locks, `pfk_kern`), visão de interfaces e endereços (`pfk_if`), saída/forward IP com semântica BSD (`pfk_ip4`, `pfk_ip6`), busca de sockets (`pfk_pcb`), API ifq com semântica priq (`pfk_ifq`), radix/rotas/crypto portados | +| `module/include/` | `pfk.h` (header mestre da compat + codificação `_IO*` estilo OpenBSD), `net/pfvar.h` staged, `pfk_mbuf.h`, `pfk_net.h`, `pfk_if.h` | +| `tools/` | Build Linux do `pfctl` do OpenBSD 8.0 (binário único), relay `pdivert`, verificador de ABI (`abitest` + `make abi`), testes de parse offline (`make test`). Deltas vs upstream: `tools/UPSTREAM.md` | +| `scripts/` | `fetch-sources.sh`, `vendorize.sh`, `stage-port.sh` (staging determinístico), `build-kernel.sh`, `build-module.sh`, automação da bancada `vm-*.sh` | +| `tests/vm/` | Gates da suíte em VM: `01-state.sh … 04-anchors.sh` + `lib.sh`; `tests/confs/` com rulesets deployados na bancada | +| `patches/kernel/` | Conjunto de patches de core — hoje **vazio** (`p1-needs.txt` registra por que nenhum export foi necessário); ver [patches.md](patches.md) | + +## Caminho de dados (M2, como implementado hoje) + +O OpenBSD roda o PF em `IPL_NET`, em contexto de processo; nossas locks da +compat são sleepáveis, então o hook **nunca chama `pf_test()` em softirq**. O +pacote é furtado (`NF_STOLEN`) para uma workqueue ordenada (`pfk_netisr`), o +veredito roda em contexto de processo e é aplicado por **reinjeção**, com +marcação por direção em `skb->mark` para que o pacotão sobrevivente não seja +re-enfileirado pelo nosso próprio hook. + +``` + Linux 7.3.4, módulo pf.ko + NIC ──► netfilter (softirq — apenas enfileira) + PRE_ROUTING PF_IN ─┐ ┌─ LOCAL_OUT PF_OUT + POST_ROUTING PF_OUT ─┤ NF_STOLEN ├─ (roda DEPOIS do conntrack) + LOCAL_IN/FORWARD: pass-through ┘ POST_ROUTING: NF_IP_PRI_LAST-1 + │ (ref da skb + kmalloc pfk_pkt; + │ falha de alocação ⇒ NF_ACCEPT = fail-open) + ▼ + workqueue pfk_netisr (contexto de processo) + │ + pfk_m_wrap_skb(skb)──► shim struct mbuf + │ + pf_test(af, dir, ifp, &m) ◄── o engine real do OpenBSD 8.0: + │ scrub/normalize → lookup de estado + │ → regras/anchors/tabelas/quick + │ → inserção de estado → veredito + ▼ + pass ──► mark |= PFK_DONE_IN/OUT ──► reinjeta: PF_IN: netif_rx() (sobrevivente + da remontagem entra no stack) + PF_OUT: skb_dst()->output() + block/drop ──► kfree_skb() (+ RST/ICMP de retorno gerados pelo próprio PF) + fragmento em buffer ──► nossa ref é liberada; pf mantém o sobrevivente (m == NULL) +``` + +Plano de controle: `/dev/pf` (miscdev, modo 0600, escrita condicionada a +`CAP_NET_ADMIN`) → `pfk_ioctl_io()` copia a estrutura de parâmetros usando o +tamanho embutido na codificação `_IOR/_IOW` OpenBSD e despacha para o +`pfioctl()` portado (`pf_ioctl.c`) — a ABI DIOC* completa, como upstream. +`pfctl -e/-f/-s/-t` é o mesmo binário do OpenBSD, recompilado. + +## Filas, divert, pflog — comportamento observável hoje + +- **Filas**: o `pfctl` faz o parse de `queue`/`queue on` (sintaxe 8.0); no + kernel a API ifq é implementada por `compat/pfk_ifq.c` com **semântica + priq apenas**; o qdisc classful `pfq` e os back ends HFSC/FQ-CoDel chegam + em **M5**. `ifq.c`/`hfsc.c`/`fq_codel.c` estão staged e prontos para + compilar, mas ainda **não** estão na lista de objetos do módulo. +- **Divert**: `divert-to`/tag `divert` produzem o veredito `PF_DIVERT` do PF; + `divert_packet()` (`pfsrc/divert_sock.c`) hoje **descarta o pacote com um + `pr_warn` rate-limited**, porque a família de sockets AF_DIVERT ainda não + foi instanciada. O relay userspace `pdivert` existe e espera + `socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`. +- **pflog**: `pflog_packet()` retorna sucesso mas **não loga nada** (um + único `pr_warn "packet not logged"`); `if_pflog.c` está staged, back end + netlink em M4/M5. + +## Milestones (status por feature em feature-matrix.md) + +| Milestone | Escopo | Status (2026-11-11) | +|---|---|---| +| M0 | repo, scripts, build do kernel, VM liga com kernel próprio | concluído | +| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f`/`-s info` | concluído | +| M2 | núcleo de filtragem: gates VM de regras/estados/tabelas/anchors | **em andamento** — caminho de dados implementado, gates 01–04 ainda não todos verdes | +| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente | +| M4 | route-to/dup-to/fail-to, divert e2e | pendente | +| M5 | filas: back ends ifq (hfsc/fq_codel) + qdisc `pfq` | pendente | +| M6 | docs bilíngues completas, patchset arrumado, suíte inteira verde, tags de release | em andamento (este conjunto de docs) | + +## Próximas leituras + +[build.md](build.md) → [vm-lab.md](vm-lab.md) → [pf-guide.md](pf-guide.md) → +[feature-matrix.md](feature-matrix.md) → [caveats.md](caveats.md). diff --git a/docs/pt-BR/install.md b/docs/pt-BR/install.md new file mode 100644 index 0000000..4f02e87 --- /dev/null +++ b/docs/pt-BR/install.md @@ -0,0 +1,105 @@ +# Aplicando o conjunto de patches PF-on-Linux / guia de compilação + +Este documento é a versão em português de `docs/en/install.md`. + +Alvo: árvore de fontes do **Linux 7.3.4** + módulos out-of-tree `pf.ko` +(engine PF, família divert(4), filas ifq). O port é entregue como +**árvore de código-fonte + zero patches de core** (ver `patches/kernel/README.md`): +o módulo linka apenas símbolos que o kernel 7.3.4 puro já exporta. + +## 1. Pré-requisitos + +| Requisito | Por quê | +|---|---| +| gcc >= 15, GNU make, flex, bison, bc | build do kernel + pfctl | +| `glibc-static` (ou musl-static) | pfctl estático para as VMs guest | +| Fontes Linux 7.3.4 | alvo da compilação | +| `sys.tar.gz` + `src.tar.gz` do OpenBSD 8.0 | fontes portados (baixados e verificados por sha256; mirror primário openbsd.c3sl.ufpr.br, fallback cdn.openbsd.org) | +| root no host de build | `insmod` como root | + +## 2. Passos + +```sh +cd pf_port + +# 2.1 busca e verifica as fontes OpenBSD 8.0 (sha256 fixados em VERSIONS) +./scripts/fetch-sources.sh +./scripts/vendorize.sh # staging em _vendor/upstream + IDS.txt + +# 2.2 prepara o kernel (config + build + preparação p/ módulos externos) +KERNELDIR=/path/to/linux-7.3.4 ./scripts/build-kernel.sh +# - defconfig + virtio/EXT4 embutidos (boot direto do kernel na VM) +# - make bzImage modules_prepare +# - copia vmlinux.symvers -> Module.symvers (necessário para builds M=) + +# 2.3 aplica os patches de core (atualmente NENHUM é necessário) +./scripts/apply-patches.sh /path/to/linux-7.3.4 +# patches/kernel/series está vazio por projeto; se futuros milestones +# precisarem de exports, cada um entra como 000N-*.patch documentado em +# docs/pt-BR/patches.md. + +# 2.4 compila módulo e userland +./scripts/build-module.sh # -> module/pf.ko +make -C tools # -> tools/out/pfctl (estático) + +# 2.5 instala num sistema 7.3.4 rodando +sudo insmod module/pf.ko # cria /dev/pf (0600 root) +sudo cp tools/out/pfctl /usr/local/sbin/ +sudo pfctl -e # habilita +sudo pfctl -f /etc/pf.conf # sintaxe real do OpenBSD 8.0 +``` + +## 3. Verificação + +```sh +pfctl -s info # "Status: Enabled", contadores ao vivo +pfctl -nf /etc/pf.conf # checagem de parse apenas +dmesg | grep pfk # hooks registrados (data path via workqueue netisr) +``` + +Bancada de VMs (Alpine em libvirt): `./scripts/vm-create.sh`, +`./scripts/vm-deploy.sh`, `./scripts/vm-run-tests.sh` — ver `docs/pt-BR/vm-lab.md`. + +## 4. Remoção / reversão + +```sh +sudo pfctl -d; sudo rmmod pf +./scripts/unpatch.sh /path/to/linux-7.3.4 # desfaz patches futuros, se houver +``` + +## 5. O que "aplicar o patch" significa aqui + +Como a entrega é um módulo out-of-tree, "aplicar o conjunto" = passos +2.1–2.4; a árvore Linux intacta é apenas *configurada e compilada* +(virtio/EXT4 =y). Nenhum arquivo do kernel é modificado, salvo se um futuro +milestone acrescentar `patches/kernel/000N-*.patch` documentado (cada patch: +uma seção em docs/pt-BR/patches.md com o quê/por quê/impacto/reversão). + +## 6. Notas sobre os passos acima (2026-11-11) + +- O passo **2.3 é um no-op documentado hoje**: o conjunto de patches está + vazio e os scripts `scripts/apply-patches.sh` / `scripts/unpatch.sh` e o + arquivo `patches/kernel/series` ainda não existem na árvore (entram junto + com o primeiro patch real de core — ver [patches.md](patches.md)). Pode + pular 2.3 com segurança. +- O passo 2.4 compila apenas o `pfctl` e o verificador de ABI (alvos de + `make -C tools`: `pfctl abitest abi test clean`). `pdivert` existe como + arquivo-fonte sem alvo no Makefile por enquanto; `pflogd` ainda não foi + portado. +- Depois do 2.5, o que dá para usar de verdade é o núcleo de filtragem M2 + (pass/block/quick/keep state/tabelas/anchors); NAT/scrub/queues/divert/ + pflog dependem dos próximos milestones — consulte a + [matriz de features](feature-matrix.md) e leia [pf-guide.md](pf-guide.md) + antes de habilitar PF num caminho que você precisa manter vivo. + +## 7. Para onde ir agora + +| | | +|---|---| +| Arquitetura em uma página | [guide.md](guide.md) | +| Etapas de build em detalhe | [build.md](build.md) | +| Bancada de VMs de validação | [vm-lab.md](vm-lab.md) | +| Regras do dia a dia e avisos | [pf-guide.md](pf-guide.md) | +| Divergências conhecidas | [caveats.md](caveats.md) | +| Mecanismo de patches de core | [patches.md](patches.md) | +| Índice da documentação | [README.md](README.md) | diff --git a/docs/pt-BR/patches.md b/docs/pt-BR/patches.md new file mode 100644 index 0000000..7d12e6e --- /dev/null +++ b/docs/pt-BR/patches.md @@ -0,0 +1,75 @@ +# Conjunto de patches de core do Linux + +English: [docs/en/patches.md](../en/patches.md). + +## Status atual: VAZIO + +O port exige **zero modificações no core do Linux 7.3.4**. O design +(spec §5) previa um patch obrigatório — **P1: exportar símbolos usados pelo +módulo PF** — mas o compile-port do M1 provou que ele é desnecessário: o +modpost reportou **nenhum símbolo indefinido** para o `pf.ko`. A camada de +compat foi escrita contra APIs que o kernel puro já exporta +(`__inet_lookup_established`, `__inet_lookup_listener`, +`__inet6_lookup_established`, `inet6_lookup_listener`, `__udp4_lib_lookup`, +`__udp6_lib_lookup`, `ip_route_output_flow`/`ip6_route_output_flags` via +wrappers inline, `icmp_send`, …). A evidência e a lista exata estão em +`patches/kernel/p1-needs.txt`, que também traz uma **watch list**: por +exemplo, `tcp_v4_send_reset` só precisaria de export se helpers de RST de +userland forem usados. + +Portanto, `patches/kernel/` não contém nenhum `000N-*.patch` hoje, e aplicar +o conjunto (vazio) é um no-op. + +> Nota honesta: o install.md e o design mencionam `scripts/apply-patches.sh`, +> `scripts/unpatch.sh` e `patches/kernel/series`. Esses helpers **ainda não +> estão na árvore**; até entrarem, "aplicar o conjunto de patches" é, por +> definição, nada a fazer. Quando um patch real for necessário, o primeiro +> commit que o adicionar DEVE também adicionar os dois scripts e o arquivo +> `series` (ver as regras abaixo). + +## O mecanismo (como o conjunto deve funcionar) + +- Toda mudança no core do kernel é um arquivo independente + `patches/kernel/000N-descricao-curta.patch`, listado em ordem em + `patches/kernel/series`, aplicado à árvore pristina com ferramenta no + estilo `git apply` e revertido do mesmo jeito (`unpatch.sh` aplica ao + contrário). +- A árvore nunca pode ficar suja em silêncio: builds reproduzíveis aplicam, + compilam e registram o estado (`out/.config`, `out/kernelversion`). +- Regras obrigatórias para qualquer patch futuro: + 1. **Aditivo, zero mudança de comportamento** (por exemplo, linhas + `EXPORT_SYMBOL_GPL()` junto aos clusters de export existentes). Qualquer + coisa que altere lógica exige antes uma emenda ao design. + 2. Uma preocupação por patch, numerado em sequência — nunca editar um + patch já aceito; substitua com um novo número. + 3. Uma seção neste arquivo (nos dois idiomas) **antes** do patch entrar: + o quê/por quê/impacto/reversão + a evidência em VM. + 4. O patch tem de sobreviver a `make -C bzImage modules` a + partir de árvore limpa (CI da bancada: `vm-run-tests.sh` depois de um + boot novo). + +## Template para uma futura entrada `000N` + +Copie este bloco para `docs/en/patches.md` e `docs/pt-BR/patches.md` (mesma +âncora) ao adicionar `000N-*.patch`: + +``` +### 000N — + +- Arquivo: `patches/kernel/000N-.patch` +- Status: aplicado (kernel 7.3.4) / substituído por 000M +- Toca: , linhas adicionadas, 0 removidas +- O quê: +- Por quê: +- Impacto: +- Reversão: `./scripts/unpatch.sh ` (aplicação reversa); o módulo + passa a não linkar, com o símbolo faltante nomeado no modpost. +- Evidência: +``` + +## Histórico deste diretório + +| Data | Mudança | +|---|---| +| 2026-11-11 | P1 avaliado após o compile-port do M1 → **não necessário**; `p1-needs.txt` registra a evidência e a watch list. Conjunto permanece vazio. | diff --git a/docs/pt-BR/pf-guide.md b/docs/pt-BR/pf-guide.md new file mode 100644 index 0000000..d11dfdb --- /dev/null +++ b/docs/pt-BR/pf-guide.md @@ -0,0 +1,98 @@ +# Usando o PF-on-Linux no dia a dia + +English: [docs/en/pf-guide.md](../en/pf-guide.md). + +Este é o guia do "o que dá para rodar hoje". Ele segue a +[matriz de features](feature-matrix.md): o escopo do **M2 (núcleo de +filtragem: pass/block/quick/keep state/tabelas/anchors + habilitar/ +desabilitar)** tem evidência em VM; tudo além disso é COMPILES-untested ou +PENDING. Instalação/build ficam em [install.md](install.md) e +[build.md](build.md). + +## Ciclo diário + +```sh +sudo insmod module/pf.ko # cria /dev/pf +sudo pfctl -nf /etc/pf.conf # checagem de parse (só userland — sempre segura) +sudo pfctl -f /etc/pf.conf # carrega o ruleset +sudo pfctl -e # HABILITA — o tráfego passa a bater nas regras AGORA +sudo pfctl -s info # status + contadores +sudo pfctl -s states # listagem de estados (ver ressalva abaixo) +sudo pfctl -t blk -T add 10.0.1.10 # atualização de tabela ao vivo +sudo pfctl -a admin -f /etc/pf.d/admin.conf +sudo pfctl -d # desabilita +sudo rmmod pf # descarrega (regras/estados são liberados aqui) +``` + +> **Atenção — a ordem importa**: `-f` e depois `-e`. Habilitar sem uma regra `pass` +> que cubra o caminho de administração corta esse caminho — este port segue +> a política padrão de bloqueio do OpenBSD, e as rodadas do gate M2 +> *realmente derrubaram o ssh da VM de teste* assim (a bancada mantém console +> serial exatamente por isso). Num host remoto, libere a interface de +> gerência antes de habilitar qualquer coisa. + +## Um ruleset que funciona neste port hoje + +```pf +# /etc/pf.conf — apenas escopo M2. Os nomes de interface aqui são os da +# bancada (mgmt/lan/wan); troque pelos seus (eth0, enp1s0, …). + +# 1) mantenha você mesmo alcançável (faça isso primeiro, sempre) +pass in quick on mgmt +pass out quick + +# 2) núcleo de filtragem — comprovado hoje: quick, match de proto/porta, state, drop +table persist # blocklist viva (padrão do gate 03) +block in quick proto tcp from to any + +pass in quick on lan proto tcp from 10.0.1.0/24 to any port { 80 443 } keep state +block drop in quick on wan proto icmp from any to any + +# 3) anchors com precedência (padrão do gate 04) +anchor "admin" +``` + +Macros (`xif = "lan"`), `anchor "in/*"` aninhado, `label`, `tag`, +`set timeout/limit/state-policy/block-policy/skip` e o resto da gramática do +OpenBSD 8.0 **fazem parse** sem problema — o `make -C tools test` exercita +tudo isso offline — mas o *comportamento no kernel* da maioria das opções +`set` ainda não foi verificado em gate; trate como "carrega e reza" até a +matriz mudar. + +## O que evitar até os milestones chegarem + +| Sintaxe | Até | O que acontece hoje | +|---|---|---| +| `nat-to`, `rdr-to`, `binat-to` | M3 | As regras carregam (código do engine compilado), mas a tradução não está fiada/verificada — **não conte com NAT**; no laboratório o masquerade de saída ainda vem do iptables/nft. | +| `scrub (…)`, `set reassemble` | M3 | O `pf_norm.c` roda dentro do `pf_test` (compilado) — o comportamento com fragmentos forjados não foi testado; trate o resultado como não validado. | +| `divert-to`, tag `divert` | M4 (+ família de sockets conforme comentário no código) | Os pacotes casados são **descartados** com um aviso de kernel rate-limited. Nunca coloque isso num host em produção. | +| `route-to`, `dup-to`, `fail-to`, `reply-to` | M4 | Ainda não implementados como glue de roteamento Linux; as regras podem carregar, mas o comportamento é indefinido — evite. | +| `queue` / `queue on` / `pfctl -s queue` | M5 | Faz parse e carrega. A fila cai no fallback priq da camada `ifq` de compat; **nenhuma limitação de banda acontece até o qdisc `pfq` chegar**. | +| `log`, `log all`, `block log`, `pflogd` | M4 | Nada é logado (o stub avisa uma vez no load: `pfk: … packet not logged`). | +| `route-label …` | nunca (não-objetivo) | Rejeitado na camada de ioctl. | +| política por container (netns) | fora de escopo | O PF é uma instância global em `init_net`; network namespaces **não** enxergam o PF. | + +## Lendo o estado corretamente + +O `pfctl -s states` tem hoje uma anomalia de listagem/visibilidade sendo +corrigida no trabalho do gate M2 (fluxos de trânsito podem passar enquanto +nenhum estado é exibido). Até lá, use contadores (`pfctl -s info`) e +observação externa (resultado do ping/nc, `tcpdump`) como evidência +primária, exatamente como os gates fazem. + +## Sinais no log do kernel que você deve conhecer + +``` +pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok +pfk: netfilter hooks registered (10, netisr wq data path) +pfk: divert v4 port N: no divert socket layer (M6 pending), dropping +pfk: … packet not logged ← stub do pflog +``` + +Descarregar o módulo primeiro para os timers de purge e drena a workqueue +(um travamento de rmmod observado no lab foi corrigido assim) — mas a rodada +de *soak* (ciclo rmmod/insmod sob tráfego) ainda é um gate aberto, então +trate o unload como correto para uso administrativo e não comprovado para +automação de hot-reload. + +Lista completa das divergências de comportamento: [caveats.md](caveats.md). diff --git a/docs/pt-BR/vm-lab.md b/docs/pt-BR/vm-lab.md new file mode 100644 index 0000000..13f662e --- /dev/null +++ b/docs/pt-BR/vm-lab.md @@ -0,0 +1,122 @@ +# Bancada de validação em VMs (libvirt / Alpine) + +English: [docs/en/vm-lab.md](../en/vm-lab.md). + +Todos os comandos abaixo foram lidos dos scripts em `scripts/` (eles são a +fonte de verdade); a bancada já foi criada e executada durante M0–M2 — uma +rodada completa e verde da suíte é o gate do M2+ e ainda está em andamento +(ver §6). + +## 1. Onde ela mora + +| Host | Papel | +|---|---| +| laptop (este repo) | compila kernel/módulo/ferramentas; comanda tudo por ssh | +| **deb** — `root@192.168.1.242` | hipervisor libvirt; roda as 4 VMs Alpine `pftest-*` | + +A bancada não precisa de NAT para a internet; o único acesso externo é a +rede libvirt `default` (192.168.122.0/24, mgmt+DHCP) usada para o cloud-init +instalar pacotes e para o ssh dos testes. A `sms-vm` existente no deb nunca é +tocada. + +## 2. Topologia (fonte única: `lab/ips.env`) + +| VM | Papel | NICs | Kernel | +|---|---|---|---| +| `pftest-pf0` | filtro sob teste (roda o `pf.ko`) | `mgmt` (dhcp, rede default), `lan` 10.0.1.1/24, `wan` 10.0.2.1/24, `wan2` 10.0.3.1/24 | Alpine padrão via boot direto do kernel extraído da imagem; **ou** o `bzImage` 7.3.4 compilado com `--pf0-kernel=` (`root=/dev/vda2 ro console=ttyS0,115200n8`, sem initrd) | +| `pftest-cli` | cliente | `mgmt` dhcp (rotas desabilitadas), `lan` 10.0.1.10/24, gw 10.0.1.1 (pf0) | Alpine padrão | +| `pftest-srv` | servidor | `mgmt` dhcp (rotas desabilitadas), `wan` 10.0.2.20/24, gw 10.0.2.1 (pf0), 10.0.3.0/24 via gw2 | Alpine padrão | +| `pftest-gw2` | gateway do segundo caminho (ip_forward=1) | `wan2` 10.0.3.2/24, `wan` 10.0.2.2/24 (sem NIC mgmt) | Alpine padrão | + +Todas as VMs recebem `net.ipv4.ip_forward=1` e `rp_filter=0` via cloud-init, +senha de root `pftest` mais um par de chaves de laboratório injetado +(`/root/.ssh/pflab_ed` no deb, cópia no laptop para ProxyJump), e os pacotes +`iperf3 tcpdump python3 scapy bash iptables` (o nome no Alpine é `scapy`; a +imagem também precisa de `iptables` para os experimentos de masquerade do +pf0). + +Redes libvirt: `default` (mantida como está, iniciada+autostart para os +pacotes) e bridges isoladas `pf-lan` 10.0.1.0/24, `pf-wan` 10.0.2.0/24, +`pf-wan2` 10.0.3.0/24 — `forward="none"`, sem DHCP, mais IPs de bridge no +host `10.0.x.254` no deb para que o deb consiga ssh/icmp na NIC de dados de +cada guest. Dimensionamento: 2 vCPU / 1 GB por VM. Os discos são qcow2 por +VM com **backing** na imagem compartilhada imutável +`/var/lib/libvirt/images/alpine-base.qcow2` (sincronizada de +`_vendor/images/alpine-3.22-x86_64-cloud.qcow2` por sha256). + +> Nota de boot: a imagem cloud do Alpine não é inicializável em BIOS/UEFI no +> QEMU como entregue (ESP vazia), então *todas* as VMs usam boot direto do +> kernel extraído da própria imagem; `--pf0-kernel=` apenas troca o kernel +> do pf0 pelo compilado. + +## 3. Recriar / deployar / destruir + +```sh +./scripts/vm-create.sh # idempotente: derruba e recria domínios + seeds pftest-* +./scripts/vm-create.sh --pf0-kernel=out/linux-7.3.4-bzImage # pf0 sobe com o kernel compilado (T6) +./scripts/vm-create.sh --recreate-base # força re-sync da imagem base +./scripts/vm-deploy.sh # laptop→deb ($DEPLOY_DIR)→pf0:/opt/pf: + # out/*bzImage, module/*.ko, tools/out/* +./scripts/vm-destroy.sh # domínios, discos, seeds, redes pf-* e fragmentos ssh +./scripts/vm-destroy.sh --purge-base --purge-keys # + imagem base + chaves do lab +``` + +O `vm-create.sh` termina validando `ssh pf0 'ip -br a'` e deixa todas as VMs +ligadas. Os aliases de ssh do laptop (`pf0|cli|srv|gw2`, ProxyJump via deb) +são gerados em `~/.ssh/config.d/pflab`. + +## 4. Rodando a suíte + +```sh +./scripts/vm-run-tests.sh # ou: make test +``` + +Determinístico por projeto: **antes de cada gate** o runner destrói e +reinicia o `pftest-pf0` (delta de disco e módulo zerados), espera o ssh no +endereço DHCP de mgmt `192.168.122.8`, re-deploya `pf.ko`/`pfctl`/`*.conf` no +guest e então roda o script do gate a partir do **laptop** (os gates falam +com as VMs através do deb via ssh aninhado — `tests/vm/lib.sh`). A saída de +cada gate é gravada em `out/test-logs/.sh.log`; o runner imprime +`PASS ` / `FAIL (rc=...)` por teste, um resumo `PASS=n FAIL=m` +e sai com código diferente de zero se algo falhou. + +## 5. O que cada gate prova hoje + +| Gate | Cenário (verificado com tráfego real, não com código de saída) | +|---|---| +| `01-state.sh` | `pass in/out quick proto icmp keep state` → cli pinga srv **através** do pf0 (trânsito); depois o pf0 lista um estado icmp via `pfctl -s states` | +| `02-block.sh` | `block drop in quick proto icmp` → o ping do cli falha; `block return-rst in quick proto tcp … port 8080` → o `nc` do cli para srv:8080 é recusado (RST), checado pelo código de saída do nc | +| `03-tables.sh` | `table { 10.0.1.10 }` + `block in quick from ` → `pfctl -t blk -T show` lista; o ping do cli é bloqueado; `-T delete` ao vivo libera o fluxo | +| `04-anchors.sh` | regra de anchor (`-a restricted -f`) liberando icmp é avaliada **antes** do `block` do ruleset principal (posicionamento top/quick); `pfctl -s Anchors` lista o anchor | + +Gates planejados (documento de plano, T11–T18): 05-nat, 06-rdr, 07-binat, +08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (tolerância +de ±15% no rate do iperf), 13-pflog, 14-soak (reload sob tráfego + ciclo de +rmmod + kmemleak). IPv6 é smoke test para state/block/nat66/scrub. O +`bench-basic.sh` (planejado, ainda não está na árvore) é informativo; não +existe gate numérico de performance. + +## 6. Status atual da suíte (2026-11-11, com honestidade) + +Os gates 01–04 do M2 existem e rodam, mas **ainda não estão todos verdes** — +o caminho de dados (modelo netisr/workqueue) e a visibilidade de estados +estão sendo corrigidos no trabalho do gate M2. A última rodada capturada +(`out/test-logs/`) mostra o estado misto: ping de trânsito OK / listagem de +estado falhando no 01, icmp bloqueado OK no 02, resultados parciais no 03, 04 +abortado. Trate "VERIFIED-in-VM" na +[matriz de features](feature-matrix.md) como reservado a gates que passam +numa rodada completa e limpa; nada além do M1 é declarado VERIFIED até lá. + +## 7. Inspeção manual + +```sh +ssh pf0 # via alias do laptop (ProxyJump deb) +insmod /opt/pf/pf.ko +/opt/pf/pfctl -e +/opt/pf/pfctl -f /tmp/g1.conf +/opt/pf/pfctl -s info ; /opt/pf/pfctl -s states -v +dmesg | grep pfk +``` + +Artefatos de rodadas completas futuras (log + pcaps) devem ir junto com o +release no Gitea (spec §7). diff --git a/module/include/compat/sys/refcnt.h b/module/include/compat/sys/refcnt.h index 9c7a3d9..81b3d69 100644 --- a/module/include/compat/sys/refcnt.h +++ b/module/include/compat/sys/refcnt.h @@ -23,10 +23,14 @@ static inline bool refcnt_take_not0(struct refcnt *r) { return refcount_inc_not_zero(&r->rc); } +/* + * OpenBSD semantics (sys/kern/kern_synch.c): refcnt_rele() returns 1 when + * the last reference is dropped and 0 otherwise. PF_REF_RELE() and the + * pf_ruleset anchor release rely on that polarity. + */ static inline unsigned int refcnt_rele(struct refcnt *r) { - return (unsigned int)refcount_dec_and_test(&r->rc) ? - refcnt_value(r) : refcnt_value(r) + 1; + return (unsigned int)refcount_dec_and_test(&r->rc) ? 1 : 0; } static inline unsigned int refcnt_rele_wake(struct refcnt *r) { diff --git a/module/include/pfk_api.h b/module/include/pfk_api.h index 9a8e15a..70761b5 100644 --- a/module/include/pfk_api.h +++ b/module/include/pfk_api.h @@ -9,5 +9,11 @@ int pfk_enable(int on); void pfk_status(struct pf_status *s); int pfk_hooks_register(void); void pfk_hooks_unregister(void); +/* AF_DIVERT family (module-internal socket protocol, PF_DIVERT=32) */ +int pfk_divert_sock_register(void); +void pfk_divert_sock_unregister(void); +void divert_packet(struct mbuf *, int, u_int16_t); +void divert6_packet(struct mbuf *, int, u_int16_t); +void pfk_hooks_unregister(void); #endif diff --git a/module/pfsrc/.divert_sock.o.cmd b/module/pfsrc/.divert_sock.o.cmd index ebbdfd0..1394ad7 100644 --- a/module/pfsrc/.divert_sock.o.cmd +++ b/module/pfsrc/.divert_sock.o.cmd @@ -1828,6 +1828,7 @@ deps_pfsrc/divert_sock.o := \ include/compat/sys/rwlock.h \ include/compat/sys/mutex.h \ include/compat/sys/pclock.h \ + include/pfk_api.h \ pfsrc/divert_sock.o: $(deps_pfsrc/divert_sock.o) diff --git a/module/pfsrc/divert_sock.c b/module/pfsrc/divert_sock.c index eac948a..1e19d43 100644 --- a/module/pfsrc/divert_sock.c +++ b/module/pfsrc/divert_sock.c @@ -1,33 +1,497 @@ /* - * PF-on-Linux divert entry points used by pf.c (ISC 2026). + * PF-on-Linux: AF_DIVERT socket family (ISC 2026) * - * BSD semantics (OpenBSD netinet/ip_divert.c): divert_packet()/divert6_packet() - * find the AF_DIVERT pcb bound to `divert_port`, queue the packet to it and - * consume the mbuf; when NO divert socket is attached, OpenBSD drops the - * packet (m_freem) — that is exactly the observable behaviour implemented - * here until the pfdiv socket layer is instantiated for Linux (milestone - * M6); a rate-limited kernel log records the event so drops are never - * silent. + * Implements OpenBSD's divert(4) as a runtime-registered protocol family + * inside the PF module: PF_DIVERT = 32 (BSD value; the unused legacy + * PF_IUCV slot on Linux, and NPROTO = AF_MAX = 45 admits it), + * IPPROTO_DIVERT = 254. No core patch is required — families are + * registered with sock_register() at module init. * - * The upstream BSD socket layer is staged in pfsrc/ip_divert.c and becomes - * the real back end in M6; these wrappers keep the same call signature - * (struct mbuf *, int dir, u_int16_t divert_port). + * Userspace ABI (see docs/en/divert-wireformat.md): + * fd = socket(PF_DIVERT, SOCK_RAW, IPPROTO_DIVERT); + * struct sockaddr_in b = { .sin_family = AF_INET, .sin_port = port }; + * bind(fd, &b); # binds the divert "port" + * n = recv(fd, pkt, ...) # full IP packet incl. header + * send(fd, pkt, n) # reinject per BSD semantics + * + * Re-injection mirrors OpenBSD netinet/ip_divert.c:divert_output(): + * type PF_DIVERT_TO -> local delivery as if arrived on the recorded + * interface (netif_rx) + * plain divert tag -> route again (dst output), matching BSD ip_output + * + * Semantics notes (PFLINUX, docs/porting-notes.md): + * - queue selection scans the bound list under pfk_divert_lock; + * - full-socket drop counts divs_fullsock like BSD; + * - no socket bound to the port: divs_noport + drop (BSD identical). */ #include -#include +#include +#include + +#define PF_DIVERT_FAMILY 32 /* BSD PF_DIVERT */ +#ifndef IPPROTO_DIVERT +#define IPPROTO_DIVERT 254 +#endif + +/* control message codes reported to userspace (sendmsg accepts them back) */ +#define PFDIVERT_CMSG_IFINDEX 1 +#define PFDIVERT_CMSG_DIR 2 +#define PFDIVERT_CMSG_TYPE 3 + +struct pfk_divert_pkt { + struct list_head node; + struct sk_buff *skb; + int dir; + int type; + u16 port; + int ifindex; +}; + +struct pfk_divert_stat { + u64 divs_ipackets; + u64 divs_noport; + u64 divs_fullsock; + u64 divs_opackets; + u64 divs_errors; +}; + +static const struct proto_ops pfk_divert_ops; + +struct pfk_divert_sock { + struct list_head link; /* global bound list */ + struct list_head q; + spinlock_t lock; + u16 port; + u32 qlen; + wait_queue_head_t wq; + u32 qspace; + struct sock *sk; + struct pfk_divert_stat stats; +}; + +static LIST_HEAD(pfk_divert_list); +static DEFINE_SPINLOCK(pfk_divert_lock); +static struct pfk_divert_stat pfk_divert_global; +static struct proto pfk_divert_proto = { + .name = "PFK_DIVERT", + .owner = THIS_MODULE, + .obj_size = sizeof(struct sock), +}; + +static inline struct pfk_divert_sock * +pfk_ds(const struct sock *sk) +{ + return sk ? (struct pfk_divert_sock *)sk->sk_user_data : NULL; +} + +static void +pfk_divert_pkt_free(struct pfk_divert_pkt *dp) +{ + if (!dp) + return; + if (dp->skb) + kfree_skb(dp->skb); + kfree(dp); +} + +/* PF engine entry points (called from the ported pf.c) */ +static int +pfk_divert_queue(struct sk_buff *skb, int dir, int type, u16 port) +{ + struct pfk_divert_sock *ds; + struct pfk_divert_pkt *dp; + unsigned long flags; + + dp = kmalloc(sizeof(*dp), GFP_ATOMIC); + if (!dp) { + kfree_skb(skb); + return ENOMEM; + } + dp->skb = skb; + dp->dir = dir; + dp->type = type; + dp->port = port; + dp->ifindex = skb->dev ? skb->dev->ifindex : 0; + + spin_lock_irqsave(&pfk_divert_lock, flags); + ds = NULL; + list_for_each_entry(ds, &pfk_divert_list, link) { + if (ds->port == port) + break; + } + if (&ds->link == &pfk_divert_list) { + pfk_divert_global.divs_noport++; + spin_unlock_irqrestore(&pfk_divert_lock, flags); + pr_info_ratelimited("pfk: divert port %u: no socket bound\n", + port); + pfk_divert_pkt_free(dp); + return ENOENT; + } + if (ds->qlen >= ds->qspace) { + ds->stats.divs_fullsock++; + pfk_divert_global.divs_fullsock++; + spin_unlock_irqrestore(&pfk_divert_lock, flags); + pfk_divert_pkt_free(dp); + return ENOBUFS; + } + spin_lock(&ds->lock); + list_add_tail(&dp->node, &ds->q); + ds->qlen++; + ds->stats.divs_ipackets++; + spin_unlock(&ds->lock); + spin_unlock_irqrestore(&pfk_divert_lock, flags); + + wake_up_interruptible(&ds->wq); + return 0; +} + +extern struct pf_divert *pf_find_divert(struct mbuf *); void divert_packet(struct mbuf *m, int dir, u_int16_t divert_port) { - pr_warn_ratelimited("pfk: divert v4 port %u: no divert socket layer (M6 pending), dropping\n", - ntohs(divert_port)); + struct sk_buff *skb; + int type = PF_DIVERT_TO; + + if (!m) + return; + { + struct pf_divert *dv = pf_find_divert(m); + + if (dv) + type = dv->type; + } + skb = m_to_skb(m); + m->skb = NULL; /* ownership moves into the divert layer */ m_freem(m); + pfk_divert_queue(skb, dir, type, divert_port); } void divert6_packet(struct mbuf *m, int dir, u_int16_t divert_port) { - pr_warn_ratelimited("pfk: divert v6 port %u: no divert socket layer (M6 pending), dropping\n", - ntohs(divert_port)); - m_freem(m); + divert_packet(m, dir, divert_port); +} + +/* ---------------- socket ops ---------------- */ + +static int +pfk_divert_create(struct net *net, struct socket *sock, int protocol, int kern) +{ + struct sock *sk; + struct pfk_divert_sock *ds; + + if (protocol != IPPROTO_DIVERT && protocol != 0) + return -EPROTONOSUPPORT; + if (sock->type != SOCK_RAW) + return -ESOCKTNOSUPPORT; + + ds = kzalloc(sizeof(*ds), GFP_KERNEL); + if (!ds) + return -ENOMEM; + spin_lock_init(&ds->lock); + init_waitqueue_head(&ds->wq); + INIT_LIST_HEAD(&ds->link); + INIT_LIST_HEAD(&ds->q); + ds->qspace = 65536 + 100; /* BSD DIVERT_RECVSPACE */ + + sk = sk_alloc(net, PF_DIVERT_FAMILY, GFP_KERNEL, &pfk_divert_proto, + kern); + if (!sk) { + kfree(ds); + return -ENOMEM; + } + sock_init_data(sock, sk); + sk->sk_user_data = ds; + ds->sk = sk; + sock->state = SS_UNCONNECTED; + sock->ops = &pfk_divert_ops; /* forward decl below via struct */ + return 0; +} + +static int +pfk_divert_bind(struct socket *sock, struct sockaddr_unsized *uaddr, + int addr_len) +{ + struct pfk_divert_sock *ds = pfk_ds(sock->sk); + unsigned long flags; + + if (!ds) + return -EINVAL; + if (addr_len < (int)sizeof(struct sockaddr_in)) + return -EINVAL; + + ds->port = ntohs(((struct sockaddr_in *)uaddr)->sin_port); + if (ds->port == 0) + return -EADDRNOTAVAIL; /* divert port 0 not allowed */ + + spin_lock_irqsave(&pfk_divert_lock, flags); + if (!list_empty(&ds->link)) { + spin_unlock_irqrestore(&pfk_divert_lock, flags); + return -EADDRINUSE; + } + list_add_tail(&ds->link, &pfk_divert_list); + spin_unlock_irqrestore(&pfk_divert_lock, flags); + return 0; +} + +static int +pfk_divert_recvmsg(struct socket *sock, struct msghdr *msg, size_t len, + int flags) +{ + struct pfk_divert_sock *ds = pfk_ds(sock->sk); + struct pfk_divert_pkt *dp; + struct sk_buff *skb; + unsigned long iflags; + int copied; + + if (!ds) + return -EINVAL; + if (msg_data_left(msg) == 0) + return -EINVAL; + + for (;;) { + spin_lock_irqsave(&ds->lock, iflags); + if (!list_empty(&ds->q)) { + dp = list_first_entry(&ds->q, struct pfk_divert_pkt, + node); + list_del_init(&dp->node); + ds->qlen--; + spin_unlock_irqrestore(&ds->lock, iflags); + goto have; + } + spin_unlock_irqrestore(&ds->lock, iflags); + if (flags & MSG_DONTWAIT || + READ_ONCE(sock->sk->sk_shutdown) != 0) + return -EAGAIN; + wait_event_interruptible(ds->wq, + !list_empty(&ds->q) || + READ_ONCE(sock->sk->sk_shutdown) != 0); + if (signal_pending(current)) + return -ERESTARTSYS; + } + +have: + skb = dp->skb; + copied = min_t(size_t, len, skb->len); + if (skb_copy_datagram_msg(skb, 0, msg, copied)) { + pfk_divert_pkt_free(dp); + return -EFAULT; + } + /* metadata as control data (BSD equivalent: sockaddr_div) */ + { + struct cmsghdr *cm = CMSG_FIRSTHDR(msg); + + if (cm && msg->msg_controllen >= sizeof(struct cmsghdr) + sizeof(int)) { + msg->msg_controllen = 0; /* busybox-safe default */ + cm = NULL; + } + if (cm) { + int ifindex = dp->ifindex; + + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = PFDIVERT_CMSG_IFINDEX; + cm->cmsg_len = CMSG_LEN(sizeof(int)); + memcpy(CMSG_DATA(cm), &ifindex, sizeof(ifindex)); + msg->msg_controllen = CMSG_SPACE(sizeof(int)); + } + } + sock_net_set(sock->sk, &init_net); + ds->stats.divs_opackets++; + pfk_divert_pkt_free(dp); + return copied; +} + +/* + * sendmsg: userspace hands the (possibly rewritten) packet back. The + * ip header inside the bytes decides the flow; for PF_DIVERT_TO we inject + * as input on the socket's last-recorded interface, otherwise we route. + */ +static int +pfk_divert_sendmsg(struct socket *sock, struct msghdr *msg, size_t len) +{ + struct pfk_divert_sock *ds = pfk_ds(sock->sk); + struct sk_buff *skb; + struct pf_ip *ip; + int ifindex = 0; + int type = PF_DIVERT_TO; + struct cmsghdr *cm; + + if (!ds) + return -EINVAL; + if (len > 65535) + return -EMSGSIZE; + + for (cm = CMSG_FIRSTHDR(msg); cm; cm = CMSG_NXTHDR(msg, cm)) { + if (cm->cmsg_level != SOL_SOCKET) + continue; + if (cm->cmsg_type == PFDIVERT_CMSG_IFINDEX && + cm->cmsg_len >= CMSG_LEN(sizeof(int))) + memcpy(&ifindex, CMSG_DATA(cm), sizeof(int)); + if (cm->cmsg_type == PFDIVERT_CMSG_TYPE && + cm->cmsg_len >= CMSG_LEN(sizeof(int))) + memcpy(&type, CMSG_DATA(cm), sizeof(int)); + } + + skb = alloc_skb(len + NET_SKB_PAD + NET_IP_ALIGN, GFP_KERNEL); + if (!skb) + return -ENOMEM; + skb_reserve(skb, NET_SKB_PAD + NET_IP_ALIGN); + if (memcpy_from_msg(skb_put(skb, len), msg, len)) { + kfree_skb(skb); + return -EFAULT; + } + skb->protocol = htons(ETH_P_IP); + + if (type == PF_DIVERT_TO) { + /* deliver locally as if arrived on ifindex (default: last + * ingress iface seen by this socket's queue) */ + struct net_device *dev = ifindex > 0 ? + dev_get_by_index(&init_net, ifindex) : NULL; + + if (dev) { + skb->dev = dev; + dev_put(dev); + skb_reset_network_header(skb); + /* netif_rx wants data at L3 */ + netif_rx(skb); + return (int)len; + } + /* no device: route below */ + } + + /* plain divert: route again (BSD divert_output -> ip_output) */ + if (pskb_may_pull(skb, sizeof(*ip))) { + struct flowi4 fl4 = { }; + struct dst_entry *d; + + ip = (struct pf_ip *)skb->data; + fl4.daddr = ip->ip_dst.s_addr; + fl4.saddr = ip->ip_src.s_addr; + { + struct rtable *rt = ip_route_output_key(&init_net, + &fl4); + + if (IS_ERR(rt)) + rt = NULL; + d = rt ? &rt->dst : NULL; + } + if (d) { + skb_dst_set(skb, d); + skb_reset_network_header(skb); + if (!skb->dev) + skb->dev = skb_dst(skb)->dev; + d->output(&init_net, NULL, skb); + return (int)len; + } + } + kfree_skb(skb); + ds->stats.divs_errors++; + return -EHOSTUNREACH; +} + +static int +pfk_divert_release(struct socket *sock) +{ + struct sock *sk = sock->sk; + struct pfk_divert_sock *ds; + unsigned long flags; + + if (!sk) + return 0; + ds = pfk_ds(sk); + if (ds) { + spin_lock_irqsave(&pfk_divert_lock, flags); + if (!list_empty(&ds->link)) + list_del_init(&ds->link); + spin_unlock_irqrestore(&pfk_divert_lock, flags); + while (!list_empty(&ds->q)) { + struct pfk_divert_pkt *dp = list_first_entry(&ds->q, + struct pfk_divert_pkt, node); + + list_del_init(&dp->node); + pfk_divert_pkt_free(dp); + } + kfree(ds); + sk->sk_user_data = NULL; + } + sock_orphan(sk); + sock_put(sk); + return 0; +} + +static __poll_t +pfk_divert_poll(struct file *file, struct socket *sock, + poll_table *wait) +{ + struct pfk_divert_sock *ds = pfk_ds(sock->sk); + __poll_t mask = 0; + + if (ds) + poll_wait(file, &ds->wq, wait); + else + sock_poll_wait(file, sock, wait); + if (ds && !list_empty(&ds->q)) + mask |= EPOLLIN | EPOLLRDNORM; + return mask; +} + +static int +pfk_divert_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg) +{ + struct pfk_divert_sock *ds = pfk_ds(sock->sk); + + if (!ds) + return -EINVAL; + switch (cmd) { + case 3: { /* DIVERTCTL_STATS-ish: export queue stats */ + struct pfk_divert_stat st; + unsigned long flags; + + spin_lock_irqsave(&ds->lock, flags); + st = ds->stats; + spin_unlock_irqrestore(&ds->lock, flags); + if (copy_to_user((void __user *)arg, &st, sizeof(st))) + return -EFAULT; + return 0; + } + default: + return -ENOIOCTLCMD; + } +} + +static const struct proto_ops pfk_divert_ops = { + .family = PF_DIVERT_FAMILY, + .owner = THIS_MODULE, + .release = pfk_divert_release, + .bind = pfk_divert_bind, + .connect = sock_no_connect, + .socketpair = sock_no_socketpair, + .accept = sock_no_accept, + .getname = sock_no_getname, + .poll = pfk_divert_poll, + .ioctl = pfk_divert_ioctl, + .listen = sock_no_listen, + .shutdown = sock_no_shutdown, + .sendmsg = pfk_divert_sendmsg, + .recvmsg = pfk_divert_recvmsg, + .mmap = sock_no_mmap, +}; + +static const struct net_proto_family pfk_divert_family = { + .family = PF_DIVERT_FAMILY, + .create = pfk_divert_create, + .owner = THIS_MODULE, +}; + +int +pfk_divert_sock_register(void) +{ + return sock_register(&pfk_divert_family); +} + +void +pfk_divert_sock_unregister(void) +{ + sock_unregister(PF_DIVERT_FAMILY); } diff --git a/module/pfsrc/pf_table.c b/module/pfsrc/pf_table.c index 32d0fac..403f149 100644 --- a/module/pfsrc/pf_table.c +++ b/module/pfsrc/pf_table.c @@ -360,6 +360,8 @@ pfr_add_addrs(struct pfr_table *tbl, struct pfr_addr *addr, int size, else ke->pfrke_fb = PFR_FB_NONE; } + pr_info("pfkD: add_addrs ke af=%d p=%p q=%p\n", + ke->pfrke_af, (void *)p, (void *)q); if (p == NULL && q == NULL) { if (pfr_route_kentry(tmpkt, ke)) { /* defer destroy after feedback is processed */ @@ -1931,6 +1933,9 @@ pfr_ina_define(struct pfr_table *tbl, struct pfr_addr *addr, int size, if (error) return (error); rs = pf_find_ruleset(tbl->pfrt_anchor); + pr_info("pfkD: ina_define name=%s anchor=%s size=%d flags=%x rs=%p topen=%d tticket=%u ticket=%u\n", + tbl->pfrt_name, tbl->pfrt_anchor, size, flags, (void *)rs, + rs ? rs->topen : -1, rs ? rs->tticket : 0, ticket); if (rs == NULL || !rs->topen || ticket != rs->tticket) return (EBUSY); tbl->pfrt_flags |= PFR_TFLAG_INACTIVE; @@ -1978,12 +1983,16 @@ _skip: senderr(EFAULT); if (pfr_validate_addr(&ad)) senderr(EINVAL); + pr_info("pfkD: ina_define addr[%d] af=%d net=%d flags=%llx ifindex=%d\n", + i, ad.pfra_af, ad.pfra_net, (unsigned long long)ad.pfra_flags, + ad.pfra_ifindex); if (pfr_lookup_addr(shadow, &ad, 1) != NULL) continue; p = pfr_create_kentry(&ad); if (p == NULL) senderr(ENOMEM); if (pfr_route_kentry(shadow, p)) { + pr_info("pfkD: ina_define route failed\n"); pfr_destroy_kentry(p); continue; } @@ -2000,6 +2009,8 @@ _skip: pfr_insert_ktables(&tableq); shadow->pfrkt_cnt = (flags & PFR_FLAG_ADDRSTOO) ? xaddr : NO_ADDRESSES; + pr_info("pfkD: ina_define done xadd=%d xaddr=%d shadow_cnt=%d\n", + xadd, xaddr, shadow->pfrkt_cnt); kt->pfrkt_shadow = shadow; } else { pfr_clean_node_mask(shadow, &addrq); @@ -2066,7 +2077,13 @@ pfr_ina_commit(struct pfr_table *trs, u_int32_t ticket, int *nadd, return (EBUSY); SLIST_INIT(&workq); + pr_info("pfkD: ina_commit name=%s anchor=%s ticket=%u\n", + trs->pfrt_name, trs->pfrt_anchor, ticket); RB_FOREACH(p, pfr_ktablehead, &pfr_ktables) { + pr_info("pfkD: ina_commit cand name=%s flags=%x inactive=%d shadow=%p\n", + p->pfrkt_t.pfrt_name, p->pfrkt_flags, + !!(p->pfrkt_flags & PFR_TFLAG_INACTIVE), + (void *)p->pfrkt_shadow); if (!(p->pfrkt_flags & PFR_TFLAG_INACTIVE) || pfr_skip_table(trs, p, 0)) continue; @@ -2098,6 +2115,10 @@ pfr_commit_ktable(struct pfr_ktable *kt, time_t tzero) struct pfr_ktable *shadow = kt->pfrkt_shadow; int nflags; + pr_info("pfkD: commit_ktable name=%s flags=%x shadow_cnt=%d\n", + kt->pfrkt_t.pfrt_name, kt->pfrkt_flags, + shadow ? shadow->pfrkt_cnt : -1); + if (shadow->pfrkt_cnt == NO_ADDRESSES) { if (!(kt->pfrkt_flags & PFR_TFLAG_ACTIVE)) pfr_clstats_ktable(kt, tzero, 1); diff --git a/module/pfsrc/pfk_mod.c b/module/pfsrc/pfk_mod.c index 3245f3f..257fac1 100644 --- a/module/pfsrc/pfk_mod.c +++ b/module/pfsrc/pfk_mod.c @@ -96,6 +96,8 @@ static struct miscdevice pfk_dev = { extern void pf_status_init(void); extern int pfattach(int); +extern int pfk_divert_sock_register(void); +extern void pfk_divert_sock_unregister(void); static void pfk_pfi_event(int added, struct pfk_ifnet *ifp) @@ -134,15 +136,20 @@ static int __init pfk_init(void) pfattach(0); pfk_if_set_event_cb(pfk_pfi_event); pfk_if_iterate(pfk_pfi_attach_one); - err = pfk_hooks_register(); + err = pfk_divert_sock_register(); if (err) goto err_if; + err = pfk_hooks_register(); + if (err) + goto err_divert; err = misc_register(&pfk_dev); if (err) goto err_hooks; pr_info("pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)\n"); return 0; err_hooks: + pfk_divert_sock_unregister(); +err_divert: pfk_hooks_unregister(); err_if: pfk_if_exit(); @@ -161,6 +168,7 @@ static void __exit pfk_exit(void) * self-rearms (pf_purge re-arms on every run) and would keep the * workqueues non-empty forever (observed rmmod hang). */ misc_deregister(&pfk_dev); + pfk_divert_sock_unregister(); pfk_net_lock(); pf_status.running = 0; pfk_net_unlock(); diff --git a/tests/vm/01-state.sh b/tests/vm/01-state.sh index e7914a2..1a9215c 100755 --- a/tests/vm/01-state.sh +++ b/tests/vm/01-state.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # M2 gate 1: stateful transit + state visibility set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh -pfensure +pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true' pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null; echo READY' | grep -q READY || { bad load; finish; } cat > /tmp/g1.conf <<'C' pass in quick proto icmp keep state diff --git a/tests/vm/02-block.sh b/tests/vm/02-block.sh index e811533..0a1d306 100755 --- a/tests/vm/02-block.sh +++ b/tests/vm/02-block.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # M2 gate 2: block drop + block return-rst set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh -pfensure; pf0 '/opt/pf/pfctl -e' >/dev/null +pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'; pf0 '/opt/pf/pfctl -e' >/dev/null cat > /tmp/g2a.conf <<'C' block drop in quick proto icmp from any to any pass out quick diff --git a/tests/vm/03-tables.sh b/tests/vm/03-tables.sh index 3eb63e2..572ac3d 100755 --- a/tests/vm/03-tables.sh +++ b/tests/vm/03-tables.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # M2 gate 3: pfr tables via pfctl -t set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh -pfensure; pf0 '/opt/pf/pfctl -e' >/dev/null +pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'; pf0 '/opt/pf/pfctl -e' >/dev/null cat > /tmp/g3.conf <<'C' table { 10.0.1.10 } block in quick proto icmp from to any diff --git a/tests/vm/04-anchors.sh b/tests/vm/04-anchors.sh index cd850cd..8154261 100755 --- a/tests/vm/04-anchors.sh +++ b/tests/vm/04-anchors.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # M2 gate 4: anchors + quick precedence set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh -pfensure; pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null +pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'; pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null cat > /tmp/g4main.conf <<'C' anchor "restricted" block drop in quick proto icmp from any to any