mirror of
https://github.com/lgcosta/pf-linux-port.git
synced 2026-11-13 16:08:04 -03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a8ba69340 | ||
|
|
928322edf7 | ||
|
|
45ba489672 | ||
|
|
131672d6ce | ||
|
|
dff2185b20 |
No files matched your search
@@ -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`,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 | remontagmento 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
@@ -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
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) |
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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. |
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) |
|
||||||
@@ -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).
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
@@ -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
|
||||||
|
|||||||
Executable
+65
@@ -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"
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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 \
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
@run clear
|
|
||||||
uname -r
|
|
||||||
lsmod | grep '^pf ' || echo 'pf: not loaded yet'
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
Executable
+118
@@ -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
|
||||||
Executable
+280
@@ -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
|
||||||
Executable
+101
@@ -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
|
||||||
Reference in new issue
Block a user