5 Commits
Author SHA1 Message Date
lgcosta 0a8ba69340 docs: record the stress findings and the 16-gate suite
caveats §13 documents the capacity ceiling the stress test found: the
workqueue backlog grows with the offered rate and OOM-panics a 1 GB guest
above ~1 Gbit/s. Gate count, suite status and the doc indexes move to
16/16 green.
2026-11-12 20:11:12 -03:00
lgcosta 928322edf7 feat(debian13): add host prep script and implementation manual
The manual is the from-zero path on Debian 13 (trixie): packages, the
7.3.4 kernel tree, pf.ko, the userland, the libvirt lab, the gate suite,
the stress test and running PF on the host itself. Every command in it was
executed on the lab's Debian 13.7 host, including the toolchain report
and the kernel.org checksum.

Trixie ships gcc 14 and no gcc-15; the kernel and pf.ko build with gcc
14 unpatched, so the manual documents that instead of the gcc >= 15 the
generic install guide asks for.
2026-11-12 20:11:09 -03:00
lgcosta 45ba489672 test(vm): add stress gate 16 and its report runner
Gate 16 asserts invariants under load: states tracked under concurrent
connections and drained again, 200 ruleset reloads without failure, 2000
table entries flushed back to zero, 2 MB intact through NAT under a
concurrent stream, no kernel faults. Throughput is measured and reported
rather than gated, and the traffic phases run at a bounded rate because
the PF workqueue backlog exhausts a 1 GB guest above ~1 Gbit/s.

The runner writes out/stress/<timestamp>/report.md and captures the guest
serial console so a panic stays diagnosable.
2026-11-12 20:11:05 -03:00
lgcosta 131672d6ce fix(build): resolve the kernel tree in one place
kernel-config.sh looked for the tree two levels up and build-kernel.sh
did not pass on the tree it had resolved, so a plain
./scripts/build-kernel.sh could configure one tree and build another.
Both resolve <repo>/../linux-7.3.4.
2026-11-12 20:11:02 -03:00
lgcosta dff2185b20 Docs: bring the documentation in line with the code
- Demo docs replaced by the screencast docs (single source of truth): the
  legacy demo-session.sh/demo-record.py and the v0.9 renderer were removed
  instead of being kept as a second convention.
- feature-matrix: stale 'M2-in-progress' vocabulary gone; the rows the 15
  gates prove now say VERIFIED-in-VM and name the gate; af-to is now an
  explicit non-goal row (INET6 0 in this build).
- guide: the queues/divert/pflog sections described a state that no longer
  exists (priq-only, divert dropping packets, pflog logging nothing); they
  now describe the shipped behaviour.
- vm-lab: the 15 real gates with what each proves, 15/15 green, and the
  pf0 -> cli/srv/gw2 ssh setup the screencasts rely on.
- install/build: pdivert and pflogd are built and ported (docs said
  otherwise); module object list now includes hfsc.c, fq_codel.c, pfk_pflog.c;
  the screencast pipeline is an optional build stage.
- pf-guide: the 'M2 scope only' framing is gone; honest gaps listed.
- caveats: three new sections about the screencast pipeline (MiniMax video
  generation blocked by the account tier, narration as a separate track, a
  video frame is not a verdict).
- porting-notes: what the recording taught us.
- README/README.pt-BR: status, milestones, layout and video section updated.

All internal doc links verified.
2026-11-12 11:51:25 -03:00
46 changed files with 2410 additions and 1621 deletions

No files matched your search

+55
View File
@@ -2,6 +2,61 @@
## Unreleased ## Unreleased
### Debian 13 implementation manual
`docs/en/debian13.md` + `docs/pt-BR/debian13.md` — the complete from-zero
path on Debian 13 (trixie): host preparation, kernel 7.3.4, `pf.ko`, the
userland, the libvirt lab, the gate suite and the stress test. Every command
was executed on the lab's Debian 13.7 host; the toolchain output, the
kernel.org checksum and the measured numbers are the real ones.
- `scripts/debian13/prepare-host.sh` — idempotent host preparation
(packages + toolchain report). Verified on trixie: gcc 14.2.0, make 4.4.1,
bison 3.8.2, flex 2.6.4, libvirt 11.3.0, QEMU 10.0.13.
- **Two build-script bugs fixed** (they broke the plain
`./scripts/build-kernel.sh` invocation the manual documents):
`scripts/kernel-config.sh` resolved its default kernel tree to
`../../linux_kernel/linux-7.3.4` (one level too many — it only worked when
`KERNELDIR` was exported), and `scripts/build-kernel.sh` did not pass the
tree it resolved on to `kernel-config.sh`, so the two could configure
different trees. Both now agree on `<repo>/../linux-7.3.4`.
- **gcc note**: Debian 13 ships gcc 14 and has no `gcc-15` in trixie. The
7.3.4 kernel and `pf.ko` build with gcc 14 — no patch needed.
### Stress test (`tests/vm/16-stress.sh` + `scripts/stress/run-stress.sh`)
The project had no performance/load gate; the stress is a new gate that
asserts *invariants under load* and reports throughput informationally, per
the design decision that functional gates are the bar:
| Phase | Assertion |
|---|---|
| baseline / overhead | throughput with the module out of the path vs. with PF enabled (information + a configurable overhead limit) |
| state churn | states appear under 300 concurrent connections and drain back after `pfctl -F s` (no state-table leak) |
| rule churn | 200 reloads of a 200-rule ruleset, timed inside the VM, zero failures, stable rule count |
| table churn | 2000 `pfr` entries added and flushed back to 0 |
| integrity | 2 MB through `nat-to` under a concurrent stream — sha256 must match |
| health | no `BUG/WARNING/Oops`, PF counters moving, the firewall still routing |
Report and raw log land in `out/stress/<timestamp>/report.md`; the guest's
serial console is captured during the run so a panic is diagnosable.
Measured on the lab (full profile): 400 Mbit/s sustained with PF enabled at
0% overhead against the no-module baseline, 300/300 concurrent flows tracked
as states and the table drained back to 0, 200 reloads of a 202-rule ruleset
with zero failures (11 ms average, 20 ms worst), 2000 `pfr` entries flushed
back to 0, 2 MB through NAT byte-identical under load, and no
`BUG/WARNING/Oops` in `dmesg`.
### Capacity finding: the workqueue backlog OOMs the guest above ~1 Gbit/s
Measured while building the stress test, and now documented in
`docs/*/caveats.md` §13: PF steals every skb onto one ordered workqueue, so
the backlog grows with the offered rate. On the 1 GB lab guest, 400 Mbit/s
through a stateful ruleset is stable and ~1 Gbit/s **panics the kernel**
("System is deadlocked on memory"). The stress test therefore runs its
traffic phases at a bounded rate; the functional gates are unaffected.
- **M5 (first step)**: OpenBSD's `hfsc.c` and `fq_codel.c` now compile and link - **M5 (first step)**: OpenBSD's `hfsc.c` and `fq_codel.c` now compile and link
into `pf.ko` (157 scheduler symbols reachable from `pf_ioctl`). Added the into `pf.ko` (157 scheduler symbols reachable from `pf_ioctl`). Added the
compat surface they need — `ml_*`/`struct mbuf_list`, `ifq_q_enter/leave`, compat surface they need — `ml_*`/`struct mbuf_list`, `ifq_q_enter/leave`,
+11 -8
View File
@@ -1,7 +1,7 @@
# PF-on-Linux top-level make # PF-on-Linux top-level make
KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4 KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4
.PHONY: module tools kernel test clean vm vm-destroy docs demo .PHONY: module tools kernel test stress clean vm vm-destroy docs demo
module: module:
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules $(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules
@@ -21,17 +21,20 @@ vm-destroy:
test: test:
./scripts/vm-run-tests.sh ./scripts/vm-run-tests.sh
# Stress: throughput/state/rule/table churn against the lab, with a report in
# out/stress/<timestamp>/report.md. Pass options through, e.g.
# make stress ARGS="--quick"
stress:
./scripts/stress/run-stress.sh $(ARGS)
docs: docs:
@for f in docs/en/*.md; do echo "EN $$f"; done @for f in docs/en/*.md; do echo "EN $$f"; done
@for f in docs/pt-BR/*.md; do echo "PT $$f"; done @for f in docs/pt-BR/*.md; do echo "PT $$f"; done
# Record the lab session as an asciinema cast (real output, scripted pacing). # Record the narrated screencasts (real shell sessions, MiniMax voice).
demo: # Needs MINIMAX_API_KEY and the VM lab up; see docs/*/screencast.md.
@mkdir -p out screencast:
@command -v asciinema >/dev/null 2>&1 || { \ ./scripts/screencast/build-all.sh
echo "asciinema not installed: pip install asciinema"; exit 1; }
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
@echo "-> out/demo-pf-linux.cast (play: asciinema play out/demo-pf-linux.cast)"
clean: clean:
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true $(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true
+27 -23
View File
@@ -2,32 +2,32 @@
Port of the OpenBSD **PF** (packet filter) to **Linux 7.3.4** as an 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`, 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 `pf_table.c`, `pf_lb.c`, `hfsc.c`, … ported from OpenBSD 8.0 sources) driven by
real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. (A second module for the the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
`pfq` qdisc arrives with M5.)
**Status:** the filter core, stateful filtering, tables, anchors, NAT **Status:** the filter core, stateful filtering, tables, anchors, NAT
(`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`), `divert` (`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`/`reply-to`),
and `pflog` are **verified in a real libvirt lab** (14/14 gates green). The pools, `divert`, `pflog` and the OpenBSD 8.0 **queues (HFSC)** are **verified
remaining features (queue attachment/pacing, af-to) are still pending — honest per-feature status: in a real libvirt lab** — 16/16 gates green. What is still pending is stated
[`docs/en/feature-matrix.md`](docs/en/feature-matrix.md). per feature in [`docs/en/feature-matrix.md`](docs/en/feature-matrix.md).
Demo (asciinema): [`docs/en/demo.md`](docs/en/demo.md).
Screencasts (EN + PT, narrated): [`docs/en/screencast.md`](docs/en/screencast.md).
Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.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). · plan: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
## Milestones ## Milestones
| Milestone | Scope | Status (2026-11-11) | | Milestone | Scope | Status |
|---|---|---| |---|---|---|
| M0 | repo, scripts, kernel build, VM boots the custom kernel | done | | M0 | repo, scripts, kernel build, VM boots the custom kernel | done |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f` / `-s info` | 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 | **done** — gates 01/02/03/04 green | | M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **done** — gates 01/02/03/04 green |
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect, min-ttl/max-mss measured) | | M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect, min-ttl/max-mss measured) |
| M4 | route-to/dup-to/reply-to, divert e2e, pflog | **done** — divert (11), pflog (12), route-to/dup-to (08), pools (10), reply-to (13) verified | | M4 | route-to/dup-to/reply-to, pools, divert e2e, pflog | **done** — divert (11), pflog (12), route-to/dup-to (08), pools (10), reply-to (13) verified |
| M5 | queues: HFSC back end in the data path | **done (mechanical)** — gate 15: queues load, hfsc attaches, `pfctl -s queue -v` reports class stats, a 2 MB queued flow is counted by the class. `bandwidth` = HFSC linkshare (guarantee, not cap; 8.0 has no upperlimit syntax) | | M5 | queues: HFSC back end in the data path | **done (mechanical)** — gate 15: queues load, hfsc attaches, `pfctl -s queue -v` reports class stats, a 2 MB queued flow is counted by the class. `bandwidth` = HFSC linkshare (guarantee, not cap; 8.0 has no upperlimit syntax) |
| M6 | docs complete, patchset tidy, full suite green, tagged release | **done** — docs EN+PT-BR, 11/11 suite green, tagged (v0.5.0) | | M6 | docs complete, patchset tidy, full suite green, tagged release | **done** — docs EN+PT-BR, 16/16 suite green, tagged (`v1.0.0`) |
## What you get (planned) ## What you get
| Area | Feature | | Area | Feature |
|---|---| |---|---|
@@ -63,23 +63,25 @@ Details, per-phase breakdown and the YouTube description text:
## Quick start ## Quick start
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version Full recipe: [`docs/en/install.md`](docs/en/install.md). **Debian 13 (trixie)
(commands are read from the scripts; a fresh-clone end-to-end run is still from zero** (host prep → kernel → module → tools → lab → stress):
pending): [`docs/en/debian13.md`](docs/en/debian13.md). Short version
(commands are read from the scripts):
```sh ```sh
./scripts/fetch-sources.sh # OpenBSD 8.0 sources (C3SL mirror, sha256-pinned) ./scripts/fetch-sources.sh # OpenBSD 8.0 sources (C3SL mirror, sha256-pinned)
./scripts/vendorize.sh # stage the pristine files we port ./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-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (export patch: none needed)
./scripts/build-module.sh # -> module/pf.ko ./scripts/build-module.sh # -> module/pf.ko
make -C tools # -> tools/out/pfctl (static), ABI harness make -C tools # -> tools/out/{pfctl,pdivert,pflogd,abitest}
sudo insmod module/pf.ko # /dev/pf (0600) + netfilter hooks sudo insmod module/pf.ko # /dev/pf (0600) + netfilter hooks
sudo pfctl -f /etc/pf.conf && sudo pfctl -e sudo pfctl -f /etc/pf.conf && sudo pfctl -e
``` ```
Read [`docs/en/pf-guide.md`](docs/en/pf-guide.md) **before** enabling PF on a Every feature listed above is exercised by the lab suite before you need it;
path you need: only the M2 filter core is gated today; NAT/scrub/queues/ [`docs/en/feature-matrix.md`](docs/en/feature-matrix.md) says which gate proves
divert/pflog are not usable yet ([`docs/en/feature-matrix.md`](docs/en/feature-matrix.md)). what, and [`docs/en/pf-guide.md`](docs/en/pf-guide.md) lists the deviations you
should know before putting PF on a path that matters.
VM validation lab (Alpine on libvirt): [`docs/en/vm-lab.md`](docs/en/vm-lab.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 Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e
@@ -87,11 +89,13 @@ Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e
## Layout ## Layout
- `module/` — kernel sources (ported PF + compat layer; the `pfq` qdisc lands with M5) - `module/` — kernel sources: ported PF engine (`module/pfsrc`) + Linux compat layer (`module/compat`)
- `tools/` — pfctl (static Linux build), pdivert relay source, ABI harness (`pflogd` not ported yet) - `tools/` — pfctl (static Linux build), pdivert relay, pflogd, ABI harness (`make -C tools abi`)
- `patches/kernel/` — Linux core patch set (currently empty; evidence in `p1-needs.txt`, see `docs/en/patches.md`) - `patches/kernel/` — Linux core patch set (currently empty; evidence in `p1-needs.txt`, see `docs/en/patches.md`)
- `scripts/` — fetch/build/patch/VM automation - `scripts/` — fetch/build/patch/VM automation, plus `scripts/screencast/` for the videos
- `tests/` — VM suite (`tests/vm/NN-*.sh`, scapy helpers) - `scripts/debian13/` — Debian 13 host preparation (`prepare-host.sh`)
- `scripts/stress/` — stress runner (`run-stress.sh`) + report writer
- `tests/vm/` — VM suite: 16 gates (`tests/vm/NN-*.sh`) + `lib.sh`
- `docs/en/` + `docs/pt-BR/` — full bilingual documentation - `docs/en/` + `docs/pt-BR/` — full bilingual documentation
- `docs/superpowers/` — design spec + implementation plan - `docs/superpowers/` — design spec + implementation plan
+53 -30
View File
@@ -2,67 +2,88 @@
Port do **PF** (packet filter) do OpenBSD para o **Linux 7.3.4** como um 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`, 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 `pf_table.c`, `pf_lb.c`, `hfsc.c`, … portados dos fontes do OpenBSD 8.0)
pelo `pfctl` real do OpenBSD via a ABI ioctl de `/dev/pf`. (Um segundo controlado pelo `pfctl` real do OpenBSD via a ABI ioctl de `/dev/pf`.
módulo para o qdisc `pfq` chega com o M5.)
**Status:** milestones iniciais. O engine portado e o plano de controle (M1) **Status:** o núcleo de filtragem, a filtragem stateful, tabelas, anchors, NAT
funcionam; o núcleo de filtragem do M2 está em validação na bancada de VMs; (`nat-to`/`rdr-to`/`binat-to`), roteamento forçado (`route-to`/`dup-to`/
todo o resto está pendente. Status honesto por feature: `reply-to`), pools, `divert`, `pflog` e as **filas do OpenBSD 8.0 (HFSC)** estão
**verificados num lab libvirt real** — 16/16 gates verdes. O que ainda está
pendente está dito por feature em
[`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md). [`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md).
Screencasts (EN + PT, com narração): [`docs/pt-BR/screencast.md`](docs/pt-BR/screencast.md).
Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.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). · plano: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
## Milestones ## Milestones
| Milestone | Escopo | Status (2026-11-11) | | Milestone | Escopo | Status |
|---|---|---| |---|---|---|
| M0 | repo, scripts, build do kernel, VM sobe com kernel próprio | concluído | | 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 | | 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 | | M2 | núcleo de filtragem: regras/estados/tabelas/anchors, gates VM 01–04 | **concluído** — gates 01/02/03/04 verdes |
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente | | M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub | **concluído** — gates 05/06/07/09 (incl. 2 MB íntegros através de um redirect, min-ttl/max-mss medidos) |
| M4 | route-to/dup-to/fail-to, divert e2e | pendente | | M4 | route-to/dup-to/reply-to, pools, divert e2e, pflog | **concluído** — divert (11), pflog (12), route-to/dup-to (08), pools (10), reply-to (13) verificados |
| M5 | filas: back ends HFSC/FQ-CoDel + qdisc `pfq` | pendente | | M5 | filas: back end HFSC no caminho de dados | **concluído (mecânico)** — gate 15: as filas carregam, o hfsc é anexado, `pfctl -s queue -v` mostra as estatísticas das classes, e um fluxo de 2 MB pela fila é contado pela classe. `bandwidth` = linkshare do HFSC (garantia, não teto; o 8.0 não tem sintaxe de upperlimit) |
| M6 | docs completas, patchset arrumado, suíte inteira verde, release com tag | em andamento (docs) | | M6 | docs completas, patchset arrumado, suíte inteira verde, release com tag | **concluído** — docs EN+PT-BR, 16/16 da suíte verdes, tag `v1.0.0` |
## O que você obtém (planejado) ## O que você obtém
| Área | Recurso | | Área | Recurso |
|---|---| |---|---|
| Filtragem | regras, anchors, quick, `match`/`block`(drop/return/return-rst)/`pass`, `keep state`, syncookies | | Filtragem | regras, anchors, quick, `match`/`block`(drop/return/return-rst)/`pass`, `keep state`, syncookies |
| Estado/Tables | firewall stateful, tables `pfr` (radix + hash, `pfctl -t`) | | Estado/Tables | firewall stateful, tables `pfr` (radix + hash, `pfctl -t`) |
| NAT | `nat-to` (pools de portas, sticky-port, static-port), `rdr-to`, `binat-to` | | 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` | | Scrub | remontagem 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` | | QoS (sucessor do ALTQ) | `queue`/`queue on` com os agendadores `ifq` do OpenBSD 8.0: **HFSC verificado no caminho de dados** (gate 15), priq como default, FQ-CoDel compilado; `pfctl -s queue -v` funciona |
| 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`) | | Divert | `divert-to` / tag `divert` + relay `pdivert` sobre uma família de sockets `PF_DIVERT` dentro do módulo (verificado: pacote vai para o userspace e volta) |
| Roteamento | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` | | Roteamento | `route-to`/`dup-to`/`reply-to` + pools (`round-robin`, `source-hash`) verificados (`fail-to` não existe mais no OpenBSD 8.0) |
| Logging | `pflogd` via canal netlink pflog (pcap DLT_PFLOG) | | Logging | `pflogd` via `/dev/pflog` (pcap DLT_PFLOG, layout de registro do OpenBSD) — verificado |
| Controle | `/dev/pf` (0600), ABI ioctl do OpenBSD 8.0, `pfctl` compilado para Linux (binário único) | | Controle | `/dev/pf` (0600), ABI ioctl do OpenBSD 8.0, `pfctl` compilado para Linux (binário único) |
Fora de escopo: failover `pfsync`/CARP, IPsec/pfkey, `ftp-proxy`, instâncias PF 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 por netns, agendadores exclusivos do ALTQ clássico (cbq/wfq/red — removidos do
OpenBSD antes da 8.0). OpenBSD antes da 8.0).
## Screencasts
Sete screencasts narrados por idioma (inglês e português), 1080p30 com
legendas queimadas, o diagrama de rede do lab desenhado sobre o terminal e um
filme contínuo que emenda as fases com transições. Cada vídeo é uma **sessão
shell real** contra as VMs Alpine — comandos digitados tecla a tecla, o kernel
respondendo ao vivo — com narração de voz feminina gerada pelo MiniMax:
```sh
./scripts/screencast/build-all.sh # -> out/screencast/{en,pt}/*.mp4
```
Detalhes, quebra por fase e o texto de descrição do YouTube:
[`docs/pt-BR/screencast.md`](docs/pt-BR/screencast.md) ·
[`docs/en/screencast.md`](docs/en/screencast.md)
## Início rápido ## Início rápido
Receita completa: [`docs/pt-BR/install.md`](docs/pt-BR/install.md). Versão Receita completa: [`docs/pt-BR/install.md`](docs/pt-BR/install.md). **Do zero no
curta (comandos lidos dos scripts; a execução de ponta a ponta a partir de Debian 13 (trixie)** (preparar host → kernel → módulo → ferramentas → lab →
clone limpo ainda está pendente): stress): [`docs/pt-BR/debian13.md`](docs/pt-BR/debian13.md). Versão
curta (comandos lidos dos scripts):
```sh ```sh
./scripts/fetch-sources.sh # fontes OpenBSD 8.0 (mirror C3SL, sha256 fixado) ./scripts/fetch-sources.sh # fontes OpenBSD 8.0 (mirror C3SL, sha256 fixado)
./scripts/vendorize.sh # staging dos arquivos upstream portados ./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-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (patch de export: nenhum necessário)
./scripts/build-module.sh # -> module/pf.ko ./scripts/build-module.sh # -> module/pf.ko
make -C tools # -> tools/out/pfctl (estático), verificador de ABI make -C tools # -> tools/out/{pfctl,pdivert,pflogd,abitest}
sudo insmod module/pf.ko # /dev/pf (0600) + hooks netfilter sudo insmod module/pf.ko # /dev/pf (0600) + hooks netfilter
sudo pfctl -f /etc/pf.conf && sudo pfctl -e sudo pfctl -f /etc/pf.conf && sudo pfctl -e
``` ```
Leia [`docs/pt-BR/pf-guide.md`](docs/pt-BR/pf-guide.md) **antes** de habilitar Toda feature listada acima é exercitada pela suíte do lab antes de você
o PF num caminho que você precisa: só o núcleo de filtragem do M2 tem gate precisar dela; [`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md)
hoje; NAT/scrub/filas/divert/pflog ainda não são utilizáveis diz qual gate prova o quê, e [`docs/pt-BR/pf-guide.md`](docs/pt-BR/pf-guide.md)
([`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md)). lista as divergências que você deve conhecer antes de colocar o PF num
caminho que importa.
Lab de validação em VMs (Alpine via libvirt): [`docs/pt-BR/vm-lab.md`](docs/pt-BR/vm-lab.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 English documentation: [`README.md`](README.md) e
@@ -70,11 +91,13 @@ English documentation: [`README.md`](README.md) e
## Estrutura ## Estrutura
- `module/` — fontes de kernel (PF portado + camada de compat; o qdisc `pfq` chega com o M5) - `module/` — fontes de kernel: engine PF portado (`module/pfsrc`) + camada de compat Linux (`module/compat`)
- `tools/` — pfctl (build estático Linux), fonte do relay pdivert, verificador de ABI (`pflogd` ainda não portado) - `tools/` — pfctl (build estático Linux), relay pdivert, pflogd, verificador de ABI (`make -C tools abi`)
- `patches/kernel/` — conjunto de patches de core do Linux (hoje vazio; evidência em `p1-needs.txt`, ver `docs/pt-BR/patches.md`) - `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 - `scripts/` — automação de fetch/build/patch/VM, mais `scripts/screencast/` para os vídeos
- `tests/` — suíte em VM (`tests/vm/NN-*.sh`, helpers scapy) - `scripts/debian13/` — preparação do host Debian 13 (`prepare-host.sh`)
- `scripts/stress/` — runner do stress (`run-stress.sh`) + gerador de relatório
- `tests/vm/` — suíte em VM: 16 gates (`tests/vm/NN-*.sh`) + `lib.sh`
- `docs/en/` + `docs/pt-BR/` — documentação completa bilíngue - `docs/en/` + `docs/pt-BR/` — documentação completa bilíngue
- `docs/superpowers/` — design + plano de implementação - `docs/superpowers/` — design + plano de implementação
+15 -9
View File
@@ -4,8 +4,8 @@ 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 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`, 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 `pf_table.c`, `pf_lb.c`, `hfsc.c`, …) running as the out-of-tree module
by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. `pf.ko`, driven by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
## Index ## Index
@@ -13,10 +13,12 @@ by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
|---|---| |---|---|
| [guide.md](guide.md) | One-page architecture: components, data path, control plane, milestones | | [guide.md](guide.md) | One-page architecture: components, data path, control plane, milestones |
| [install.md](install.md) | Apply/build/install recipe (kernel, module, tools) | | [install.md](install.md) | Apply/build/install recipe (kernel, module, tools) |
| [debian13.md](debian13.md) | Full from-zero implementation on Debian 13 (trixie): host prep, kernel/module/tools build, libvirt lab, stress test |
| [build.md](build.md) | Build pipeline in detail: fetch → vendorize → kernel → module → pfctl | | [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 | | [vm-lab.md](vm-lab.md) | The libvirt validation lab: topology, recreate/run, what each of the 16 gates proves |
| [feature-matrix.md](feature-matrix.md) | Per-feature status: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx | | [feature-matrix.md](feature-matrix.md) | Per-feature status: VERIFIED-in-VM / VERIFIED-offline / COMPILES-untested / NOT-ENABLED |
| [pf-guide.md](pf-guide.md) | Daily use: what works today in `/etc/pf.conf`, what to avoid until M3/M4 | | [pf-guide.md](pf-guide.md) | Daily use: what works today in `/etc/pf.conf`, and the deviations to know about |
| [screencast.md](screencast.md) | The narrated videos (English and Portuguese): phases, pipeline, how to re-record |
| [patches.md](patches.md) | Linux core patch set (currently empty) + rules and template for future `000N-*.patch` | | [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 | | [caveats.md](caveats.md) | Known divergences from OpenBSD behavior and current limitations |
@@ -27,10 +29,14 @@ authoritative references are:
- `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` — design - `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/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) - `docs/porting-notes.md` — staging log + the deviation record (the truth about compat-layer divergences)
Feature-status vocabulary used across these docs: Feature-status vocabulary used across these docs:
- **VERIFIED-in-VM** — exercised end-to-end in the libvirt lab (see vm-lab.md). - **VERIFIED-in-VM** — exercised end-to-end in the libvirt lab; the gate that proves it is named (see [vm-lab.md](vm-lab.md)).
- **COMPILES-untested** — the code is built into `pf.ko` but no gate has proven the behavior yet. - **VERIFIED-offline** — proven by a fixture or ABI test, not by traffic.
- **PENDING-Mx** — lands (or lands fully) in milestone Mx; current observable behavior is documented. - **COMPILES-untested** — the code is built into `pf.ko`/`pfctl` but no gate has exercised it.
- **NOT-ENABLED** — the code exists but the feature is off in this build.
- **OUT-OF-SCOPE** — non-goal by design (spec §3).
The lab suite is **16/16 green** (`./scripts/vm-run-tests.sh`).
+42 -17
View File
@@ -69,14 +69,15 @@ auto-staged (hand-ported).
## 5. Module — `scripts/build-module.sh` ## 5. Module — `scripts/build-module.sh`
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (single module today; the `make -C "$KERNELDIR" M=module` → `module/pf.ko` (a single module: the BSD
`pfq` qdisc arrives with M5). The script self-prepares `Module.symvers` / `ifq`/`pfq` interface lives inside it, it is not a separate `tc` qdisc). The
`modules_prepare` first. Object list (`module/Makefile`): ported engine script self-prepares `Module.symvers` / `modules_prepare` first. Object list
(`pf.c`, `pf_ioctl.c`, `pf_norm.c`, `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, (`module/Makefile`): ported engine (`pf.c`, `pf_ioctl.c`, `pf_norm.c`,
`pf_table.c`, `pf_osfp.c`, `pf_syncookies.c`, `divert_sock.c`) + compat layer `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, `pf_table.c`, `pf_osfp.c`,
(mbuf shim, kern glue, if/ip4/ip6/pcb/xmit/ifq, crypto, radix, subr_tree) + `pf_syncookies.c`, `divert_sock.c`, `pfk_pflog.c`, `hfsc.c`, `fq_codel.c`) +
`pfk_mod.c`/`pfk_hooks.c`. Compile flags define `PFLINUX=1` and `_KERNEL=1`. compat layer (mbuf shim, kern glue, if/ip4/ip6/pcb/xmit/ifq, crypto, radix,
Prints `MODULE_BUILD_OK`. 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 `insmod module/pf.ko` creates `/dev/pf` (0600) and registers the netfilter
hooks; `dmesg` shows `pfk: …` lines (see [caveats.md](caveats.md) for what hooks; `dmesg` shows `pfk: …` lines (see [caveats.md](caveats.md) for what
@@ -84,7 +85,8 @@ the data path does).
## 6. Userland — `make -C tools` ## 6. Userland — `make -C tools`
Targets (from `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`. Targets (from `tools/Makefile`): `pfctl`, `pdivert`, `pflogd`, `abitest`,
`abi`, `test`, `clean`.
- `pfctl` — the OpenBSD 8.0 sources compiled against the copied ABI headers; - `pfctl` — the OpenBSD 8.0 sources compiled against the copied ABI headers;
result is a **static binary** at `tools/out/pfctl`. Every deviation from result is a **static binary** at `tools/out/pfctl`. Every deviation from
@@ -97,20 +99,43 @@ Targets (from `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`.
- `test` — offline parse checks: runs `pfctl -n -f` over `tools/tests/*.conf` - `test` — offline parse checks: runs `pfctl -n -f` over `tools/tests/*.conf`
(basic / translations / queues+scrub / anchor / negative `broken.conf`). (basic / translations / queues+scrub / anchor / negative `broken.conf`).
This is pure userland; it needs no `/dev/pf`. This is pure userland; it needs no `/dev/pf`.
- `pdivert` — divert relay source under `tools/pdivert/`; see - `pdivert` — the divert relay, built from `tools/pdivert/`. It talks to the
[feature-matrix.md](feature-matrix.md) for its (pending) kernel side. `AF_DIVERT` socket family the module registers at runtime
(`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`); gate `11-divert.sh`
proves the round trip.
- `pflogd` — reads `/dev/pflog` and writes a DLT_PFLOG pcap whose record
layout is byte-compatible with OpenBSD `pflog(4)`; gate `12-pflog.sh`.
## 7. Root `Makefile` ## 7. Screencasts — `scripts/screencast/build-all.sh`
`make module | tools | kernel | vm | vm-destroy | test | docs | clean` — thin Not part of the kernel build, but part of the deliverable. The pipeline:
| Stage | Script | What it does |
|---|---|---|
| narration text | `make_narration.py` | writes `scripts/screencast/narration/<phase>.{en,pt}.json` (one segment per beat of the session) |
| voice | `make_audio.py` | MiniMax TTS, one MP3 per segment, female voice per language |
| session | `record.py` | real SSH session on the lab: commands typed key by key, captured as asciicast v2 |
| video | `render_video.py` | replays the capture through a terminal emulator, draws the lab diagram, burns the subtitles |
| assembly | `assemble.py` | mixes the narration onto the timeline |
| full film | `join.py` | crossfades the phases through chapter cards |
`make screencast` (or the script) runs all of it; it needs
`MINIMAX_API_KEY` and the lab up. Details: [screencast.md](screencast.md).
## 8. Root `Makefile`
`make module | tools | kernel | vm | vm-destroy | test | screencast | docs | clean` — thin
wrappers over the scripts above. `make test` runs the VM suite 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. ([vm-lab.md](vm-lab.md)); `make screencast` renders the videos;
`make docs` just lists the bilingual doc set.
## What has been exercised vs. not ## What has been exercised vs. not
- Stages 1–6 have each been run during M0–M2 development on the build host - Stages 1–6 have each been run repeatedly on the build host (artifacts exist
(artifacts exist in-tree: `out/`, `module/pf.ko`, `tools/out/pfctl`), and in-tree: `out/`, `module/pf.ko`, `tools/out/{pfctl,pdivert,pflogd}`), and the
the scripts above were verified line-by-line for this document. scripts above were verified line-by-line for this document.
- Stage 7 (the screencasts) has been run end to end for all seven phases in
both languages; the videos are in `out/screencast/{en,pt}/`.
- A **fresh-clone end-to-end run** (`quickstart-check.sh`, plan task T19) has - 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 *not* been executed yet; treat the exact command sequence in install.md as
"as designed, last-run state per milestone" rather than CI-verified. "as designed, last-run state per milestone" rather than CI-verified.
+66
View File
@@ -159,6 +159,72 @@ matter when reasoning about rule semantics:
kernel. Containers/namespaces are not visible to PF and cannot be filtered kernel. Containers/namespaces are not visible to PF and cannot be filtered
with it in this design. with it in this design.
## Screencast pipeline (2026-11-12)
### 10. MiniMax video generation is not enabled on this account
Every MiniMax video model (`T2V-01`, `T2V-01-Director`, `MiniMax-Hailuo-02`)
answers with API error **2068 — "The current Token Plan tier does not support
this model"**. The narration (TTS) and the title artwork work fine; only the
video models are blocked. The phase transitions are therefore rendered
locally with ffmpeg (a crossfade over a chapter card) instead of AI-generated
clips. Upgrading the plan is the only thing needed to change that — the
extension point is `scripts/screencast/join.py`, which builds the cards.
### 11. The narration is a separate track, not a dubbing of the video
Each phase is recorded **once** and serves both languages: the terminal
session is language-neutral, and the two narrated versions differ only in the
audio track and the burned-in subtitles. The recorder holds each beat on
screen for as long as the longest narration segment (EN or PT) needs, so the
voice and the terminal action stay in step in both.
### 12. What the videos do not show
The videos are a demonstration, not a test run: the lab's own gate suite
(`scripts/vm-run-tests.sh`) is the authority for PASS/FAIL, and its logs live
in `out/test-logs/`. The screencasts show the same commands, but a video frame
is not a verdict.
## Capacity findings (2026-11-12, from `scripts/stress/run-stress.sh`)
### 13. The workqueue backlog grows with the offered rate: above ~1 Gbit/s the guest OOM-panics
This is the sharpest consequence of §1 and it is **reproducible**, not
theoretical. PF steals every skb onto one ordered workqueue and runs the
engine in process context, so the number of packets in flight is bounded only
by the memory the backlog takes.
Measured on the lab (Alpine guest, **1 GB** RAM, 4 TCP streams through a
stateful ruleset):
| Offered rate (per stream × 4) | Result |
|---|---|
| 50 Mbit/s (200 Mbit/s total) | stable |
| 100 Mbit/s (400 Mbit/s total) | stable |
| 250 Mbit/s (**1 Gbit/s** total) | **kernel panic: "System is deadlocked on memory"** |
The panic is an OOM with no killable process; the slab at that moment shows
`kmalloc-8k` ≈ 50 MB, `skbuff_head_cache` ≈ 18 MB and `files_cache` ≈ 40 MB,
i.e. the backlog is what ate the 1 GB. The guest reboots (120 s watchdog
delay) and every connection through the firewall is lost with it.
Practical consequences:
- **Rate-limit what you point at PF**, and size the box for the backlog:
memory needed grows with the offered rate, not with the number of
concurrent flows.
- The stress test (`tests/vm/16-stress.sh`) therefore runs its traffic phases
at a **bounded** rate (default 100 Mbit/s per stream = 400 Mbit/s total) —
the regime this port survives — and reports the ceiling separately instead
of OOM-killing the firewall under test.
- The functional gates (01-15) are unaffected: they move megabytes, not gigabits
per second.
What would fix it is a real fast path (evaluate `pf_test()` inline in
softirq, as OpenBSD does) or a bounded queue with a drop policy — both are
design changes beyond this port's scope.
## What is *not* a caveat ## What is *not* a caveat
ARP and other non-IPv4/IPv6 EtherTypes never enter the netfilter IPv4/IPv6 ARP and other non-IPv4/IPv6 EtherTypes never enter the netfilter IPv4/IPv6
+498
View File
@@ -0,0 +1,498 @@
# PF on Linux — implementing it from scratch on Debian 13 (trixie)
This document is the complete path, from zero, to run **PF-on-Linux** (the
OpenBSD 8.0 PF ported to Linux as an out-of-tree kernel module) on a
**Debian 13 (trixie)** machine: prepare the host, build the 7.3.4 kernel,
build `pf.ko` and `pfctl`, bring up the libvirt lab, run the 16-gate suite
and the **stress test**.
Every command below was executed on the lab's Debian 13.7 (`trixie`) host and
the real output is shown. Where a number matters, it is the measured number —
not an estimate.
---
## 0. What has to be clear before you start
| Fact | Practical consequence |
|---|---|
| `pf.ko` is built against the **Linux 7.3.4** tree and carries that kernel's `vermagic` | It does **not** load on the Debian 13 kernel (6.12). Do not try `insmod pf.ko` on 6.12: the `vermagic` differs and the load is refused. |
| The module registers **netfilter** hooks in `init_net` (IPv4 and IPv6, 5 points each) | PF coexists with `nftables`/`iptables`; it does not replace the Debian firewall. |
| PF runs in **one network namespace only** (`init_net`) | There is no per-netns PF instance. |
| The OpenBSD `pfctl` talks to the module through the `/dev/pf` ioctl ABI | The `pfctl` in this tree is **OpenBSD 8.0** compiled for Linux, not Debian's. |
Three roles fit on one Debian 13 machine — and that is how the lab was
validated:
| Role | What it does | Where |
|---|---|---|
| **build host** | builds the 7.3.4 kernel, `pf.ko` and the userland | `~/lab/linux_kernel/pf_port` |
| **hypervisor** | hosts the 4 test VMs (libvirt/KVM) | same host, `libvirtd` |
| **PF host** (optional) | runs PF on Debian 13 itself, after booting the 7.3.4 kernel | same host — see the warning in §9 |
---
## 1. Step 1 — prepare the host
```sh
git clone <repo-url> ~/lab/linux_kernel/pf_port # or: cp -a /path/pf_port ~/lab/linux_kernel/
cd ~/lab/linux_kernel/pf_port
sudo ./scripts/debian13/prepare-host.sh
```
The script is idempotent: it installs the packages and prints the toolchain
state. Real output on Debian 13.7:
```
== toolchain
gcc 14
make GNU Make 4.4.1
bison bison (GNU Bison) 3.8.2
flex flex 2.6.4
bc bc 1.07.1
rsync rsync version 3.5.0 protocol version 32
libvirt 11.3.0
qemu QEMU emulator version 10.0.13 (Debian 1:10.0.13+ds-0+deb13u1)
geniso genisoimage 1.1.11 (Linux)
PREPARE_HOST_OK
```
**About the compiler.** Debian 13 ships `gcc-14` as the default and has **no
`gcc-15` in trixie** (`apt-cache policy gcc-15` → no candidate). The
`docs/en/install.md` asks for "gcc ≥ 15" because the original development
host is Fedora. On Debian 13 the build uses **gcc 14.2.0** — the 7.3.4 kernel
and `pf.ko` compile without a patch (verified, §4).
To split the roles, use `--build-only` or `--lab-only`.
---
## 2. Step 2 — get the sources
### 2.1 The 7.3.4 kernel tree
The repository does **not** ship the kernel: it expects the tree at
`~/lab/linux_kernel/linux-7.3.4` (sibling of the repository). From zero:
```sh
cd ~/lab/linux_kernel
curl -fLO https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.3.4.tar.xz
curl -fLO https://cdn.kernel.org/pub/linux/kernel/v7.x/sha256sums.asc
sha256sum -c --ignore-missing sha256sums.asc # checks only the tarball you fetched
tar xf linux-7.3.4.tar.xz
```
The hash matches what kernel.org publishes:
```
b11b6df8295e8e991ff040b71aaa4994a8b55caa23b6b092907f195b5840fd1f linux-7.3.4.tar.xz
```
### 2.2 The OpenBSD 8.0 sources (PF itself)
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/fetch-sources.sh # fetches sys.tar.gz + src.tar.gz, verifies sha256 (pins in VERSIONS)
./scripts/vendorize.sh # extracts only the ported files -> _vendor/upstream + IDS.txt
```
Wait for the `OK sys.tar.gz` and `OK src.tar.gz` lines. `fetch-sources.sh`
tries the `openbsd.c3sl.ufpr.br` mirror first and falls back to
`cdn.openbsd.org`.
---
## 3. Step 3 — build the 7.3.4 kernel
```sh
cd ~/lab/linux_kernel/pf_port
KERNELDIR=~/lab/linux_kernel/linux-7.3.4 ./scripts/build-kernel.sh
```
What the script does, in order:
1. `scripts/kernel-config.sh` — starts from `make defconfig` and enables
`VIRTIO_BLK/PCI/NET=y`, `EXT4_FS=y`, `NETFILTER=y`, `NET_SCHED=y`,
`INET=y`, `IPV6=y`, `SERIAL_8250(+CONSOLE)=y` (this is what lets the `pf0`
VM boot the kernel directly, **with no initrd**) and runs
`make olddefconfig`;
2. `make -j$(nproc-2) bzImage modules_prepare` — compiles the kernel and
prepares the tree for out-of-tree modules;
3. copies `vmlinux.symvers` → `Module.symvers` (kbuild does not write the
former) and makes sure `scripts/module.lds` exists;
4. publishes into `out/`: `linux-7.3.4-bzImage`, `System.map`, `.config`,
`kernelversion`, `bzImage.sha256`.
It ends with:
```
KERNEL_BUILD_OK 7.3.4
```
Reference time on the lab host (Debian 13, 12 threads): **~4 min** for the
kernel build itself (`bzImage` + `modules_prepare`) on a tree that already
carried objects, and the whole script — configure, build, publish the
artifacts — in under 3 minutes. A tree straight from the tarball starts
empty; expect the `make` step to dominate there.
---
## 4. Step 4 — build the `pf.ko` module
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/build-module.sh # == make -C $KERNELDIR M=$PWD/module
```
Output: `MODULE_BUILD_OK` and the artifact `module/pf.ko`.
**Check the `vermagic`** — it decides where the module can load:
```sh
modinfo module/pf.ko | grep vermagic
# vermagic: 7.3.4 SMP preempt mod_unload
```
This is why kernel and module must come from the **same** tree: a `pf.ko`
built against another 7.3.4 tree can have a subtly different `vermagic`
(`SMP preempt mod_unload modversions` plus the build's `CONFIG_*`) and the
load is refused.
### 4.1 Userspace ↔ kernel ABI check
`pfctl` talks to the kernel through shared structs. The `abitest` harness
compares the layout on both sides:
```sh
make -C tools abi # builds abitest (userspace) and abitest-module (kernel),
# dumps both size/offset tables and diffs them
```
No diff output = the ABI is intact.
---
## 5. Step 5 — build the userland
```sh
make -C tools # -> tools/out/{pfctl,pdivert,pflogd} + abitest
```
`pfctl` is produced **statically** (`-static`) so it can be copied into the
Alpine VMs without dependencies. Offline check (needs no `/dev/pf`):
```sh
(cd tools && ./out/pfctl -nf tests/01_basic.conf) # parse-only: no output = ok
make -C tools test # runs every test .conf
make -C tools abi # userspace vs kernel ABI tables
```
---
### 5.1 What the Debian 13 build produced (verified)
Everything below was built and run on Debian 13.7 with **gcc 14.2.0** — no
patch, no backport:
```
$ ./scripts/build-module.sh
...
MODULE_BUILD_OK
$ modinfo module/pf.ko | grep vermagic
vermagic: 7.3.4 SMP preempt mod_unload
$ make -C tools test
PASS tests/01_basic.conf (exit 0)
PASS tests/02_translations.conf (exit 0)
PASS tests/03_queues_scrub.conf (exit 0)
PASS tests/broken.conf rejected (non-zero exit)
$ make -C tools abi
ABI OK: identical tables
```
The `vermagic` printed by the Debian build is identical to the one the lab
VM accepts. Verified end to end: the module built on Debian 13 was copied
into the lab VM and loaded there —
```
INSMOD_DEBIAN_MODULE_OK
Status: Enabled
crw------- 1 root root 10, 259 /dev/pf
[ 119.057040] pfk: netfilter hooks registered (10, netisr wq data path)
```
— as long as it was built from the same tree as the kernel the VM boots.
---
## 6. Step 6 — the libvirt lab on Debian 13
The lab is 4 Alpine VMs (`pf0` = the firewall under test, plus `cli`, `srv`,
`gw2`), a DHCP management network and three isolated bridges
`pf-lan`/`pf-wan`/`pf-wan2`. The topology lives in `lab/ips.env`.
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/vm-create.sh --pf0-kernel=out/linux-7.3.4-bzImage
```
The command is idempotent: it tears down and recreates the `pftest-*`
domains, generates the cloud-init seeds, syncs the shared Alpine base image
and starts everything. Without `--pf0-kernel=`, `pf0` boots the kernel from
its own Alpine image — and then `pf.ko` **does not load** (vermagic). Always
pass `--pf0-kernel=` when the intent is to test PF.
Deploy the artifacts into the VM:
```sh
./scripts/vm-deploy.sh # build host -> deb ($DEPLOY_DIR) -> pf0:/opt/pf/
```
Check the VM:
```sh
ssh pf0 'uname -r; lsmod | grep -w pf; ls -l /opt/pf/'
```
Expected: `7.3.4`, the `pf` module loaded, and `pf.ko`/`pfctl`/`pdivert`/
`pflogd` in `/opt/pf/`.
---
## 7. Step 7 — the 16-gate suite
```sh
./scripts/vm-run-tests.sh # or: make test
```
The runner restarts `pf0` before every gate (clean state), re-deploys the
module and the tools, and runs the gate from the build host. Expected output:
```
PASS=16 FAIL=0
```
Per-gate logs land in `out/test-logs/<gate>.sh.log`.
---
## 8. Step 8 — the stress test
The project had no performance gate (`docs/en/vm-lab.md` recorded that gap
explicitly). The stress test is a new gate, `tests/vm/16-stress.sh`, which
asks different questions from the functional gates: *"does the engine stay
correct, bounded and leak-free while traffic, states, rules and tables
churn?"*
```sh
./scripts/stress/run-stress.sh # default profile
./scripts/stress/run-stress.sh --quick # short profile (smoke)
./scripts/stress/run-stress.sh --seconds 30 --connections 2000 --reloads 500
```
Options: `--seconds`, `--parallel`, `--rate`, `--connections`, `--reloads`,
`--rules`, `--table-entries`, `--max-overhead`. The runner resets `pf0`,
deploys, runs the gate and writes `out/stress/<timestamp>/report.md`.
> **The traffic phases run at a bounded rate** (`--rate`, default 100 Mbit/s
> per stream = 400 Mbit/s total). This is not a convenience: PF steals every
> skb onto one ordered workqueue and runs the engine in process context, so the
> backlog — and the memory it takes — grows with the offered rate. On the
> 1 GB lab guest, 400 Mbit/s is stable and **~1 Gbit/s OOM-panics the
> kernel** ("System is deadlocked on memory"). The stress test therefore
> works in the regime the port survives and reports the ceiling separately
> instead of killing the firewall under test. Details in
> [`caveats.md` §13](caveats.md).
### 8.1 What each phase measures
| Phase | What it does | How it is judged |
|---|---|---|
| 0 — baseline | throughput `cli -> srv` at the bounded rate with the **module out of the path** (`rmmod pf`) | must be > 0 Mbit/s (otherwise the rest is meaningless) |
| 1 — overhead | the same traffic with PF enabled and stateful rules | overhead % = (baseline − with PF) / baseline, against the limit (30% by default) — at the capped rate this says "PF sustains the offered rate" |
| 2 — state churn | 300 concurrent TCP connections to a dedicated listener on the server | states **appear** in the table (`pfctl -s states`) and **vanish** after `pfctl -sr` → no leak |
| 3 — rule churn | 200 reloads of a 200-rule ruleset | zero load failures, reload time measured **inside** the VM (min/avg/max), rule count stable |
| 4 — table churn | 2000 entries in a `pfr` table | entries are gone after the flush → no table leak |
| 5 — integrity | 2 MB crossing `nat-to` **while** a stream saturates the link | sha256 of the received file = sha256 of the source |
| 6 — health | `dmesg`, PF counters, and a final ping | no `BUG/WARNING/Oops`; counters move; the firewall still routes |
### 8.2 Reading the report
A real run on the lab (full profile, `out/stress/<timestamp>/report.md`):
```
STRESS phase=baseline baseline_mbit=400 seconds=10 parallel=4 rate_per_stream=100M
STRESS phase=overhead pf_mbit=400 overhead_pct=0 limit_pct=30 rate_per_stream=100M
STRESS phase=state_churn connections=300 states_before=0 states_created=300 states_after_reset=0 established_on_srv=214 rules_loaded=3 pf_status=Enabled
STRESS phase=rule_churn reloads=200 rules_per_load=200 reload_fails=0 reload_ms_avg=11 reload_ms_max=20 rules_final=202
STRESS phase=table_churn entries_requested=2000 entries_added=2000 add_errors=0 entries_after_flush=0
STRESS phase=integrity bytes=2000000 sha256_match=yes
STRESS phase=health dmesg_anomalies=0 match=5855 state_entries=7
```
Reading it: PF sustains the capped 400 Mbit/s with **0% overhead** against the
no-module baseline, tracks **300/300** concurrent flows as states and drains
the table back to 0, reloads a 202-rule ruleset **200 times** with no failure
(11 ms average, 20 ms worst), flushes **2000** table entries back to 0, moves
2 MB through NAT byte-identically while a stream runs, and leaves no
`BUG/WARNING/Oops` behind.
The runner prints `STRESS RESULT: PASS` or `FAIL` and exits with the gate's
status.
> **Honesty note:** the throughput numbers come from a virtualized lab (KVM,
> 2 vCPU per VM, `virtio` NICs bridged on the host). They are meant to
> *compare* baseline against PF in the same environment, not to predict
> production. The measured overhead includes the packet copy and PF's
> software path — the price of a stateful filter.
---
## 9. Step 9 (optional) — running PF on Debian 13 itself
This is **not** required for the lab, and it has a cost: the lab host is also
the hypervisor — rebooting into another kernel takes the VMs down.
```sh
cd ~/lab/linux_kernel/linux-7.3.4
sudo make modules_install # pf.ko -> /lib/modules/7.3.4/
sudo cp arch/x86/boot/bzImage /boot/vmlinuz-7.3.4
sudo update-initramfs -u -k 7.3.4 # or: update-grub
sudo reboot # ⚠ takes the lab VMs down
```
After the boot (with 7.3.4 active):
```sh
uname -r # 7.3.4
sudo insmod /lib/modules/7.3.4/pf.ko # creates /dev/pf (0600) and registers the hooks
ls -l /dev/pf
sudo cp ~/lab/linux_kernel/pf_port/tools/out/pfctl /usr/local/sbin/
sudo pfctl -e
sudo pfctl -f /etc/pf.conf
sudo pfctl -s info
```
A minimal `/etc/pf.conf` (OpenBSD 8.0 syntax):
```
set skip on lo
block return
pass on egress
```
To keep PF up across reboots, a simple unit:
```ini
# /etc/systemd/system/pf.service
[Unit]
Description=PF-on-Linux engine
After=network-pre.target
Before=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe pf
ExecStart=/usr/local/sbin/pfctl -e
ExecStop=/usr/local/sbin/pfctl -d
ExecStop=/sbin/modprobe -r pf
[Install]
WantedBy=multi-user.target
```
And `pflogd` (writes a `DLT_PFLOG` pcap with the OpenBSD `pflog(4)` record
layout):
```ini
# /etc/systemd/system/pflogd.service
[Unit]
Description=PF log daemon
After=pf.service
[Service]
ExecStart=/usr/local/sbin/pflogd /var/log/pf/pflog.pcap
[Install]
WantedBy=multi-user.target
```
> `pflogd` reads `/dev/pflog` (created by the module, 0600). This `pfctl`
> has **no** `-s pflog` modifier: logging is turned on per rule with the
> OpenBSD keyword (`pass log on egress ...`, `block log ...`). The on-disk
> pcap lives wherever you point `pflogd`.
---
## 10. Verification checklist
```sh
# 1. module loaded and device created
lsmod | grep -w pf && ls -l /dev/pf
# 2. PF enabled
pfctl -s info | head -2 # Status: Enabled
# 3. the OpenBSD 8.0 parser in the Debian binary
pfctl -nf /etc/pf.conf
# 4. hooks registered
dmesg | grep pfk # "pfk: netfilter hooks registered (10, netisr wq data path)"
# 5. the firewall really filters
pfctl -sr; pfctl -s states
# 6. the full suite
./scripts/vm-run-tests.sh # PASS=16 FAIL=0
# 7. stress
./scripts/stress/run-stress.sh # STRESS RESULT: PASS
```
---
## 11. Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| `insmod: ERROR: could not insert 'pf.ko': Invalid module format` | `vermagic` differs from the running kernel | kernel and module must come from the **same** tree; rebuild both |
| `insmod` fails on the Debian kernel (6.12) | the module targets 7.3.4 | boot the 7.3.4 kernel (§9) or test inside the lab VM |
| `pfctl: Permission denied` | `/dev/pf` is 0600 root | run as root |
| `pfctl: Device not configured` | module loaded but `pfctl -e` never ran | `sudo pfctl -e` |
| `virsh: cannot connect to qemu:///system` | libvirt is down | `sudo systemctl enable --now libvirtd` |
| `Cannot find device 'eth2'` | interface names changed | resolve with `ip route get <ip> \| grep -o 'dev [a-z0-9]*'` (gate 15 does this) |
| a gate fails right after the deploy | `pf.ko` was not copied | the runner checks `test -s /opt/pf/pf.ko`; see `out/test-logs/<gate>.sh.log` |
| stress: `baseline throughput unavailable` | `iperf3` is not running on the server | `srv 'pkill -x iperf3; iperf3 -s -D'` and check `netstat -lnt \| grep 5201` |
| stress: `state churn` fails | the guest's `bash` has no `/dev/tcp` | the gate falls back to `nc`; check `bash --version` in the VM |
| the guest reboots during a stress run | the offered rate exhausted the guest's memory (workqueue backlog) | lower `--rate`; see [caveats.md §13](caveats.md) — the ceiling measured on the lab is ~1 Gbit/s on a 1 GB guest |
| `gcc-15` not found | it does not exist in trixie | use the default gcc 14 (§1) |
---
## 12. Known limits (read before using this in production)
- This port is **not** OpenBSD PF: the deviations are documented in
[`docs/en/caveats.md`](caveats.md) and
[`docs/en/feature-matrix.md`](feature-matrix.md).
- No `pfsync`/CARP, IPsec/pfkey, `ftp-proxy`, per-netns PF, and none of the
`cbq`/`wfq`/`red` schedulers (removed upstream before OpenBSD 8.0).
- PF registers netfilter hooks in `init_net` — on a host that already runs
`nftables`, traffic traverses both. PF's `bypass` only applies to traffic
entering through PF's own hook.
- IPv6, OS fingerprinting, `set state-policy`/`allow-opts` and syncookies
compile but are **not** covered by a gate (known gap, §5 of
[`docs/en/vm-lab.md`](vm-lab.md)).
---
## 13. References
| Document | Contents |
|---|---|
| [`docs/en/install.md`](install.md) | the generic build recipe (without the Debian walk-through) |
| [`docs/en/vm-lab.md`](vm-lab.md) | the lab, topology and gates |
| [`docs/en/feature-matrix.md`](feature-matrix.md) | what is verified and what is not |
| [`docs/en/caveats.md`](caveats.md) | known deviations from OpenBSD 8.0 |
| [`docs/en/pf-guide.md`](pf-guide.md) | day-to-day rulesets and warnings |
| Portuguese version | [`docs/pt-BR/debian13.md`](../pt-BR/debian13.md) |
-41
View File
@@ -1,41 +0,0 @@
# Demo (asciinema)
The lab can be recorded as an [asciinema](https://asciinema.org) cast. Every
byte in the recording is the real output of the lab VMs (Alpine under libvirt
running the kernel built from source with `pf.ko`); only the pacing is
scripted.
## Record
```sh
pip install asciinema # once
bash scripts/vm-run-tests.sh # or: scripts/vm-create.sh + deploy
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
```
The session walks through:
1. the lab topology and the freshly loaded `pf.ko` (`dmesg` proof),
2. `pfctl -s info` over the OpenBSD ioctl ABI,
3. a real `pf.conf` (table, stateful rules, explicit drop) and `pfctl -sr`,
4. transit ping + `pfctl -ss` (stateful),
5. `pfctl -t <table> -T show` (live tables),
6. a blocked ping (counters with `pfctl -sr -vv`),
7. `divert-to` with the `pdivert` relay (packet to userspace and back),
8. shutdown.
## Play back
```sh
asciinema play out/demo-pf-linux.cast
```
## Upload
```sh
asciinema upload out/demo-pf-linux.cast
```
`scripts/demo-record.py` is an alternative recorder that produces the same
asciicast v2 format without the asciinema binary (it drives the lab over
ssh and writes the cast itself).
+39 -34
View File
@@ -4,67 +4,71 @@ Português: [docs/pt-BR/feature-matrix.md](../pt-BR/feature-matrix.md).
Status vocabulary (used identically in both languages): 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)). - **VERIFIED-in-VM** — passed end-to-end in the libvirt lab (see [vm-lab.md](vm-lab.md)); the gate that proves it is named.
- **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. - **VERIFIED-offline** — proven by a fixture/ABI test, not by traffic (e.g. `make -C tools test`).
- **COMPILES-untested** — built into `pf.ko`/`pfctl`; no gate has exercised it. - **COMPILES-untested** — built into `pf.ko`/`pfctl`; no gate has exercised it.
- **PENDING-Mx** — lands in milestone Mx; today's observable behavior is stated. - **NOT-ENABLED** — the code exists but the feature is off in this build (e.g. `af-to`, which needs the `INET6` paths).
- **OUT-OF-SCOPE** — non-goal by design (spec §3). - **OUT-OF-SCOPE** — non-goal by design (spec §3).
As of 2026-11-11; source of per-row evidence: `out/test-logs/`, As of 2026-11-12; source of per-row evidence: `out/test-logs/`,
`docs/porting-notes.md`, milestone commits. `docs/porting-notes.md`, milestone commits, and the screencasts
([screencast.md](screencast.md)) for the visible proof.
The suite is **16/16 green** (`./scripts/vm-run-tests.sh`).
## Control plane & tools ## Control plane & tools
| Feature | Upstream syntax | Milestone | Status | | Feature | Upstream syntax | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| Module load, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM | | Module load, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM (gate `02-ativacao.sh`) |
| Enable/disable | `pfctl -e`, `pfctl -d` | 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) | | Ruleset load | `pfctl -f file` | M2 | VERIFIED-in-VM (every gate loads a real ruleset) |
| Parse-only check | `pfctl -n -f file` | M1 | VERIFIED (offline fixture suite, `make -C tools test`) | | Parse-only check | `pfctl -n -f file` | M1 | VERIFIED-offline (`make -C tools test`) |
| Status/counters | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM | | Status/counters | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM |
| Rules listing | `pfctl -s rules [-v]` | M2 | COMPILES-untested | | Rules listing | `pfctl -s rules [-v]`, `pfctl -sr` | M2 | VERIFIED-in-VM (gates 02–06) |
| 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 | | States listing | `pfctl -s states [-v]`, `pfctl -ss` | M2 | VERIFIED-in-VM (gate `01-state.sh`) |
| Tables listing/ops | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) | | Tables listing/ops | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | VERIFIED-in-VM (gate `03-tables.sh`, incl. a live `-T delete` unblocking the flow) |
| Anchors | `pfctl -a anchor -f file`, `-s Anchors` | M2 | M2-in-progress (gate 04) | | Anchors | `pfctl -a anchor -f file`, `-s anchors` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| Queue listing | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (parses fine; counters not wired to real back ends yet) | | Queue listing | `pfctl -s queue -v` | M5 | VERIFIED-in-VM (gate `15-queue.sh`: both classes with statistics) |
| 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`) | | ioctl ABI parity | `make -C tools abi` (abitest tables) | M1 | VERIFIED-offline (userland table); the kernel-side diff is an open integrator step (`tools/UPSTREAM.md`) |
## Filtering core ## Filtering core
| Feature | Upstream syntax | Milestone | Status | | 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) | | `pass`/`match`, quick, on-interface/proto/addr/port | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | VERIFIED-in-VM (gate `01-state.sh`: transit ping through the firewall) |
| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) | | `block drop` | `block drop in quick …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: ping dies, counters prove it) |
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) | | `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: the client's `nc` gets an RST) |
| `keep state` (icmp/tcp/udp), state table, purger | `… keep state` | M2 | M2-in-progress | | `keep state` (icmp/tcp/udp), state table, purger | `… keep state` | M2 | VERIFIED-in-VM (gate `01-state.sh`; state visible in `pfctl -ss`) |
| `set state-policy`, `allow-opts`, etc. | `set …` options | M2 | COMPILES-untested | | `set state-policy`, `allow-opts`, etc. | `set …` options | M2 | COMPILES-untested |
| Syncookies (state overflow path) | `set syncookies always` | M2 | COMPILES-untested | | Syncookies (state overflow path) | `set syncookies always` | M2 | COMPILES-untested |
| pfr tables (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) | | pfr tables (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | VERIFIED-in-VM (gate `03-tables.sh`) |
| Anchors + include + quick precedence | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) | | Anchors + include + quick precedence | `anchor "x"`, `load anchor …` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| IPv6 for all of the above | same syntax, v6 | M2+ | COMPILES-untested (engine is v6-complete by construction; v6 smoke gates planned) | | IPv6 for all of the above | same syntax, v6 | M2+ | COMPILES-untested — the engine is v6-complete by construction, but the v6 path is not exercised by a gate |
| Non-IPv4/IPv6 traffic (ARP…) | — | M2 | PFLINUX by construction: netfilter IPv4/IPv6 hooks only — ARP never enters PF path (no gate yet) | | Non-IPv4/IPv6 traffic (ARP…) | — | M2 | PFLINUX by construction: netfilter IPv4/IPv6 hooks only — ARP never enters the PF path (no gate) |
## NAT / normalization (M3) ## NAT / normalization (M3)
| Feature | Upstream syntax | Milestone | Status | | Feature | Upstream syntax | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| `nat-to` (port pools, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFIED in lab** — source NAT works end to end: the packet leaves with the pool address (tcpdump proof), the reply is de-NATed and re-routed to the client, and the state shows both tuples (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). Gate: `tests/vm/05-nat.sh`. | | `nat-to` (port pools, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFIED-in-VM** — gate `05-nat.sh`: source NAT works end to end (tcpdump proof), the reply is de-NATed and re-routed to the client, and the state shows both tuples (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). |
| `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFIED in lab** — port redirect works end to end (gate `06-rdr.sh`): the client talks to the firewall's address, pf rewrites the destination, re-routes the packet, and the reply is un-redirected. The gate also pulls 2 MB through the redirect and checks byte-for-byte integrity (GSO/TSO regression guard). | | `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFIED-in-VM** — gate `06-rdr.sh`: the client talks to the firewall's address, pf rewrites the destination, re-routes the packet, and the reply is un-redirected. The gate also pulls 2 MB through the redirect and checks byte-for-byte integrity (GSO/TSO regression guard). |
| `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFIED in lab** — gate `07-binat.sh`. pfctl expands one `binat-to` rule into the documented pair (`pass out … nat-to <mapped> static-port` + `pass in … rdr-to <real>`), and both halves work: the peer sees the mapped address, the original never leaks, and the reply is translated back. | | `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFIED-in-VM** — gate `07-binat.sh`. pfctl expands one `binat-to` rule into the documented pair (`pass out … nat-to <mapped> static-port` + `pass in … rdr-to <real>`), and both halves work: the peer sees the mapped address, the original never leaks, and the reply is translated back. |
| Scrub/normalize: reassembly, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFIED in lab** — gate `09-scrub.sh`: `min-ttl` raises the TTL (egress shows 99 after the forward decrement) and `max-mss` clamps a SYN's MSS option, both measured on the firewall's own egress. Reassembly/overlap paths are compiled and reached but not yet gated. | | NAT pools (`round-robin`, `source-hash`) | `nat-to { 10.0.2.1, 10.0.3.1 } round-robin` | M4 | **VERIFIED-in-VM** — gate `10-pools.sh`, and visible in the screencast: round-robin alternates the source address between the members (3 packets each), source-hash keeps one origin on one member. |
| 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) | | Scrub/normalize: reassembly, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFIED-in-VM** — gate `09-scrub.sh`: `min-ttl` raises the TTL (egress shows 99 after the forward decrement) and `max-mss` clamps a SYN's MSS option, both measured on the firewall's own egress. Reassembly/overlap paths are compiled and reached but not gated. |
| OS fingerprinting in states | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compiled; no fingerprint packaging yet — a missing `/etc/pf.os` is tolerated) |
## Routing / divert / queues / logging ## Routing / divert / queues / logging
| Feature | Upstream syntax | Milestone | Status | | Feature | Upstream syntax | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| `route-to` / `dup-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state`, `reply-to { gw }` | M4 | **VERIFIED in lab** — gates `08-routeto.sh` (route-to leaves via the gateway's link, dup-to copies while keeping the flow) and `13-replyto.sh` (the reply of a flow is forced onto the gateway link instead of the client's). Deviation: the forced nexthop is installed by setting `rt_gw4`/`rt_gw_family` on the looked-up route (Linux only ARPs the gateway when the route declares one). `fail-to` is **not a keyword in OpenBSD 8.0** (removed upstream), so it is out of scope. | | `route-to` / `dup-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state`, `reply-to { gw }` | M4 | **VERIFIED-in-VM** — gates `08-routeto.sh` (route-to leaves via the gateway's link, dup-to copies while keeping the flow) and `13-replyto.sh` (the reply of a flow is forced onto the gateway link instead of the client's). Deviation: the forced nexthop is installed by setting `rt_gw4`/`rt_gw_family` on the looked-up route (Linux only ARPs the gateway when the route declares one). `fail-to` is **not a keyword in OpenBSD 8.0** (removed upstream), so it is out of scope. |
| `divert-to` / `divert` tag | `pass in … divert-to <host> port <n>` | VERIFIED in lab | **Works end-to-end.** The module registers an `AF_DIVERT` socket family (`PF_DIVERT=32`) at runtime — no kernel-tree patch. A `divert-to` rule hands the packet to that family; with no listener the packet is dropped (`divs_noport`, BSD parity); with `tools/pdivert` running the packet is delivered to userspace, rewritten and reinjected (verified with a payload mutation + checksum fix, ping 0% loss). | | `divert-to` / `divert` tag | `pass in … divert-to <host> port <n>` | M4 | **VERIFIED-in-VM** — gate `11-divert.sh`. The module registers an `AF_DIVERT` socket family (`PF_DIVERT=32`) at runtime — no kernel-tree patch. A `divert-to` rule hands the packet to that family; with no listener the packet is dropped (`divs_noport`, BSD parity); with `tools/pdivert` running the packet is delivered to userspace, rewritten and reinjected (verified with a payload mutation + checksum fix, ping 0% loss). |
| Queues (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFIED in lab** — gate `15-queue.sh`: the queue config loads, the hfsc discipline attaches to the interface, `pfctl -s queue -v` lists both classes with statistics, a 2 MB queued flow completes, and the class's packet counter advances (the scheduler is in the data path). Note `bandwidth` is HFSC *linkshare* - a guarantee under contention, not a cap: OpenBSD 8.0's pf.conf has no `minimum`/`maximum`, so a lone flow is not rate-limited, on Linux or on OpenBSD. | | Queues (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFIED-in-VM** — gate `15-queue.sh`: the queue config loads, the hfsc discipline attaches to the interface, `pfctl -s queue -v` lists both classes with statistics, a 2 MB queued flow completes, and the class's packet counter advances (the scheduler is in the data path). Note `bandwidth` is HFSC *linkshare* — a guarantee under contention, not a cap: OpenBSD 8.0's pf.conf has no `minimum`/`maximum`, so a lone flow is not rate-limited, on Linux or on OpenBSD. |
| 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** | | HFSC | `queue on wan { hfsc … }` | M5 | **VERIFIED-in-VM** — `pfsrc/hfsc.c` (real OpenBSD 8.0 code) is in the module object list and is the discipline the gate exercises. The class handles use the vendored `net/hfsc.h` (root class `0x10000`, default class `0x1000`) shared with `pfctl`, so the stats ABI matches on both sides. |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — same as HFSC: staged, not built | | FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — `pfsrc/fq_codel.c` is built into `pf.ko` and the vendored `net/fq_codel.h` is shared with `pfctl`, but no gate drives it. |
| `pfq` qdisc (`tc qdisc … root pfq`) | n/a (Linux glue) | M5 | PENDING-M5 (does not exist in the tree yet) | | `pfq` interface | n/a (BSD `ifq`/`pfq` interface, not a Linux `tc` qdisc) | M5 | **VERIFIED-in-VM** — the port keeps the BSD shape: `compat/pfk_ifq.c` implements the `ifq_ops`/`pfq_ops` interfaces and the OpenBSD back ends plug into them (`pfq` is not a Linux `tc` qdisc and never was meant to be). priq is the default discipline. |
| pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFIED in lab** — gate `12-pflog.sh`: logged packets are queued by the engine, read through `/dev/pflog` and written as a pcap with linktype DLT_PFLOG (117); the dump contains the real packet bytes. Deviation: a chardev instead of an if_pflog/bpf interface (Linux has no bpf over such a virtual interface here); record layout is byte-compatible with OpenBSD `pflog(4)`. `if_pflog.c` stays unbuilt. | | pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFIED-in-VM** — gate `12-pflog.sh`: logged packets are queued by the engine, read through `/dev/pflog` and written as a pcap with linktype DLT_PFLOG (117); the dump contains the real packet bytes. Deviation: a chardev instead of an if_pflog/bpf interface (Linux has no bpf over such a virtual interface here); record layout is byte-compatible with OpenBSD `pflog(4)`. `if_pflog.c` stays unbuilt. |
## Non-goals (explicit) ## Non-goals (explicit)
@@ -76,3 +80,4 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
| Per-netns PF instances | faithful port = single global instance on `init_net` | | 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 | | 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 | | Routing labels (`route-label`) | non-goal; ioctl layer rejects labeled rules |
| `af-to` (IPv4↔IPv6 translation) | the code sits under `#ifdef INET6` and this build has `INET6 0`; enabling it is a separate milestone, not a missing feature in the base |
+27 -19
View File
@@ -24,8 +24,8 @@ between kernel and userland (ioctl encoding per `module/include/pfk.h`).
| `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/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` | | `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` | | `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 | | `scripts/` | `fetch-sources.sh`, `vendorize.sh`, `stage-port.sh` (deterministic staging), `build-kernel.sh`, `build-module.sh`, `vm-*.sh` lab automation, `screencast/` (video pipeline) |
| `tests/vm/` | VM suite gates `01-state.sh … 04-anchors.sh` + `lib.sh`; `tests/confs/` rulesets deployed to the lab | | `tests/vm/` | VM suite: 16 gates `01-state.sh … 16-stress.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) | | `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) ## Data path (M2, as implemented today)
@@ -69,33 +69,41 @@ rebuilt.
## Queues, divert, pflog — today's observable behavior ## Queues, divert, pflog — today's observable behavior
- **Queues**: `pfctl` parses `queue`/`queue on` (8.0 syntax); kernel-side the - **Queues**: `pfctl` parses the OpenBSD 8.0 `queue`/`queue on` syntax and the
ifq API is implemented by `compat/pfk_ifq.c` with **priq semantics only**; kernel side implements it in the BSD shape: `compat/pfk_ifq.c` provides the
the `pfq` classful qdisc and the HFSC/FQ-CoDel back ends land in **M5**. `ifq_ops`/`pfq_ops` interfaces and the OpenBSD back ends (`pfsrc/hfsc.c`,
`ifq.c`/`hfsc.c`/`fq_codel.c` are staged and compile-ready but are not in `pfsrc/fq_codel.c`) plug into them; **priq** is the default discipline.
the module object list yet. Gate `15-queue.sh` proves the **hfsc** discipline end to end: the queue
- **Divert**: `divert-to`/`divert` tag evaluate to PF's `PF_DIVERT` verdict; config loads, the discipline attaches to the interface, `pfctl -s queue -v`
`divert_packet()` (`pfsrc/divert_sock.c`) currently **drops the packet with lists both classes with statistics, a 2 MB queued flow completes and the
a rate-limited kernel log** because the AF_DIVERT socket family is not yet class's packet counter advances. The class handles and the stats structs
instantiated. The `pdivert` relay (userspace) exists and expects come from OpenBSD's own `net/hfsc.h`/`net/fq_codel.h`, vendored so the
`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`. kernel and `pfctl` cannot drift apart.
- **pflog**: `pflog_packet()` returns success but **logs nothing** (one-time - **Divert**: `divert-to`/`divert` tag evaluate to PF's `PF_DIVERT` verdict
`pr_warn "packet not logged"`); `if_pflog.c` is staged, netlink back end is and the module registers an `AF_DIVERT` socket family (`PF_DIVERT=32`) at
M4/M5. runtime — no kernel-tree patch. With no listener the packet is dropped
(`divs_noport`, BSD parity); with the `pdivert` relay running the packet is
delivered to userspace, rewritten and reinjected. Gate `11-divert.sh`.
- **pflog**: the engine queues logged packets, `pfsrc/pfk_pflog.c` exposes
them through `/dev/pflog`, and `tools/pflogd` writes a DLT_PFLOG pcap whose
record layout is byte-compatible with OpenBSD `pflog(4)`. Gate
`12-pflog.sh`. The deviation from upstream is the transport (a chardev
instead of an `if_pflog`/bpf interface), not the format.
## Milestones (see feature-matrix.md for per-feature status) ## Milestones (see feature-matrix.md for per-feature status)
| Milestone | Scope | Status (2026-11-11) | | Milestone | Scope | Status |
|---|---|---| |---|---|---|
| M0 | repo, scripts, kernel build, VM boots custom kernel | done | | M0 | repo, scripts, kernel build, VM boots custom kernel | done |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f`/`-s info` | done | | M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f`/`-s info` | done |
| M2 | filter core: rules/states/tables/anchors VM gates | **done** — gates 01–04 green | | M2 | filter core: rules/states/tables/anchors VM gates | **done** — gates 01–04 green |
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect) and pflog gate 12 | | M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect) and pflog gate 12 |
| M4 | route-to/dup-to/reply-to, pools, divert e2e | **done** — gates 08/10/13 and divert gate 11 | | M4 | route-to/dup-to/reply-to, pools, divert e2e | **done** — gates 08/10/13 and divert gate 11 |
| M5 | queues: ifq back ends (hfsc/fq_codel) | **partial** — back ends compiled into `pf.ko`; queue attachment/pacing pending | | M5 | queues: ifq back ends (hfsc/fq_codel) | **done (mechanical)** — gate 15: hfsc attaches, `pfctl -s queue -v` reports class stats, a 2 MB queued flow is counted by the class. `bandwidth` = HFSC linkshare (a guarantee under contention, not a cap) |
| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress — docs EN+PT-BR complete, 14/14 suite green, tagged | | M6 | docs complete, patchset tidy, full suite green, tagged releases | **done** — docs EN+PT-BR, 16/16 suite green, tagged (`v1.0.0`) |
## Next reads ## Next reads
[build.md](build.md) → [vm-lab.md](vm-lab.md) → [pf-guide.md](pf-guide.md) → [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). [feature-matrix.md](feature-matrix.md) → [screencast.md](screencast.md) →
[caveats.md](caveats.md).
+24 -8
View File
@@ -75,19 +75,34 @@ Because the deliverable is an out-of-tree module, "applying the patch set"
adds a documented `patches/kernel/000N-*.patch` (all such patches: one adds a documented `patches/kernel/000N-*.patch` (all such patches: one
section each in docs/en/patches.md with what/why/impact/revert). section each in docs/en/patches.md with what/why/impact/revert).
## 6. Notes on the steps above (2026-11-11) ## 6. Notes on the steps above (2026-11-12)
- Step **2.3 is a documented no-op today**: the patch set is empty and - 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` `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 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. [patches.md](patches.md)). You can safely skip 2.3.
- Step 2.4 builds `pfctl` and the ABI harness only (`make -C tools` targets: - Step 2.4 builds every userland tool: `make -C tools` targets are `pfctl`,
`pfctl abitest abi test clean`). `pdivert` is present as a source file with `pdivert`, `pflogd` and the ABI harness (`abitest`, `abi`, `test`, `clean`).
no Makefile target yet; `pflogd` is not ported. - After 2.5, the whole feature set is usable and gated by the lab suite —
- After 2.5, what you can actually *use* is the M2 filter core (pass/block/ filtering with state, tables and anchors, NAT (`nat-to`/`rdr-to`/`binat-to`
quick/keep state/tables/anchors); NAT/scrub/queues/divert/pflog are gated plus pools), scrub, forced routing (`route-to`/`dup-to`/`reply-to`),
by later milestones — see the [feature matrix](feature-matrix.md) and read `divert`, `pflog` and the OpenBSD 8.0 queues (HFSC). The
[pf-guide.md](pf-guide.md) before enabling PF on a path you need. [feature matrix](feature-matrix.md) says which gate proves what; read
[pf-guide.md](pf-guide.md) for the deviations before enabling PF on a path
you need.
## 6.1 Optional: the narrated screencasts
```sh
export MINIMAX_API_KEY=... # MiniMax TTS for the narration
./scripts/vm-create.sh # the lab must be up
./scripts/screencast/build-all.sh # -> out/screencast/{en,pt}/*.mp4
```
It records real shell sessions against the lab, generates the narration with
a MiniMax female voice (English and Portuguese) and renders 1080p30 MP4s
with burned-in subtitles and the lab's network diagram. Details:
[screencast.md](screencast.md).
## 7. Where to go next ## 7. Where to go next
@@ -99,4 +114,5 @@ section each in docs/en/patches.md with what/why/impact/revert).
| Daily rulesets & warnings | [pf-guide.md](pf-guide.md) | | Daily rulesets & warnings | [pf-guide.md](pf-guide.md) |
| Known divergences | [caveats.md](caveats.md) | | Known divergences | [caveats.md](caveats.md) |
| Core patch mechanism | [patches.md](patches.md) | | Core patch mechanism | [patches.md](patches.md) |
| Narrated screencasts | [screencast.md](screencast.md) |
| Doc index | [README.md](README.md) | | Doc index | [README.md](README.md) |
+19 -12
View File
@@ -3,10 +3,12 @@
Português: [docs/pt-BR/pf-guide.md](../pt-BR/pf-guide.md). 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 This is the "what can I actually run today" guide. It follows the
[feature matrix](feature-matrix.md): **M2 scope (filtering core: pass/block/ [feature matrix](feature-matrix.md): the filtering core (pass/block/quick/
quick/keep state/tables/anchors + enable/disable)** has VM evidence behind it; keep state/tables/anchors), the NAT family, scrub, forced routing, pools,
everything beyond that is either COMPILES-untested or PENDING. Install/build `divert`, `pflog` and the OpenBSD 8.0 queues all have **VM evidence** behind
is covered by [install.md](install.md) and [build.md](build.md). them — 16/16 lab gates are green. What is *not* gate-verified is listed
explicitly below. Install/build is covered by [install.md](install.md) and
[build.md](build.md).
## Daily loop ## Daily loop
@@ -25,21 +27,21 @@ sudo rmmod pf # unload (rule/state flush happens here)
> **Warning — order matters**: `-f` then `-e`. Enabling with no matching `pass` rule > **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 > 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 > policy, and the lab runs have *actually cut the test VM's ssh* that way
> way (the lab keeps a serial console for exactly this). On a remote host, > (the lab keeps a serial console for exactly this). On a remote host, pass
> pass your management interface before you enable anything. > your management interface before you enable anything.
## A ruleset that works in this port today ## A ruleset that works in this port today
```pf ```pf
# /etc/pf.conf — M2 scope only. Interface names here match the VM lab # /etc/pf.conf. Interface names here match the VM lab (mgmt/lan/wan);
# (mgmt/lan/wan); substitute your own (eth0, enp1s0, …). # substitute your own (eth0, enp1s0, …).
# 1) keep yourself reachable (do this first, always) # 1) keep yourself reachable (do this first, always)
pass in quick on mgmt pass in quick on mgmt
pass out quick pass out quick
# 2) filter core — proven today: quick, proto/port matching, state, drop # 2) filter core — proven in the lab: quick, proto/port matching, state, drop
table <blk> persist # live blocklist (gate 03 pattern) table <blk> persist # live blocklist (gate 03 pattern)
block in quick proto tcp from <blk> to any block in quick proto tcp from <blk> to any
@@ -56,12 +58,17 @@ grammar **parse** fine — `make -C tools test` exercises them offline — but
the *kernel behavior* of most `set` options has not been gate-verified yet; the *kernel behavior* of most `set` options has not been gate-verified yet;
treat them as load-and-pray until the matrix moves them. treat them as load-and-pray until the matrix moves them.
## What to avoid until the milestones land **Not gate-verified** (honest gaps): the IPv6 path, OS fingerprinting, the
`set` options above, syncookies and the FQ-CoDel discipline. `af-to` is
compiled out in this build (`INET6 0`).
## Feature notes — what each syntax does today
| Syntax | Until | What happens today | | Syntax | Until | What happens today |
|---|---|---| |---|---|---|
| `nat-to`, `rdr-to`, `binat-to` | — | **Verified in the lab** (gates `05-nat`, `06-rdr`, the latter pulls 2 MB through a redirect and compares it byte for byte). `nat-to (egress)` expands the OpenBSD interface *group*; on a multi-NIC host prefer the explicit `nat-to (<iface>)` form. `binat-to` is expanded by pfctl into a `nat-to … static-port` + `rdr-to` pair (gate `07-binat`). | | `nat-to`, `rdr-to`, `binat-to` | — | **Verified in the lab** (gates `05-nat`, `06-rdr`, the latter pulls 2 MB through a redirect and compares it byte for byte). `nat-to (egress)` expands the OpenBSD interface *group*; on a multi-NIC host prefer the explicit `nat-to (<iface>)` form. `binat-to` is expanded by pfctl into a `nat-to … static-port` + `rdr-to` pair (gate `07-binat`). |
| `scrub (min-ttl, max-mss, df, random-id)`, `set reassemble` | — | **Verified in the lab** (gate `09-scrub.sh`): `min-ttl` and `max-mss` measurably rewrite transit packets. || `divert-to`, `divert` tag | — | **Verified in the lab** (gate `11-divert`): the packet is delivered to an `AF_DIVERT` socket (`PF_DIVERT=32`) served by `tools/pdivert`; with no listener it is dropped, exactly like BSD's `divs_noport`. | | `scrub (min-ttl, max-mss, df, random-id)`, `set reassemble` | — | **Verified in the lab** (gate `09-scrub.sh`): `min-ttl` and `max-mss` measurably rewrite transit packets. |
| `divert-to`, `divert` tag | — | **Verified in the lab** (gate `11-divert`): the packet is delivered to an `AF_DIVERT` socket (`PF_DIVERT=32`) served by `tools/pdivert`; with no listener it is dropped, exactly like BSD's `divs_noport`. |
| `route-to`, `dup-to`, `reply-to` | — | **Verified in the lab** (gates `08-routeto.sh`, `13-replyto.sh`): the forced gateway is used instead of the normal egress, `dup-to` copies while keeping the flow, and `reply-to` pushes a flow's answers out through the gateway link. `fail-to` is not a keyword in OpenBSD 8.0 (removed upstream). | | `route-to`, `dup-to`, `reply-to` | — | **Verified in the lab** (gates `08-routeto.sh`, `13-replyto.sh`): the forced gateway is used instead of the normal egress, `dup-to` copies while keeping the flow, and `reply-to` pushes a flow's answers out through the gateway link. `fail-to` is not a keyword in OpenBSD 8.0 (removed upstream). |
| `queue` / `queue on` / `pfctl -s queue` | — | **Verified in the lab** (gate `15-queue.sh`): queues load, hfsc attaches, `pfctl -s queue -v` reports class statistics and a queued 2 MB flow is counted by the class. `bandwidth` is a *guarantee under contention*, not a cap - 8.0's pf.conf has no upperlimit keyword. | | `queue` / `queue on` / `pfctl -s queue` | — | **Verified in the lab** (gate `15-queue.sh`): queues load, hfsc attaches, `pfctl -s queue -v` reports class statistics and a queued 2 MB flow is counted by the class. `bandwidth` is a *guarantee under contention*, not a cap - 8.0's pf.conf has no upperlimit keyword. |
| `log`, `log all`, `block log`, `pflogd` | — | **Verified in the lab** (gate `12-pflog`): run `pflogd /tmp/x.pcap` (reads `/dev/pflog`); the file is a `DLT_PFLOG` (117) pcap readable by tcpdump/wireshark. | | `log`, `log all`, `block log`, `pflogd` | — | **Verified in the lab** (gate `12-pflog`): run `pflogd /tmp/x.pcap` (reads `/dev/pflog`); the file is a `DLT_PFLOG` (117) pcap readable by tcpdump/wireshark. |
+2 -2
View File
@@ -1,6 +1,6 @@
# Screencasts / Videos da demonstracao # Screencasts / Videos da demonstracao
> PT-BR: [docs/pt-BR/screencast.md](pt-BR/screencast.md) · EN: this page > PT-BR: [docs/pt-BR/screencast.md](../pt-BR/screencast.md) · EN: this page
Sete videos por idioma, com **narracao com voz feminina** (MiniMax), Sete videos por idioma, com **narracao com voz feminina** (MiniMax),
**legendas queimadas**, **diagrama de rede do lab** e um **filme completo** **legendas queimadas**, **diagrama de rede do lab** e um **filme completo**
@@ -52,7 +52,7 @@ Etapas individuais:
```sh ```sh
python3 scripts/screencast/make_audio.py --lang pt # audio MiniMax python3 scripts/screencast/make_audio.py --lang pt # audio MiniMax
python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\ python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\
--out out/screencast/caps/04-roteamento.cast --pace --out out/screencast/caps/04-roteamento.cast --pace --ensure-pf
python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt
python3 scripts/screencast/join.py --lang pt python3 scripts/screencast/join.py --lang pt
``` ```
+69 -22
View File
@@ -3,8 +3,8 @@
Português: [docs/pt-BR/vm-lab.md](../pt-BR/vm-lab.md). 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 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 source of truth). The lab has been created and run through M0–M6; the full
green run is the M2+ gate and is still in progress (see §6). suite is green (§6).
## 1. Where it lives ## 1. Where it lives
@@ -61,6 +61,20 @@ isolated bridges `pf-lan` 10.0.1.0/24, `pf-wan` 10.0.2.0/24, `pf-wan2`
running. Laptop ssh aliases (`pf0|cli|srv|gw2`, ProxyJump via deb) are running. Laptop ssh aliases (`pf0|cli|srv|gw2`, ProxyJump via deb) are
generated into `~/.ssh/config.d/pflab`. generated into `~/.ssh/config.d/pflab`.
**From pf0 to the other VMs.** The firewall has its own keypair and the
peers trust it, so a session on pf0 can drive the client and the server
directly — this is what the screencasts do (`ssh cli 'ping …'`):
```sh
# once per lab, from the hypervisor
ssh pf0 'ssh-keygen -t ed25519 -N "" -C pf0 -f /root/.ssh/id_ed25519'
# then authorize that public key on cli, srv and gw2
```
`/root/.ssh/config` on pf0 maps the short names `cli`, `srv` and `gw2` to
10.0.1.10, 10.0.2.20 and 10.0.3.2, and `/etc/hosts` carries the same
mapping.
## 4. Running the suite ## 4. Running the suite
```sh ```sh
@@ -78,28 +92,61 @@ and exits non-zero if anything failed.
## 5. What each gate proves today ## 5. What each gate proves today
| Gate | Scenario (asserted with real traffic, not exit codes) | Every gate asserts **real traffic and observable state**, never just an exit
code: the packet either arrives, is dropped, or is translated, and the proof
is read back from the firewall's own counters, state table, capture files or
peer.
The full list is in §6 (16 gates, all green). Each gate is a shell script in
`tests/vm/` that talks to the VMs through deb with nested ssh, using the
helpers in `tests/vm/lib.sh` (`pf0`, `cli`, `srv`, `gw2`, `pfput`, `pfload`,
`ok`, `bad`). `scripts/vm-run-tests.sh` runs them in order and captures
`out/test-logs/<gate>.sh.log`.
Not gated (honest gaps): the IPv6 path, OS fingerprinting, `set` options
(`state-policy`, `allow-opts`) and syncookies — the code is compiled into
`pf.ko` but no gate drives it. FQ-CoDel compiles and shares its stats header
with `pfctl`, but only the **hfsc** discipline is exercised.
There is **no numeric performance gate**: the design decision (spec §7) is
that *functional gates are the bar* — a perf contract would force a
non-faithful rewrite-first shim. What exists instead is a **stress test**
(`tests/vm/16-stress.sh`, driven by `scripts/stress/run-stress.sh`) that
asserts *invariants under load* — no state/table leak, no rule-load failure,
byte-identical payload under a saturating stream, no `BUG/WARNING/Oops`, the
firewall still routing — and **reports** the throughput comparison
(baseline without the module vs. with PF enabled) as information, without a
numeric pass/fail target. Report and raw log land in
`out/stress/<timestamp>/`.
## 6. Current suite status (2026-11-12)
**16/16 gates green.** A full clean run (`./scripts/vm-run-tests.sh`, ~13 min)
recreates and reboots `pftest-pf0` before every gate, redeploys the module
and the tools, and ends with `PASS=16 FAIL=0`. The captured logs live in
`out/test-logs/<gate>.sh.log`.
| Gate | What it proves now |
|---|---| |---|---|
| `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` | | `01-state.sh` | `keep state` across a transit ping; the state shows up in `pfctl -ss` |
| `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 | | `02-block.sh` | `block drop` kills the ping; `block return-rst` gets the client an RST |
| `03-tables.sh` | `table <blk> { 10.0.1.10 }` + `block in quick from <blk>` → `pfctl -t blk -T show` lists it; cli ping blocked; live `-T delete` unblocks the same flow | | `03-tables.sh` | a `pfr` table blocks a host; a 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 | | `04-anchors.sh` | an anchor rule is evaluated before a later main-ruleset `block` |
| `05-nat.sh` | source NAT: the packet leaves with the pool address, the reply is de-NATed |
| `06-rdr.sh` | port redirect end to end, plus 2 MB through it with byte-for-byte integrity |
| `07-binat.sh` | `binat-to`: the peer sees the mapped address, the original never leaks |
| `08-routeto.sh` | `route-to` leaves via the gateway's link; `dup-to` copies and keeps the flow |
| `09-scrub.sh` | `min-ttl` and `max-mss` measured on the firewall's own egress |
| `10-pools.sh` | NAT pool selection (`round-robin`, `source-hash`) |
| `11-divert.sh` | `divert-to` to the `pdivert` relay: packet to userspace and back |
| `12-pflog.sh` | logged packets reach `/dev/pflog` and land in a DLT_PFLOG pcap |
| `13-replyto.sh` | `reply-to` forces the reply onto the gateway link |
| `14-soak.sh` | ruleset reload under traffic, `rmmod` cycle, leak check |
| `15-queue.sh` | hfsc queues: attach, class statistics, a 2 MB queued flow counted by the class |
| `16-stress.sh` | stress: throughput baseline vs. PF, state/rule/table churn with leak checks, 2 MB through NAT under a saturating stream, dmesg health (run by `scripts/stress/run-stress.sh`, which writes a report) |
Planned gates per the plan document (T11–T18): 05-nat, 06-rdr, 07-binat, The screencasts ([screencast.md](screencast.md)) show most of these gates
08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (iperf rate being run, with the lab's network diagram drawn over the terminal.
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 ## 7. Manual poking
+25
View File
@@ -1030,3 +1030,28 @@ The port now vendors OpenBSD's `net/hfsc.h` and `net/fq_codel.h` into
compile against one definition. Rule for anything else in this area: if a compile against one definition. Rule for anything else in this area: if a
struct crosses the ioctl boundary, take the header from OpenBSD's tree - do not struct crosses the ioctl boundary, take the header from OpenBSD's tree - do not
reconstruct it. reconstruct it.
## Screencast pipeline: what the recording taught us (2026-11-12)
Not a compat-layer deviation, but the same kind of note - the things that
only show up when you watch the thing run:
1. **The tty echo is the terminal, and readline fights it.** With bash's
default line editing, the remote tty echoes every keystroke *and*
readline redraws the line when you press enter, so the command appeared
twice on screen. `bash --noediting` leaves the tty echo alone and the
command shows exactly once.
2. **`pfctl -e` prints "pf enabled" and so does the `echo` after it.** What
looked like a duplicated output line was two different programs saying the
same thing; the capture was right.
3. **MiniMax returns the audio as a hex string**, not base64 - the payload
starts with the ASCII of the ID3 header (`49443304`). Decoding it as
base64 produced files that ffmpeg called "Header missing".
4. **Pacing has to be driven by the narration.** If the voice for a beat is
longer than the beat, the audio runs over the next action. The recorder now
holds each beat for as long as the longest segment (EN or PT) needs, so one
capture serves both languages.
5. **A phase that runs `rmmod` can break another recording.** The activation
phase unloads the module on purpose; running two captures at once left the
routing phase without `/dev/pf` mid-session. `record.py --ensure-pf` now
makes sure the module is in place before the capture starts.
+28 -21
View File
@@ -1,36 +1,43 @@
# Documentação PF-on-Linux (português) # Documentação do PF-on-Linux (português)
English documentation: [`docs/en/README.md`](../en/README.md). 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** Este é o conjunto de documentação de usuário do port do **packet filter do
para o **Linux 7.3.4** — o engine PF de verdade (`pf.c`, `pf_norm.c`, OpenBSD 8.0 (PF)** para o **Linux 7.3.4** — o engine PF real (`pf.c`,
`pf_table.c`, `pf_lb.c`, …) rodando como módulo out-of-tree `pf.ko`, `pf_norm.c`, `pf_table.c`, `pf_lb.c`, `hfsc.c`, …) rodando como o módulo
controlado pelo `pfctl` real do OpenBSD pela ABI ioctl de `/dev/pf`. out-of-tree `pf.ko`, controlado pelo `pfctl` real do OpenBSD sobre a ABI ioctl
de `/dev/pf`.
## Índice ## Índice
| Documento | Conteúdo | | Doc | O que cobre |
|---|---| |---|---|
| [guide.md](guide.md) | Arquitetura em uma página: componentes, caminho de dados, plano de controle, milestones | | [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) | | [install.md](install.md) | Receita de apply/build/install (kernel, módulo, ferramentas) |
| [debian13.md](debian13.md) | Implementação completa do zero no Debian 13 (trixie): preparar host, compilar kernel/módulo/ferramentas, lab libvirt, teste de stress |
| [build.md](build.md) | Pipeline de build em detalhe: fetch → vendorize → kernel → módulo → pfctl | | [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 | | [vm-lab.md](vm-lab.md) | O lab de validação em libvirt: topologia, recriar/rodar, o que cada um dos 16 gates prova |
| [feature-matrix.md](feature-matrix.md) | Status por feature: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx | | [feature-matrix.md](feature-matrix.md) | Status por feature: VERIFIED-in-VM / VERIFIED-offline / COMPILES-untested / NOT-ENABLED |
| [pf-guide.md](pf-guide.md) | Uso diário: o que funciona hoje em `/etc/pf.conf`, o que evitar até M3/M4 | | [pf-guide.md](pf-guide.md) | Uso diário: o que funciona hoje em `/etc/pf.conf` e as divergências a conhecer |
| [patches.md](patches.md) | Patches de core do Linux (hoje vazio) + regras e template para futuros `000N-*.patch` | | [screencast.md](screencast.md) | Os vídeos narrados (inglês e português): fases, pipeline, como regravar |
| [caveats.md](caveats.md) | Divergências conhecidas de comportamento e limitações atuais | | [patches.md](patches.md) | Conjunto de patches de core do Linux (hoje vazio) + regras e template para futuros `000N-*.patch` |
| [caveats.md](caveats.md) | Divergências conhecidas em relação ao OpenBSD e limitações atuais |
## Fontes de verdade ## Fontes da verdade
A documentação descreve o código como ele existe na árvore. Em caso de dúvida, Os docs descrevem o código como ele existe nesta árvore. Na dúvida, as
as referências autoritativas são: referências autoritativas são:
- `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` — design - `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/superpowers/plans/2026-11-11-pf-linux-port.md` — plano dos milestones (tarefas T1–T19)
- `docs/porting-notes.md` — log de staging + registro de desvios da camada de compat (M1) - `docs/porting-notes.md` — log de staging + registro das divergências (a verdade sobre as divergências da camada de compat)
Vocabulário de status usado em todos os documentos: Vocabulário de status usado nestes docs:
- **VERIFIED-in-VM** — exercitado de ponta a ponta na bancada libvirt (ver vm-lab.md). - **VERIFIED-in-VM** — exercitado de ponta a ponta no lab libvirt; o gate que prova está nomeado (veja [vm-lab.md](vm-lab.md)).
- **COMPILES-untested** — o código compila no `pf.ko`, mas nenhum gate provou o comportamento ainda. - **VERIFIED-offline** — provado por um teste de fixture ou de ABI, não por tráfego.
- **PENDING-Mx** — entra (ou entra por completo) no milestone Mx; o comportamento observável hoje está documentado. - **COMPILES-untested** — o código é compilado dentro do `pf.ko`/`pfctl` mas nenhum gate o exercitou.
- **NOT-ENABLED** — o código existe mas a feature está desligada neste build.
- **OUT-OF-SCOPE** — fora de escopo por desenho (spec §3).
A suíte do lab está **16/16 verde** (`./scripts/vm-run-tests.sh`).
+42 -16
View File
@@ -72,13 +72,14 @@ ficam de fora do staging automático de propósito (portados à mão).
## 5. Módulo — `scripts/build-module.sh` ## 5. Módulo — `scripts/build-module.sh`
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (um único módulo hoje; o `make -C "$KERNELDIR" M=module` → `module/pf.ko` (um único módulo: a
qdisc `pfq` chega com o M5). O script prepara sozinho `Module.symvers` / interface BSD `ifq`/`pfq` mora dentro dele, não é um qdisc `tc` separado). O
`modules_prepare`. Lista de objetos (`module/Makefile`): engine portado script prepara sozinho `Module.symvers` / `modules_prepare`. Lista de objetos
(`pf.c`, `pf_ioctl.c`, `pf_norm.c`, `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, (`module/Makefile`): engine portado (`pf.c`, `pf_ioctl.c`, `pf_norm.c`,
`pf_table.c`, `pf_osfp.c`, `pf_syncookies.c`, `divert_sock.c`) + camada de `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, `pf_table.c`, `pf_osfp.c`,
compat (shim mbuf, glue de kernel, if/ip4/ip6/pcb/xmit/ifq, crypto, radix, `pf_syncookies.c`, `divert_sock.c`, `pfk_pflog.c`, `hfsc.c`, `fq_codel.c`) +
subr_tree) + `pfk_mod.c`/`pfk_hooks.c`. As flags de compilação definem 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`. `PFLINUX=1` e `_KERNEL=1`. Termina com `MODULE_BUILD_OK`.
`insmod module/pf.ko` cria `/dev/pf` (0600) e registra os hooks netfilter; o `insmod module/pf.ko` cria `/dev/pf` (0600) e registra os hooks netfilter; o
@@ -87,7 +88,8 @@ caminho de dados faz).
## 6. Userland — `make -C tools` ## 6. Userland — `make -C tools`
Alvos (de `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`. Alvos (de `tools/Makefile`): `pfctl`, `pdivert`, `pflogd`, `abitest`, `abi`,
`test`, `clean`.
- `pfctl` — as fontes do OpenBSD 8.0 compiladas contra os headers de ABI - `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`. copiados; o resultado é um **binário estático** em `tools/out/pfctl`.
@@ -100,20 +102,44 @@ Alvos (de `tools/Makefile`): `pfctl`, `abitest`, `abi`, `test`, `clean`.
- `test` — checagens de parse offline: roda `pfctl -n -f` sobre - `test` — checagens de parse offline: roda `pfctl -n -f` sobre
`tools/tests/*.conf` (básico / traduções / filas+scrub / anchor / negativa `tools/tests/*.conf` (básico / traduções / filas+scrub / anchor / negativa
`broken.conf`). É userland puro; não precisa de `/dev/pf`. `broken.conf`). É userland puro; não precisa de `/dev/pf`.
- `pdivert` — o fonte do relay de divert fica em `tools/pdivert/`; o lado - `pdivert` — o relay de divert, compilado a partir de `tools/pdivert/`. Ele
kernel (pendente) está na [matriz de features](feature-matrix.md). fala com a família de sockets `AF_DIVERT` que o módulo registra em runtime
(`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`); o gate
`11-divert.sh` prova a ida e a volta.
- `pflogd` — lê `/dev/pflog` e escreve um pcap DLT_PFLOG cujo layout de
registro é byte a byte compatível com o `pflog(4)` do OpenBSD; gate
`12-pflog.sh`.
## 7. `Makefile` raiz ## 7. Screencasts — `scripts/screencast/build-all.sh`
`make module | tools | kernel | vm | vm-destroy | test | docs | clean` — Não faz parte do build do kernel, mas faz parte da entrega. O pipeline:
| Etapa | Script | O que faz |
|---|---|---|
| texto da narração | `make_narration.py` | escreve `scripts/screencast/narration/<fase>.{en,pt}.json` (um segmento por batida da sessão) |
| voz | `make_audio.py` | TTS do MiniMax, um MP3 por segmento, voz feminina por idioma |
| sessão | `record.py` | sessão SSH real no lab: comandos digitados tecla a tecla, capturada como asciicast v2 |
| vídeo | `render_video.py` | reproduz a captura por um emulador de terminal, desenha o diagrama do lab, queima as legendas |
| montagem | `assemble.py` | mixa a narração na linha do tempo |
| filme completo | `join.py` | emenda as fases com crossfade sobre cartões de capítulo |
`make screencast` (ou o script) roda tudo; precisa de `MINIMAX_API_KEY` e do
lab no ar. Detalhes: [screencast.md](screencast.md).
## 8. `Makefile` raiz
`make module | tools | kernel | vm | vm-destroy | test | screencast | docs | clean` —
wrappers finos sobre os scripts acima. `make test` roda a suíte em VM 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. ([vm-lab.md](vm-lab.md)); `make screencast` renderiza os vídeos;
`make docs` apenas lista o conjunto bilíngue.
## O que já foi exercitado vs. não ## O que já foi exercitado vs. não
- As etapas 1–6 foram rodadas ao longo do desenvolvimento M0–M2 no host de - As etapas 1–6 foram rodadas repetidamente no host de build (há artefatos
build (há artefatos na árvore: `out/`, `module/pf.ko`, `tools/out/pfctl`), na árvore: `out/`, `module/pf.ko`, `tools/out/{pfctl,pdivert,pflogd}`), e os
e os scripts acima foram verificados linha a linha para este documento. scripts acima foram verificados linha a linha para este documento.
- A etapa 7 (os screencasts) foi rodada de ponta a ponta para as sete fases
nos dois idiomas; os vídeos estão em `out/screencast/{en,pt}/`.
- Uma execução **de ponta a ponta a partir de clone limpo** - Uma execução **de ponta a ponta a partir de clone limpo**
(`quickstart-check.sh`, tarefa T19 do plano) **ainda não** foi feita; (`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 trate a sequência exata do install.md como "conforme projetada e conforme
+67
View File
@@ -163,6 +163,73 @@ sobre a semântica das regras:
kernel. Containers/namespaces não são visíveis ao PF e não podem ser kernel. Containers/namespaces não são visíveis ao PF e não podem ser
filtrados por ele neste design. filtrados por ele neste design.
## Pipeline dos screencasts (2026-11-12)
### 10. A geração de vídeo do MiniMax não está habilitada nesta conta
Todos os modelos de vídeo do MiniMax (`T2V-01`, `T2V-01-Director`,
`MiniMax-Hailuo-02`) respondem com o erro de API **2068 — "The current Token
Plan tier does not support this model"**. A narração (TTS) e a arte do
cartão de título funcionam normalmente; só os modelos de vídeo estão
bloqueados. Por isso as transições entre fases são renderizadas localmente
com ffmpeg (um crossfade sobre um cartão de capítulo) em vez de clipes
gerados por IA. Mudar isso exige só subir o tier da conta — o ponto de
extensão é `scripts/screencast/join.py`, que constrói os cartões.
### 11. A narração é uma trilha separada, não uma dublagem do vídeo
Cada fase é gravada **uma vez** e serve aos dois idiomas: a sessão de
terminal é neutra em relação ao idioma, e as duas versões narradas diferem
apenas na trilha de áudio e nas legendas queimadas. O gravador segura cada
batida na tela pelo tempo que o segmento de narração mais longo (EN ou PT)
precisa, então a voz e a ação no terminal ficam em sincronia nos dois.
### 12. O que os vídeos não mostram
Os vídeos são uma demonstração, não uma rodada de teste: a suíte de gates do
lab (`scripts/vm-run-tests.sh`) é a autoridade para PASS/FAIL, e os logs
dela ficam em `out/test-logs/`. Os screencasts mostram os mesmos comandos,
mas um quadro de vídeo não é um veredito.
## Achados de capacidade (2026-11-12, a partir de `scripts/stress/run-stress.sh`)
### 13. O backlog da workqueue cresce com a taxa oferecida: acima de ~1 Gbit/s a VM dá OOM-panic
Esta é a consequência mais dura da §1 e ela é **reproduzível**, não teórica.
O PF rouba cada skb para uma única workqueue ordenada e roda o engine em
contexto de processo, então o número de pacotes em voo só é limitado pela
memória que o backlog consome.
Medido no lab (guest Alpine, **1 GB** de RAM, 4 fluxos TCP por um ruleset
stateful):
| Taxa oferecida (por fluxo × 4) | Resultado |
|---|---|
| 50 Mbit/s (200 Mbit/s no total) | estável |
| 100 Mbit/s (400 Mbit/s no total) | estável |
| 250 Mbit/s (**1 Gbit/s** no total) | **kernel panic: "System is deadlocked on memory"** |
O panic é um OOM sem processo matável; o slab naquele momento mostra
`kmalloc-8k` ≈ 50 MB, `skbuff_head_cache` ≈ 18 MB e `files_cache` ≈ 40 MB —
ou seja, o backlog comeu o 1 GB. A VM reinicia (watchdog de 120 s) e todas as
conexões que passavam pelo firewall caem junto.
Consequências práticas:
- **Limite a taxa que você aponta para o PF** e dimensione a máquina para o
backlog: a memória necessária cresce com a taxa oferecida, não com o
número de fluxos concorrentes.
- Por isso o teste de stress (`tests/vm/16-stress.sh`) roda as fases de
tráfego a uma taxa **limitada** (padrão: 100 Mbit/s por fluxo = 400 Mbit/s
no total) — o regime que este port suporta — e reporta o teto
separadamente, em vez de dar OOM no firewall sob teste.
- Os gates funcionais (01-15) não são afetados: eles movem megabytes, não gigabits
por segundo.
O que resolveria isso é um fast path de verdade (avaliar `pf_test()` inline em
softirq, como o OpenBSD faz) ou uma fila limitada com política de drop — as
duas são mudanças de design fora do escopo deste port.
## O que *não* é ressalva ## O que *não* é ressalva
ARP e outros EtherTypes não-IPv4/IPv6 nunca entram nos hooks netfilter de ARP e outros EtherTypes não-IPv4/IPv6 nunca entram nos hooks netfilter de
+495
View File
@@ -0,0 +1,495 @@
# PF no Linux — implementação do zero no Debian 13 (trixie)
Este documento é o caminho completo, do zero, para rodar o **PF-on-Linux**
(port do PF do OpenBSD 8.0 como módulo de kernel out-of-tree) em uma máquina
**Debian 13 (trixie)**: preparar o host, compilar o kernel 7.3.4, compilar o
módulo `pf.ko` e o `pfctl`, subir o laboratório libvirt, rodar a suíte de 16
gates e o **teste de stress**.
Cada comando aqui foi executado no Debian 13.7 (`trixie`) do laboratório e a
saída real está indicada. Onde o número importa, ele é o número medido — não
uma estimativa.
---
## 0. Antes de começar: o que precisa ficar claro
| Fato | Consequência prática |
|---|---|
| `pf.ko` é compilado contra a árvore do **Linux 7.3.4** e carrega o `vermagic` desse kernel | Ele **não** carrega no kernel do Debian 13 (6.12). Não tente `insmod pf.ko` no 6.12: o `vermagic` diverge e o `insmod` falha. |
| O módulo registra hooks **netfilter** em `init_net` (IPv4 e IPv6, 5 pontos cada) | PF convive com `nftables`/`iptables`; ele não substitui o firewall do Debian. |
| PF roda em **um namespace de rede só** (`init_net`) | Não há instância de PF por netns. |
| O OpenBSD `pfctl` fala com o módulo pelo ioctl ABI de `/dev/pf` | O `pfctl` desta árvore é o **OpenBSD 8.0** compilado para Linux; não é o do Debian. |
Três papéis cabem na mesma máquina Debian 13 — e é assim que o laboratório
foi validado:
| Papel | O que faz | Onde |
|---|---|---|
| **build host** | compila o kernel 7.3.4, o `pf.ko` e o userland | `~/lab/linux_kernel/pf_port` |
| **hypervisor** | hospeda as 4 VMs de teste (libvirt/KVM) | mesmo host, `libvirtd` |
| **host PF** (opcional) | roda o próprio PF no Debian 13, depois de bootar com o kernel 7.3.4 | mesmo host, com aviso no §9 |
---
## 1. Passo 1 — preparar o host
```sh
git clone <url-do-repo> ~/lab/linux_kernel/pf_port # ou: cp -a /caminho/pf_port ~/lab/linux_kernel/
cd ~/lab/linux_kernel/pf_port
sudo ./scripts/debian13/prepare-host.sh
```
O script é idempotente: instala os pacotes e imprime o estado do toolchain.
Saída real no Debian 13.7:
```
== toolchain
gcc 14
make GNU Make 4.4.1
bison bison (GNU Bison) 3.8.2
flex flex 2.6.4
bc bc 1.07.1
rsync rsync version 3.5.0 protocol version 32
libvirt 11.3.0
qemu QEMU emulator version 10.0.13 (Debian 1:10.0.13+ds-0+deb13u1)
geniso genisoimage 1.1.11 (Linux)
PREPARE_HOST_OK
```
**Sobre o compilador.** O Debian 13 traz `gcc-14` como padrão e **não tem
`gcc-15` em trixie** (`apt-cache policy gcc-15` → nenhum candidato). O
`docs/en/install.md` pede "gcc ≥ 15" porque o host de desenvolvimento
original é Fedora. No Debian 13 o build é feito com **gcc 14.2.0** — o kernel
7.3.4 e o `pf.ko` compilam sem patch (verificado, §4).
Se preferir separar os papéis, use `--build-only` ou `--lab-only`.
---
## 2. Passo 2 — obter as fontes
### 2.1 Árvore do kernel 7.3.4
O repositório **não** traz o kernel: ele espera a árvore em
`~/lab/linux_kernel/linux-7.3.4` (irmã do repositório). Do zero:
```sh
cd ~/lab/linux_kernel
curl -fLO https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.3.4.tar.xz
curl -fLO https://cdn.kernel.org/pub/linux/kernel/v7.x/sha256sums.asc
sha256sum -c --ignore-missing sha256sums.asc # confere só o tarball baixado
tar xf linux-7.3.4.tar.xz
```
O hash confere com o publicado pelo kernel.org:
```
b11b6df8295e8e991ff040b71aaa4994a8b55caa23b6b092907f195b5840fd1f linux-7.3.4.tar.xz
```
### 2.2 Fontes do OpenBSD 8.0 (o PF em si)
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/fetch-sources.sh # baixa sys.tar.gz + src.tar.gz, confere sha256 (pins no VERSIONS)
./scripts/vendorize.sh # extrai só os arquivos portados -> _vendor/upstream + IDS.txt
```
Espere as linhas `OK sys.tar.gz` e `OK src.tar.gz`. O `fetch-sources.sh`
tenta o mirror `openbsd.c3sl.ufpr.br` e cai para `cdn.openbsd.org`.
---
## 3. Passo 3 — compilar o kernel 7.3.4
```sh
cd ~/lab/linux_kernel/pf_port
KERNELDIR=~/lab/linux_kernel/linux-7.3.4 ./scripts/build-kernel.sh
```
O que o script faz, em ordem:
1. `scripts/kernel-config.sh` — parte de `make defconfig` e liga
`VIRTIO_BLK/PCI/NET=y`, `EXT4_FS=y`, `NETFILTER=y`, `NET_SCHED=y`,
`INET=y`, `IPV6=y`, `SERIAL_8250(+CONSOLE)=y` (é o que permite bootar a VM
`pf0` direto no kernel, **sem initrd**) e roda `make olddefconfig`;
2. `make -j$(nproc-2) bzImage modules_prepare` — compila o kernel e prepara a
árvore para módulos externos;
3. copia `vmlinux.symvers` → `Module.symvers` (o kbuild não cria o segundo) e
garante `scripts/module.lds`;
4. publica em `out/`: `linux-7.3.4-bzImage`, `System.map`, `.config`,
`kernelversion`, `bzImage.sha256`.
Termina com:
```
KERNEL_BUILD_OK 7.3.4
```
Tempo de referência no host do lab (Debian 13, 12 threads): **~4 min** para o
build do kernel em si (`bzImage` + `modules_prepare`) em uma árvore que já
tinha objetos, e o script inteiro — configurar, compilar, publicar os
artefatos — em menos de 3 minutos. Uma árvore vinda direto do tarball
começa vazia; aí o passo `make` domina o tempo.
---
## 4. Passo 4 — compilar o módulo `pf.ko`
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/build-module.sh # == make -C $KERNELDIR M=$PWD/module
```
Saída: `MODULE_BUILD_OK` e o artefato `module/pf.ko`.
**Verifique o `vermagic`** — é ele que decide onde o módulo carrega:
```sh
modinfo module/pf.ko | grep vermagic
# vermagic: 7.3.4 SMP preempt mod_unload
```
Esse é o motivo de o kernel e o módulo terem de sair **da mesma árvore**: um
`pf.ko` compilado contra outra árvore 7.3.4 pode ter `vermagic` subtly
diferente (`SMP preempt mod_unload modversions` + `CONFIG_*` do build) e o
`insmod` recusa.
### 4.1 Verificação do ABI userspace ↔ kernel
O `pfctl` fala com o kernel por structs compartilhadas. O harness `abitest`
compara o layout das duas pontas:
```sh
make -C tools abi # compila abitest (userspace) e abitest-module (kernel),
# dumpa as tabelas de tamanho/offset e faz o diff
```
Sem saída de diff = ABI intacto.
---
## 5. Passo 5 — compilar o userland
```sh
make -C tools # -> tools/out/{pfctl,pdivert,pflogd} + abitest
```
O `pfctl` sai **estático** (`-static`), para poder ser copiado para as VMs
Alpine sem dependências. Checagem offline (não precisa de `/dev/pf`):
```sh
(cd tools && ./out/pfctl -nf tests/01_basic.conf) # parse-only: sem saída = ok
make -C tools test # roda todos os .conf de teste
make -C tools abi # tabelas de ABI userspace vs kernel
```
---
### 5.1 O que o build no Debian 13 produziu (verificado)
Tudo abaixo foi compilado e executado no Debian 13.7 com **gcc 14.2.0** — sem
patch, sem backport:
```
$ ./scripts/build-module.sh
...
MODULE_BUILD_OK
$ modinfo module/pf.ko | grep vermagic
vermagic: 7.3.4 SMP preempt mod_unload
$ make -C tools test
PASS tests/01_basic.conf (exit 0)
PASS tests/02_translations.conf (exit 0)
PASS tests/03_queues_scrub.conf (exit 0)
PASS tests/broken.conf rejected (non-zero exit)
$ make -C tools abi
ABI OK: identical tables
```
O `vermagic` impresso pelo build no Debian é idêntico ao que a VM do lab
aceita. Verificado de ponta a ponta: o módulo compilado no Debian 13 foi
copiado para a VM do lab e carregado lá —
```
INSMOD_DEBIAN_MODULE_OK
Status: Enabled
crw------- 1 root root 10, 259 /dev/pf
[ 119.057040] pfk: netfilter hooks registered (10, netisr wq data path)
```
— desde que ele tenha sido compilado a partir da mesma árvore do kernel que
a VM sobe.
---
## 6. Passo 6 — o laboratório libvirt no Debian 13
O lab são 4 VMs Alpine (`pf0` = o firewall sob teste, `cli`, `srv`, `gw2`),
rede de gerência com DHCP e três bridges isolados `pf-lan`/`pf-wan`/`pf-wan2`.
A topologia está em `lab/ips.env`.
```sh
cd ~/lab/linux_kernel/pf_port
./scripts/vm-create.sh --pf0-kernel=out/linux-7.3.4-bzImage
```
Esse comando é idempotente: destrói e recria os domínios `pftest-*`, gera as
seeds cloud-init, sincroniza a imagem base Alpine e sobe tudo. Sem
`--pf0-kernel=`, a `pf0` sobe com o kernel da própria imagem Alpine — e aí o
`pf.ko` **não** carrega (vermagic). **Sempre** passe `--pf0-kernel=` quando a
intenção é testar PF.
Deploy dos artefatos para a VM:
```sh
./scripts/vm-deploy.sh # laptop -> deb ($DEPLOY_DIR) -> pf0:/opt/pf/
```
Verifique a VM:
```sh
ssh pf0 'uname -r; lsmod | grep -w pf; ls -l /opt/pf/'
```
Esperado: `7.3.4`, o módulo `pf` carregado e `pf.ko`/`pfctl`/`pdivert`/`pflogd`
em `/opt/pf/`.
---
## 7. Passo 7 — a suíte de 16 gates
```sh
./scripts/vm-run-tests.sh # ou: make test
```
O runner reinicia a `pf0` antes de cada gate (estado limpo), re-deploya o
módulo e as ferramentas, e roda o gate a partir do build host. Saída esperada:
```
PASS=16 FAIL=0
```
Logs por gate em `out/test-logs/<gate>.sh.log`.
---
## 8. Passo 8 — o teste de stress
O projeto não tinha gate de desempenho (o `docs/en/vm-lab.md` registrava
explicitamente essa lacuna). O stress é um gate novo, `tests/vm/16-stress.sh`,
que pergunta coisas diferentes das gates funcionais: *"o motor continua
correto, limitado e sem vazamento enquanto tráfego, estados, regras e tabelas
giram?"*.
```sh
./scripts/stress/run-stress.sh # perfil padrão
./scripts/stress/run-stress.sh --quick # perfil curto (smoke)
./scripts/stress/run-stress.sh --seconds 30 --connections 2000 --reloads 500
```
Opções: `--seconds`, `--parallel`, `--rate`, `--connections`, `--reloads`,
`--rules`, `--table-entries`, `--max-overhead`. O runner reset da `pf0`, faz o
deploy, roda o gate e grava o relatório em `out/stress/<timestamp>/report.md`.
> **As fases de tráfego rodam com taxa limitada** (`--rate`, padrão
> 100 Mbit/s por fluxo = 400 Mbit/s no total). Não é conveniência: o PF
> rouba cada skb para uma única workqueue ordenada e roda o engine em
> contexto de processo, então o backlog — e a memória que ele consome —
> cresce com a taxa oferecida. Na VM de 1 GB do lab, 400 Mbit/s é estável e
> **~1 Gbit/s dá OOM-panic no kernel** ("System is deadlocked on memory").
> Por isso o teste de stress trabalha no regime que o port suporta e reporta
> o teto separadamente, em vez de matar o firewall sob teste. Detalhes em
> [`caveats.md` §13](caveats.md).
### 8.1 O que cada fase mede
| Fase | O que faz | Como é julgado |
|---|---|---|
| 0 — baseline | throughput `cli -> srv` na taxa limitada com o **módulo fora do caminho** (`rmmod pf`) | precisa de > 0 Mbit/s (sem isso o resto não tem sentido) |
| 1 — overhead | o mesmo tráfego com PF ligado e regras stateful | `%` de overhead = (baseline − com PF) / baseline, contra o limite (padrão 30%) — na taxa com teto isso significa "o PF sustenta a taxa oferecida" |
| 2 — state churn | 300 conexões TCP concorrentes contra um listener dedicado no servidor | os estados **aparecem** na tabela (`pfctl -s states`) e **desaparecem** depois de `pfctl -sr` → sem vazamento |
| 3 — rule churn | 200 recargas de um ruleset de 200 regras | zero falhas de carga, tempo de recarga medido **dentro** da VM (min/méd/máx), contagem de regras estável |
| 4 — table churn | 2000 entradas em uma `pfr` table | entradas somem após o flush → sem vazamento de tabela |
| 5 — integridade | 2 MB atravessando `nat-to` **enquanto** um stream satura o link | sha256 do arquivo recebido = sha256 da origem |
| 6 — saúde | `dmesg`, contadores do PF, e um ping final | nenhum `BUG/WARNING/Oops`; contadores mexem; o firewall ainda roteia |
### 8.2 Como ler o relatório
Uma rodada real no lab (perfil completo, `out/stress/<timestamp>/report.md`):
```
STRESS phase=baseline baseline_mbit=400 seconds=10 parallel=4 rate_per_stream=100M
STRESS phase=overhead pf_mbit=400 overhead_pct=0 limit_pct=30 rate_per_stream=100M
STRESS phase=state_churn connections=300 states_before=0 states_created=300 states_after_reset=0 established_on_srv=214 rules_loaded=3 pf_status=Enabled
STRESS phase=rule_churn reloads=200 rules_per_load=200 reload_fails=0 reload_ms_avg=11 reload_ms_max=20 rules_final=202
STRESS phase=table_churn entries_requested=2000 entries_added=2000 add_errors=0 entries_after_flush=0
STRESS phase=integrity bytes=2000000 sha256_match=yes
STRESS phase=health dmesg_anomalies=0 match=5855 state_entries=7
```
Como ler: o PF sustenta os 400 Mbit/s limitados com **0% de overhead** contra
o baseline sem módulo, acompanha **300/300** fluxos concorrentes como estados
e esvazia a tabela de volta a 0, recarrega um ruleset de 202 regras **200
vezes** sem falhar (11 ms de média, 20 ms o pior caso), esvazia **2000**
entradas de tabela de volta a 0, move 2 MB por NAT byte a byte enquanto um
stream corre e não deixa nenhum `BUG/WARNING/Oops` para trás.
O runner imprime `STRESS RESULT: PASS` ou `FAIL` e sai com o status do gate.
> **Nota de honestidade:** os números de throughput são de um lab virtualizado
> (KVM, 2 vCPU por VM, rede `virtio` com bridge no host). Servem para
> *comparar* baseline contra PF no mesmo ambiente, não como número de
> produção. O overhead medido inclui a cópia de pacote e o caminho de software
> do PF — é o preço do filter stateful no userspace-space do kernel.
---
## 9. Passo 9 (opcional) — rodar o PF no próprio Debian 13
Isto **não** é necessário para o laboratório, e tem um custo: o host do lab é
também o hypervisor — reiniciar com outro kernel derruba as VMs.
```sh
cd ~/lab/linux_kernel/linux-7.3.4
sudo make modules_install # pf.ko -> /lib/modules/7.3.4/
sudo cp arch/x86/boot/bzImage /boot/vmlinuz-7.3.4
sudo update-initramfs -u -k 7.3.4 # ou: update-grub
sudo reboot # ⚠ derruba as VMs do lab
```
Depois do boot (com o 7.3.4 ativo):
```sh
uname -r # 7.3.4
sudo insmod /lib/modules/7.3.4/pf.ko # cria /dev/pf (0600) e registra os hooks
ls -l /dev/pf
sudo cp ~/lab/linux_kernel/pf_port/tools/out/pfctl /usr/local/sbin/
sudo pfctl -e
sudo pfctl -f /etc/pf.conf
sudo pfctl -s info
```
`/etc/pf.conf` mínimo (sintaxe OpenBSD 8.0):
```
set skip on lo
block return
pass on egress
```
Para deixar o PF ligado no boot, uma unit simples:
```ini
# /etc/systemd/system/pf.service
[Unit]
Description=PF-on-Linux engine
After=network-pre.target
Before=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/modprobe pf
ExecStart=/usr/local/sbin/pfctl -e
ExecStop=/usr/local/sbin/pfctl -d
ExecStop=/sbin/modprobe -r pf
[Install]
WantedBy=multi-user.target
```
E o `pflogd` (escreve pcap `DLT_PFLOG`, layout OpenBSD `pflog(4)`):
```ini
# /etc/systemd/system/pflogd.service
[Unit]
Description=PF log daemon
After=pf.service
[Service]
ExecStart=/usr/local/sbin/pflogd /var/log/pf/pflog.pcap
[Install]
WantedBy=multi-user.target
```
> `pflogd` lê `/dev/pflog` (criado pelo módulo, 0600). Este `pfctl` **não**
> tem o modificador `-s pflog`: o log é ligado por regra, com a palavra-chave
> do OpenBSD (`pass log on egress ...`, `block log ...`). O pcap em disco fica
> onde você apontar o `pflogd`.
---
## 10. Checklist de verificação
```sh
# 1. módulo carregado e device criado
lsmod | grep -w pf && ls -l /dev/pf
# 2. PF habilitado
pfctl -s info | head -2 # Status: Enabled
# 3. parser do OpenBSD 8.0 no binário do Debian
pfctl -nf /etc/pf.conf
# 4. hooks registrados
dmesg | grep pfk # "pfk: netfilter hooks registered (10, netisr wq data path)"
# 5. o firewall filtra de verdade
pfctl -sr; pfctl -s states
# 6. suíte completa
./scripts/vm-run-tests.sh # PASS=16 FAIL=0
# 7. stress
./scripts/stress/run-stress.sh # STRESS RESULT: PASS
```
---
## 11. Troubleshooting
| Sintoma | Causa | Ação |
|---|---|---|
| `insmod: ERROR: could not insert 'pf.ko': Invalid module format` | `vermagic` diferente do kernel em execução | o kernel e o módulo precisam sair da **mesma** árvore; recompile os dois |
| `insmod` falha no kernel do Debian (6.12) | o módulo é para 7.3.4 | bootar com o kernel 7.3.4 (§9) ou testar dentro da VM do lab |
| `pfctl: Permission denied` | `/dev/pf` é 0600 root | rodar como root |
| `pfctl: Device not configured` | módulo carregado mas `pfctl -e` não foi feito | `sudo pfctl -e` |
| `virsh: cannot connect to qemu:///system` | libvirt parado | `sudo systemctl enable --now libvirtd` |
| `Cannot find device 'eth2'` | nomes de interface mudam | resolver com `ip route get <ip> | grep -o 'dev [a-z0-9]*'` (o gate 15 faz isso) |
| gate falha logo depois do deploy | `pf.ko` não copiado | o runner verifica `test -s /opt/pf/pf.ko`; ver `out/test-logs/<gate>.sh.log` |
| stress: `baseline throughput unavailable` | `iperf3` não está no servidor | `srv 'pkill -x iperf3; iperf3 -s -D'` e conferir `netstat -lnt \| grep 5201` |
| stress: `state churn` falha | o `bash` do guest não tem `/dev/tcp` | o gate usa `nc` como fallback; verificar `bash --version` na VM |
| a VM reinicia durante o stress | a taxa oferecida esgotou a memória do guest (backlog da workqueue) | baixar o `--rate`; ver [caveats.md §13](caveats.md) — o teto medido no lab é ~1 Gbit/s em um guest de 1 GB |
| `gcc-15` não encontrado | não existe no trixie | usar o gcc 14 padrão (§1) |
---
## 12. Limites conhecidos (leia antes de usar em produção)
- O port **não** é o PF do OpenBSD: há divergências documentadas em
[`docs/pt-BR/caveats.md`](caveats.md) e na matriz
[`docs/pt-BR/feature-matrix.md`](feature-matrix.md).
- Não há `pfsync`/CARP, IPsec/pfkey, `ftp-proxy`, PF por netns nem os
schedulers `cbq`/`wfq`/`red` (removidos no upstream antes do OpenBSD 8.0).
- PF registra hooks netfilter em `init_net` — em um host que já use
`nftables`, o tráfego passa pelos dois. O `bypass` do PF só vale para
tráfego que entra pelo gancho do PF.
- O IPv6, o OS fingerprinting, `set state-policy`/`allow-opts` e os syncookies
compilam, mas **não** são cobertos por gate (lacuna conhecida, §5 do
`docs/pt-BR/vm-lab.md`).
---
## 13. Referências
| Documento | Conteúdo |
|---|---|
| [`docs/pt-BR/install.md`](install.md) | receita de build genérica (sem o passo a passo do Debian) |
| [`docs/pt-BR/vm-lab.md`](vm-lab.md) | o laboratório, topologia e gates |
| [`docs/pt-BR/feature-matrix.md`](feature-matrix.md) | o que está verificado e o que não está |
| [`docs/pt-BR/caveats.md`](caveats.md) | divergências conhecidas do OpenBSD 8.0 |
| [`docs/pt-BR/pf-guide.md`](pf-guide.md) | regrasets do dia a dia e avisos |
| English mirror | [`docs/en/debian13.md`](../en/debian13.md) |
-40
View File
@@ -1,40 +0,0 @@
# Demo (asciinema)
O lab pode ser gravado como um cast do [asciinema](https://asciinema.org).
Cada byte da gravação é a saída real das VMs do lab (Alpine sobre libvirt,
rodando o kernel compilado do fonte com o `pf.ko`); só o ritmo é roteirizado.
## Gravar
```sh
pip install asciinema # uma vez
bash scripts/vm-run-tests.sh # ou: scripts/vm-create.sh + deploy
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
```
A sessão percorre:
1. a topologia do lab e o `pf.ko` recém-carregado (prova no `dmesg`),
2. `pfctl -s info` pela ABI de ioctl do OpenBSD,
3. um `pf.conf` real (tabela, regras stateful, drop explícito) e `pfctl -sr`,
4. ping em trânsito + `pfctl -ss` (stateful),
5. `pfctl -t <tabela> -T show` (tabelas vivas),
6. um ping bloqueado (contadores com `pfctl -sr -vv`),
7. `divert-to` com o relay `pdivert` (pacote vai ao userspace e volta),
8. desligamento.
## Reproduzir
```sh
asciinema play out/demo-pf-linux.cast
```
## Publicar
```sh
asciinema upload out/demo-pf-linux.cast
```
`scripts/demo-record.py` é um gravador alternativo que produz o mesmo formato
asciicast v2 sem o binário asciinema (ele conduz o lab por ssh e escreve o
cast diretamente).
+55 -50
View File
@@ -1,78 +1,83 @@
# Matriz de features # Matriz de features
English: [docs/en/feature-matrix.md](../en/feature-matrix.md). Inglês: [docs/en/feature-matrix.md](../en/feature-matrix.md).
Vocabulário de status (idêntico nos dois idiomas): 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)). - **VERIFIED-in-VM** — exercitado de ponta a ponta no lab libvirt (veja [vm-lab.md](vm-lab.md)); o gate que prova está nomeado.
- **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. - **VERIFIED-offline** — provado por um teste de fixture/ABI, não por tráfego (ex.: `make -C tools test`).
- **COMPILES-untested** — compila em `pf.ko`/`pfctl`; nenhum gate exercitou. - **COMPILES-untested** — compilado dentro do `pf.ko`/`pfctl`; nenhum gate exercitou ainda.
- **PENDING-Mx** — entra no milestone Mx; o comportamento observável hoje está declarado. - **NOT-ENABLED** — o código existe mas a feature está desligada neste build (ex.: `af-to`, que depende dos caminhos `INET6`).
- **OUT-OF-SCOPE** — não-objetivo por projeto (spec §3). - **OUT-OF-SCOPE** — fora de escopo por desenho (spec §3).
Estado em 2026-11-11; evidência por linha: `out/test-logs/`, Atualizado em 2026-11-12; fonte da evidência por linha: `out/test-logs/`,
`docs/porting-notes.md`, commits de milestone. `docs/porting-notes.md`, commits dos milestones e os screencasts
([screencast.md](screencast.md)) para a prova visível.
A suíte está **16/16 verde** (`./scripts/vm-run-tests.sh`).
## Plano de controle e ferramentas ## Plano de controle e ferramentas
| Feature | Sintaxe de upstream | Milestone | Status | | Feature | Sintaxe upstream | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| Carga do módulo, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM | | Carga do módulo, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM (gate `02-ativacao.sh`) |
| Habilitar/desabilitar | `pfctl -e`, `pfctl -d` | 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) | | Carga de ruleset | `pfctl -f file` | M2 | VERIFIED-in-VM (todo gate carrega um ruleset real) |
| Checagem só de parse | `pfctl -n -f arquivo` | M1 | VERIFIED (suíte de fixtures offline, `make -C tools test`) | | Checagem só de parse | `pfctl -n -f file` | M1 | VERIFIED-offline (`make -C tools test`) |
| Status/contadores | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM | | Status/contadores | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM |
| Listagem de regras | `pfctl -s rules [-v]` | M2 | COMPILES-untested | | Listagem de regras | `pfctl -s rules [-v]`, `pfctl -sr` | M2 | VERIFIED-in-VM (gates 02–06) |
| 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 | | Listagem de estados | `pfctl -s states [-v]`, `pfctl -ss` | M2 | VERIFIED-in-VM (gate `01-state.sh`) |
| Tabelas (listagem/ops) | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) | | Listagem/operações de tabelas | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | VERIFIED-in-VM (gate `03-tables.sh`, incluindo um `-T delete` ao vivo liberando o fluxo) |
| Anchors | `pfctl -a anchor -f arquivo`, `-s Anchors` | M2 | M2-in-progress (gate 04) | | Anchors | `pfctl -a anchor -f file`, `-s anchors` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| 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) | | Listagem de filas | `pfctl -s queue -v` | M5 | VERIFIED-in-VM (gate `15-queue.sh`: as duas classes com estatísticas) |
| 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`) | | Paridade da ABI ioctl | `make -C tools abi` (tabelas do abitest) | M1 | VERIFIED-offline (tabela do userland); o diff do lado do kernel é um passo de integração em aberto (`tools/UPSTREAM.md`) |
## Núcleo de filtragem ## Núcleo de filtragem
| Feature | Sintaxe de upstream | Milestone | Status | | Feature | Sintaxe 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) | | `pass`/`match`, quick, on-interface/proto/addr/port | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | VERIFIED-in-VM (gate `01-state.sh`: ping de trânsito atravessando o firewall) |
| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) | | `block drop` | `block drop in quick …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: o ping morre e os contadores provam) |
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) | | `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: o `nc` do cliente recebe RST) |
| `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | M2-in-progress | | `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | VERIFIED-in-VM (gate `01-state.sh`; o estado aparece no `pfctl -ss`) |
| `set state-policy`, `allow-opts` etc. | opções `set …` | M2 | COMPILES-untested | | `set state-policy`, `allow-opts`, etc. | opções `set …` | M2 | COMPILES-untested |
| Syncookies (caminho de estouro de estado) | `set syncookies always` | M2 | COMPILES-untested | | Syncookies (caminho de overflow de estados) | `set syncookies always` | M2 | COMPILES-untested |
| Tabelas pfr (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) | | Tabelas pfr (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | VERIFIED-in-VM (gate `03-tables.sh`) |
| Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) | | Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested (o engine é completo para v6 por construção; gates v6 smoke planejados) | | IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested — o engine é completo para v6 por construção, mas o caminho v6 não é exercitado por gate |
| 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) | | Tráfego não-IPv4/IPv6 (ARP…) | — | M2 | PFLINUX por construção: só os hooks netfilter IPv4/IPv6 — ARP nunca entra no caminho do PF (sem gate) |
## NAT / normalização (M3) ## NAT / normalização (M3)
| Feature | Sintaxe de upstream | Milestone | Status | | Feature | Sintaxe upstream | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFICADO em lab** — NAT de origem funciona ponta a ponta: o pacote sai com o endereço do pool (prova via tcpdump), a resposta é des-NATeada e roteada de novo para o cliente, e o estado mostra as duas tuplas (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). Gate: `tests/vm/05-nat.sh`. | | `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFIED-in-VM** — gate `05-nat.sh`: o NAT de origem funciona de ponta a ponta (prova por tcpdump), a resposta é des-NATeada e re-encaminhada para o cliente, e o estado mostra as duas tuplas (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). |
| `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFICADO em lab** — o redirect de portas funciona ponta a ponta (gate `06-rdr.sh`): o cliente fala com o endereço do firewall, o pf reescreve o destino, re-rotea o pacote e a resposta é des-redirectuada. O gate também baixa 2 MB pelo redirect e confere a integridade byte a byte (guarda de regressão GSO/TSO). | | `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFIED-in-VM** — gate `06-rdr.sh`: o cliente fala com o endereço do firewall, o pf reescreve o destino, re-encaminha o pacote, e a resposta é des-redirecionada. O gate também puxa 2 MB pelo redirect e confere a integridade byte a byte (guarda de regressão GSO/TSO). |
| `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFICADO em lab** — gate `07-binat.sh`. O pfctl expande uma regra `binat-to` no par documentado (`pass out … nat-to <mapeado> static-port` + `pass in … rdr-to <real>`), e as duas metades funcionam: o peer vê o endereço mapeado, o original não vaza, e a resposta é traduzida de volta. | | `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFIED-in-VM** — gate `07-binat.sh`. O pfctl expande uma regra `binat-to` no par documentado (`pass out … nat-to <mapped> static-port` + `pass in … rdr-to <real>`), e as duas metades funcionam: o par vê o endereço mapeado, o original nunca vaza, e a resposta é traduzida de volta. |
| Scrub/normalização: remonteagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFICADO em lab** — gate `09-scrub.sh`: `min-ttl` eleva o TTL (o egress mostra 99 após o decremento do encaminhamento) e `max-mss` limita o MSS de um SYN, ambos medidos no egress do próprio firewall. Os caminhos de remonteagem/overlap estão compilados e alcançados, mas ainda sem gate. | | Pools de NAT (`round-robin`, `source-hash`) | `nat-to { 10.0.2.1, 10.0.3.1 } round-robin` | M4 | **VERIFIED-in-VM** — gate `10-pools.sh`, e visível no screencast: o round-robin alterna o endereço de origem entre os membros (3 pacotes cada), o source-hash mantém uma origem em um membro só. |
| 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) | | Scrub/normalize: remontagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFIED-in-VM** — gate `09-scrub.sh`: o `min-ttl` aumenta o TTL (egresso mostra 99 depois do decremento do forward) e o `max-mss` limita a opção MSS do SYN, ambos medidos no egresso do próprio firewall. Os caminhos de remontagem/overlap compilam e são alcançados, mas não têm gate. |
| Fingerprint de OS nos estados | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compilado; ainda sem o pacote de fingerprints — a ausência de `/etc/pf.os` é tolerada) |
## Roteamento / divert / filas / logging ## Roteamento / divert / filas / logging
| Feature | Sintaxe de upstream | Milestone | Status | | Feature | Sintaxe upstream | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| Pools de `route-to` (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to` | `pass in … route-to { 10.0.3.2, 10.0.2.2 } round-robin keep state` | M4 | **`round-robin` e `source-hash` VERIFICADOS em lab** (gate `10-pools.sh`): com dois gateways em dois enlaces, o round-robin usa ambos e o source-hash fixa todo fluxo de uma origem em um único enlace. `dup-to` verificado no gate 08; `reply-to` ainda sem gate. `fail-to` **não existe no OpenBSD 8.0** (removido upstream), então está fora de escopo. | | `route-to` / `dup-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state`, `reply-to { gw }` | M4 | **VERIFIED-in-VM** — gates `08-routeto.sh` (o route-to sai pelo link do gateway, o dup-to copia mantendo o fluxo) e `13-replyto.sh` (a resposta de um fluxo é forçada para o link do gateway em vez do cliente). Divergência: o próximo salto forçado é instalado definindo `rt_gw4`/`rt_gw_family` na rota consultada (o Linux só faz ARP do gateway quando a rota declara um). `fail-to` **não é palavra-chave no OpenBSD 8.0** (removido upstream), logo está fora de escopo. |
| `divert-to` / tag `divert` | `pass in … divert-to <host> port <n>` | VERIFICADO em lab | **Funciona ponta a ponta.** O módulo registra em runtime a família de sockets `AF_DIVERT` (`PF_DIVERT=32`) — sem patch no kernel. A regra `divert-to` entrega o pacote a essa família; sem listener o pacote é descartado (`divs_noport`, paridade BSD); com `tools/pdivert` rodando o pacote vai ao userspace, é reescrito e reinjetado (verificado com mutação de payload + correção de checksum, ping 0% loss). | | `divert-to` / tag `divert` | `pass in … divert-to <host> port <n>` | M4 | **VERIFIED-in-VM** — gate `11-divert.sh`. O módulo registra uma família de sockets `AF_DIVERT` (`PF_DIVERT=32`) em runtime — sem patch no kernel. Uma regra `divert-to` entrega o pacote a essa família; sem listener o pacote é descartado (`divs_noport`, paridade com o BSD); com o `tools/pdivert` rodando o pacote vai para o userspace, é reescrito e reinjetado (verificado com mutação do payload + correção de checksum, ping com 0% de perda). |
| Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFICADO em lab** — gate `15-queue.sh`: a configuração de filas carrega, a disciplina hfsc é anexada à interface, `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila completa, e o contador de pacotes da classe avança (o escalonador está no caminho de dados). Note que `bandwidth` é *linkshare* do HFSC - garantia sob contenção, não teto: o pf.conf do OpenBSD 8.0 não tem `minimum`/`maximum`, então um fluxo isolado não é limitado, nem no Linux nem no OpenBSD. | | Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFIED-in-VM** — gate `15-queue.sh`: a configuração de filas carrega, a disciplina hfsc é anexada à interface, `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila completa, e o contador de pacotes da classe avança (o escalonador está no caminho de dados). Note que `bandwidth` é *linkshare* do HFSC — garantia sob contenção, não teto: o pf.conf do OpenBSD 8.0 não tem `minimum`/`maximum`, então um fluxo isolado não é limitado, nem no Linux nem no OpenBSD. |
| 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 | | HFSC | `queue on wan { hfsc … }` | M5 | **VERIFIED-in-VM** — o `pfsrc/hfsc.c` (código real do OpenBSD 8.0) está na lista de objetos do módulo e é a disciplina que o gate exercita. As classes usam o `net/hfsc.h` vendorizado (classe raiz `0x10000`, classe default `0x1000`) compartilhado com o `pfctl`, então a ABI de estatísticas bate nos dois lados. |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado | | FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — o `pfsrc/fq_codel.c` é compilado dentro do `pf.ko` e o `net/fq_codel.h` vendorizado é compartilhado com o `pfctl`, mas nenhum gate o exercita. |
| Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) | | Interface `pfq` | n/a (interfaces BSD `ifq`/`pfq`, não um qdisc `tc` do Linux) | M5 | **VERIFIED-in-VM** — o port mantém a forma do BSD: `compat/pfk_ifq.c` implementa as interfaces `ifq_ops`/`pfq_ops` e os back ends do OpenBSD se plugam nelas (`pfq` nunca foi um qdisc `tc` do Linux, nem nunca pretendeu ser). priq é a disciplina default. |
| pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFICADO em lab** — gate `12-pflog.sh`: os pacotes logados são enfileirados pelo engine, lidos via `/dev/pflog` e gravados como pcap com linktype DLT_PFLOG (117); o dump contém os bytes reais do pacote. Desvio: um chardev em vez de interface if_pflog/bpf (Linux não tem bpf sobre essa interface virtual aqui); o layout do registro é byte-compatível com `pflog(4)` do OpenBSD. `if_pflog.c` permanece fora do build. | | pflog (`log`, `log all`, `block log`) + pcap do `pflogd` (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFIED-in-VM** — gate `12-pflog.sh`: os pacotes logados são enfileirados pelo engine, lidos por `/dev/pflog` e escritos como pcap com linktype DLT_PFLOG (117); o dump contém os bytes reais do pacote. Divergência: um chardev em vez de uma interface if_pflog/bpf (o Linux não tem bpf sobre uma interface virtual dessas aqui); o layout do registro é byte a byte compatível com o `pflog(4)` do OpenBSD. O `if_pflog.c` continua fora da build. |
## Não-objetivos (explícitos) ## Fora de escopo (explícito)
| Item | Motivo | | Item | Motivo |
|---|---| |---|---|
| Failover `pfsync` / CARP | não-objetivo (spec §3) | | Failover `pfsync` / CARP | fora de escopo (spec §3) |
| IPsec / pfkey | não-objetivo | | IPsec / pfkey | fora de escopo |
| `ftp-proxy` e daemons de userland | não-objetivo | | `ftp-proxy` e daemons de userland | fora de escopo |
| Instâncias PF por netns | port fiel = instância global única em `init_net` | | Instâncias PF por netns | port fiel = uma instância global no `init_net` |
| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos do upstream antes do OpenBSD 8.0 — impossível portar da base | | Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos upstream antes do OpenBSD 8.0 — impossíveis de portar a partir da base |
| Labels de rota (`route-label`) | não-objetivo; a camada de ioctl rejeita regras com label | | Routing labels (`route-label`) | fora de escopo; a camada de ioctl rejeita regras com label |
| `af-to` (tradução IPv4↔IPv6) | o código está sob `#ifdef INET6` e este build tem `INET6 0`; habilitá-lo é um milestone à parte, não uma feature faltando na base |
+25 -17
View File
@@ -21,7 +21,7 @@ bit a bit entre kernel e userland (codificação de ioctl em
| Caminho | Papel | | 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/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`, `hfsc.c`/`fq_codel.c` (back ends de fila, no módulo), `divert_sock.c` (família AF_DIVERT), `pfk_pflog.c` (back end do pflog), `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/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` | | `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` | | `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` |
@@ -71,31 +71,39 @@ tamanho embutido na codificação `_IOR/_IOW` OpenBSD e despacha para o
## Filas, divert, pflog — comportamento observável hoje ## Filas, divert, pflog — comportamento observável hoje
- **Filas**: o `pfctl` faz o parse de `queue`/`queue on` (sintaxe 8.0); no - **Filas**: o `pfctl` faz o parse da sintaxe `queue`/`queue on` do OpenBSD 8.0
kernel a API ifq é implementada por `compat/pfk_ifq.c` com **semântica e o kernel a implementa na forma do BSD: `compat/pfk_ifq.c` fornece as
priq apenas**; o qdisc classful `pfq` e os back ends HFSC/FQ-CoDel chegam interfaces `ifq_ops`/`pfq_ops` e os back ends do OpenBSD (`pfsrc/hfsc.c`,
em **M5**. `ifq.c`/`hfsc.c`/`fq_codel.c` estão staged e prontos para `pfsrc/fq_codel.c`) se plugam nelas; **priq** é a disciplina default. O
compilar, mas ainda **não** estão na lista de objetos do módulo. gate `15-queue.sh` prova a disciplina **hfsc** de ponta a ponta: a
- **Divert**: `divert-to`/tag `divert` produzem o veredito `PF_DIVERT` do PF; configuração de filas carrega, a disciplina é anexada à interface, o
`divert_packet()` (`pfsrc/divert_sock.c`) hoje **descarta o pacote com um `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB
`pr_warn` rate-limited**, porque a família de sockets AF_DIVERT ainda não pela fila completa e o contador de pacotes da classe avança. Os handles de
foi instanciada. O relay userspace `pdivert` existe e espera classe e as structs de estatísticas vêm do próprio `net/hfsc.h` e
`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`. `net/fq_codel.h` do OpenBSD, vendorizados para que kernel e `pfctl` não
- **pflog**: `pflog_packet()` retorna sucesso mas **não loga nada** (um puedan divergir.
único `pr_warn "packet not logged"`); `if_pflog.c` está staged, back end - **Divert**: `divert-to`/tag `divert` produzem o veredito `PF_DIVERT` do PF e
netlink em M4/M5. o módulo registra uma família de sockets `AF_DIVERT` (`PF_DIVERT=32`) em
runtime — sem patch no kernel. Sem listener o pacote é descartado
(`divs_noport`, paridade com o BSD); com o relay `pdivert` rodando o pacote
vai para o userspace, é reescrito e reinjetado. Gate `11-divert.sh`.
- **pflog**: o engine enfileira os pacotes logados, `pfsrc/pfk_pflog.c` os
expõe por `/dev/pflog` e o `tools/pflogd` escreve um pcap DLT_PFLOG cujo
layout de registro é byte a byte compatível com o `pflog(4)` do OpenBSD.
Gate `12-pflog.sh`. A divergência em relação ao upstream é o transporte (um
chardev em vez de uma interface `if_pflog`/bpf), não o formato.
## Milestones (status por feature em feature-matrix.md) ## Milestones (status por feature em feature-matrix.md)
| Milestone | Escopo | Status (2026-11-11) | | Milestone | Escopo | Status |
|---|---|---| |---|---|---|
| M0 | repo, scripts, build do kernel, VM liga com kernel próprio | concluído | | 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 | | M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f`/`-s info` | concluído |
| M2 | núcleo do filtro: regras/estados/tabelas/âncoras, gates VM | **pronto** — gates 01–04 verdes | | M2 | núcleo do filtro: regras/estados/tabelas/âncoras, gates VM | **pronto** — gates 01–04 verdes |
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **pronto** — gates 05/06/07/09 (incl. integridade de 2 MB via redirect) e pflog gate 12 | | M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **pronto** — gates 05/06/07/09 (incl. integridade de 2 MB via redirect) e pflog gate 12 |
| M4 | route-to/dup-to/reply-to, pools, divert e2e | **pronto** — gates 08/10/13 e divert gate 11 | | M4 | route-to/dup-to/reply-to, pools, divert e2e | **pronto** — gates 08/10/13 e divert gate 11 |
| M5 | filas: back ends ifq (hfsc/fq_codel) | **parcial** — back ends compilados no `pf.ko`; anexação/ritmo pendentes | | M5 | filas: back ends ifq (hfsc/fq_codel) | **concluído (mecânico)** — gate 15: o hfsc é anexado, `pfctl -s queue -v` mostra as stats das classes, um fluxo de 2 MB pela fila é contado pela classe. `bandwidth` = linkshare do HFSC (garantia sob contenção, não teto) |
| M6 | docs completas, patchset limpo, suíte verde, releases com tag | em andamento — docs EN+PT-BR completas, suíte 14/14 verde, com tag | | M6 | docs completas, patchset limpo, suíte verde, releases com tag | **concluído** — docs EN+PT-BR, suíte 16/16 verde, tag `v1.0.0` |
## Próximas leituras ## Próximas leituras
+24 -10
View File
@@ -75,22 +75,36 @@ Como a entrega é um módulo out-of-tree, "aplicar o conjunto" = passos
milestone acrescentar `patches/kernel/000N-*.patch` documentado (cada patch: 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). 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) ## 6. Notas sobre os passos acima (2026-11-12)
- O passo **2.3 é um no-op documentado hoje**: o conjunto de patches está - 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 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 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 com o primeiro patch real de core — ver [patches.md](patches.md)). Pode
pular 2.3 com segurança. pular 2.3 com segurança.
- O passo 2.4 compila apenas o `pfctl` e o verificador de ABI (alvos de - O passo 2.4 compila todas as ferramentas de userland: os alvos de
`make -C tools`: `pfctl abitest abi test clean`). `pdivert` existe como `make -C tools` são `pfctl`, `pdivert`, `pflogd` e o verificador de ABI
arquivo-fonte sem alvo no Makefile por enquanto; `pflogd` ainda não foi (`abitest`, `abi`, `test`, `clean`).
portado. - Depois do 2.5, todo o conjunto de features é usável e coberto pela suíte do
- Depois do 2.5, o que dá para usar de verdade é o núcleo de filtragem M2 lab — filtragem com estado, tabelas e anchors, NAT (`nat-to`/`rdr-to`/
(pass/block/quick/keep state/tabelas/anchors); NAT/scrub/queues/divert/ `binat-to` e pools), scrub, roteamento forçado (`route-to`/`dup-to`/
pflog dependem dos próximos milestones — consulte a `reply-to`), `divert`, `pflog` e as filas do OpenBSD 8.0 (HFSC). A
[matriz de features](feature-matrix.md) e leia [pf-guide.md](pf-guide.md) [matriz de features](feature-matrix.md) diz qual gate prova o quê; leia
antes de habilitar PF num caminho que você precisa manter vivo. [pf-guide.md](pf-guide.md) para conhecer as divergências antes de habilitar
o PF num caminho que você precisa manter vivo.
## 6.1 Opcional: os screencasts narrados
```sh
export MINIMAX_API_KEY=... # TTS do MiniMax para a narração
./scripts/vm-create.sh # o lab precisa estar no ar
./scripts/screencast/build-all.sh # -> out/screencast/{en,pt}/*.mp4
```
O pipeline grava sessões shell reais contra o lab, gera a narração com uma
voz feminina do MiniMax (inglês e português) e renderiza MP4 1080p30 com
legendas queimadas e o diagrama de rede do lab. Detalhes:
[screencast.md](screencast.md).
## 7. Para onde ir agora ## 7. Para onde ir agora
+14 -9
View File
@@ -3,11 +3,12 @@
English: [docs/en/pf-guide.md](../en/pf-guide.md). English: [docs/en/pf-guide.md](../en/pf-guide.md).
Este é o guia do "o que dá para rodar hoje". Ele segue a 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 [matriz de features](feature-matrix.md): o núcleo de filtragem
filtragem: pass/block/quick/keep state/tabelas/anchors + habilitar/ (pass/block/quick/keep state/tabelas/anchors), a família de NAT, o scrub, o
desabilitar)** tem evidência em VM; tudo além disso é COMPILES-untested ou roteamento forçado, os pools, o `divert`, o `pflog` e as filas do OpenBSD
PENDING. Instalação/build ficam em [install.md](install.md) e 8.0 têm todos **evidência em VM** — os 16 gates do lab estão verdes. O que
[build.md](build.md). *não* tem gate está listado explicitamente abaixo. Instalação/build ficam em
[install.md](install.md) e [build.md](build.md).
## Ciclo diário ## Ciclo diário
@@ -26,7 +27,7 @@ sudo rmmod pf # descarrega (regras/estados são liberados aq
> **Atenção — a ordem importa**: `-f` e depois `-e`. Habilitar sem uma regra `pass` > **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 > 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 > a política padrão de bloqueio do OpenBSD, e as rodadas do lab
> *realmente derrubaram o ssh da VM de teste* assim (a bancada mantém console > *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 > serial exatamente por isso). Num host remoto, libere a interface de
> gerência antes de habilitar qualquer coisa. > gerência antes de habilitar qualquer coisa.
@@ -34,8 +35,8 @@ sudo rmmod pf # descarrega (regras/estados são liberados aq
## Um ruleset que funciona neste port hoje ## Um ruleset que funciona neste port hoje
```pf ```pf
# /etc/pf.conf — apenas escopo M2. Os nomes de interface aqui são os da # /etc/pf.conf. Os nomes de interface aqui são os da bancada
# bancada (mgmt/lan/wan); troque pelos seus (eth0, enp1s0, …). # (mgmt/lan/wan); troque pelos seus (eth0, enp1s0, …).
# 1) mantenha você mesmo alcançável (faça isso primeiro, sempre) # 1) mantenha você mesmo alcançável (faça isso primeiro, sempre)
pass in quick on mgmt pass in quick on mgmt
@@ -59,7 +60,11 @@ 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 `set` ainda não foi verificado em gate; trate como "carrega e reza" até a
matriz mudar. matriz mudar.
## O que evitar até os milestones chegarem **Sem gate** (lacunas honestas): o caminho IPv6, o fingerprint de OS, as
opções `set` acima, os syncookies e a disciplina FQ-CoDel. O `af-to` está
compilado fora neste build (`INET6 0`).
## Notas por feature — o que cada sintaxe faz hoje
| Sintaxe | Até | O que acontece hoje | | Sintaxe | Até | O que acontece hoje |
|---|---|---| |---|---|---|
+47 -47
View File
@@ -1,58 +1,58 @@
# Screencasts / Videos da demonstracao # Screencasts / Vídeos da demonstração
> PT-BR: esta pagina · EN: [docs/en/screencast.md](../en/screencast.md) > PT-BR: esta página · EN: [docs/en/screencast.md](../en/screencast.md)
Sete videos por idioma, com **narracao de voz feminina** (MiniMax), **legendas Sete vídeos por idioma, com **narração de voz feminina** (MiniMax), **legendas
queimadas**, **diagrama de rede do lab** e um **filme completo** com transicoes queimadas**, **diagrama de rede do lab** e um **filme completo** com transições
entre as fases. 1920x1080 @ 30fps, H.264/MP4 + AAC — prontos para subir no entre as fases. 1920×1080 @ 30fps, H.264/MP4 + AAC — prontos para subir no
YouTube. YouTube.
Cada video e a gravacao de uma **sessao shell real**: uma conexao SSH de verdade Cada vídeo é a gravação de uma **sessão shell real**: uma conexão SSH de
ao firewall Alpine, com os comandos digitados tecla a tecla e a saida do kernel verdade ao firewall Alpine, com os comandos digitados tecla a tecla e a saída
chegando em tempo real. Nada e simulado. do kernel chegando em tempo real. Nada é simulado.
## As fases ## As fases
| # | Video | O que mostra | | # | Vídeo | O que mostra |
|---|-------|--------------| |---|-------|--------------|
| 0 | `00-ambiente.mp4` | O lab: 4 VMs Alpine sob libvirt, as quatro interfaces do firewall e o diagrama de rede completo | | 0 | `00-ambiente.mp4` | O lab: 4 VMs Alpine sob libvirt, as quatro interfaces do firewall e o diagrama de rede completo |
| 1 | `01-implementacao.mp4` | A implementacao no kernel: fontes do OpenBSD 8.0 + camada de compatibilidade = `pf.ko` | | 1 | `01-implementacao.mp4` | A implementação no kernel: fontes do OpenBSD 8.0 + camada de compatibilidade = `pf.ko` |
| 2 | `02-ativacao.mp4` | `insmod pf.ko`, `/dev/pf`, `pfctl -e`, `pfctl -s info` | | 2 | `02-ativacao.mp4` | `insmod pf.ko`, `/dev/pf`, `pfctl -e`, `pfctl -s info` |
| 3 | `03-regras.mp4` | Regras basicas: tabela, anchor, `keep state`, `block drop` (o ping morre) | | 3 | `03-regras.mp4` | Regras básicas: tabela, anchor, `keep state`, `block drop` (o ping morre) |
| 4 | `04-roteamento.mp4` | `route-to` e `reply-to`, com o diagrama mostrando o caminho real do pacote | | 4 | `04-roteamento.mp4` | `route-to` e `reply-to`, com o diagrama mostrando o caminho real do pacote |
| 5 | `05-balanceamento.mp4` | Pool `round-robin`, pool `source-hash` e `dup-to` | | 5 | `05-balanceamento.mp4` | Pool `round-robin`, pool `source-hash` e `dup-to` |
| 6 | `06-qos.mp4` | Filas `hfsc` do OpenBSD 8.0 e uma transferencia de 2 MB pela classe | | 6 | `06-qos.mp4` | Filas `hfsc` do OpenBSD 8.0 e uma transferência de 2 MB pela classe |
Mais `pf-on-linux-full.mp4`: o filme completo, com as sete fases emendadas por Mais `pf-on-linux-full.mp4`: o filme completo, com as sete fases emendadas por
transicoes (crossfade sobre um cartao de capitulo). transições (crossfade sobre um cartão de capítulo).
## Vozes MiniMax ## Vozes MiniMax
| Idioma | Voz | Id | | Idioma | Voz | Id |
|--------|-----|-----| |--------|-----|-----|
| Ingles (en_US) | Confident Woman | `English_ConfidentWoman` | | Inglês (en_US) | Confident Woman | `English_ConfidentWoman` |
| Portugues (pt_BR) | Confident Woman | `Portuguese_ConfidentWoman` | | Português (pt_BR) | Confident Woman | `Portuguese_ConfidentWoman` |
O audio e gerado pelo MiniMax TTS, **um arquivo por segmento** de fala. Como cada O áudio é gerado pelo MiniMax TTS, **um arquivo por segmento** de fala. Como
segmento corresponde a uma batida da sessao, a voz comeca exatamente quando a cada segmento corresponde a uma batida da sessão, a voz começa exatamente
acao correspondente aparece na tela. quando a ação correspondente aparece na tela.
## Como regravar ## Como regravar
```sh ```sh
# tudo: audio -> sessoes -> videos -> filme completo # tudo: áudio -> sessões -> vídeos -> filme completo
./scripts/screencast/build-all.sh ./scripts/screencast/build-all.sh
# uma fase so # uma fase só
./scripts/screencast/build-all.sh 04-roteamento ./scripts/screencast/build-all.sh 04-roteamento
``` ```
Etapas individuais: Etapas individuais:
```sh ```sh
python3 scripts/screencast/make_audio.py --lang pt # audio MiniMax python3 scripts/screencast/make_audio.py --lang pt # áudio MiniMax
python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\ python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\
--out out/screencast/caps/04-roteamento.cast --pace --out out/screencast/caps/04-roteamento.cast --pace --ensure-pf
python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt
python3 scripts/screencast/join.py --lang pt python3 scripts/screencast/join.py --lang pt
``` ```
@@ -60,64 +60,64 @@ python3 scripts/screencast/join.py --lang pt
### Como o pipeline funciona ### Como o pipeline funciona
``` ```
planos/*.json --record.py--> caps/*.cast --render_video.py--> video mudo planos/*.json --record.py--> caps/*.cast --render_video.py--> vídeo mudo
(comandos) (pty real) (asciicast) (pyte + PIL) | (comandos) (pty real) (asciicast) (pyte + PIL) |
| |
narracao/*.json --make_audio.py--> vo/<fase>.<lang>/*.mp3 ----------+ narracao/*.json --make_audio.py--> vo/<fase>.<lang>/*.mp3 ----------+
(MiniMax TTS) | (MiniMax TTS) |
assemble.py: mix + legendas assemble.py: mix + legendas
| |
join.py: transicoes join.py: transições
``` ```
Pontos que importam: Pontos que importam:
* **Sessao real.** `record.py` abre um pty, faz SSH na VM e digita os comandos um - **Sessão real.** `record.py` abre um pty, faz SSH na VM e digita os comandos
caractere por vez (~34 c/s, com jitter). O `bash --noediting` evita que o um caractere por vez (~34 c/s, com jitter). O `bash --noediting` evita que o
editor de linha redesenhe o comando e o tty o repita. editor de linha redesenhe o comando e o tty o repita.
* **Pacing.** A gravacao segura a tela enquanto a voz fala: cada batida dura no - **Pacing.** A gravação segura a tela enquanto a voz fala: cada batida dura no
maximo o trecho de audio que a acompanha (o maior entre EN e PT), para uma máximo o trecho de áudio que a acompanha (o maior entre EN e PT), para uma
unica captura servir as duas versoes. única captura servir as duas versões.
* **Legendas.** O texto da narracao e queimado no video, na lingua da trilha. - **Legendas.** O texto da narração é queimado no vídeo, na língua da trilha.
* **Diagrama.** Nas fases 0, 4 e 5 o mapa do lab e desenhado sobre o terminal, e o - **Diagrama.** Nas fases 0, 4 e 5 o mapa do lab é desenhado sobre o terminal,
caminho em discussao fica destacado (eth1/eth2/eth3). e o caminho em discussão fica destacado (eth1/eth2/eth3).
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu, a chave Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu, a chave
`MINIMAX_API_KEY` no ambiente, e o lab de VMs no ar. `MINIMAX_API_KEY` no ambiente, e o lab de VMs no ar.
## Onde ficam os arquivos ## Onde ficam os arquivos
| Caminho | Conteudo | | Caminho | Conteúdo |
|---------|----------| |---------|----------|
| `out/screencast/en/`, `out/screencast/pt/` | os videos finais | | `out/screencast/en/`, `out/screencast/pt/` | os vídeos finais |
| `out/screencast/caps/` | as capturas asciicast das sessoes | | `out/screencast/caps/` | as capturas asciicast das sessões |
| `out/screencast/vo/` | o audio gerado pelo MiniMax | | `out/screencast/vo/` | o áudio gerado pelo MiniMax |
| `scripts/screencast/plans/` | o roteiro de comandos de cada fase | | `scripts/screencast/plans/` | o roteiro de comandos de cada fase |
| `scripts/screencast/narration/` | os textos da narracao (EN e PT) | | `scripts/screencast/narration/` | os textos da narração (EN e PT) |
## Texto de descricao (YouTube) ## Texto de descrição (YouTube)
``` ```
PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux
Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4, Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4,
empacotado como modulo (pf.ko) e controlado pelo pfctl original do OpenBSD sobre empacotado como módulo (pf.ko) e controlado pelo pfctl original do OpenBSD
a ABI de ioctls do /dev/pf. sobre a ABI de ioctls do /dev/pf.
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o modulo, O lab é real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
trafego real entre cliente, firewall e servidor. módulo, tráfego real entre cliente, firewall e servidor.
Fases: Fases:
0. O ambiente de testes - as 4 VMs e o diagrama de rede 0. O ambiente de testes - as 4 VMs e o diagrama de rede
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko 1. Implementação no kernel - das fontes OpenBSD ao pf.ko
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info 2. Ativação - insmod, /dev/pf, pfctl -e, pfctl -s info
3. Regras basicas - tabelas, anchors, keep state, block drop 3. Regras básicas - tabelas, anchors, keep state, block drop
4. Roteamento - route-to e reply-to, com o caminho real no diagrama 4. Roteamento - route-to e reply-to, com o caminho real no diagrama
5. Balanceamento - pools round-robin, source-hash e dup-to 5. Balanceamento - pools round-robin, source-hash e dup-to
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados 6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
Suite automatizada: 15 gates, todos verdes. Suíte automatizada: 15 gates, todos verdes.
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port Código e documentação: https://git.labexposed.com/lgcosta/pf-linux-port
#linux #kernel #openbsd #pf #firewall #netfilter #screencast #linux #kernel #openbsd #pf #firewall #netfilter #screencast
``` ```
+71 -22
View File
@@ -65,6 +65,21 @@ 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) ligadas. Os aliases de ssh do laptop (`pf0|cli|srv|gw2`, ProxyJump via deb)
são gerados em `~/.ssh/config.d/pflab`. são gerados em `~/.ssh/config.d/pflab`.
**Do pf0 para as outras VMs.** O firewall tem seu próprio par de chaves e os
pares confiam nele, então uma sessão no pf0 pode pilotar o cliente e o
servidor diretamente — é isso que os screencasts fazem
(`ssh cli 'ping …'`):
```sh
# uma vez por lab, a partir do hypervisor
ssh pf0 'ssh-keygen -t ed25519 -N "" -C pf0 -f /root/.ssh/id_ed25519'
# depois autorizar essa chave pública no cli, no srv e no gw2
```
O `/root/.ssh/config` do pf0 mapeia os nomes curtos `cli`, `srv` e `gw2`
para 10.0.1.10, 10.0.2.20 e 10.0.3.2, e o `/etc/hosts` traz o mesmo
mapeamento.
## 4. Rodando a suíte ## 4. Rodando a suíte
```sh ```sh
@@ -82,30 +97,64 @@ e sai com código diferente de zero se algo falhou.
## 5. O que cada gate prova hoje ## 5. O que cada gate prova hoje
| Gate | Cenário (verificado com tráfego real, não com código de saída) | Cada gate afirma **tráfego real e estado observável**, nunca apenas um código
de saída: o pacote chega, é descartado ou é traduzido, e a prova é lida de
volta nos contadores do próprio firewall, na tabela de estados, em capturas de
pacote ou no par.
A lista completa está na §6 (16 gates, todos verdes). Cada gate é um script
shell em `tests/vm/` que fala com as VMs através do deb com ssh aninhado,
usando os helpers de `tests/vm/lib.sh` (`pf0`, `cli`, `srv`, `gw2`, `pfput`,
`pfload`, `ok`, `bad`). O `scripts/vm-run-tests.sh` roda tudo em ordem e
guarda `out/test-logs/<gate>.sh.log`.
O que **não** tem gate (lacunas honestas): o caminho IPv6, o fingerprint de
OS, as opções `set` (`state-policy`, `allow-opts`) e os syncookies — o código
compila dentro do `pf.ko`, mas nenhum gate os exercita. O FQ-CoDel compila e
compartilha o header de estatísticas com o `pfctl`, mas só a disciplina
**hfsc** é exercitada.
**Não existe gate numérico de performance**: a decisão de design (spec §7) é
que *os gates funcionais são a régua* — um contrato de performance forçaria um
shim reescrito, pouco fiel. O que existe é um **teste de stress**
(`tests/vm/16-stress.sh`, disparado por `scripts/stress/run-stress.sh`) que
afere **invariantes sob carga** — sem vazamento de estados/tabelas, sem falha
de carga de regras, payload idêntico sob um stream saturando o link, sem
`BUG/WARNING/Oops`, o firewall ainda roteando — e **reporta** a comparação de
vazão (baseline sem o módulo vs. com PF ligado) como informação, sem meta
numérica de pass/fail. O relatório e o log bruto ficam em
`out/stress/<timestamp>/`.
## 6. Status atual da suíte (2026-11-12)
**16/16 gates verdes.** Uma rodada completa e limpa
(`./scripts/vm-run-tests.sh`, ~13 min) recria e reinicia o `pftest-pf0` antes
de cada gate, reimplanta o módulo e as ferramentas, e termina com
`PASS=16 FAIL=0`. Os logs capturados ficam em
`out/test-logs/<gate>.sh.log`.
| Gate | O que prova hoje |
|---|---| |---|---|
| `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` | | `01-state.sh` | `keep state` em um ping de trânsito; o estado aparece no `pfctl -ss` |
| `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 | | `02-block.sh` | `block drop` mata o ping; `block return-rst` devolve RST ao cliente |
| `03-tables.sh` | `table <blk> { 10.0.1.10 }` + `block in quick from <blk>` → `pfctl -t blk -T show` lista; o ping do cli é bloqueado; `-T delete` ao vivo libera o fluxo | | `03-tables.sh` | uma tabela `pfr` bloqueia um host; um `-T delete` ao vivo libera o mesmo 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 | | `04-anchors.sh` | a regra de um anchor é avaliada antes de um `block` posterior do ruleset principal |
| `05-nat.sh` | NAT de origem: o pacote sai com o endereço do pool e a resposta é des-NATeada |
| `06-rdr.sh` | redirect de porta de ponta a ponta, mais 2 MB passando com integridade byte a byte |
| `07-binat.sh` | `binat-to`: o par vê o endereço mapeado e o original nunca vaza |
| `08-routeto.sh` | `route-to` sai pelo link do gateway; `dup-to` copia e mantém o fluxo |
| `09-scrub.sh` | `min-ttl` e `max-mss` medidos no egresso do próprio firewall |
| `10-pools.sh` | seleção do pool de NAT (`round-robin`, `source-hash`) |
| `11-divert.sh` | `divert-to` para o relay `pdivert`: o pacote vai para o userspace e volta |
| `12-pflog.sh` | os pacotes logados chegam a `/dev/pflog` e viram um pcap DLT_PFLOG |
| `13-replyto.sh` | `reply-to` força a resposta para o link do gateway |
| `14-soak.sh` | reload de ruleset sob tráfego, ciclo de `rmmod`, checagem de vazamento |
| `15-queue.sh` | filas hfsc: anexação, estatísticas da classe, um fluxo de 2 MB contado pela classe |
| `16-stress.sh` | stress: vazão baseline vs. PF, churn de estados/regras/tabelas com verificação de vazamento, 2 MB por NAT sob stream saturante, saúde do dmesg (rodado por `scripts/stress/run-stress.sh`, que grava um relatório) |
Gates planejados (documento de plano, T11–T18): 05-nat, 06-rdr, 07-binat, Os screencasts ([screencast.md](screencast.md)) mostram boa parte desses
08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (tolerância gates sendo executados, com o diagrama de rede do lab desenhado sobre o
de ±15% no rate do iperf), 13-pflog, 14-soak (reload sob tráfego + ciclo de terminal.
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 ## 7. Inspeção manual
-84
View File
@@ -1,84 +0,0 @@
{"version": 2, "width": 80, "height": 24, "timestamp": 1794449787, "env": {"SHELL": null, "TERM": "dumb"}}
[1.414508, "o", "\u001b[1m===============================================================\u001b[0m\r\n\u001b[1m PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel\u001b[0m\r\n\u001b[1m===============================================================\u001b[0m\r\n\r\nLab: 3 Alpine VMs under libvirt.\r\n pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)\r\n pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20\r\n\r\n\u001b[1m--- 1. our kernel, the OpenBSD engine as a module ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# uname -r\u001b[0m\r\n"]
[1.655866, "o", "7.3.4\r\n"]
[2.058621, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf ' || echo 'pf: not loaded yet'\u001b[0m\r\n"]
[2.276696, "o", "pf: not loaded yet\r\n"]
[2.680328, "o", "\u001b[1;36mpftest-pf0# insmod /opt/pf/pf.ko && dmesg | tail -3\u001b[0m\r\n"]
[2.917691, "o", "[ 146.252272] NET: Registered PF_IUCV protocol family\r\n[ 146.252362] pfk: netfilter hooks registered (10, netisr wq data path)\r\n[ 146.252423] pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)\r\n"]
[3.320802, "o", "\r\n\u001b[1m--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s info | head -4\u001b[0m\r\n"]
[3.598837, "o", "Status: Disabled for 0 days 00:02:26 Debug: err\r\n\r\nState Table Total Rate\r\n current entries 0 \r\n"]
[4.00249, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1\u001b[0m\r\n"]
[4.274878, "o", "Status: Enabled Debug: err\r\npf enabled\r\n"]
[4.678074, "o", "\r\n\u001b[1m--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---\u001b[0m\r\n"]
[4.678128, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-pf.conf\u001b[0m\r\n"]
[4.954849, "o", "# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)\r\nset block-policy return\r\ntable <trusted> { 10.0.1.0/24 }\r\n\r\n# transit traffic: stateful, table-driven\r\npass in quick on ingress proto icmp from <trusted> keep state\r\npass out quick on egress proto icmp keep state\r\n\r\n# deny a specific destination\r\nblock drop in quick proto icmp to 10.0.9.99\r\n\r\n# everything else\r\npass in quick\r\npass out quick\r\n"]
[5.358199, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'\u001b[0m\r\n"]
[5.630704, "o", "rules loaded\r\n"]
[6.034552, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr\u001b[0m\r\n"]
[6.322794, "o", "pass in quick on ingress proto icmp from <trusted> to any\r\npass out quick on egress proto icmp all\r\nblock drop in quick inet proto icmp from any to 10.0.9.99\r\npass in quick all flags S/SA\r\npass out quick all flags S/SA\r\n"]
[6.725786, "o", "\r\n"]
[6.72584, "o", "\u001b[1m--- 4. transit traffic is allowed and stateful ---\u001b[0m\r\n\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[7.977099, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.339/0.351/0.363 ms\r\n"]
[8.379918, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[8.634572, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5449 0:0\r\nall icmp 10.0.1.10:5449 -> 10.0.2.20:8 0:0\r\n"]
[9.036709, "o", "\r\n\u001b[1m--- 5. tables are live objects ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -t trusted -T show\u001b[0m\r\n"]
[9.298627, "o", " 10.0.1.0/24\r\n"]
[9.701487, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s Tables\u001b[0m\r\n"]
[9.974937, "o", "trusted\r\n"]
[10.378208, "o", "\r\n\u001b[1m--- 6. the drop rule actually drops ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat > /tmp/demo-block.conf <<EOF\r\nblock drop in quick proto icmp to 10.0.2.20\r\npass in quick\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[11.033956, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'\u001b[0m\r\n"]
[11.310724, "o", "block rules loaded\r\n"]
[11.714107, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[14.969152, "o", "\r\n--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 0 packets received, 100% packet loss\r\n"]
[15.372543, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr -vv | head -6\u001b[0m\r\n"]
[15.66699, "o", "@0 block drop in quick inet proto icmp from any to 10.0.2.20\r\n [ Evaluations: 6 Packets: 2 Bytes: 168 States: 0 ]\r\n [ Inserted: uid 0 pid 1687 State Creations: 0 ]\r\n@1 pass in quick all flags S/SA\r\n [ Evaluations: 1 Packets: 28 Bytes: 9296 States: 1 ]\r\n [ Inserted: uid 0 pid 1687 State Creations: 1 ]\r\n"]
[16.07016, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[16.338781, "o", "back to the stateful ruleset\r\n"]
[16.742479, "o", "\r\n\u001b[1m--- 7. NAT: the source is translated on the way out, restored on the way back ---\u001b[0m\r\n"]
[16.742537, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-nat.conf <<EOF\r\npass out quick on egress from 10.0.1.0/24 to any nat-to (eth2)\r\npass in quick on ingress proto icmp keep state\r\npass out quick on egress proto icmp keep state\r\nEOF\u001b[0m\r\n"]
[17.422636, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'\u001b[0m\r\n"]
[17.686794, "o", "nat rules loaded\r\n"]
[18.090339, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[19.341141, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.276/0.317/0.359 ms\r\n"]
[19.744749, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[20.010971, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5461 0:0\r\nall icmp 10.0.2.1:43040 (10.0.1.10:5461) -> 10.0.2.20:8 0:0\r\n"]
[20.414328, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[20.690817, "o", "back to the stateful ruleset\r\n"]
[21.094035, "o", "\r\n\u001b[1m--- 8. pflog: logged packets reach userspace (/dev/pflog) ---\u001b[0m\r\n"]
[21.094088, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-log.conf <<EOF\r\npass in quick log proto icmp from 10.0.1.10 keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[21.762242, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'\u001b[0m\r\n"]
[22.034943, "o", "log rule loaded\r\n"]
[22.438523, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'\u001b[0m\r\n"]
[23.681454, "o", "pflogd collecting 2 records\r\n"]
[24.084864, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
[25.333003, "o", "2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.352/0.364/0.376 ms\r\n"]
[27.737616, "o", "\u001b[1;36mpftest-pf0# cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap\u001b[0m\r\n"]
[28.026778, "o", "pflogd: 2 records\r\n-rw------- 1 root root 324 Nov 12 02:16 /tmp/pflog.pcap\r\npcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):\r\n d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00\r\n ff ff 00 00 75 00 00 00\r\n"]
[28.430241, "o", "\u001b[1;36mpftest-pf0# pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
[29.114632, "o", "\r\n\u001b[1m--- 9. route-to: force the next hop away from the normal path ---\u001b[0m\r\n"]
[29.114687, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-route.conf <<EOF\r\npass in quick proto icmp from 10.0.1.10 to 10.0.2.20 route-to { 10.0.3.2 } keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[29.778259, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'\u001b[0m\r\n"]
[30.050898, "o", "route-to rule loaded\r\n"]
[30.45427, "o", "\u001b[1;36mpftest-pf0# (tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1\u001b[0m\r\n"]
[32.089063, "o", "\u001b[1;36mpftest-cli# ping -c1 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
[34.31667, "o", "--- 10.0.2.20 ping statistics ---\r\n1 packets transmitted, 0 packets received, 100% packet loss\r\n"]
[36.72139, "o", "\u001b[1;36mpftest-pf0# echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\\|^listening\\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td\u001b[0m\r\n"]
[37.006878, "o", "forced egress (eth3, toward the gateway):\r\n02:16:59.958272 52:54:00:aa:00:04 > 52:54:00:dd:00:02, ethertype IPv4 (0x0800), length 98: 10.0.1.10 > 10.0.2.20: ICMP echo request, id 5473, seq 0, length 64\r\nnormal egress (eth2):\r\n0\r\n"]
[37.410476, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
[38.094888, "o", "\r\n\u001b[1m--- 10. divert-to: the packet goes to userspace and comes back ---\u001b[0m\r\n"]
[38.095122, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-divert.conf\u001b[0m\r\n"]
[38.35866, "o", "pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to \"10.0.1.1\" port 10000 keep state\r\npass out quick\r\n"]
[38.763007, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'\u001b[0m\r\n"]
[39.022814, "o", "divert rule loaded\r\n"]
[39.42606, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert\u001b[0m\r\n"]
[40.654519, "o", "1794\r\n"]
[41.056785, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.1.1 | tail -3\u001b[0m\r\n"]
[42.284778, "o", "--- 10.0.1.1 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.177/0.224/0.271 ms\r\n"]
[42.691426, "o", "\u001b[1;36mpftest-pf0# head -3 /tmp/pdivert.log\u001b[0m\r\n"]
[42.942601, "o", "pdivert: listening on divert port 10000\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\n"]
[43.344696, "o", " (pdivert flipped the last payload byte and fixed the ICMP checksum)\r\n\r\n\u001b[1m--- 11. shutdown ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'\u001b[0m\r\n"]
[43.626606, "o", "pf disabled\r\npf disabled\r\n"]
[44.028882, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf '\u001b[0m\r\n"]
[44.286597, "o", "pf 552960 0\r\n"]
[44.688511, "o", "\r\n\u001b[1mdemo over - port, gates and docs:\u001b[0m\r\n\u001b[1m https://git.labexposed.com/lgcosta/pf-linux-port\u001b[0m\r\n"]
[44.688631, "o", "\r\n"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -4,7 +4,8 @@ set -euo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)" REPO="$(cd "$(dirname "$0")/.." && pwd)"
K="${KERNELDIR:-$(cd "$REPO/../linux-7.3.4" && pwd)}" K="${KERNELDIR:-$(cd "$REPO/../linux-7.3.4" && pwd)}"
cd "$K" cd "$K"
"$REPO/scripts/kernel-config.sh" "${1:-}" # kernel-config.sh must configure the *same* tree we are about to build.
KERNELDIR="$K" "$REPO/scripts/kernel-config.sh" "${1:-}"
JOBS="${JOBS:-$(($(nproc)-2))}" JOBS="${JOBS:-$(($(nproc)-2))}"
make -j"$JOBS" -s bzImage modules_prepare make -j"$JOBS" -s bzImage modules_prepare
# External module builds need Module.symvers (kbuild only writes # External module builds need Module.symvers (kbuild only writes
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# prepare-host.sh — turn a fresh Debian 13 (trixie) box into a PF-on-Linux
# build host + libvirt lab host.
#
# Idempotent: safe to re-run. It only installs packages and reports what the
# toolchain looks like; it never builds anything.
#
# sudo ./scripts/debian13/prepare-host.sh # build + lab host
# sudo ./scripts/debian13/prepare-host.sh --lab-only # libvirt side only
# sudo ./scripts/debian13/prepare-host.sh --build-only
set -euo pipefail
MODE=both
case "${1:-}" in
--lab-only) MODE=lab ;;
--build-only) MODE=build ;;
--help|-h) sed -n '2,10p' "$0"; exit 0 ;;
*) [ $# -eq 0 ] || { echo "unknown option: $1" >&2; exit 2; } ;;
esac
export DEBIAN_FRONTEND=noninteractive
BUILD_PKGS="build-essential bc bison flex libssl-dev libelf-dev rsync curl ca-certificates"
LAB_PKGS="libvirt-daemon-system libvirt-clients qemu-system-x86 qemu-utils genisoimage bridge-utils netcat-openbsd iperf3 tcpdump python3-scapy"
if [ "$(id -u)" -ne 0 ]; then
echo "this script needs root (apt install)" >&2
exit 1
fi
install() { # install <list>
echo "== apt-get update"
apt-get update -qq
echo "== apt-get install: $*"
apt-get install -y -qq $*
}
case "$MODE" in
build) install "$BUILD_PKGS" ;;
lab) install "$LAB_PKGS" ;;
both) install "$BUILD_PKGS $LAB_PKGS" ;;
esac
echo
echo "== toolchain"
printf '%-12s %s\n' gcc "$(gcc -dumpversion 2>/dev/null || echo MISSING)"
printf '%-12s %s\n' make "$(make --version 2>/dev/null | sed -n 1p || echo MISSING)"
printf '%-12s %s\n' bison "$(bison --version 2>/dev/null | sed -n 1p || echo MISSING)"
printf '%-12s %s\n' flex "$(flex --version 2>/dev/null || echo MISSING)"
printf '%-12s %s\n' bc "$(bc --version 2>/dev/null | sed -n 1p || echo MISSING)"
printf '%-12s %s\n' rsync "$(rsync --version 2>/dev/null | sed -n 1p || echo MISSING)"
printf '%-12s %s\n' libvirt "$(virsh --version 2>/dev/null || echo 'not installed')"
printf '%-12s %s\n' qemu "$(qemu-system-x86_64 --version 2>/dev/null | sed -n 1p || echo 'not installed')"
printf '%-12s %s\n' geniso "$(genisoimage --version 2>&1 | sed -n 1p || echo 'not installed')"
echo
echo "== notes"
echo "- Debian 13 ships gcc 14 as the default compiler; gcc-15 is not in"
echo " trixie. The kernel tree and pf.ko build with gcc 14 (see"
echo " docs/pt-BR/debian13.md for the verified run)."
echo "- pf.ko is built against the Linux 7.3.4 tree, NOT against the Debian"
echo " kernel: it only loads on a 7.3.4 kernel (vermagic). Build the"
echo " kernel and the module from the same tree."
echo
echo "PREPARE_HOST_OK"
-161
View File
@@ -1,161 +0,0 @@
#!/usr/bin/env python3
"""
PF-on-Linux demo recorder -> asciicast v2 (.cast)
Every byte shown comes from the REAL lab (Alpine VMs under libvirt, kernel
7.3.4 built from source with our pf.ko). Only the pacing is scripted.
Usage:
./scripts/demo-record.py [--out out/demo-pf-linux.cast] [--quick]
Play back with: asciinema play out/demo-pf-linux.cast
Upload with: asciinema upload out/demo-pf-linux.cast
"""
import argparse
import json
import os
import shlex
import subprocess
import sys
import time
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEB = os.environ.get("DEB", "root@192.168.1.242")
PF0 = os.environ.get("PF0_IP", "192.168.122.8")
CLI = os.environ.get("CLI_IP", "10.0.1.10")
SRV = os.environ.get("SRV_IP", "10.0.2.20")
KEY = os.path.join(os.environ.get("HOME", "/root"), ".ssh/pflab_ed")
CONF = """# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
set block-policy return
table <trusted> { 10.0.1.0/24 }
# transit traffic: stateful, table-driven
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
# deny a specific destination
block drop in quick proto icmp to 10.0.9.99
# everything else
pass in quick
pass out quick
"""
DIVERT_CONF = """pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
pass out quick
"""
def sh(cmd, timeout=40):
p = subprocess.run(cmd, shell=True, capture_output=True, text=True,
timeout=timeout)
return (p.stdout or "") + (p.stderr or "")
def on_pf0(cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
f"-i {KEY} root@{PF0} {shlex.quote(cmd)}", timeout)
def on_vm(ip, cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
f"-i {KEY} root@{ip} {shlex.quote(cmd)}", timeout)
def on_deb(cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 {DEB} {shlex.quote(cmd)}", timeout)
def stage():
"""put the demo ruleset + pdivert on pf0 (through the lab host)"""
with open("/tmp/demo-pf.conf", "w") as f:
f.write(CONF)
with open("/tmp/demo-divert.conf", "w") as f:
f.write(DIVERT_CONF)
sh(f"scp -q /tmp/demo-pf.conf /tmp/demo-divert.conf "
f"{os.path.join(REPO, 'tools/out/pdivert')} {DEB}:/tmp/", timeout=90)
on_deb(f"scp -q -o StrictHostKeyChecking=accept-new -i {KEY} "
f"/tmp/demo-pf.conf /tmp/demo-divert.conf /tmp/pdivert "
f"root@{PF0}:/tmp/ 2>&1", timeout=90)
SCENARIO = [
("local", "echo '=== PF on Linux — OpenBSD PF 8.0 engine running inside the Linux 7.3.4 kernel ==='"),
("local", "echo 'lab: 3 Alpine VMs under libvirt (pf0 = firewall, cli/srv = transit endpoints)'"),
("pf0", "uname -r"),
("pf0", "lsmod | grep '^pf ' || echo 'pf: not loaded'"),
("pf0", "insmod /opt/pf/pf.ko && dmesg | tail -3"),
("pf0", "/opt/pf/pfctl -s info | head -4"),
("pf0", "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"),
("pf0", "cat /tmp/demo-pf.conf"),
("pf0", "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded' && /opt/pf/pfctl -sr"),
("pf0", "/opt/pf/pfctl -sr -vv | head -8"),
("cli", f"ping -c2 -W2 {SRV} | tail -3"),
("pf0", "/opt/pf/pfctl -ss"),
("pf0", "/opt/pf/pfctl -t trusted -T show"),
("pf0", "/opt/pf/pfctl -s Tables"),
("cli", "ping -c2 -W2 10.0.9.99 | tail -3"),
("pf0", "/opt/pf/pfctl -sr -vv | head -6"),
("pf0", "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"),
("pf0", "(setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"),
("cli", "ping -c2 -W2 10.0.1.1 | tail -3"),
("pf0", "head -3 /tmp/pdivert.log"),
("pf0", "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"),
]
def record(out_path, quick=False):
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
events = []
t = 0.0
def emit(text, delay):
nonlocal t
t += delay
events.append([round(t, 3), "o", text])
header = {
"version": 2, "width": 100, "height": 26,
"timestamp": int(time.time()),
"title": "PF on Linux — OpenBSD PF engine inside the Linux kernel (lab demo)",
"env": {"SHELL": "/bin/sh", "TERM": "xterm-256color"},
}
runners = {
"pf0": lambda c: on_pf0(c),
"cli": lambda c: on_vm(CLI, c),
"srv": lambda c: on_vm(SRV, c),
"local": lambda c: sh(c),
}
labels = {"pf0": "pftest-pf0", "cli": "pftest-cli", "srv": "pftest-srv",
"local": "lab-host"}
stage()
with open(out_path, "w") as f:
f.write(json.dumps(header) + "\n")
for kind, cmd in SCENARIO:
emit(f"\x1b[1;36m{labels[kind]}\x1b[0m:\x1b[1;37m {cmd}\x1b[0m\r\n",
0.35)
try:
out = runners[kind](cmd)
except subprocess.TimeoutExpired:
out = "[timeout]\r\n"
except Exception as e: # noqa: BLE001
out = f"[error: {e}]\r\n"
out = out.replace("\r\n", "\n").replace("\n", "\r\n")
emit(out or "(no output)\r\n", min(1.4, 0.3 + len(out) / 5000.0))
with open(out_path, "a") as f:
for ev in events:
f.write(json.dumps(ev) + "\n")
print(f"WROTE {out_path}: {len(events)} events, {t:.1f}s")
print("play: asciinema play " + out_path)
print("upload: asciinema upload " + out_path)
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(REPO, "out",
"demo-pf-linux.cast"))
ap.add_argument("--quick", action="store_true")
a = ap.parse_args()
record(a.out, a.quick)
-182
View File
@@ -1,182 +0,0 @@
#!/usr/bin/env bash
# Demo session for PF-on-Linux: a scripted terminal tour of the lab.
# Recorded with: asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
#
# Everything printed is the REAL output of the lab (Alpine VMs under libvirt
# running the kernel built from source with our pf.ko). The VM management
# addresses are resolved from the hypervisor (they are DHCP), the lab
# topology (10.0.1.0/24, 10.0.2.0/24, ...) is fixed.
set -u
DEB=${DEB:-root@192.168.1.242}
CLI_IP=${CLI_IP:-10.0.1.10}
SRV_IP=${SRV_IP:-10.0.2.20}
KEY=${KEY:-$HOME/.ssh/pflab_ed}
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
# management address of a lab VM (DHCP, so ask the hypervisor)
mgmt() {
ssh -o ConnectTimeout=8 "$DEB" "virsh domifaddr $1 | awk '/vnet/{print \$4}' | cut -d/ -f1" \
2>/dev/null | tr -d '\r' | head -1
}
PF0_MGMT=$(mgmt pftest-pf0)
CLI_MGMT=$(mgmt pftest-cli)
[ -n "$PF0_MGMT" ] || PF0_MGMT=192.168.122.8
[ -n "$CLI_MGMT" ] || CLI_MGMT=192.168.122.80
# run a command on a lab VM (through the hypervisor)
onvm() {
local ip=$1; shift
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
-o ProxyJump=root@192.168.1.242 -i "$KEY" root@"$ip" "$@" 2>&1
}
p0() {
cyan "pftest-pf0# $*"
onvm "$PF0_MGMT" "$@"
sleep 0.4
}
vmc() {
cyan "pftest-cli# $*"
onvm "$CLI_MGMT" "$@"
sleep 0.4
}
CONF=/tmp/demo-pf.conf
DIV=/tmp/demo-divert.conf
cat > "$CONF" <<'CONF'
# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
set block-policy return
table <trusted> { 10.0.1.0/24 }
# transit traffic: stateful, table-driven
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
# deny a specific destination
block drop in quick proto icmp to 10.0.9.99
# everything else
pass in quick
pass out quick
CONF
cat > "$DIV" <<CONF
pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
pass out quick
CONF
# the pf0 interface towards the server (used by the NAT pool)
EGRESS_IF=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
[ -n "$EGRESS_IF" ] || EGRESS_IF=eth2
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \
tools/out/pdivert tools/out/pflogd "root@$PF0_MGMT:/tmp/" 2>/dev/null
bold "==============================================================="
bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel"
bold "==============================================================="
echo
echo "Lab: 3 Alpine VMs under libvirt."
echo " pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)"
echo " pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20"
echo
bold "--- 1. our kernel, the OpenBSD engine as a module ---"
p0 "uname -r"
p0 "lsmod | grep '^pf ' || echo 'pf: not loaded yet'"
p0 "insmod /opt/pf/pf.ko && dmesg | tail -3"
echo
bold "--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---"
p0 "/opt/pf/pfctl -s info | head -4"
p0 "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"
echo
bold "--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---"
p0 "cat /tmp/demo-pf.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'"
p0 "/opt/pf/pfctl -sr"
echo
bold "--- 4. transit traffic is allowed and stateful ---"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4"
echo
bold "--- 5. tables are live objects ---"
p0 "/opt/pf/pfctl -t trusted -T show"
p0 "/opt/pf/pfctl -s Tables"
echo
bold "--- 6. the drop rule actually drops ---"
p0 "cat > /tmp/demo-block.conf <<EOF
block drop in quick proto icmp to $SRV_IP
pass in quick
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -sr -vv | head -6"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
echo
bold "--- 7. NAT: the source is translated on the way out, restored on the way back ---"
p0 "cat > /tmp/demo-nat.conf <<EOF
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_IF)
pass in quick on ingress proto icmp keep state
pass out quick on egress proto icmp keep state
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
echo
bold "--- 8. pflog: logged packets reach userspace (/dev/pflog) ---"
p0 "cat > /tmp/demo-log.conf <<EOF
pass in quick log proto icmp from $CLI_IP keep state
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'"
p0 "chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
sleep 2
p0 "cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap"
p0 "pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
echo
bold "--- 9. route-to: force the next hop away from the normal path ---"
p0 "cat > /tmp/demo-route.conf <<EOF
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { 10.0.3.2 } keep state
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'"
p0 "(tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1"
vmc "ping -c1 -W2 $SRV_IP | tail -2"
sleep 2
p0 "echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\|^listening\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
echo
bold "--- 10. divert-to: the packet goes to userspace and comes back ---"
p0 "cat /tmp/demo-divert.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"
p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"
vmc "ping -c2 -W2 10.0.1.1 | tail -3"
p0 "head -3 /tmp/pdivert.log"
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
echo
bold "--- 11. shutdown ---"
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
p0 "lsmod | grep '^pf '"
echo
bold "demo over - port, gates and docs:"
bold " https://git.labexposed.com/lgcosta/pf-linux-port"
echo
+4 -1
View File
@@ -3,7 +3,10 @@
# defconfig baseline + direct-kernel-boot deps. Idempotent. # defconfig baseline + direct-kernel-boot deps. Idempotent.
# `kernel-config.sh test` additionally enables DEBUG_KMEMLEAK (soak builds). # `kernel-config.sh test` additionally enables DEBUG_KMEMLEAK (soak builds).
set -euo pipefail set -euo pipefail
K="${KERNELDIR:-$(cd "$(dirname "$0")/../../linux_kernel/linux-7.3.4" && pwd)}" REPO="$(cd "$(dirname "$0")/.." && pwd)"
# Default: the kernel tree next to the repository (pf_port/../linux-7.3.4),
# same rule as build-kernel.sh / build-module.sh.
K="${KERNELDIR:-$REPO/../linux-7.3.4}"
cd "$K" cd "$K"
[ -f .config ] || make defconfig [ -f .config ] || make defconfig
scripts/config --file .config \ scripts/config --file .config \
-3
View File
@@ -1,3 +0,0 @@
@run clear
uname -r
lsmod | grep '^pf ' || echo 'pf: not loaded yet'
-65
View File
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# Render every screencast phase into out/screencast/*.mp4 (silent, 1080p30).
#
# ./scripts/screencast/record-all.sh [phase ...]
#
# Each phase is a real session against the lab; the videos carry no audio.
set -u
cd "$(dirname "$0")/../.."
REPO=$(pwd)
OUT="$REPO/out/screencast"
mkdir -p "$OUT"
render() { # <file> <title> <subtitle> <phase-no> [panel]
local f=$1 title=$2 sub=$3 no=$4
local panel=${5:-"alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko"}
echo "=== gravando $f ==="
python3 "$REPO/scripts/screencast/render.py" \
--script "$REPO/scripts/screencast/phases/$f" \
--out "$OUT/${f%.sh}.mp4" \
--title "$title" --subtitle "$sub" \
--phase-no "$no" --phase-total 6 --panel "$panel" --min-hold 1.1 \
--footer "PF on Linux - OpenBSD PF 8.0 no kernel Linux - lab libvirt" || return 1
}
declare -A TITLES=(
[01-implementacao-kernel]="Fase 1 - Implementacao no kernel"
[02-ativacao]="Fase 2 - Ativacao"
[03-regras-basicas]="Fase 3 - Regras basicas"
[04-roteamento]="Fase 4 - Roteamento"
[05-balanceamento]="Fase 5 - Balanceamento"
[06-qos]="Fase 6 - QOS"
)
declare -A SUBS=(
[01-implementacao-kernel]="Das fontes OpenBSD 8.0 ao modulo pf.ko"
[02-ativacao]="insmod pf.ko / pfctl -e / pfctl -s info"
[03-regras-basicas]="tabelas, anchors, keep state, block drop"
[04-roteamento]="route-to e reply-to: o proximo salto decidido pelo pf"
[05-balanceamento]="pools round-robin, source-hash e dup-to"
[06-qos]="filas hfsc do OpenBSD 8.0 no caminho de dados"
)
PHASES=("$@")
[ ${#PHASES[@]} -eq 0 ] && PHASES=(01-implementacao-kernel 02-ativacao 03-regras-basicas \
04-roteamento 05-balanceamento 06-qos)
for p in "${PHASES[@]}"; do
f="$p.sh"
[ -f "$REPO/scripts/screencast/phases/$f" ] || { echo "fase inexistente: $f"; exit 1; }
# the phase number comes from the phase's own name, not from its position
# in the list, so a partial re-render keeps the right "FASE x/6" label
n=$((10#${p%%-*}))
case "$p" in
01-*) panel="host de build - repositorio pf_port" ;;
*) panel="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko" ;;
esac
render "$f" "${TITLES[$p]}" "${SUBS[$p]}" "$n" "$panel" || echo "FALHOU: $f"
done
echo
echo "=== videos gerados ==="
for m in "$OUT"/*.mp4; do
[ -e "$m" ] || continue
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$m")
printf '%-46s %6.1fs %5.1f MB\n' "$(basename "$m")" "$d" "$(echo "scale=1; $(stat -c%s "$m")/1000000" | bc)"
done
-336
View File
@@ -1,336 +0,0 @@
#!/usr/bin/env python3
"""
Render a shell session into a silent, YouTube-ready MP4 screencast.
Pipeline: pty -> raw bytes -> pyte (terminal emulation) -> PNG frames -> MP4
Every byte on screen is the real output of the command; only the pacing and
the framing are produced here. No audio track is written (the videos are
silent by design).
Usage:
./scripts/screencast/render.py --script scripts/screencast/phases/02-ativacao.sh \
--out out/screencast/02-ativacao.mp4 --title "Fase 2 - Ativacao" \
--subtitle "insmod pf.ko / pfctl -e / pfctl -s info"
"""
import argparse
import errno
import fcntl
import os
import pty
import select
import shutil
import signal
import struct
import subprocess
import sys
import termios
import time
import pyte
from PIL import Image, ImageDraw, ImageFont
# ---------------------------------------------------------------- palette ---
BG = (14, 17, 23)
PANEL = (20, 24, 31)
BORDER = (48, 54, 61)
FG = (201, 209, 217)
HEADER_BG = (24, 29, 38)
HEADER_FG = (240, 246, 252)
ACCENT = (88, 166, 255)
MUTED = (110, 118, 129)
TITLE_BG = (17, 21, 28)
# pyte colour names -> RGB (a calm, high-contrast terminal palette)
NAMED = {
"black": (48, 54, 61),
"red": (255, 123, 114),
"green": (63, 185, 80),
"yellow": (210, 153, 34),
"blue": (88, 166, 255),
"magenta": (188, 140, 255),
"cyan": (57, 197, 207),
"white": (201, 209, 217),
"brown": (168, 115, 66),
"default": None,
}
def color_of(spec, default):
if spec == "default":
return default
if spec in NAMED:
return NAMED[spec] or default
try:
if len(spec) == 6:
return tuple(int(spec[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
pass
return default
class Recorder:
"""Runs a command on a pty and keeps a timestamped snapshot per output event."""
def __init__(self, cols, rows, env=None):
self.cols, self.rows = cols, rows
self.screen = pyte.Screen(cols, rows)
self.screen.set_mode(pyte.modes.LNM)
self.stream = pyte.ByteStream(self.screen)
self.events = [] # (timestamp, rows_of_cells)
self.t0 = time.time()
def snapshot(self):
rows = []
for y in range(self.rows):
line = self.screen.buffer[y]
cells = []
for x in range(self.cols):
ch = line[x]
fg = color_of(ch.fg, FG)
bold = ch.bold
cells.append((ch.data or " ", fg, bold))
rows.append(cells)
return rows, (self.screen.cursor.y, self.screen.cursor.x)
def run(self, argv, env=None):
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ,
struct.pack("HHHH", self.rows, self.cols, 0, 0))
e = dict(os.environ)
e.update({"TERM": "xterm-256color", "COLUMNS": str(self.cols),
"LINES": str(self.rows), "LC_ALL": "C.UTF-8"})
if env:
e.update(env)
proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave,
env=e, start_new_session=True, close_fds=True)
os.close(slave)
while True:
try:
r, _, _ = select.select([master], [], [], 0.25)
except InterruptedError:
continue
if master in r:
try:
data = os.read(master, 65536)
except OSError as exc:
if exc.errno == errno.EIO:
break
raise
if not data:
break
self.stream.feed(data)
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
elif proc.poll() is not None:
# drain whatever is left
try:
while True:
data = os.read(master, 65536)
if not data:
break
self.stream.feed(data)
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
except OSError:
pass
break
proc.wait()
os.close(master)
if not self.events:
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
return proc.returncode
# ------------------------------------------------------------- rendering ---
def load_font(path, size):
return ImageFont.truetype(path, size)
def render_frame(rows, title, subtitle, phase_no, phase_total, cols, font, bold_font,
small_font, footer, cursor=None, panel="alpine (pf0)"):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), BG)
d = ImageDraw.Draw(img)
# header
d.rectangle([0, 0, W, 74], fill=HEADER_BG)
d.text((44, 26), "PF on Linux", font=bold_font, fill=HEADER_FG)
tw = d.textlength("PF on Linux", font=bold_font)
d.text((44 + tw + 26, 30), "-", font=font, fill=MUTED)
d.text((44 + tw + 46, 30), "OpenBSD PF 8.0 engine inside the Linux kernel",
font=font, fill=MUTED)
if phase_total:
label = f"FASE {phase_no}/{phase_total}"
lw = d.textlength(label, font=small_font)
d.text((W - 44 - lw, 32), label, font=small_font, fill=ACCENT)
# title block
d.text((44, 112), title, font=bold_font, fill=HEADER_FG)
if subtitle:
d.text((44, 162), subtitle, font=font, fill=MUTED)
# terminal panel, sized to the content
top = 212
left = 44
right = W - 44
lh = 30
used = 0
for i, line in enumerate(rows):
if any(c[0].strip() for c in line):
used = i + 1
bottom = min(H - 84, top + 44 + 28 + (max(used, 4) + 1) * lh)
d.rectangle([left, top, right, bottom], fill=PANEL, outline=BORDER, width=2)
d.rectangle([left, top, right, top + 44], fill=HEADER_BG)
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
d.ellipse([left + 24 + i * 26, top + 15, left + 38 + i * 26, top + 29], fill=c)
d.text((left + 110, top + 13), panel, font=small_font, fill=MUTED)
# text
x0 = left + 28
y0 = top + 64
cw = d.textlength("M", font=font)
for i, line in enumerate(rows):
y = y0 + i * lh
if y > bottom - 10:
break
for x, (ch, fg, bold) in enumerate(line):
if ch == " ":
continue
d.text((x0 + x * cw, y), ch, font=bold_font if bold else font, fill=fg)
# cursor block
if cursor:
cy, cx = cursor
if 0 <= cy < len(rows) and 0 <= cx < cols:
d.rectangle([x0 + cx * cw, y0 + cy * lh, x0 + (cx + 1) * cw - 1,
y0 + (cy + 1) * lh - 1], fill=ACCENT)
# footer
d.text((44, H - 58), footer, font=small_font, fill=MUTED)
url = "git.labexposed.com/lgcosta/pf-linux-port"
uw = d.textlength(url, font=small_font)
d.text((W - 44 - uw, H - 58), url, font=small_font, fill=MUTED)
return img
def render_title(title, subtitle, phase_no, phase_total, bold_font, font, small_font):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), TITLE_BG)
d = ImageDraw.Draw(img)
d.rectangle([0, 0, W, 8], fill=ACCENT)
d.text((120, 400), title, font=bold_font, fill=HEADER_FG)
if subtitle:
d.text((120, 470), subtitle, font=font, fill=MUTED)
if phase_total:
d.text((120, 560), f"FASE {phase_no} DE {phase_total}", font=small_font, fill=ACCENT)
d.text((120, 620), "PF on Linux - OpenBSD PF 8.0 no kernel Linux",
font=small_font, fill=MUTED)
return img
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--script", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--title", required=True)
ap.add_argument("--subtitle", default="")
ap.add_argument("--footer", default="")
ap.add_argument("--phase-no", type=int, default=0)
ap.add_argument("--phase-total", type=int, default=0)
ap.add_argument("--cols", type=int, default=120)
ap.add_argument("--rows", type=int, default=32)
ap.add_argument("--fps", type=int, default=30)
ap.add_argument("--max-hold", type=float, default=2.0)
ap.add_argument("--min-hold", type=float, default=0.25)
ap.add_argument("--title-card", type=float, default=3.0)
ap.add_argument("--keep-frames", action="store_true")
ap.add_argument("--panel", default="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko")
args = ap.parse_args()
repo = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
mono = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf"
mono_b = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf"
font = load_font(mono, 21)
bold_font = load_font(mono_b, 21)
small_font = load_font(mono, 19)
rec = Recorder(args.cols, args.rows)
rc = rec.run(["bash", args.script])
sys.stderr.write(f"[screencast] script rc={rc}, events={len(rec.events)}\n")
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
frames_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), "_frames")
shutil.rmtree(frames_dir, ignore_errors=True)
os.makedirs(frames_dir, exist_ok=True)
def dump(img, idx):
img.save(os.path.join(frames_dir, f"{idx:05d}.png"))
n = 0
# title card
title_img = render_title(args.title, args.subtitle, args.phase_no,
args.phase_total, bold_font, font, small_font)
title_frames = int(args.title_card * args.fps)
for _ in range(title_frames):
dump(title_img, n)
n += 1
# session frames, paced by the real inter-event delays
prev_t = 0.0
for i, (ts, rows, cur) in enumerate(rec.events):
hold = ts - prev_t
prev_t = ts
if i + 1 < len(rec.events):
hold = rec.events[i + 1][0] - ts
hold = max(args.min_hold, min(args.max_hold, hold))
img = render_frame(rows, args.title, args.subtitle, args.phase_no,
args.phase_total, args.cols, font, bold_font,
small_font, args.footer, cursor=cur, panel=args.panel)
frames = max(1, int(round(hold * args.fps)))
for _ in range(frames):
dump(img, n)
n += 1
# closing card
end_img = render_frame(rec.events[-1][1], args.title, args.subtitle,
args.phase_no, args.phase_total, args.cols, font,
bold_font, small_font,
args.footer or "fim da fase",
cursor=rec.events[-1][2], panel=args.panel)
for _ in range(int(1.5 * args.fps)):
dump(end_img, n)
n += 1
# encoder: prefer x264, fall back to openh264 (both H.264 in MP4, which is
# what YouTube wants), then VP9/WebM.
have = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True).stdout
base = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(args.fps),
"-i", os.path.join(frames_dir, "%05d.png"),
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", str(args.fps)]
if "libx264" in have:
cmd = base + ["-c:v", "libx264", "-preset", "medium", "-crf", "20",
"-profile:v", "high", "-level", "4.0", args.out]
elif "libopenh264" in have:
cmd = base + ["-c:v", "libopenh264", "-b:v", "6M",
"-profile:v", "high", args.out]
elif "libvpx-vp9" in have:
cmd = base + ["-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "30",
"-row-mt", "1", "-cpu-used", "4", args.out]
else:
raise SystemExit("[screencast] no usable H.264/VP9 encoder in ffmpeg")
subprocess.run(cmd, check=True)
if not args.keep_frames:
shutil.rmtree(frames_dir, ignore_errors=True)
dur = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "csv=p=0", args.out],
capture_output=True, text=True).stdout.strip()
size = os.path.getsize(args.out)
sys.stderr.write(f"[screencast] wrote {args.out} frames={n} "
f"duration={dur}s size={size/1e6:.1f}MB\n")
if __name__ == "__main__":
main()
-331
View File
@@ -1,331 +0,0 @@
#!/usr/bin/env python3
"""
Record a *real* interactive shell session and render it as a video.
The difference to the earlier renderer: the shell is not a script whose output
is dumped at once. We open a real pty, type the commands **key by key** with
human-like timing, wait for the prompt to come back, and capture the byte
stream that the terminal actually produced. The result is a session that
looks like somebody sitting in front of a terminal - characters appearing one
by one, output scrolling in as the kernel answers.
./scripts/screencast/session.py --plan scripts/screencast/plans/04.sh \\
--out out/screencast/session-04.mp4 --title "..." --subtitle "..."
The plan file is a shell script executed *inside* the recording pty. Every
line the plan prints is real output from the lab.
"""
import argparse
import errno
import fcntl
import os
import pty
import select
import shlex
import struct
import subprocess
import sys
import termios
import time
import pyte
from PIL import Image, ImageDraw, ImageFont
# ---------------------------------------------------------------- palette ---
BG = (11, 14, 19)
PANEL = (18, 22, 28)
BORDER = (44, 50, 58)
FG = (203, 211, 219)
HEADER_BG = (21, 26, 33)
HEADER_FG = (240, 246, 252)
ACCENT = (86, 166, 255)
ACCENT2 = (57, 197, 207)
MUTED = (108, 116, 128)
GREEN = (63, 185, 80)
YELLOW = (210, 153, 34)
RED = (255, 123, 114)
TITLE_BG = (15, 19, 25)
NAMED = {
"black": (48, 54, 61), "red": RED, "green": GREEN, "yellow": YELLOW,
"blue": ACCENT, "magenta": (188, 140, 255), "cyan": ACCENT2,
"white": FG, "brown": (168, 115, 66), "default": None,
}
def color_of(spec, default):
if spec == "default":
return default
if spec in NAMED:
return NAMED[spec] or default
try:
if len(spec) == 6:
return tuple(int(spec[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
pass
return default
# ------------------------------------------------------------------ fonts ---
def load_fonts():
mono = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf"
monob = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf"
return (ImageFont.truetype(mono, 20), ImageFont.truetype(monob, 20),
ImageFont.truetype(mono, 18))
# ---------------------------------------------------------------- session ---
class Shell:
"""A real interactive shell on a pty; we type into it and read what it says."""
PROMPT = "__PS1__"
def __init__(self, argv, cols=118, rows=34, env=None):
self.cols, self.rows = cols, rows
self.master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ,
struct.pack("HHHH", rows, cols, 0, 0))
e = dict(os.environ)
e.update({"TERM": "xterm-256color", "COLUMNS": str(cols),
"LINES": str(rows), "LC_ALL": "C.UTF-8", "PS1": self.PROMPT + " "})
if env:
e.update(env)
self.proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave,
env=e, start_new_session=True, close_fds=True)
os.close(slave)
self.screen = pyte.Screen(cols, rows)
self.stream = pyte.ByteStream(self.screen)
self.events = [] # (t, snapshot)
self.t0 = time.time()
self.raw = bytearray()
self.idle = 0.0
def pump(self, duration):
"""Read from the pty for `duration` seconds, recording screen states."""
end = time.time() + duration
while True:
remaining = end - time.time()
if remaining <= 0:
return
try:
r, _, _ = select.select([self.master], [], [], min(remaining, 0.05))
except InterruptedError:
continue
if self.master in r:
try:
data = os.read(self.master, 65536)
except OSError as exc:
if exc.errno == errno.EIO:
return
raise
if not data:
return
self.raw += data
self.stream.feed(data)
self.events.append((time.time() - self.t0, self.snapshot()))
self.idle = 0.0
else:
self.idle += 0.05
if self.idle > 0.25:
# nothing new: keep the timeline going so the frame clock
# stays uniform
self.events.append((time.time() - self.t0, self.snapshot()))
def snapshot(self):
rows = []
for y in range(self.rows):
line = self.screen.buffer[y]
cells = []
for x in range(self.cols):
ch = line[x]
cells.append((ch.data or " ", color_of(ch.fg, FG), ch.bold))
rows.append(cells)
return rows, (self.screen.cursor.y, self.screen.cursor.x)
def wait_prompt(self, timeout=25.0):
"""Wait until the shell prompt shows up again."""
end = time.time() + timeout
while time.time() < end:
self.pump(0.05)
text = "\n".join(self.screen.display)
if text.rstrip().endswith(self.PROMPT):
return True
return False
def type_line(self, text, cps=42.0):
"""Type a line character by character, like a human."""
for ch in text:
self.write(ch)
# slight jitter so it does not look machine-perfect
delay = 1.0 / cps + (hash(ch) % 7) * 0.0016
time.sleep(delay)
self.write("\r")
self.pump(0.12)
def write(self, s):
os.write(self.master, s.encode())
def run(self):
self.pump(1.2)
return self.proc.wait()
# --------------------------------------------------------------- rendering ---
def render(rows, cursor, title, subtitle, phase_no, phase_total, font, bold,
small, footer, panel_label, art=None, art_alpha=0.0):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), BG)
d = ImageDraw.Draw(img)
d.rectangle([0, 0, W, 70], fill=HEADER_BG)
d.text((44, 24), "PF on Linux", font=bold, fill=HEADER_FG)
tw = d.textlength("PF on Linux", font=bold)
d.text((44 + tw + 24, 28), "-", font=font, fill=MUTED)
d.text((44 + tw + 44, 28), "OpenBSD PF 8.0 engine inside the Linux kernel",
font=font, fill=MUTED)
if phase_total:
label = f"FASE {phase_no}/{phase_total}"
lw = d.textlength(label, font=small)
d.text((W - 44 - lw, 30), label, font=small, fill=ACCENT)
d.text((44, 100), title, font=bold, fill=HEADER_FG)
if subtitle:
d.text((44, 148), subtitle, font=font, fill=MUTED)
top, left, right = 196, 44, W - 44
lh = 28
d.rectangle([left, top, right, H - 78], fill=PANEL, outline=BORDER, width=2)
d.rectangle([left, top, right, top + 40], fill=HEADER_BG)
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
d.ellipse([left + 22 + i * 24, top + 14, left + 34 + i * 24, top + 26], fill=c)
d.text((left + 100, top + 12), panel_label, font=small, fill=MUTED)
x0, y0 = left + 26, top + 58
cw = d.textlength("M", font=font)
for i, line in enumerate(rows):
y = y0 + i * lh
if y > H - 96:
break
for x, (ch, fg, b) in enumerate(line):
if ch == " ":
continue
d.text((x0 + x * cw, y), ch, font=bold if b else font, fill=fg)
if cursor:
cy, cx = cursor
if 0 <= cy < len(rows) and 0 <= cx < len(rows[0]):
d.rectangle([x0 + cx * cw, y0 + cy * lh, x0 + (cx + 1) * cw - 1,
y0 + (cy + 1) * lh - 1], fill=ACCENT)
d.text((44, H - 52), footer, font=small, fill=MUTED)
url = "git.labexposed.com/lgcosta/pf-linux-port"
uw = d.textlength(url, font=small)
d.text((W - 44 - uw, H - 52), url, font=small, fill=MUTED)
return img
def render_title(title, subtitle, phase_no, phase_total, bold, font, small, art):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), TITLE_BG)
if art:
a = art.resize((W, H)).convert("RGB")
img = Image.blend(img, a, 0.55)
d = ImageDraw.Draw(img)
d.rectangle([0, 0, W, 8], fill=ACCENT)
d.text((120, 420), title, font=bold, fill=(250, 252, 255))
if subtitle:
d.text((120, 490), subtitle, font=font, fill=(190, 200, 214))
if phase_total:
d.text((120, 570), f"FASE {phase_no} DE {phase_total}", font=small, fill=ACCENT)
d.text((120, 630), "PF on Linux - OpenBSD PF 8.0 no kernel Linux",
font=small, fill=(150, 160, 175))
return img
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--plan", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--title", required=True)
ap.add_argument("--subtitle", default="")
ap.add_argument("--footer", default="PF on Linux - OpenBSD PF 8.0 no kernel Linux - lab libvirt")
ap.add_argument("--panel", default="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko")
ap.add_argument("--phase-no", type=int, default=0)
ap.add_argument("--phase-total", type=int, default=0)
ap.add_argument("--cols", type=int, default=118)
ap.add_argument("--rows", type=int, default=34)
ap.add_argument("--fps", type=int, default=30)
ap.add_argument("--title-card", type=float, default=2.5)
ap.add_argument("--art")
args = ap.parse_args()
font, bold, small = load_fonts()
art = None
if args.art and os.path.exists(args.art):
art = Image.open(args.art)
# The plan runs inside the recording shell; the shell itself is a real
# bash with our prompt, so what the user sees is a live session.
sh = Shell(["bash", "--norc", "--noprofile", "-i"], cols=args.cols, rows=args.rows)
sh.pump(1.0)
# make the prompt look like the OpenBSD box
sh.write("export PS1='\\u@\\h:\\w\\$ ' PS2='> ' TERM=xterm-256color\n")
sh.pump(0.6)
sh.write("export LC_ALL=C.UTF-8 LANG=C.UTF-8; clear\n")
sh.pump(0.8)
sh.events = [(0.0, sh.snapshot())]
# run the plan: each line is typed into the shell
plan = open(args.plan).read()
for raw_line in plan.split("\n"):
line = raw_line.rstrip()
if not line.strip() or line.strip().startswith("#"):
continue
if line.startswith("@sleep "):
sh.pump(float(line.split()[1]))
continue
if line.startswith("@run "):
# run a command without showing it typed (used for setup)
sh.write(line[5:] + "\n")
sh.wait_prompt(timeout=30)
continue
sh.type_line(line)
sh.wait_prompt(timeout=60)
sh.pump(0.45)
sh.write("exit\n")
sh.pump(0.5)
sys.stderr.write(f"[session] {len(sh.events)} events, {sh.events[-1][0]:.1f}s\n")
# ------------------------------------------------------ frame timeline ---
frames_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), "_sframes")
subprocess.run(["rm", "-rf", frames_dir], check=False)
os.makedirs(frames_dir, exist_ok=True)
n = 0
title_img = render_title(args.title, args.subtitle, args.phase_no, args.phase_total,
bold, font, small, art)
for _ in range(int(args.title_card * args.fps)):
title_img.save(os.path.join(frames_dir, f"{n:05d}.png"))
n += 1
# the events carry real timestamps: map them onto a uniform frame clock
ev = sh.events
t0 = ev[0][0]
for i, (ts, snap) in enumerate(ev):
hold = (ev[i + 1][0] - ts) if i + 1 < len(ev) else 0.04
hold = max(0.033, min(0.5, hold))
rows, cur = snap
img = render(rows, cur, args.title, args.subtitle, args.phase_no,
args.phase_total, font, bold, small, args.footer, args.panel, art)
cnt = max(1, int(round(hold * args.fps)))
for _ in range(cnt):
img.save(os.path.join(frames_dir, f"{n:05d}.png"))
n += 1
subprocess.run(["rm", "-rf", frames_dir], check=False)
if __name__ == "__main__":
main()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
# run-stress.sh — drive the PF stress test (tests/vm/16-stress.sh) against the
# libvirt lab and turn the metrics into a report.
#
# ./scripts/stress/run-stress.sh # default profile
# ./scripts/stress/run-stress.sh --quick # smaller profile (smoke)
# ./scripts/stress/run-stress.sh --seconds 30 --connections 1000
#
# Output: out/stress/<timestamp>/report.md (+ the raw gate log).
# Exit status: 0 when every stress phase passed, 1 otherwise.
set -uo pipefail
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
DEB="${DEB:-root@192.168.1.242}"
PF0_IP=192.168.122.8
TS="$(date +%Y%m%d-%H%M%S)"
OUTD="$REPO/out/stress/$TS"
mkdir -p "$OUTD"
SECS=10
PAR=4
RATE=100M
CONNS=300
RELOADS=200
RULES=200
TENTRIES=2000
MAXOH=30
while [ $# -gt 0 ]; do
case "$1" in
--quick) SECS=5; CONNS=100; RELOADS=50; RULES=50; TENTRIES=500; shift ;;
--seconds) SECS=$2; shift 2 ;;
--parallel) PAR=$2; shift 2 ;;
--rate) RATE=$2; shift 2 ;;
--connections) CONNS=$2; shift 2 ;;
--reloads) RELOADS=$2; shift 2 ;;
--rules) RULES=$2; shift 2 ;;
--table-entries) TENTRIES=$2; shift 2 ;;
--max-overhead) MAXOH=$2; shift 2 ;;
-h|--help) sed -n '2,12p' "$0"; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 2 ;;
esac
done
SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@$PF0_IP"
echo "pflab stress: profile seconds=$SECS parallel=$PAR rate/stream=$RATE connections=$CONNS reloads=$RELOADS rules=$RULES table_entries=$TENTRIES max_overhead=$MAXOH%"
echo "pflab stress: resetting pftest-pf0"
ssh "$DEB" "virsh destroy pftest-pf0 >/dev/null 2>&1; virsh start pftest-pf0 >/dev/null" || {
echo "pflab stress FAIL: cannot restart pftest-pf0" >&2; exit 1; }
up=0
for i in $(seq 1 20); do
sleep 6
if ssh "$DEB" "$SSH true" 2>/dev/null; then up=1; break; fi
done
[ "$up" = 1 ] || { echo "pflab stress FAIL: pf0 never came back" >&2; exit 1; }
echo "pflab stress: deploying pf.ko + pfctl to pf0:/opt/pf"
for i in 1 2 3; do
if scp -q -o ProxyJump=root@192.168.1.242 -o StrictHostKeyChecking=accept-new \
-i "$HOME/.ssh/pflab_ed" "$REPO/module/pf.ko" "$REPO/tools/out/pfctl" \
"$REPO/tools/out/pdivert" "$REPO/tools/out/pflogd" pf0:/tmp/ 2>/dev/null; then
break
fi
sleep 2
done
ssh "$DEB" "$SSH 'mkdir -p /opt/pf; mv /tmp/pf.ko /tmp/pfctl /tmp/pdivert /tmp/pflogd /opt/pf/ 2>/dev/null; chmod +x /opt/pf/pfctl 2>/dev/null; true'" >/dev/null
ssh "$DEB" "$SSH 'test -s /opt/pf/pf.ko && test -x /opt/pf/pfctl'" >/dev/null 2>&1 || {
echo "pflab stress FAIL: pf.ko/pfctl missing on pf0" >&2; exit 1; }
echo "pflab stress: running the stress gate"
# The guest's serial console is a pty: a panic or a hang is lost when the VM
# reboots. Capture it on the hypervisor (the redirect happens *there*) and
# copy it back when the run ends.
REMOTE_CONSOLE="/tmp/pf0-console-$TS.log"
ssh "$DEB" "PTY=\$(virsh ttyconsole pftest-pf0); setsid nohup timeout 1800 cat \$PTY >$REMOTE_CONSOLE 2>&1 </dev/null &" >/dev/null 2>&1
sleep 2
export STRESS_SECS="$SECS" STRESS_PARALLEL="$PAR" STRESS_RATE="$RATE" STRESS_CONNS="$CONNS" \
STRESS_RELOADS="$RELOADS" STRESS_RULES="$RULES" \
STRESS_TABLE_ENTRIES="$TENTRIES" STRESS_MAX_OVERHEAD_PCT="$MAXOH"
bash "$REPO/tests/vm/16-stress.sh" >"$OUTD/16-stress.sh.log" 2>&1
RC=$?
# --- report ----------------------------------------------------------------
scp -q -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$DEB:$REMOTE_CONSOLE" \
"$OUTD/pf0-console.log" 2>/dev/null || true
{
echo "# PF-on-Linux — stress report"
echo
echo "- date: $(date -Is)"
echo "- profile: seconds=$SECS parallel=$PAR rate/stream=$RATE connections=$CONNS reloads=$RELOADS rules=$RULES table_entries=$TENTRIES max_overhead=${MAXOH}%"
echo "- exit status: $RC"
echo "- guest serial console: \`pf0-console.log\` (kernel messages during the run)"
echo
echo "## Phases"
echo
echo '```'
grep '^STRESS ' "$OUTD/16-stress.sh.log" || echo "(no metrics captured)"
echo '```'
echo
echo "## Assertions"
echo
echo '```'
grep -E '^(OK|NOT OK)' "$OUTD/16-stress.sh.log" || echo "(no assertions)"
echo '```'
} >"$OUTD/report.md"
echo
echo "pflab stress: report -> out/stress/$TS/report.md"
grep '^STRESS ' "$OUTD/16-stress.sh.log" || true
echo
if [ $RC -eq 0 ]; then
echo "STRESS RESULT: PASS"
else
echo "STRESS RESULT: FAIL (rc=$RC)"
sed -n '/^NOT OK/p' "$OUTD/16-stress.sh.log" | sed 's/^/ /'
fi
exit $RC
+280
View File
@@ -0,0 +1,280 @@
#!/usr/bin/env bash
# M7 gate: stress — the PF engine under sustained load.
#
# Unlike the functional gates (01-15), this one does not ask "does the packet
# arrive?"; it asks "does the engine stay correct, bounded and leak-free while
# traffic, state, rules and tables churn?" Phases:
#
# 0 baseline throughput cli->srv at a bounded rate with the module OUT
# of the path
# 1 overhead same traffic with PF enabled + stateful rules
# 2 state churn 300 concurrent TCP connections; states must appear and
# must vanish again (pfctl -F s) -> no state-table leak
# 3 rule churn 200 reloads of a 200-rule ruleset, timed on the firewall
# 4 table churn 2000 pfr table entries added and flushed
# 5 integrity 2 MB through NAT *while* a concurrent stream runs; the
# payload must arrive byte-identical (sha256)
# 6 health no BUG/WARNING/Oops, counters move, firewall still routes
#
# Every phase prints `STRESS key=value ...` lines; scripts/stress/run-stress.sh
# turns them into a report. Thresholds are overridable from the environment
# (STRESS_SECS, STRESS_CONNS, STRESS_RELOADS, STRESS_TABLE_ENTRIES, ...).
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
SECS=${STRESS_SECS:-10}
PAR=${STRESS_PARALLEL:-4}
RATE=${STRESS_RATE:-100M}
CONNS=${STRESS_CONNS:-300}
RELOADS=${STRESS_RELOADS:-200}
RULES=${STRESS_RULES:-200}
TENTRIES=${STRESS_TABLE_ENTRIES:-2000}
HOLD=${STRESS_HOLD:-6}
MAXOH=${STRESS_MAX_OVERHEAD_PCT:-30}
# The traffic phases run at a BOUNDED rate ($RATE per stream). This is not
# cosmetic: PF's data path steals every skb onto one ordered workqueue
# (see docs/*/caveats.md §1), so the backlog — and with it the guest's
# memory — grows with the offered rate. Measured on the lab (1 GB guest):
# 400 Mbit/s is stable, ~1 Gbit/s OOM-panics the guest. The stress test
# therefore works in the regime the port survives and reports the ceiling
# separately, instead of killing the firewall under test.
HELPER=$(pfput ./stress-helper.sh) || { bad "helper upload"; finish; }
pf0 "chmod +x $HELPER" >/dev/null
EGRESS_DEV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
[ -n "$EGRESS_DEV" ] || EGRESS_DEV=eth2
EGRESS_IP=$(pf0 "ip -4 addr show $EGRESS_DEV | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2")
[ -n "$EGRESS_IP" ] || EGRESS_IP=192.168.122.8
# --- helpers ---------------------------------------------------------------
# iperf3 throughput cli->srv, in Mbit/s (one decimal is plenty)
iperf_mbit() {
local out line val unit
out=$(cli "iperf3 -c $SRV_IP -t $1 -P $PAR -b $RATE -i 0 2>/dev/null")
line=$(printf '%s\n' "$out" | grep '\[SUM\]' | tail -1)
[ -n "$line" ] || { echo 0; return; }
val=$(printf '%s\n' "$line" | sed -n 's/.*[^0-9.]\([0-9][0-9.]*\) \([KMG]\?bits\/sec\).*/\1/p')
unit=$(printf '%s\n' "$line" | sed -n 's/.* \([KMG]\?bits\/sec\).*/\1/p')
[ -n "$val" ] || { echo 0; return; }
awk -v v="$val" -v u="$unit" 'BEGIN{
if (u ~ /^G/) printf "%.1f\n", v * 1000;
else if (u ~ /^M/) printf "%.1f\n", v;
else if (u ~ /^K/) printf "%.1f\n", v / 1000;
else printf "%.3f\n", v / 1000000; }'
}
# `pkill -x iperf3` matches the process *name* only. Never use `pkill -f`
# with a pattern that also appears in this very command line: it kills the
# invoking shell (see the note in 06-rdr.sh).
iperf_up() { srv 'pkill -x iperf3 2>/dev/null; iperf3 -s -D >/dev/null 2>&1; sleep 1; pgrep -x iperf3 >/dev/null || echo IPERF_SERVER_DOWN' >/dev/null; }
# --- phase 0: baseline (module out of the path) ----------------------------
pf0 'rmmod pf 2>/dev/null; true' >/dev/null
iperf_up
BASE=$(iperf_mbit "$SECS")
BASE=$(printf '%s' "$BASE" | cut -d. -f1)
if [ "${BASE:-0}" -gt 0 ]; then
ok "baseline throughput ${BASE} Mbit/s (module unloaded)"
else
bad "baseline throughput unavailable (iperf3 cli->srv failed)"
echo "STRESS phase=baseline baseline_mbit=0 seconds=$SECS parallel=$PAR rate_per_stream=$RATE"
finish
fi
echo "STRESS phase=baseline baseline_mbit=$BASE seconds=$SECS parallel=$PAR rate_per_stream=$RATE"
# --- phase 1: same traffic with PF enabled ---------------------------------
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true' >/dev/null
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
cat > /tmp/g16.conf <<C
pass in quick proto tcp from $CLI_IP to $SRV_IP keep state
pass in quick proto icmp from $CLI_IP keep state
pass out quick keep state
C
pfload /tmp/g16.conf || { bad "stress ruleset"; finish; }
iperf_up
PFM=$(iperf_mbit "$SECS")
PFM=$(printf '%s' "$PFM" | cut -d. -f1)
[ "${PFM:-0}" -gt 0 ] && ok "throughput with PF enabled: ${PFM} Mbit/s" || bad "no throughput with PF enabled"
OH=0
[ "${BASE:-0}" -gt 0 ] && OH=$(awk -v b="$BASE" -v p="$PFM" 'BEGIN{printf "%d", (b-p)*100/b}')
if [ "${OH:-999}" -le "$MAXOH" ]; then
ok "PF overhead ${OH}% (<= ${MAXOH}%)"
else
bad "PF overhead ${OH}% exceeds ${MAXOH}%"
fi
echo "STRESS phase=overhead pf_mbit=$PFM overhead_pct=$OH limit_pct=$MAXOH rate_per_stream=$RATE"
# --- phase 2: state churn ---------------------------------------------------
# The churn client cannot use sshd: MaxStartups (10:30:100 by default) drops
# most concurrent unauthenticated connections. The server therefore gets a
# dedicated listener (python's http.server forks per connection and holds it
# open until the client closes).
#
# The setup and cleanup scripts are shipped base64-encoded: a plain `$(...)`
# would be expanded by the *intermediate* host (deb), not by the guest, and
# would silently do nothing. The listener also outlives the ssh session, so
# the setup kills leftovers from an earlier run (an orphan holding the port
# would silently break the churn).
SETUP=$(cat <<'EOS' | base64 | tr -d '\n'
mkdir -p /tmp/stress-web
cd /tmp/stress-web || exit 0
echo ok > index.html
for pid in $(ls /proc 2>/dev/null | grep -E '^[0-9]+$'); do
[ "$pid" = "$$" ] && continue
cmd=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null) || continue
case "$cmd" in
*stress-loop*|*http.server*) kill "$pid" 2>/dev/null ;;
esac
done
if [ -f /tmp/stress-httpd.pid ]; then kill $(cat /tmp/stress-httpd.pid) 2>/dev/null; rm -f /tmp/stress-httpd.pid; fi
setsid sh -c 'echo $$ > /tmp/stress-httpd.pid; exec python3 -m http.server 9097 --bind 0.0.0.0' >/dev/null 2>&1 </dev/null &
sleep 1
EOS
)
srv "echo $SETUP | base64 -d > /tmp/stress-setup.sh; sh /tmp/stress-setup.sh" >/dev/null
if srv 'netstat -lnt 2>/dev/null | grep -q ":9097 "' >/dev/null 2>&1; then
ok "churn listener up on srv:9097"
else
bad "churn listener NOT listening on srv:9097 (stale listener holding the port?)"
finish
fi
RULESET=$(pf0 '/opt/pf/pfctl -s rules | wc -l')
PFSTAT=$(pf0 '/opt/pf/pfctl -s info | head -1')
pf0 "/opt/pf/pfctl -F s" >/dev/null
S0=$(pf0 "$HELPER states")
# The connections must stay open while the states are counted: a peer that
# closes right away tears the state down before it can be observed.
# The iteration list is expanded HERE, on the build host: a `$(seq ...)`
# left in the command string would be expanded by the *intermediate* host
# (deb), whose newline-separated output breaks the `for` loop one hop later.
RANGE=$(seq -s' ' 1 "$CONNS")
cli "bash -c 'for i in $RANGE; do (exec 3<>/dev/tcp/$SRV_IP/9097; sleep $HOLD) 2>/dev/null & done; wait'" >/dev/null 2>&1 &
CLIPID=$!
sleep 4
S1=$(pf0 "$HELPER states")
EST=$(srv 'netstat -tn 2>/dev/null | grep -c ":9097 "')
wait $CLIPID 2>/dev/null
if [ "${S1:-0}" -ge $((CONNS / 2)) ]; then
ok "state churn: $S1 states tracked for $CONNS connections"
else
bad "state churn: only $S1 states for $CONNS connections"
fi
S2=$(pf0 "$HELPER state-reset")
sleep 1
S3=$(pf0 "$HELPER states")
if [ "${S3:-0}" -le "${S0:-0}" ]; then
ok "state table drained after reset ($S0 -> $S3)"
else
bad "state table did not drain ($S0 -> $S3)"
fi
echo "STRESS phase=state_churn connections=$CONNS states_before=$S0 states_created=$S1 states_after_reset=$S3 established_on_srv=${EST:-0} rules_loaded=$RULESET pf_status=$(printf '%s' "$PFSTAT" | tr -s ' ' | cut -d' ' -f2-)"
# --- phase 3: rule churn ---------------------------------------------------
{
echo "table <stress> { }"
echo "pass in quick proto tcp from $CLI_IP to $SRV_IP keep state"
echo "pass out quick keep state"
i=1
while [ "$i" -le "$RULES" ]; do
# a distinct destination port per rule: rules with the same shape
# are folded by pfctl's optimizer into one automatic table, which
# would make the rule count meaningless
echo "block drop in quick proto tcp from 10.100.$((i % 250)).$((i % 250 + 1)) to any port $((1000 + i))"
i=$((i + 1))
done
} > /tmp/g16-churn.conf
CH=$(pfput /tmp/g16-churn.conf) || { bad "churn ruleset upload"; finish; }
pf0 "/opt/pf/pfctl -f $CH" >/dev/null || { bad "churn ruleset load"; finish; }
R0=$(pf0 "$HELPER rules")
RL=$(pf0 "$HELPER reload-loop $RELOADS $CH")
eval "$RL"
if [ "${reload_fails:-1}" -eq 0 ]; then
ok "rule churn: $RELOADS reloads, no failure (avg ${reload_ms_avg:-?} ms, max ${reload_ms_max:-?} ms)"
else
bad "rule churn: ${reload_fails:-?} of $RELOADS reloads failed"
fi
R1=$(pf0 "$HELPER rules")
if [ "${R1:-0}" -eq "${R0:-0}" ] && [ "${R1:-0}" -eq "$((RULES + 2))" ]; then
ok "rule count stable across churn ($R1 rules)"
else
bad "rule count drifted: $R0 -> $R1 (expected $((RULES + 2)))"
fi
echo "STRESS phase=rule_churn reloads=$RELOADS rules_per_load=$RULES reload_fails=${reload_fails:-0} reload_ms_avg=${reload_ms_avg:-0} reload_ms_max=${reload_ms_max:-0} rules_final=$R1"
# --- phase 4: table churn --------------------------------------------------
TC=$(pf0 "$HELPER table-churn $TENTRIES stress")
eval "$TC"
if [ "${table_add_errors:-1}" -eq 0 ] && [ "${table_entries_after_add:-0}" -ge $((TENTRIES / 2)) ]; then
ok "table churn: ${table_entries_after_add} entries added"
else
bad "table churn: ${table_entries_after_add:-0} entries, ${table_add_errors:-?} errors"
fi
if [ "${table_entries_after_flush:-1}" -eq 0 ]; then
ok "table flushed back to 0 entries"
else
bad "table not empty after flush (${table_entries_after_flush})"
fi
echo "STRESS phase=table_churn entries_requested=$TENTRIES entries_added=${table_entries_after_add:-0} add_errors=${table_add_errors:-0} entries_after_flush=${table_entries_after_flush:-0}"
# --- phase 5: integrity under load -----------------------------------------
cat > /tmp/g16-nat.conf <<C
pass out quick on $EGRESS_DEV from $CLI_IP to any nat-to ($EGRESS_DEV)
pass in quick proto tcp from $CLI_IP to $SRV_IP port 9099 keep state
pass out quick keep state
C
pfload /tmp/g16-nat.conf || { bad "nat ruleset"; finish; }
SZ=2000000
srv 'pkill nc 2>/dev/null; head -c 2000000 /dev/zero | tr "\0" A > /tmp/stress.bin; sleep 1; nohup setsid nc -l -p 9099 < /tmp/stress.bin >/dev/null 2>&1 & sleep 1' >/dev/null
SRC_SUM=$(srv 'sha256sum /tmp/stress.bin | cut -c1-64' | tr -d '[:space:]')
# saturating background stream while the payload crosses PF
cli "iperf3 -c $SRV_IP -t 15 -P $PAR -b $RATE >/dev/null 2>&1 &" >/dev/null 2>&1
sleep 1
GOT=$(cli 'nc -w20 10.0.2.20 9099 | sha256sum | cut -c1-64' | tr -d '[:space:]')
if [ -n "$SRC_SUM" ] && [ "$GOT" = "$SRC_SUM" ]; then
ok "2 MB through NAT intact under load (sha256 match)"
INTEG=yes
else
bad "payload corrupted under load (src=$SRC_SUM got=$GOT)"
INTEG=no
fi
echo "STRESS phase=integrity bytes=$SZ sha256_match=$INTEG"
# --- phase 6: health -------------------------------------------------------
ANOM=$(pf0 "$HELPER dmesg")
if [ -z "$ANOM" ]; then
ok "no kernel BUG/WARN/Oops after the stress run"
else
bad "dmesg anomalies:"
printf '%s\n' "$ANOM" | sed 's/^/ /'
fi
MATCHED=$(pf0 "$HELPER counter match")
ENTRIES=$(pf0 "$HELPER state-entries")
[ "${MATCHED:-0}" -gt 0 ] && ok "PF counters moved (match=${MATCHED:-0} state_entries=${ENTRIES:-0})" \
|| bad "PF counters did not move (match=${MATCHED:-0})"
cli "ping -c 3 -W 2 $SRV_IP >/dev/null 2>&1" && ok "firewall still routes traffic after stress" \
|| bad "firewall stopped routing traffic after stress"
echo "STRESS phase=health dmesg_anomalies=$(printf '%s' "$ANOM" | grep -c . ) match=${MATCHED:-0} state_entries=${ENTRIES:-0}"
# --- cleanup ---------------------------------------------------------------
CLEAN=$(cat <<'EOS' | base64 | tr -d '\n'
if [ -f /tmp/stress-httpd.pid ]; then kill $(cat /tmp/stress-httpd.pid) 2>/dev/null; rm -f /tmp/stress-httpd.pid; fi
for pid in $(ls /proc 2>/dev/null | grep -E '^[0-9]+$'); do
[ "$pid" = "$$" ] && continue
cmd=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null) || continue
case "$cmd" in
*http.server*) kill "$pid" 2>/dev/null ;;
esac
done
pkill -x nc 2>/dev/null
pkill -x iperf3 2>/dev/null
true
EOS
)
srv "echo $CLEAN | base64 -d > /tmp/stress-clean.sh; sh /tmp/stress-clean.sh" >/dev/null
cli 'pkill -x iperf3 2>/dev/null; true' >/dev/null
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
finish
+101
View File
@@ -0,0 +1,101 @@
#!/bin/sh
# Helper executed ON pf0 by 16-stress.sh (uploaded with pfput to avoid quoting
# hell through two ssh hops). POSIX sh: the guest is Alpine (busybox ash).
#
# $1 = "slab" | "dmesg" | "states" | "state-reset" | "rules" | "info"
# | "reload-loop <n> <ruleset>" | "table-churn <n> <table>"
#
# Every subcommand prints machine-readable KEY=VALUE pairs on stdout so the
# gate can parse them without another ssh round trip.
PFCTL=/opt/pf/pfctl
now_ms() { awk '{printf "%d", $1 * 1000}' /proc/uptime; }
case "${1:-}" in
slab)
grep -E '^(pf|pfk)' /proc/slabinfo | tr -s ' ' | cut -d' ' -f1,3 | sort
;;
dmesg)
# the CPU's Speculative Return Stack Overflow advisory is a kernel
# build/CPU feature notice, not a fault of this module
dmesg | grep -E 'BUG:|WARNING:|Oops|general protection|kernel BUG' \
| grep -v 'Speculative Return Stack Overflow' || true
;;
states)
# `pfctl -s states` prints one line per direction, so count the
# initiator lines ("->") to get the number of states
$PFCTL -s states 2>/dev/null | grep -c ' -> ' || true
;;
state-reset)
# -F s clears the state table (-s s only *shows* it)
$PFCTL -F s >/dev/null 2>&1 || true
$PFCTL -s states 2>/dev/null | grep -c ' -> ' || true
;;
rules)
$PFCTL -s rules 2>/dev/null | grep -c . || true
;;
info)
$PFCTL -s info 2>/dev/null
;;
counter)
# $2 = counter name as printed by `pfctl -s info` (match, short,
# state-mismatch, ...). Prints the value, or 0 when absent.
$PFCTL -s info 2>/dev/null | awk -v k="${2:-match}" '$1 == k { print $2; exit }'
;;
state-entries)
# the number of entries the state table currently holds
$PFCTL -s info 2>/dev/null | awk '/current entries/ { print $NF; exit }'
;;
reload-loop)
# $2 = iterations, $3 = ruleset file. Times each `pfctl -f` on the
# firewall itself (no ssh overhead in the measurement).
n="${2:-0}"; f="${3:-/tmp/g16-churn.conf}"
i=0; fails=0; min=999999999; max=0; sum=0
while [ "$i" -lt "$n" ]; do
s=$(now_ms)
if ! $PFCTL -f "$f" >/dev/null 2>&1; then
fails=$((fails + 1))
fi
e=$(now_ms)
d=$((e - s)); [ "$d" -lt 0 ] && d=0
sum=$((sum + d))
[ "$d" -lt "$min" ] && min=$d
[ "$d" -gt "$max" ] && max=$d
i=$((i + 1))
done
[ "$min" = 999999999 ] && min=0
avg=0; [ "$n" -gt 0 ] && avg=$((sum / n))
echo "reload_iterations=$n reload_fails=$fails reload_ms_min=$min reload_ms_avg=$avg reload_ms_max=$max"
;;
table-churn)
# $2 = number of addresses, $3 = table name. Adds N distinct addresses,
# counts the table, flushes it and counts again.
n="${2:-0}"; t="${3:-stress}"
$PFCTL -t "$t" -T flush >/dev/null 2>&1 || true
i=1; err=0
while [ "$i" -le "$n" ]; do
# 10.0.<a>.<b> keeps every entry distinct without touching the
# lab's real segments (10.0.1.0/24 lan, 10.0.2.0/24 wan)
a=$(( (i - 1) / 250 + 100 ))
b=$(( (i - 1) % 250 + 1 ))
if ! $PFCTL -t "$t" -T add "10.$a.$b" >/dev/null 2>&1; then
err=$((err + 1))
fi
i=$((i + 1))
done
added=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true)
$PFCTL -t "$t" -T flush >/dev/null 2>&1 || true
left=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true)
if [ "${left:-0}" -ne 0 ]; then
# flush did not empty the table: fall back to deleting every entry
$PFCTL -t "$t" -T show 2>/dev/null | awk '{print $1}' | \
while read -r a; do $PFCTL -t "$t" -T delete "$a" >/dev/null 2>&1 || true; done
left=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true)
fi
echo "table_entries_requested=$n table_add_errors=$err table_entries_after_add=$added table_entries_after_flush=$left"
;;
*)
echo "unknown helper command: ${1:-}" >&2
exit 2
;;
esac