Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dff2185b20 |
No files matched your search
@@ -25,13 +25,10 @@ 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** — 15/15 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, 15/15 suite green, tagged (`v1.0.0`) |
|
||||||
|
|
||||||
## What you get (planned)
|
## What you get
|
||||||
|
|
||||||
| Area | Feature |
|
| Area | Feature |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -64,22 +64,22 @@ 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). Short version
|
||||||
(commands are read from the scripts; a fresh-clone end-to-end run is still
|
(commands are read from the scripts):
|
||||||
pending):
|
|
||||||
|
|
||||||
```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 +87,11 @@ 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)
|
- `tests/vm/` — VM suite: 15 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
|
||||||
|
|
||||||
|
|||||||
+48
-29
@@ -2,67 +2,86 @@
|
|||||||
|
|
||||||
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** — 15/15 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, 15/15 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). Versão
|
||||||
curta (comandos lidos dos scripts; a execução de ponta a ponta a partir de
|
curta (comandos lidos dos scripts):
|
||||||
clone limpo ainda está pendente):
|
|
||||||
|
|
||||||
```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 +89,11 @@ 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)
|
- `tests/vm/` — suíte em VM: 15 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
|
||||||
|
|
||||||
|
|||||||
+14
-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
|
||||||
|
|
||||||
@@ -14,9 +14,10 @@ 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) |
|
||||||
| [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 15 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 +28,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 **15/15 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,33 @@ 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.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -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 **15/15 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: 15 gates `01-state.sh … 15-queue.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, 15/15 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 — 15/15 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
|
||||||
```
|
```
|
||||||
|
|||||||
+60
-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,52 @@ 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 (15 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**: `bench-basic.sh` does not exist in
|
||||||
|
the tree and no throughput target is claimed.
|
||||||
|
|
||||||
|
## 6. Current suite status (2026-11-12)
|
||||||
|
|
||||||
|
**15/15 gates green.** A full clean run (`./scripts/vm-run-tests.sh`, ~9 min)
|
||||||
|
recreates and reboots `pftest-pf0` before every gate, redeploys the module
|
||||||
|
and the tools, and ends with `PASS=15 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 |
|
||||||
|
|
||||||
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.
|
||||||
+27
-21
@@ -1,36 +1,42 @@
|
|||||||
# 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) |
|
||||||
| [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 15 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á **15/15 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,34 @@ 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.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -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á **15/15 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 15/15 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 15 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
|
||||||
```
|
```
|
||||||
+62
-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,55 @@ 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 (15 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**: o `bench-basic.sh` não está na
|
||||||
|
árvore e nenhuma meta de vazão é reivindicada.
|
||||||
|
|
||||||
|
## 6. Status atual da suíte (2026-11-12)
|
||||||
|
|
||||||
|
**15/15 gates verdes.** Uma rodada completa e limpa
|
||||||
|
(`./scripts/vm-run-tests.sh`, ~9 min) recria e reinicia o `pftest-pf0` antes
|
||||||
|
de cada gate, reimplanta o módulo e as ferramentas, e termina com
|
||||||
|
`PASS=15 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 |
|
||||||
|
|
||||||
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.
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
Reference in new issue
Block a user