4 Commits
Author SHA1 Message Date
lgcosta dff2185b20 Docs: bring the documentation in line with the code
- Demo docs replaced by the screencast docs (single source of truth): the
  legacy demo-session.sh/demo-record.py and the v0.9 renderer were removed
  instead of being kept as a second convention.
- feature-matrix: stale 'M2-in-progress' vocabulary gone; the rows the 15
  gates prove now say VERIFIED-in-VM and name the gate; af-to is now an
  explicit non-goal row (INET6 0 in this build).
- guide: the queues/divert/pflog sections described a state that no longer
  exists (priq-only, divert dropping packets, pflog logging nothing); they
  now describe the shipped behaviour.
- vm-lab: the 15 real gates with what each proves, 15/15 green, and the
  pf0 -> cli/srv/gw2 ssh setup the screencasts rely on.
- install/build: pdivert and pflogd are built and ported (docs said
  otherwise); module object list now includes hfsc.c, fq_codel.c, pfk_pflog.c;
  the screencast pipeline is an optional build stage.
- pf-guide: the 'M2 scope only' framing is gone; honest gaps listed.
- caveats: three new sections about the screencast pipeline (MiniMax video
  generation blocked by the account tier, narration as a separate track, a
  video frame is not a verdict).
- porting-notes: what the recording taught us.
- README/README.pt-BR: status, milestones, layout and video section updated.

All internal doc links verified.
2026-11-12 11:51:25 -03:00
lgcosta cc67531602 Screencasts v2: real shell sessions, MiniMax narration (EN/PT), lab diagram
O problema da primeira serie era a sensacao de 'quadro a quadro': a captura
era a saida de um script despejada em blocos.  Agora cada video e uma sessao
shell de verdade.

- record.py: abre um pty, faz SSH no firewall e digita os comandos tecla a
  tecla (~34 c/s, com jitter).  O tty remoto ecoa, o kernel responde ao vivo;
  a captura e asciicast v2 com timestamps.  'bash --noediting' evita que o
  editor de linha redesenhe o comando e o tty o repita (a linha aparecia
  duplicada).
- record.py --pace: segura a tela enquanto a voz fala - cada batida dura o
  trecho de audio que a acompanha, de modo que uma unica captura serve as
  duas versoes de idioma.
- record.py --ensure-pf: garante pf.ko carregado antes de gravar.  Sem isso
  uma fase que roda 'rmmod' (ativacao) pode derrubar o /dev/pf de outra
  gravacao concorrentes - foi exatamente o que quebrou a fase de roteamento
  na primeira tentativa.
- make_audio.py: geracao da narracao pelo MiniMax TTS, uma voz feminina por
  idioma (English_ConfidentWoman / Portuguese_ConfidentWoman), um arquivo
  por segmento.  A API devolve o audio em hexadecimal, nao base64.
- render_video.py: reproduz a captura por um emulador de terminal e escreve
  os frames direto no ffmpeg como RGB bruto (um PNG por frame era uma ordem
  de grandeza mais lento).  Desenha o diagrama de rede do lab por cima do
  terminal nas fases de roteamento e balanceamento, destacando o caminho em
  discussao, e queima a legenda da narracao.
- assemble.py: mixa os segmentos de audio na linha do tempo.
- join.py: emenda as fases com crossfade sobre um cartao de capitulo e
  sincroniza o audio na mesma linha do tempo.
- build-all.sh: roda o pipeline inteiro.

Arte de fundo e audio vem do MiniMax; a geracao de video do MiniMax nao esta
habilitada nesta conta (erro 2068 em todos os modelos), entao as transicoes
sao renderizadas localmente com ffmpeg.

16 videos: 7 fases + filme completo, em ingles e portugues, com legenda
queimada.
2026-11-12 10:57:29 -03:00
lgcosta fe324259c0 Screencasts: seis videos silenciosos 1080p30 da demonstracao
Novo pipeline de video: scripts/screencast/render.py roda uma fase em um pty,
emula o terminal com pyte, gera frames PNG e mux com ffmpeg (H.264,
yuv420p, +faststart, sem trilha de audio). Fallback de encoder: libx264 ->
libopenh264 -> VP9.

Fases (scripts/screencast/phases/, uma sessao real contra o lab por video):
  1. implementacao no kernel - fontes OpenBSD + compat + build limpo do pf.ko
  2. ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
  3. regras basicas - tabela, anchor, keep state, block drop
  4. roteamento - route-to e reply-to (provado com tcpdump nas 2 interfaces)
  5. balanceamento - pool round-robin, pool source-hash, dup-to
  6. QOS - filas hfsc, pfctl -s queue -v, transferencia de 2 MB pela fila

Os videos ficam versionados em out/screencast/ (.gitignore ajustado para
nao ignorar *.mp4) e o indice/texto de descricao do YouTube em
docs/{en,pt-BR}/screencast.md.

Correcoes na propria demo (encontradas gravando): a acao de uma regra no pf
e pass OU block (nunca os dois); as regras de um anchor vivem em um ruleset
proprio (pfctl -a <anchor> -f); o nc do QOS precisa rodar na VM do servidor,
nao no firewall; o rotulo 'FASE x/6' vem do numero da fase, nao da posicao
na lista de gravacao.
2026-11-12 08:41:41 -03:00
lgcosta 90c51f43fc M5: queue gate 15 green (hfsc in the data path, pfctl -s queue works); vendor OpenBSD net/hfsc.h + net/fq_codel.h so kernel and pfctl share the ABI; pflog payload anchored; splassert no longer BUGs 2026-11-12 03:10:46 -03:00
119 changed files with 4098 additions and 958 deletions

No files matched your search

+14 -2
View File
@@ -1,6 +1,16 @@
_vendor/
out/
out/*
!out/demo-pf-linux.cast
!out/screencast/
out/screencast/*
!out/screencast/art/
!out/screencast/art/*
!out/screencast/en/
out/screencast/en/*
!out/screencast/en/*.mp4
!out/screencast/pt/
out/screencast/pt/*
!out/screencast/pt/*.mp4
*.o
*.ko
*.mod
@@ -11,7 +21,9 @@ out/
modules.order
Module.symvers
tools/out/
!out/demo-pf-linux.cast
lab/seeds/
*.pcap
*.log
__pycache__/
*.pyc
+4 -7
View File
@@ -25,13 +25,10 @@ docs:
@for f in docs/en/*.md; do echo "EN $$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).
demo:
@mkdir -p out
@command -v asciinema >/dev/null 2>&1 || { \
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)"
# Record the narrated screencasts (real shell sessions, MiniMax voice).
# Needs MINIMAX_API_KEY and the VM lab up; see docs/*/screencast.md.
screencast:
./scripts/screencast/build-all.sh
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true
+40 -24
View File
@@ -2,32 +2,32 @@
Port of the OpenBSD **PF** (packet filter) to **Linux 7.3.4** as an
out-of-tree kernel module — the real PF engine (`pf.c`, `pf_norm.c`,
`pf_table.c`, `pf_lb.c`, … ported from OpenBSD 8.0 sources) driven by the
real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. (A second module for the
`pfq` qdisc arrives with M5.)
`pf_table.c`, `pf_lb.c`, `hfsc.c`, … ported from OpenBSD 8.0 sources) driven by
the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
**Status:** the filter core, stateful filtering, tables, anchors, NAT
(`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`), `divert`
and `pflog` are **verified in a real libvirt lab** (14/14 gates green). The
remaining features (queue attachment/pacing, af-to) are still pending — honest per-feature status:
[`docs/en/feature-matrix.md`](docs/en/feature-matrix.md).
Demo (asciinema): [`docs/en/demo.md`](docs/en/demo.md).
(`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`/`reply-to`),
pools, `divert`, `pflog` and the OpenBSD 8.0 **queues (HFSC)** are **verified
in a real libvirt lab** — 15/15 gates green. What is still pending is stated
per feature in [`docs/en/feature-matrix.md`](docs/en/feature-matrix.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)
· plan: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
## Milestones
| Milestone | Scope | Status (2026-11-11) |
| Milestone | Scope | Status |
|---|---|---|
| M0 | repo, scripts, kernel build, VM boots the custom kernel | done |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f` / `-s info` | done |
| M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **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) |
| 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 |
| M5 | queues: HFSC/FQ-CoDel back ends + queue attachment | **partial** — both back ends now compile and link into `pf.ko` (suite still 14/14); priq remains attached, no bandwidth enforced yet |
| M6 | docs complete, patchset tidy, full suite green, tagged release | **done** — docs EN+PT-BR, 11/11 suite green, tagged (v0.5.0) |
| 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) |
| 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 |
|---|---|
@@ -35,7 +35,7 @@ Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/super
| State/Tables | stateful firewall engine, `pfr` tables (radix + hash, `pfctl -t`) |
| NAT | `nat-to` (port pools, sticky-port, static-port), `rdr-to`, `binat-to` |
| Scrub | fragment reassembly, overlap drop, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` |
| QoS (ALTQ successor) | `queue`/`queue on` with OpenBSD 8.0 `ifq` schedulers: **default (priq), HFSC, FQ-CoDel**, attached via the `pfq` qdisc; `pfctl -s queue -v` stats |
| QoS (ALTQ successor) | `queue`/`queue on` with OpenBSD 8.0 `ifq` schedulers: **HFSC verified in the data path** (gate 15), priq default, FQ-CoDel compiled; `pfctl -s queue -v` works |
| Divert | `divert-to` / `divert` tag + `pdivert` relay over an in-module `PF_DIVERT` socket family (verified: packet to userspace and back) |
| Route | `route-to`/`dup-to`/`reply-to` + pools (`round-robin`, `source-hash`) verified (`fail-to` no longer exists in OpenBSD 8.0) |
| Logging | `pflogd` via `/dev/pflog` (DLT_PFLOG pcap, OpenBSD record layout) — verified |
@@ -45,25 +45,41 @@ Not in scope: `pfsync`/CARP failover, IPsec/pfkey, `ftp-proxy`, per-netns PF
instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before
OpenBSD 8.0).
## Screencasts
Seven narrated screencasts per language (English and Portuguese), 1080p30 with
burned-in subtitles, the lab's network diagram drawn over the terminal, and a
continuous film that crossfades the phases together. Every video is a **real
shell session** against the Alpine VMs — commands typed key by key, the kernel
answering live — with a MiniMax female voice narrating:
```sh
./scripts/screencast/build-all.sh # -> out/screencast/{en,pt}/*.mp4
```
Details, per-phase breakdown and the YouTube description text:
[`docs/en/screencast.md`](docs/en/screencast.md) ·
[`docs/pt-BR/screencast.md`](docs/pt-BR/screencast.md)
## Quick start
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
(commands are read from the scripts; a fresh-clone end-to-end run is still
pending):
(commands are read from the scripts):
```sh
./scripts/fetch-sources.sh # OpenBSD 8.0 sources (C3SL mirror, sha256-pinned)
./scripts/vendorize.sh # stage the pristine files we port
./scripts/build-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (export patch: none needed)
./scripts/build-module.sh # -> module/pf.ko
make -C tools # -> tools/out/pfctl (static), ABI harness
make -C tools # -> tools/out/{pfctl,pdivert,pflogd,abitest}
sudo insmod module/pf.ko # /dev/pf (0600) + netfilter hooks
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
path you need: only the M2 filter core is gated today; NAT/scrub/queues/
divert/pflog are not usable yet ([`docs/en/feature-matrix.md`](docs/en/feature-matrix.md)).
Every feature listed above is exercised by the lab suite before you need it;
[`docs/en/feature-matrix.md`](docs/en/feature-matrix.md) says which gate proves
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).
Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e
@@ -71,11 +87,11 @@ Documentação em português: [`README.pt-BR.md`](README.pt-BR.md) e
## Layout
- `module/` — kernel sources (ported PF + compat layer; the `pfq` qdisc lands with M5)
- `tools/` — pfctl (static Linux build), pdivert relay source, ABI harness (`pflogd` not ported yet)
- `module/` — kernel sources: ported PF engine (`module/pfsrc`) + Linux compat layer (`module/compat`)
- `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`)
- `scripts/` — fetch/build/patch/VM automation
- `tests/` — VM suite (`tests/vm/NN-*.sh`, scapy helpers)
- `scripts/` — fetch/build/patch/VM automation, plus `scripts/screencast/` for the videos
- `tests/vm/` — VM suite: 15 gates (`tests/vm/NN-*.sh`) + `lib.sh`
- `docs/en/` + `docs/pt-BR/` — full bilingual documentation
- `docs/superpowers/` — design spec + implementation plan
+48 -29
View File
@@ -2,67 +2,86 @@
Port do **PF** (packet filter) do OpenBSD para o **Linux 7.3.4** como um
módulo de kernel out-of-tree — o engine PF real (`pf.c`, `pf_norm.c`,
`pf_table.c`, `pf_lb.c`, … portados dos fontes do OpenBSD 8.0) controlado
pelo `pfctl` real do OpenBSD via a ABI ioctl de `/dev/pf`. (Um segundo
módulo para o qdisc `pfq` chega com o M5.)
`pf_table.c`, `pf_lb.c`, `hfsc.c`, … portados dos fontes do OpenBSD 8.0)
controlado pelo `pfctl` real do OpenBSD via a ABI ioctl de `/dev/pf`.
**Status:** milestones iniciais. O engine portado e o plano de controle (M1)
funcionam; o núcleo de filtragem do M2 está em validação na bancada de VMs;
todo o resto está pendente. Status honesto por feature:
**Status:** o núcleo de filtragem, a filtragem stateful, tabelas, anchors, NAT
(`nat-to`/`rdr-to`/`binat-to`), roteamento forçado (`route-to`/`dup-to`/
`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).
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)
· plano: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
## Milestones
| Milestone | Escopo | Status (2026-11-11) |
| Milestone | Escopo | Status |
|---|---|---|
| M0 | repo, scripts, build do kernel, VM sobe com kernel próprio | concluído |
| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f` / `-s info` | concluído |
| M2 | núcleo de filtragem: regras/estados/tabelas/anchors, gates VM 01–04 | **em andamento** — caminho de dados implementado, gates ainda não todos verdes |
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente |
| M4 | route-to/dup-to/fail-to, divert e2e | pendente |
| M5 | filas: back ends HFSC/FQ-CoDel + qdisc `pfq` | pendente |
| M6 | docs completas, patchset arrumado, suíte inteira verde, release com tag | em andamento (docs) |
| M2 | núcleo de filtragem: regras/estados/tabelas/anchors, gates VM 01–04 | **concluído** — gates 01/02/03/04 verdes |
| 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/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 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 | **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 |
|---|---|
| Filtragem | regras, anchors, quick, `match`/`block`(drop/return/return-rst)/`pass`, `keep state`, syncookies |
| Estado/Tables | firewall stateful, tables `pfr` (radix + hash, `pfctl -t`) |
| NAT | `nat-to` (pools de portas, sticky-port, static-port), `rdr-to`, `binat-to` |
| Scrub | remonta­gmento de fragmentos, drop por overlap, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` |
| QoS (sucessor do ALTQ) | `queue`/`queue on` com os agendadores `ifq` do OpenBSD 8.0: **default (priq), HFSC, FQ-CoDel**, anexados via qdisc `pfq`; estatísticas `pfctl -s queue -v` |
| Divert | `divert-to` / tag `divert` + relay `pdivert` sobre uma família de sockets `PF_DIVERT` dentro do módulo (pendente — ver `docs/pt-BR/feature-matrix.md`) |
| Roteamento | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` |
| Logging | `pflogd` via canal netlink pflog (pcap DLT_PFLOG) |
| 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: **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 (verificado: pacote vai para o userspace e volta) |
| 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 `/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) |
Fora de escopo: failover `pfsync`/CARP, IPsec/pfkey, `ftp-proxy`, instâncias PF
por netns, agendadores exclusivos do ALTQ clássico (cbq/wfq/red — removidos do
OpenBSD antes da 8.0).
## 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
Receita completa: [`docs/pt-BR/install.md`](docs/pt-BR/install.md). Versão
curta (comandos lidos dos scripts; a execução de ponta a ponta a partir de
clone limpo ainda está pendente):
curta (comandos lidos dos scripts):
```sh
./scripts/fetch-sources.sh # fontes OpenBSD 8.0 (mirror C3SL, sha256 fixado)
./scripts/vendorize.sh # staging dos arquivos upstream portados
./scripts/build-kernel.sh # kernel 7.3.4: bzImage + modules_prepare (patch de export: nenhum necessário)
./scripts/build-module.sh # -> module/pf.ko
make -C tools # -> tools/out/pfctl (estático), verificador de ABI
make -C tools # -> tools/out/{pfctl,pdivert,pflogd,abitest}
sudo insmod module/pf.ko # /dev/pf (0600) + hooks netfilter
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
o PF num caminho que você precisa: só o núcleo de filtragem do M2 tem gate
hoje; NAT/scrub/filas/divert/pflog ainda não são utilizáveis
([`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md)).
Toda feature listada acima é exercitada pela suíte do lab antes de você
precisar dela; [`docs/pt-BR/feature-matrix.md`](docs/pt-BR/feature-matrix.md)
diz qual gate prova o quê, e [`docs/pt-BR/pf-guide.md`](docs/pt-BR/pf-guide.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).
English documentation: [`README.md`](README.md) e
@@ -70,11 +89,11 @@ English documentation: [`README.md`](README.md) e
## Estrutura
- `module/` — fontes de kernel (PF portado + camada de compat; o qdisc `pfq` chega com o M5)
- `tools/` — pfctl (build estático Linux), fonte do relay pdivert, verificador de ABI (`pflogd` ainda não portado)
- `module/` — fontes de kernel: engine PF portado (`module/pfsrc`) + camada de compat Linux (`module/compat`)
- `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`)
- `scripts/` — automação de fetch/build/patch/VM
- `tests/` — suíte em VM (`tests/vm/NN-*.sh`, helpers scapy)
- `scripts/` — automação de fetch/build/patch/VM, mais `scripts/screencast/` para os vídeos
- `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/superpowers/` — design + plano de implementação
+14 -9
View File
@@ -4,8 +4,8 @@ Documentação em português: [`docs/pt-BR/README.md`](../pt-BR/README.md).
This is the user documentation set for the port of the **OpenBSD 8.0 PF
packet filter** to **Linux 7.3.4** — the real PF engine (`pf.c`, `pf_norm.c`,
`pf_table.c`, `pf_lb.c`, …) running as the out-of-tree module `pf.ko`, driven
by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
`pf_table.c`, `pf_lb.c`, `hfsc.c`, …) running as the out-of-tree module
`pf.ko`, driven by the real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI.
## 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 |
| [install.md](install.md) | Apply/build/install recipe (kernel, module, tools) |
| [build.md](build.md) | Build pipeline in detail: fetch → vendorize → kernel → module → pfctl |
| [vm-lab.md](vm-lab.md) | The libvirt validation lab: topology, recreate/run, what each gate proves |
| [feature-matrix.md](feature-matrix.md) | Per-feature status: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx |
| [pf-guide.md](pf-guide.md) | Daily use: what works today in `/etc/pf.conf`, what to avoid until M3/M4 |
| [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 / VERIFIED-offline / COMPILES-untested / NOT-ENABLED |
| [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` |
| [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/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:
- **VERIFIED-in-VM** — exercised end-to-end in the libvirt lab (see vm-lab.md).
- **COMPILES-untested** — the code is built into `pf.ko` but no gate has proven the behavior yet.
- **PENDING-Mx** — lands (or lands fully) in milestone Mx; current observable behavior is documented.
- **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)).
- **VERIFIED-offline** — proven by a fixture or ABI test, not by traffic.
- **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
View File
@@ -69,14 +69,15 @@ auto-staged (hand-ported).
## 5. Module — `scripts/build-module.sh`
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (single module today; the
`pfq` qdisc arrives with M5). The script self-prepares `Module.symvers` /
`modules_prepare` first. Object list (`module/Makefile`): ported engine
(`pf.c`, `pf_ioctl.c`, `pf_norm.c`, `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`,
`pf_table.c`, `pf_osfp.c`, `pf_syncookies.c`, `divert_sock.c`) + compat layer
(mbuf shim, kern glue, if/ip4/ip6/pcb/xmit/ifq, crypto, radix, subr_tree) +
`pfk_mod.c`/`pfk_hooks.c`. Compile flags define `PFLINUX=1` and `_KERNEL=1`.
Prints `MODULE_BUILD_OK`.
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (a single module: the BSD
`ifq`/`pfq` interface lives inside it, it is not a separate `tc` qdisc). The
script self-prepares `Module.symvers` / `modules_prepare` first. Object list
(`module/Makefile`): ported engine (`pf.c`, `pf_ioctl.c`, `pf_norm.c`,
`pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, `pf_table.c`, `pf_osfp.c`,
`pf_syncookies.c`, `divert_sock.c`, `pfk_pflog.c`, `hfsc.c`, `fq_codel.c`) +
compat layer (mbuf shim, kern glue, if/ip4/ip6/pcb/xmit/ifq, crypto, radix,
subr_tree) + `pfk_mod.c`/`pfk_hooks.c`. Compile flags define `PFLINUX=1` and
`_KERNEL=1`. Prints `MODULE_BUILD_OK`.
`insmod module/pf.ko` creates `/dev/pf` (0600) and registers the netfilter
hooks; `dmesg` shows `pfk: …` lines (see [caveats.md](caveats.md) for what
@@ -84,7 +85,8 @@ the data path does).
## 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;
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`
(basic / translations / queues+scrub / anchor / negative `broken.conf`).
This is pure userland; it needs no `/dev/pf`.
- `pdivert` — divert relay source under `tools/pdivert/`; see
[feature-matrix.md](feature-matrix.md) for its (pending) kernel side.
- `pdivert` — the divert relay, built from `tools/pdivert/`. It talks to the
`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
([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
- Stages 1–6 have each been run during M0–M2 development on the build host
(artifacts exist in-tree: `out/`, `module/pf.ko`, `tools/out/pfctl`), and
the scripts above were verified line-by-line for this document.
- Stages 1–6 have each been run repeatedly on the build host (artifacts exist
in-tree: `out/`, `module/pf.ko`, `tools/out/{pfctl,pdivert,pflogd}`), and the
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
*not* been executed yet; treat the exact command sequence in install.md as
"as designed, last-run state per milestone" rather than CI-verified.
+47 -20
View File
@@ -111,29 +111,29 @@ kernel prints `pfk: divert port N: no socket bound`. `log` needs
(256 records / 1 MiB) and drops. Neither is a stub any more, but neither does
anything until its consumer runs (see [pf-guide.md](pf-guide.md)).
### 9. Queue configuration hits a lock assertion in the purge path (open)
### 9. Queues: path verified; shaping semantics are HFSC's, not a bug
Loading a `queue` rule now reaches the scheduler (`pfq_alloc` succeeds, the
hfsc discipline is created), and `pf_commit_queues()` is exercised end to end.
Two bugs were found and fixed while getting there — the root-class handle
colliding with odd pf queue ids (`pfq_addqueue` returned EBUSY), and a NULL
discipline not being checked.
`15-queue.sh` proves the queue path works end to end: the 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 - so the scheduler really is in the data path.
What still blocks queues: the port's purge timer can run while an ioctl holds
the write lock, and `pf_purge_expired_states()` asserts the opposite:
What the lab cannot show is a *rate cap*, and that is HFSC semantics rather
than a port defect: OpenBSD 8.0's `pf.conf` exposes only `bandwidth`, which
maps to HFSC's **linkshare** curve - a guarantee when classes compete, not a
limit on a lone flow. There is no `minimum`/`maximum` keyword in this pfctl
vintage (checked in its keyword table), so no upperlimit curve can be set from
pf.conf. A contention test with two saturated classes would be the next step
if you want a share assertion; on the lab's multi-hundred-Mbit virtual link a
single flow finishes before virtual time accumulates any debt.
```
splassert: pf_purge_expired_states: expected 0, got 2
kernel BUG at compat/pfk_kern.c:51
```
OpenBSD serialises this with `splnet()`; the port replaced the spl layer with
rw-semas and a work queue, so the assertion is now a real race rather than a
formality. Until it is fixed (release the lock around the call, or give the
state tree its own lock as upstream does for `pf_state_lock`), a host that
configures queues can wedge the module in `Unloading`. **Do not enable
`queue` rules on a live host yet** — the 14 lab gates never configure queues,
which is why they stay green.
Bugs found and fixed while getting here (details in `docs/porting-notes.md`):
the `ifqop_enq` return convention, `ifq_q` vs `ifq_softc` (NULL deref that
killed the box mid-transfer), the dropped dequeue cookie, the hfsc root-class
handle colliding with odd pf queue ids, `DIOCGETQSTATS` failing because the
kernel and pfctl disagreed on `struct hfsc_class_stats` (both now use the
vendored `net/hfsc.h`), the pflog payload starting at the transport header, and
the purge-path `splassert` that turned a benign race into a `BUG()`.
## Compat-layer deviations (from `docs/porting-notes.md`, M1 record)
@@ -159,6 +159,33 @@ matter when reasoning about rule semantics:
kernel. Containers/namespaces are not visible to PF and cannot be filtered
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
ARP and other non-IPv4/IPv6 EtherTypes never enter the netfilter IPv4/IPv6
-41
View File
@@ -1,41 +0,0 @@
# Demo (asciinema)
The lab can be recorded as an [asciinema](https://asciinema.org) cast. Every
byte in the recording is the real output of the lab VMs (Alpine under libvirt
running the kernel built from source with `pf.ko`); only the pacing is
scripted.
## Record
```sh
pip install asciinema # once
bash scripts/vm-run-tests.sh # or: scripts/vm-create.sh + deploy
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
```
The session walks through:
1. the lab topology and the freshly loaded `pf.ko` (`dmesg` proof),
2. `pfctl -s info` over the OpenBSD ioctl ABI,
3. a real `pf.conf` (table, stateful rules, explicit drop) and `pfctl -sr`,
4. transit ping + `pfctl -ss` (stateful),
5. `pfctl -t <table> -T show` (live tables),
6. a blocked ping (counters with `pfctl -sr -vv`),
7. `divert-to` with the `pdivert` relay (packet to userspace and back),
8. shutdown.
## Play back
```sh
asciinema play out/demo-pf-linux.cast
```
## Upload
```sh
asciinema upload out/demo-pf-linux.cast
```
`scripts/demo-record.py` is an alternative recorder that produces the same
asciicast v2 format without the asciinema binary (it drives the lab over
ssh and writes the cast itself).
+39 -34
View File
@@ -4,67 +4,71 @@ Português: [docs/pt-BR/feature-matrix.md](../pt-BR/feature-matrix.md).
Status vocabulary (used identically in both languages):
- **VERIFIED-in-VM** — passed end-to-end in the libvirt lab (see [vm-lab.md](vm-lab.md)).
- **M2-in-progress** — implemented; VM gates 01–04 exist but the suite is **not all green yet**; no VERIFIED claim until a clean full run.
- **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.
- **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.
- **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).
As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
`docs/porting-notes.md`, milestone commits.
As of 2026-11-12; source of per-row evidence: `out/test-logs/`,
`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
| 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 |
| Ruleset load | `pfctl -f file` | M2 | VERIFIED-in-VM (loads succeeded in every captured gate run; run status itself is mixed — see [vm-lab.md](vm-lab.md) §6) |
| Parse-only check | `pfctl -n -f file` | M1 | VERIFIED (offline fixture suite, `make -C tools test`) |
| 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 (`make -C tools test`) |
| Status/counters | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM |
| Rules listing | `pfctl -s rules [-v]` | M2 | COMPILES-untested |
| States listing | `pfctl -s states [-v]` | M2 | M2-in-progress — gate 01 shows a listing anomaly (transit ping OK, no icmp state displayed) while state machinery is being fixed |
| Tables listing/ops | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) |
| Anchors | `pfctl -a anchor -f file`, `-s Anchors` | M2 | M2-in-progress (gate 04) |
| Queue listing | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (parses fine; counters not wired to real back ends yet) |
| ioctl ABI parity | `make -C tools abi` (abitest tables) | M1 | VERIFIED build-side (userland table); kernel-side diff is an open integrator step (`tools/UPSTREAM.md`) |
| Rules listing | `pfctl -s rules [-v]`, `pfctl -sr` | M2 | VERIFIED-in-VM (gates 02–06) |
| 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 | VERIFIED-in-VM (gate `03-tables.sh`, incl. a live `-T delete` unblocking the flow) |
| Anchors | `pfctl -a anchor -f file`, `-s anchors` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| 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-offline (userland table); the kernel-side diff is an open integrator step (`tools/UPSTREAM.md`) |
## Filtering core
| Feature | Upstream syntax | Milestone | Status |
|---|---|---|---|
| `pass`/`match`, quick, on-interface/proto/addr/port | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | M2-in-progress (data path runs the real `pf_test`; latest partial run: transit ping OK, icmp block OK) |
| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) |
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) |
| `keep state` (icmp/tcp/udp), state table, purger | `… keep state` | M2 | M2-in-progress |
| `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 | 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 | 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 | VERIFIED-in-VM (gate `01-state.sh`; state visible in `pfctl -ss`) |
| `set state-policy`, `allow-opts`, etc. | `set …` options | M2 | COMPILES-untested |
| Syncookies (state overflow path) | `set syncookies always` | M2 | COMPILES-untested |
| pfr tables (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) |
| Anchors + include + quick precedence | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) |
| IPv6 for all of the above | same syntax, v6 | M2+ | COMPILES-untested (engine is v6-complete by construction; v6 smoke gates planned) |
| Non-IPv4/IPv6 traffic (ARP…) | — | M2 | PFLINUX by construction: netfilter IPv4/IPv6 hooks only — ARP never enters PF path (no gate yet) |
| 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 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| 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 the PF path (no gate) |
## NAT / normalization (M3)
| 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`. |
| `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). |
| `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. |
| 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. |
| 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) |
| `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-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-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. |
| 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. |
| 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
| 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. |
| `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). |
| Queues (`queue`, `queue on`, `pfctl -s queue -v`) | `queue root q1 bandwidth 1Mbit; queue on egress all ...` | M5 | **Back ends built into `pf.ko`, attachment pending** — OpenBSD's `hfsc.c` and `fq_codel.c` now compile and link (157 scheduler symbols, `hfsc_initialize`/`hfsc_pf_addqueue`/`fqcodel_pf_addqueue` reachable from `pf_ioctl`), and the full suite still passes with them in. The data path can now hand packets to an attached scheduler (`pf_kifq_input()` + a real `ifq_start()` that drains and transmits), but queues are **not safe to enable yet**: the purge path hits a lock assertion (see caveats #9), so no bandwidth is enforced and no gate covers it. |
| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **priq fallback until M5**; `pfsrc/hfsc.c` staged (real OpenBSD 8.0 code) but **not in the module object list** |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — same as HFSC: staged, not built |
| `pfq` qdisc (`tc qdisc … root pfq`) | n/a (Linux glue) | M5 | PENDING-M5 (does not exist in the tree yet) |
| pflog (`log`, `log all`, `block log`) + `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. |
| `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>` | 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-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 | **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 — `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` 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-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)
@@ -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` |
| 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 |
| `af-to` (IPv4↔IPv6 translation) | the code sits under `#ifdef INET6` and this build has `INET6 0`; enabling it is a separate milestone, not a missing feature in the base |
+27 -19
View File
@@ -24,8 +24,8 @@ between kernel and userland (ioctl encoding per `module/include/pfk.h`).
| `module/compat/` | Linux plumbing under the BSD-shaped sources: mbuf-over-`sk_buff` shim (`pfk_mbuf`), kernel glue (malloc/pools/timeouts/locks, `pfk_kern`), interface+address views (`pfk_if`), BSD-semantics IP output/forward (`pfk_ip4`, `pfk_ip6`), socket lookups (`pfk_pcb`), ifq API with priq semantics (`pfk_ifq`), radix/route-table/crypto ports |
| `module/include/` | `pfk.h` (master compat header + OpenBSD-style `_IO*` encoding), staged `net/pfvar.h`, `pfk_mbuf.h`, `pfk_net.h`, `pfk_if.h` |
| `tools/` | Linux build of OpenBSD 8.0 `pfctl` (single binary), `pdivert` relay, ABI parity harness (`abitest` + `make abi`), offline parse tests (`make test`). Deltas vs upstream: `tools/UPSTREAM.md` |
| `scripts/` | `fetch-sources.sh`, `vendorize.sh`, `stage-port.sh` (deterministic staging), `build-kernel.sh`, `build-module.sh`, `vm-*.sh` lab automation |
| `tests/vm/` | VM suite gates `01-state.sh … 04-anchors.sh` + `lib.sh`; `tests/confs/` rulesets deployed to the lab |
| `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: 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) |
## Data path (M2, as implemented today)
@@ -69,33 +69,41 @@ rebuilt.
## Queues, divert, pflog — today's observable behavior
- **Queues**: `pfctl` parses `queue`/`queue on` (8.0 syntax); kernel-side the
ifq API is implemented by `compat/pfk_ifq.c` with **priq semantics only**;
the `pfq` classful qdisc and the HFSC/FQ-CoDel back ends land in **M5**.
`ifq.c`/`hfsc.c`/`fq_codel.c` are staged and compile-ready but are not in
the module object list yet.
- **Divert**: `divert-to`/`divert` tag evaluate to PF's `PF_DIVERT` verdict;
`divert_packet()` (`pfsrc/divert_sock.c`) currently **drops the packet with
a rate-limited kernel log** because the AF_DIVERT socket family is not yet
instantiated. The `pdivert` relay (userspace) exists and expects
`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`.
- **pflog**: `pflog_packet()` returns success but **logs nothing** (one-time
`pr_warn "packet not logged"`); `if_pflog.c` is staged, netlink back end is
M4/M5.
- **Queues**: `pfctl` parses the OpenBSD 8.0 `queue`/`queue on` syntax and the
kernel side implements it in the BSD shape: `compat/pfk_ifq.c` provides the
`ifq_ops`/`pfq_ops` interfaces and the OpenBSD back ends (`pfsrc/hfsc.c`,
`pfsrc/fq_codel.c`) plug into them; **priq** is the default discipline.
Gate `15-queue.sh` proves the **hfsc** discipline end to end: the queue
config loads, the 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 class handles and the stats structs
come from OpenBSD's own `net/hfsc.h`/`net/fq_codel.h`, vendored so the
kernel and `pfctl` cannot drift apart.
- **Divert**: `divert-to`/`divert` tag evaluate to PF's `PF_DIVERT` verdict
and the module registers an `AF_DIVERT` socket family (`PF_DIVERT=32`) at
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)
| Milestone | Scope | Status (2026-11-11) |
| Milestone | Scope | Status |
|---|---|---|
| M0 | repo, scripts, kernel build, VM boots custom kernel | done |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f`/`-s info` | done |
| M2 | filter core: rules/states/tables/anchors VM gates | **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 |
| 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 |
| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress — docs EN+PT-BR complete, 14/14 suite green, tagged |
| 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 | **done** — docs EN+PT-BR, 15/15 suite green, tagged (`v1.0.0`) |
## Next reads
[build.md](build.md) → [vm-lab.md](vm-lab.md) → [pf-guide.md](pf-guide.md) →
[feature-matrix.md](feature-matrix.md) → [caveats.md](caveats.md).
[feature-matrix.md](feature-matrix.md) → [screencast.md](screencast.md) →
[caveats.md](caveats.md).
+24 -8
View File
@@ -75,19 +75,34 @@ Because the deliverable is an out-of-tree module, "applying the patch set"
adds a documented `patches/kernel/000N-*.patch` (all such patches: one
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
`scripts/apply-patches.sh` / `scripts/unpatch.sh` / `patches/kernel/series`
are not yet in the tree (they land with the first real core patch — see
[patches.md](patches.md)). You can safely skip 2.3.
- Step 2.4 builds `pfctl` and the ABI harness only (`make -C tools` targets:
`pfctl abitest abi test clean`). `pdivert` is present as a source file with
no Makefile target yet; `pflogd` is not ported.
- After 2.5, what you can actually *use* is the M2 filter core (pass/block/
quick/keep state/tables/anchors); NAT/scrub/queues/divert/pflog are gated
by later milestones — see the [feature matrix](feature-matrix.md) and read
[pf-guide.md](pf-guide.md) before enabling PF on a path you need.
- Step 2.4 builds every userland tool: `make -C tools` targets are `pfctl`,
`pdivert`, `pflogd` and the ABI harness (`abitest`, `abi`, `test`, `clean`).
- After 2.5, the whole feature set is usable and gated by the lab suite —
filtering with state, tables and anchors, NAT (`nat-to`/`rdr-to`/`binat-to`
plus pools), scrub, forced routing (`route-to`/`dup-to`/`reply-to`),
`divert`, `pflog` and the OpenBSD 8.0 queues (HFSC). The
[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
@@ -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) |
| Known divergences | [caveats.md](caveats.md) |
| Core patch mechanism | [patches.md](patches.md) |
| Narrated screencasts | [screencast.md](screencast.md) |
| Doc index | [README.md](README.md) |
+20 -13
View File
@@ -3,10 +3,12 @@
Português: [docs/pt-BR/pf-guide.md](../pt-BR/pf-guide.md).
This is the "what can I actually run today" guide. It follows the
[feature matrix](feature-matrix.md): **M2 scope (filtering core: pass/block/
quick/keep state/tables/anchors + enable/disable)** has VM evidence behind it;
everything beyond that is either COMPILES-untested or PENDING. Install/build
is covered by [install.md](install.md) and [build.md](build.md).
[feature matrix](feature-matrix.md): the filtering core (pass/block/quick/
keep state/tables/anchors), the NAT family, scrub, forced routing, pools,
`divert`, `pflog` and the OpenBSD 8.0 queues all have **VM evidence** behind
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
@@ -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
> for your admin path blocks it — this port follows OpenBSD's default-block
> policy, and the M2 gate runs have *actually cut the test VM's ssh* that
> way (the lab keeps a serial console for exactly this). On a remote host,
> pass your management interface before you enable anything.
> policy, and the lab runs have *actually cut the test VM's ssh* that way
> (the lab keeps a serial console for exactly this). On a remote host, pass
> your management interface before you enable anything.
## A ruleset that works in this port today
```pf
# /etc/pf.conf — M2 scope only. Interface names here match the VM lab
# (mgmt/lan/wan); substitute your own (eth0, enp1s0, …).
# /etc/pf.conf. Interface names here match the VM lab (mgmt/lan/wan);
# substitute your own (eth0, enp1s0, …).
# 1) keep yourself reachable (do this first, always)
pass in quick on mgmt
pass out quick
# 2) filter core — proven today: quick, proto/port matching, state, drop
# 2) filter core — proven in the lab: quick, proto/port matching, state, drop
table <blk> persist # live blocklist (gate 03 pattern)
block in quick proto tcp from <blk> to any
@@ -56,14 +58,19 @@ grammar **parse** fine — `make -C tools test` exercises them offline — but
the *kernel behavior* of most `set` options has not been gate-verified yet;
treat them as load-and-pray until the matrix moves them.
## What to avoid until the milestones land
**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 |
|---|---|---|
| `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). |
| `queue` / `queue on` / `pfctl -s queue` | M5 | Parse and load. Queuing falls back to the priq-shaped compat `ifq` layer; **no bandwidth enforcement happens until the `pfq` qdisc lands**. |
| `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. |
| `route-label …` | never (non-goal) | Rejected at the ioctl layer. |
| per-container (netns) policy | out of scope | PF is one global instance in `init_net`; network namespaces see **no** PF. |
+123
View File
@@ -0,0 +1,123 @@
# Screencasts / Videos da demonstracao
> PT-BR: [docs/pt-BR/screencast.md](../pt-BR/screencast.md) · EN: this page
Sete videos por idioma, com **narracao com voz feminina** (MiniMax),
**legendas queimadas**, **diagrama de rede do lab** e um **filme completo**
com transicoes entre as fases. 1920x1080 @ 30fps, H.264/MP4 + AAC — prontos
para subir no YouTube.
Cada video e a gravacao de uma **sessao shell real**: uma conexao SSH de
verdade ao firewall Alpine, com os comandos digitados tecla a tecla e a saida
do kernel chegando em tempo real. Nada e simulado.
## As fases
| # | Video | 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 |
| 1 | `01-implementacao.mp4` | A implementacao 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` |
| 3 | `03-regras.mp4` | Regras basicas: 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 |
| 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 |
Mais `pf-on-linux-full.mp4`: o filme completo, com as sete fases emendadas por
transicoes (crossfade sobre um cartao de capitulo).
## Vozes MiniMax
| Idioma | Voz | Id |
|--------|-----|-----|
| Ingles (en_US) | Confident Woman | `English_ConfidentWoman` |
| Portugues (pt_BR) | Confident Woman | `Portuguese_ConfidentWoman` |
A audio e gerada pelo MiniMax TTS, **um arquivo por segmento** de fala. Como
cada segmento corresponde a uma batida da sessao, a voz comeca exatamente
quando a acao correspondente aparece na tela.
## Como regravar
```sh
# tudo: audio -> sessoes -> videos -> filme completo
./scripts/screencast/build-all.sh
# uma fase so
./scripts/screencast/build-all.sh 04-roteamento
```
Etapas individuais:
```sh
python3 scripts/screencast/make_audio.py --lang pt # audio MiniMax
python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\
--out out/screencast/caps/04-roteamento.cast --pace --ensure-pf
python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt
python3 scripts/screencast/join.py --lang pt
```
### Como o pipeline funciona
```
planos/*.json --record.py--> caps/*.cast --render_video.py--> video mudo
(comandos) (pty real) (asciicast) (pyte + PIL) |
|
narracao/*.json --make_audio.py--> vo/<fase>.<lang>/*.mp3 ----------+
(MiniMax TTS) |
assemble.py: mix + legendas
|
join.py: transicoes
```
Pontos que importam:
* **Sessao real.** `record.py` abre um pty, faz SSH na VM e digita os comandos
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.
* **Pacing.** A gravacao 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 unica captura servir as duas versoes.
* **Legendas.** O texto da narracao e queimado no video, na lingua da trilha.
* **Diagrama.** Nas fases 0, 4 e 5 o mapa do lab e desenhado sobre o
terminal, e o caminho em discussao fica destacado (eth1/eth2/eth3).
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu, a chave
`MINIMAX_API_KEY` no ambiente, e o lab de VMs no ar.
## Onde ficam os arquivos
| Caminho | Conteudo |
|---------|----------|
| `out/screencast/en/`, `out/screencast/pt/` | os videos finais |
| `out/screencast/caps/` | as capturas asciicast das sessoes |
| `out/screencast/vo/` | o audio gerado pelo MiniMax |
| `scripts/screencast/plans/` | o roteiro de comandos de cada fase |
| `scripts/screencast/narration/` | os textos da narracao (EN e PT) |
## Texto de descricao (YouTube)
```
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,
empacotado como modulo (pf.ko) e controlado pelo pfctl original do OpenBSD
sobre a ABI de ioctls do /dev/pf.
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
modulo, trafego real entre cliente, firewall e servidor.
Fases:
0. O ambiente de testes - as 4 VMs e o diagrama de rede
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
3. Regras basicas - tabelas, anchors, keep state, block drop
4. Roteamento - route-to e reply-to, com o caminho real no diagrama
5. Balanceamento - pools round-robin, source-hash e dup-to
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
Suite automatizada: 15 gates, todos verdes.
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
```
+60 -22
View File
@@ -3,8 +3,8 @@
Português: [docs/pt-BR/vm-lab.md](../pt-BR/vm-lab.md).
All commands below were read from the scripts in `scripts/` (they are the
source of truth); the lab has been created and run during M0–M2 — a full-suite
green run is the M2+ gate and is still in progress (see §6).
source of truth). The lab has been created and run through M0–M6; the full
suite is green (§6).
## 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
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
```sh
@@ -78,28 +92,52 @@ and exits non-zero if anything failed.
## 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` |
| `02-block.sh` | `block drop in quick proto icmp` → cli ping fails; `block return-rst in quick proto tcp … port 8080` → cli's `nc` to srv:8080 is refused (RST), checked via nc exit |
| `03-tables.sh` | `table <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 |
| `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 |
| `01-state.sh` | `keep state` across a transit ping; the state shows up in `pfctl -ss` |
| `02-block.sh` | `block drop` kills the ping; `block return-rst` gets the client an RST |
| `03-tables.sh` | a `pfr` table blocks a host; a live `-T delete` unblocks the same flow |
| `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,
08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (iperf rate
tolerance ±15%), 13-pflog, 14-soak (reload under traffic + rmmod cycle +
kmemleak). IPv6 is smoke-only for state/block/nat66/scrub. `bench-basic.sh`
(planned, not in the tree yet) is informational; there is no numeric perf gate.
## 6. Current suite status (2026-11-11, honestly)
The M2 gates 01–04 exist and run, but **are not all green yet** — the data
path (netisr workqueue model) and state visibility are being fixed in the M2
gate work. The latest captured run (`out/test-logs/`) shows the mixed state:
transit ping OK / state listing failing on 01, icmp-blocked OK on 02, partial
results on 03, 04 aborted. Treat "VERIFIED-in-VM" in the
[feature matrix](feature-matrix.md) as reserved for gates that pass in a
full clean run; nothing beyond M1 is claimed VERIFIED until then.
The screencasts ([screencast.md](screencast.md)) show most of these gates
being run, with the lab's network diagram drawn over the terminal.
## 7. Manual poking
+86
View File
@@ -969,3 +969,89 @@ Two port fixes worth recording:
kernel macro** (`get_current()`), which turned the definition into a K&R
function and produced a baffling "makes pointer from integer" error at the
call site.
## ifq contract details found while wiring the schedulers (M5)
The staged OpenBSD back ends assume three things the first version of the
port's ifq shim got wrong. All three were reproduced in the lab and fixed:
| Assumption | What the port did | Fix |
|---|---|---|
| `ifqop_enq()` returns **the packet to drop** (NULL = accepted) | treated a non-NULL return as an error (`ENOMEM`) and the NULL return as success → every accepted packet was reported as dropped | `ifq_enqueue()` frees the returned mbuf and reports success |
| the scheduler state lives in `ifq->ifq_q` | `ifq_attach()` stored it in `ifq_softc` only → `hfsc_enq()` dereferenced NULL and the box died mid-transfer | `ifq_attach()` keeps both fields in sync |
| `ifq_deq_begin()`'s cookie is consumed by `ifq_deq_commit()` | the cookie was dropped on the floor (always NULL) | a per-queue `ifq_dequeue` slot carries it |
Also fixed: the hfsc **root-class handle** used bit `0x1`, which is exactly the
handle of any odd pf queue id, so `hfsc_pf_addqueue()` saw the root class as a
duplicate of the queue being added and returned `EBUSY` ("cannot add queue").
Handles now use bit `0x80000000`; pf queue ids are `u_int16_t`, so they can
never collide.
### `splassert` under rw-semas
`PF_ASSERT_UNLOCKED()` (used by `pf_purge_expired_states()`) is a BSD
per-CPU-spl check: "this context did not take the lock". With an rw-semaphore
`rw_status()` is global, so the assert fires whenever *another* CPU holds the
lock - which is not a bug, and `BUG()` there turned a harmless race into a
wedged module (`Unloading`, refcount -1). It now logs once
(`pr_warn_ratelimited`) instead of killing the host; the invariant it protects
(purge never acquires `pf_lock` itself) is upheld by the call sites, which are
only reached from `systqmp` tasks and timer work.
### What `bandwidth` can and cannot do here
OpenBSD 8.0's `pfctl` keyword table exposes `burst`, `flows`, `qlimit`,
`parent`, `default` - **not** `minimum`/`maximum`. A pf `queue` therefore only
sets HFSC's **linkshare** curve (`qspec.linkshare = scspec` in `expand_queue`).
Linkshare is a *guarantee under contention*, not a cap: a single active class
may run at line rate, and `bandwidth 2Mbit` alone will not make one flow
measure 2 Mbit/s. Verifying HFSC in this port therefore requires two competing
leaf queues and comparing their shares - which is the remaining M5 item, not a
code defect in the queue path (attach, enqueue, drain and transmit are
verified: `eth2` carries the hfsc ops, 117 drains observed, transfer completes,
box stays alive).
## Scheduler ABI must be shared, not re-implemented (M5)
The first version of this note hand-wrote `struct hfsc_class_stats`,
`struct fqcodel_stats`, `struct hfsc_sc` and the `HFSC_*` constants in `pfk.h`
from memory of the BSD sources. Two consequences showed up in the lab:
1. `pfctl -s queue -v` failed with `EINVAL` - `hfsc_pf_qstats()` rejects the
call when `*nbytes < sizeof(stats)`, and pfctl's struct (from the vendored
`net/hfsc.h`) was smaller than the kernel's invented one.
2. `HFSC_ROOT_CLASS` was guessed as `0x1`, which is exactly the handle of every
odd pf queue id, so `hfsc_pf_addqueue()` refused every root queue with
`EBUSY`. The real value is `0x10000`, safely above the `u_int16_t` qid
range.
The port now vendors OpenBSD's `net/hfsc.h` and `net/fq_codel.h` into
`module/include/net/` and includes them from `pfk.h`, so kernel and pfctl
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
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
View File
@@ -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).
Este é o conjunto de documentação de usuário do port do **PF do OpenBSD 8.0**
para o **Linux 7.3.4** — o engine PF de verdade (`pf.c`, `pf_norm.c`,
`pf_table.c`, `pf_lb.c`, …) rodando como módulo out-of-tree `pf.ko`,
controlado pelo `pfctl` real do OpenBSD pela ABI ioctl de `/dev/pf`.
Este é o conjunto de documentação de usuário do port do **packet filter do
OpenBSD 8.0 (PF)** para o **Linux 7.3.4** — o engine PF real (`pf.c`,
`pf_norm.c`, `pf_table.c`, `pf_lb.c`, `hfsc.c`, …) rodando como o módulo
out-of-tree `pf.ko`, controlado pelo `pfctl` real do OpenBSD sobre a ABI ioctl
de `/dev/pf`.
## Í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 |
| [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 |
| [vm-lab.md](vm-lab.md) | Bancada de validação em libvirt: topologia, recriar/rodar, o que cada gate prova |
| [feature-matrix.md](feature-matrix.md) | Status por feature: VERIFIED-in-VM / COMPILES-untested / PENDING-Mx |
| [pf-guide.md](pf-guide.md) | Uso diário: o que funciona hoje em `/etc/pf.conf`, o que evitar até M3/M4 |
| [patches.md](patches.md) | Patches de core do Linux (hoje vazio) + regras e template para futuros `000N-*.patch` |
| [caveats.md](caveats.md) | Divergências conhecidas de comportamento e limitações atuais |
| [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 / VERIFIED-offline / COMPILES-untested / NOT-ENABLED |
| [pf-guide.md](pf-guide.md) | Uso diário: o que funciona hoje em `/etc/pf.conf` e as divergências a conhecer |
| [screencast.md](screencast.md) | Os vídeos narrados (inglês e português): fases, pipeline, como regravar |
| [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,
as referências autoritativas são:
Os docs descrevem o código como ele existe nesta árvore. Na dúvida, as
referências autoritativas são:
- `docs/superpowers/specs/2026-11-11-pf-linux-port-design.md` — design
- `docs/superpowers/plans/2026-11-11-pf-linux-port.md` — plano de milestones (tarefas T1–T19)
- `docs/porting-notes.md` — log de staging + registro de desvios da camada de compat (M1)
- `docs/superpowers/plans/2026-11-11-pf-linux-port.md` — plano dos milestones (tarefas T1–T19)
- `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).
- **COMPILES-untested** — o código compila no `pf.ko`, mas nenhum gate provou o comportamento ainda.
- **PENDING-Mx** — entra (ou entra por completo) no milestone Mx; o comportamento observável hoje está documentado.
- **VERIFIED-in-VM** — exercitado de ponta a ponta no lab libvirt; o gate que prova está nomeado (veja [vm-lab.md](vm-lab.md)).
- **VERIFIED-offline** — provado por um teste de fixture ou de ABI, não por tráfego.
- **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
View File
@@ -72,13 +72,14 @@ ficam de fora do staging automático de propósito (portados à mão).
## 5. Módulo — `scripts/build-module.sh`
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (um único módulo hoje; o
qdisc `pfq` chega com o M5). O script prepara sozinho `Module.symvers` /
`modules_prepare`. Lista de objetos (`module/Makefile`): engine portado
(`pf.c`, `pf_ioctl.c`, `pf_norm.c`, `pf_if.c`, `pf_lb.c`, `pf_ruleset.c`,
`pf_table.c`, `pf_osfp.c`, `pf_syncookies.c`, `divert_sock.c`) + camada de
compat (shim mbuf, glue de kernel, if/ip4/ip6/pcb/xmit/ifq, crypto, radix,
subr_tree) + `pfk_mod.c`/`pfk_hooks.c`. As flags de compilação definem
`make -C "$KERNELDIR" M=module` → `module/pf.ko` (um único módulo: a
interface BSD `ifq`/`pfq` mora dentro dele, não é um qdisc `tc` separado). O
script prepara sozinho `Module.symvers` / `modules_prepare`. Lista de objetos
(`module/Makefile`): engine portado (`pf.c`, `pf_ioctl.c`, `pf_norm.c`,
`pf_if.c`, `pf_lb.c`, `pf_ruleset.c`, `pf_table.c`, `pf_osfp.c`,
`pf_syncookies.c`, `divert_sock.c`, `pfk_pflog.c`, `hfsc.c`, `fq_codel.c`) +
camada de compat (shim mbuf, glue de kernel, if/ip4/ip6/pcb/xmit/ifq, crypto,
radix, subr_tree) + `pfk_mod.c`/`pfk_hooks.c`. As flags de compilação definem
`PFLINUX=1` e `_KERNEL=1`. Termina com `MODULE_BUILD_OK`.
`insmod module/pf.ko` cria `/dev/pf` (0600) e registra os hooks netfilter; o
@@ -87,7 +88,8 @@ caminho de dados faz).
## 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
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
`tools/tests/*.conf` (básico / traduções / filas+scrub / anchor / negativa
`broken.conf`). É userland puro; não precisa de `/dev/pf`.
- `pdivert` — o fonte do relay de divert fica em `tools/pdivert/`; o lado
kernel (pendente) está na [matriz de features](feature-matrix.md).
- `pdivert` — o relay de divert, compilado a partir de `tools/pdivert/`. Ele
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
([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
- As etapas 1–6 foram rodadas ao longo do desenvolvimento M0–M2 no host de
build (há artefatos na árvore: `out/`, `module/pf.ko`, `tools/out/pfctl`),
e os scripts acima foram verificados linha a linha para este documento.
- As etapas 1–6 foram rodadas repetidamente no host de build (há artefatos
na árvore: `out/`, `module/pf.ko`, `tools/out/{pfctl,pdivert,pflogd}`), e os
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**
(`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
+50 -20
View File
@@ -111,29 +111,31 @@ lendo `/dev/pflog`; sem ele a fila atinge o limite (256 registros / 1 MiB) e
descarta. Nenhum dos dois é mais stub, mas nenhum faz algo sem o consumidor
rodar (ver [pf-guide.md](pf-guide.md)).
### 9. Configurar filas esbarra numa asserção de lock no caminho de purge (aberto)
### 9. Filas: caminho verificado; a semântica de modelagem é do HFSC, não um bug
Carregar uma regra `queue` agora alcança o escalonador (`pfq_alloc` tem sucesso,
a disciplina hfsc é criada) e `pf_commit_queues()` é exercitada de ponta a ponta.
Dois bugs foram encontrados e corrigidos no caminho: o handle da classe raiz
colidia com qids ímpares do pf (`pfq_addqueue` devolvia EBUSY) e a disciplina
NULL não era verificada.
`15-queue.sh` prova que o caminho de filas funciona de ponta a ponta: a
configuração 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á mesmo
no caminho de dados.
O que ainda bloqueia as filas: o timer de purge do port pode rodar enquanto um
ioctl segura o lock de escrita, e `pf_purge_expired_states()` afirma o contrário:
O que o lab não mostra é um *teto de taxa*, e isso é semântica do HFSC, não
defeito do port: o `pf.conf` do OpenBSD 8.0 expõe apenas `bandwidth`, que mapeia
para a curva **linkshare** do HFSC - garantia quando classes competem, não limite
de um fluxo isolado. Não existe palavra-chave `minimum`/`maximum` nesta versão do
pfctl (verificado na sua tabela de palavras-chave), logo não há como definir
upperlimit pelo pf.conf. Um teste de contenção com duas classes saturadas seria o
próximo passo se você quiser asserir sobre share; no enlace virtual de centenas de
Mbit do lab, um único fluxo termina antes de acumular dívida de tempo virtual.
```
splassert: pf_purge_expired_states: expected 0, got 2
kernel BUG at compat/pfk_kern.c:51
```
O OpenBSD serializa isso com `splnet()`; o port substituiu a camada spl por
rw-semáforos e work queue, então a asserção passou a ser uma corrida real em vez
de formalidade. Até corrigi-la (liberar o lock em torno da chamada, ou dar ao
estado o seu próprio lock como o upstream faz com `pf_state_lock`), um host que
configure filas pode travar o módulo em `Unloading`. **Não habilite regras
`queue` em host real ainda** — os 14 gates do lab nunca configuram filas, e é
por isso que continuam verdes.
Bugs encontrados e corrigidos no caminho (detalhes em `docs/porting-notes.md`): a
convenção de retorno de `ifqop_enq`, `ifq_q` vs `ifq_softc` (deref de NULL que
matava a máquina no meio da transferência), o cookie de dequeue perdido, o
handle da classe raiz do hfsc colidindo com qids ímpares do pf, o
`DIOCGETQSTATS` falhando porque kernel e pfctl discordavam sobre
`struct hfsc_class_stats` (ambos agora usam o `net/hfsc.h` vendorizado), o
payload do pflog começando no cabeçalho de transporte, e o `splassert` do caminho
de purge que transformava uma corrida benigna em `BUG()`.
## Desvios da camada de compat (de `docs/porting-notes.md`, registro do M1)
@@ -161,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
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
ARP e outros EtherTypes não-IPv4/IPv6 nunca entram nos hooks netfilter de
-40
View File
@@ -1,40 +0,0 @@
# Demo (asciinema)
O lab pode ser gravado como um cast do [asciinema](https://asciinema.org).
Cada byte da gravação é a saída real das VMs do lab (Alpine sobre libvirt,
rodando o kernel compilado do fonte com o `pf.ko`); só o ritmo é roteirizado.
## Gravar
```sh
pip install asciinema # uma vez
bash scripts/vm-run-tests.sh # ou: scripts/vm-create.sh + deploy
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
```
A sessão percorre:
1. a topologia do lab e o `pf.ko` recém-carregado (prova no `dmesg`),
2. `pfctl -s info` pela ABI de ioctl do OpenBSD,
3. um `pf.conf` real (tabela, regras stateful, drop explícito) e `pfctl -sr`,
4. ping em trânsito + `pfctl -ss` (stateful),
5. `pfctl -t <tabela> -T show` (tabelas vivas),
6. um ping bloqueado (contadores com `pfctl -sr -vv`),
7. `divert-to` com o relay `pdivert` (pacote vai ao userspace e volta),
8. desligamento.
## Reproduzir
```sh
asciinema play out/demo-pf-linux.cast
```
## Publicar
```sh
asciinema upload out/demo-pf-linux.cast
```
`scripts/demo-record.py` é um gravador alternativo que produz o mesmo formato
asciicast v2 sem o binário asciinema (ele conduz o lab por ssh e escreve o
cast diretamente).
+55 -50
View File
@@ -1,78 +1,83 @@
# Matriz de features
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):
- **VERIFIED-in-VM** — passou de ponta a ponta na bancada libvirt (ver [vm-lab.md](vm-lab.md)).
- **M2-in-progress** — implementado; os gates VM 01–04 existem, mas a suíte **ainda não está toda verde**; sem alegação de VERIFIED até uma rodada limpa.
- **COMPILES-untested** — compila em `pf.ko`/`pfctl`; nenhum gate exercitou.
- **PENDING-Mx** — entra no milestone Mx; o comportamento observável hoje está declarado.
- **OUT-OF-SCOPE** — não-objetivo por projeto (spec §3).
- **VERIFIED-in-VM** — exercitado de ponta a ponta no lab libvirt (veja [vm-lab.md](vm-lab.md)); o gate que prova está nomeado.
- **VERIFIED-offline** — provado por um teste de fixture/ABI, não por tráfego (ex.: `make -C tools test`).
- **COMPILES-untested** — compilado dentro do `pf.ko`/`pfctl`; nenhum gate exercitou ainda.
- **NOT-ENABLED** — o código existe mas a feature está desligada neste build (ex.: `af-to`, que depende dos caminhos `INET6`).
- **OUT-OF-SCOPE** — fora de escopo por desenho (spec §3).
Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
`docs/porting-notes.md`, commits de milestone.
Atualizado em 2026-11-12; fonte da evidência por linha: `out/test-logs/`,
`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
| 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 |
| Carga de ruleset | `pfctl -f arquivo` | M2 | VERIFIED-in-VM (cargas bem-sucedidas em toda rodada capturada; o status geral da rodada é misto — ver [vm-lab.md](vm-lab.md) §6) |
| Checagem só de parse | `pfctl -n -f arquivo` | M1 | VERIFIED (suíte de fixtures offline, `make -C tools test`) |
| Carga de ruleset | `pfctl -f file` | M2 | VERIFIED-in-VM (todo gate carrega um ruleset real) |
| 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 |
| Listagem de regras | `pfctl -s rules [-v]` | M2 | COMPILES-untested |
| Listagem de estados | `pfctl -s states [-v]` | M2 | M2-in-progress — o gate 01 mostra anomalia de listagem (ping passa, nenhum estado icmp exibido) enquanto a máquina de estados está sendo corrigida |
| Tabelas (listagem/ops) | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) |
| Anchors | `pfctl -a anchor -f arquivo`, `-s Anchors` | M2 | M2-in-progress (gate 04) |
| Listagem de filas | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (o parse funciona; contadores ainda não conectados aos back ends reais) |
| Paridade de ABI ioctl | `make -C tools abi` (tabelas do abitest) | M1 | VERIFIED no lado do build (tabela userland); o diff do lado kernel é um passo aberto para o integrador (`tools/UPSTREAM.md`) |
| Listagem de regras | `pfctl -s rules [-v]`, `pfctl -sr` | M2 | VERIFIED-in-VM (gates 02–06) |
| Listagem de estados | `pfctl -s states [-v]`, `pfctl -ss` | M2 | VERIFIED-in-VM (gate `01-state.sh`) |
| 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 file`, `-s anchors` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
| Listagem de filas | `pfctl -s queue -v` | M5 | VERIFIED-in-VM (gate `15-queue.sh`: as duas classes com estatísticas) |
| 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
| 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) |
| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) |
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) |
| `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | M2-in-progress |
| `set state-policy`, `allow-opts` etc. | opções `set …` | M2 | COMPILES-untested |
| Syncookies (caminho de estouro de estado) | `set syncookies always` | M2 | COMPILES-untested |
| Tabelas pfr (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) |
| Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) |
| IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested (o engine é completo para v6 por construção; gates v6 smoke planejados) |
| Tráfego não-IPv4/IPv6 (ARP…) | — | M2 | PFLINUX por construção: hooks netfilter só de IPv4/IPv6 — ARP nunca entra no caminho do PF (sem gate ainda) |
| `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 | 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 | 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 | 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 |
| 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 | VERIFIED-in-VM (gate `03-tables.sh`) |
| 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, mas o caminho v6 não é exercitado por gate |
| 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)
| 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`. |
| `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). |
| `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. |
| 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. |
| 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) |
| `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 | **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 | **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. |
| 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ó. |
| 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
| 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. |
| `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). |
| Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue root q1 bandwidth 1Mbit; queue on egress all ...` | M5 | **Back ends compilados no `pf.ko`, anexação pendente** — `hfsc.c` e `fq_codel.c` do OpenBSD agora compilam e linkam (157 símbolos do escalonador; `hfsc_initialize`/`hfsc_pf_addqueue`/`fqcodel_pf_addqueue` alcançáveis pelo `pf_ioctl`), e a suíte inteira continua passando com eles dentro. O caminho de dados já pode entregar pacotes a um escalonador anexado (`pf_kifq_input()` + um `ifq_start()` real que drena e transmite), mas as filas **ainda não são seguras para habilitar**: o caminho de purge esbarra numa asserção de lock (ver caveats #9), então nenhuma banda é aplicada e não há gate para isso. |
| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **fallback priq até o M5**; `pfsrc/hfsc.c` staged (código real do OpenBSD 8.0) mas **fora da lista de objetos** do módulo |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado |
| Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) |
| pflog (`log`, `log all`, `block log`) + `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. |
| `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>` | 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 | **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 | **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 — 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. |
| 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`) + 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 |
|---|---|
| Failover `pfsync` / CARP | não-objetivo (spec §3) |
| IPsec / pfkey | não-objetivo |
| `ftp-proxy` e daemons de userland | não-objetivo |
| Instâncias PF por netns | port fiel = instância global única em `init_net` |
| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos do upstream antes do OpenBSD 8.0 — impossível portar da base |
| Labels de rota (`route-label`) | não-objetivo; a camada de ioctl rejeita regras com label |
| Failover `pfsync` / CARP | fora de escopo (spec §3) |
| IPsec / pfkey | fora de escopo |
| `ftp-proxy` e daemons de userland | fora de escopo |
| Instâncias PF por netns | port fiel = uma instância global no `init_net` |
| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos upstream antes do OpenBSD 8.0 — impossíveis de portar a partir da base |
| Routing labels (`route-label`) | fora de escopo; a camada de ioctl rejeita regras com label |
| `af-to` (tradução IPv4↔IPv6) | o código está sob `#ifdef INET6` e este build tem `INET6 0`; habilitá-lo é um milestone à parte, não uma feature faltando na base |
+25 -17
View File
@@ -21,7 +21,7 @@ bit a bit entre kernel e userland (codificação de ioctl em
| Caminho | Papel |
|---|---|
| `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/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` |
@@ -71,31 +71,39 @@ tamanho embutido na codificação `_IOR/_IOW` OpenBSD e despacha para o
## Filas, divert, pflog — comportamento observável hoje
- **Filas**: o `pfctl` faz o parse de `queue`/`queue on` (sintaxe 8.0); no
kernel a API ifq é implementada por `compat/pfk_ifq.c` com **semântica
priq apenas**; o qdisc classful `pfq` e os back ends HFSC/FQ-CoDel chegam
em **M5**. `ifq.c`/`hfsc.c`/`fq_codel.c` estão staged e prontos para
compilar, mas ainda **não** estão na lista de objetos do módulo.
- **Divert**: `divert-to`/tag `divert` produzem o veredito `PF_DIVERT` do PF;
`divert_packet()` (`pfsrc/divert_sock.c`) hoje **descarta o pacote com um
`pr_warn` rate-limited**, porque a família de sockets AF_DIVERT ainda não
foi instanciada. O relay userspace `pdivert` existe e espera
`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`.
- **pflog**: `pflog_packet()` retorna sucesso mas **não loga nada** (um
único `pr_warn "packet not logged"`); `if_pflog.c` está staged, back end
netlink em M4/M5.
- **Filas**: o `pfctl` faz o parse da sintaxe `queue`/`queue on` do OpenBSD 8.0
e o kernel a implementa na forma do BSD: `compat/pfk_ifq.c` fornece as
interfaces `ifq_ops`/`pfq_ops` e os back ends do OpenBSD (`pfsrc/hfsc.c`,
`pfsrc/fq_codel.c`) se plugam nelas; **priq** é a disciplina default. O
gate `15-queue.sh` prova a disciplina **hfsc** de ponta a ponta: a
configuração de filas carrega, a disciplina é anexada à interface, o
`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. Os handles de
classe e as structs de estatísticas vêm do próprio `net/hfsc.h` e
`net/fq_codel.h` do OpenBSD, vendorizados para que kernel e `pfctl` não
puedan divergir.
- **Divert**: `divert-to`/tag `divert` produzem o veredito `PF_DIVERT` do PF e
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)
| Milestone | Escopo | Status (2026-11-11) |
| Milestone | Escopo | Status |
|---|---|---|
| M0 | repo, scripts, build do kernel, VM liga com kernel próprio | concluído |
| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f`/`-s info` | concluído |
| M2 | núcleo 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 |
| 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 |
| 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 |
| 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 | **concluído** — docs EN+PT-BR, suíte 15/15 verde, tag `v1.0.0` |
## Próximas leituras
+24 -10
View File
@@ -75,22 +75,36 @@ Como a entrega é um módulo out-of-tree, "aplicar o conjunto" = passos
milestone acrescentar `patches/kernel/000N-*.patch` documentado (cada patch:
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á
vazio e os scripts `scripts/apply-patches.sh` / `scripts/unpatch.sh` e o
arquivo `patches/kernel/series` ainda não existem na árvore (entram junto
com o primeiro patch real de core — ver [patches.md](patches.md)). Pode
pular 2.3 com segurança.
- O passo 2.4 compila apenas o `pfctl` e o verificador de ABI (alvos de
`make -C tools`: `pfctl abitest abi test clean`). `pdivert` existe como
arquivo-fonte sem alvo no Makefile por enquanto; `pflogd` ainda não foi
portado.
- Depois do 2.5, o que dá para usar de verdade é o núcleo de filtragem M2
(pass/block/quick/keep state/tabelas/anchors); NAT/scrub/queues/divert/
pflog dependem dos próximos milestones — consulte a
[matriz de features](feature-matrix.md) e leia [pf-guide.md](pf-guide.md)
antes de habilitar PF num caminho que você precisa manter vivo.
- O passo 2.4 compila todas as ferramentas de userland: os alvos de
`make -C tools` são `pfctl`, `pdivert`, `pflogd` e o verificador de ABI
(`abitest`, `abi`, `test`, `clean`).
- Depois do 2.5, todo o conjunto de features é usável e coberto pela suíte do
lab — filtragem com estado, tabelas e anchors, NAT (`nat-to`/`rdr-to`/
`binat-to` e pools), scrub, roteamento forçado (`route-to`/`dup-to`/
`reply-to`), `divert`, `pflog` e as filas do OpenBSD 8.0 (HFSC). A
[matriz de features](feature-matrix.md) diz qual gate prova o quê; leia
[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
+15 -10
View File
@@ -3,11 +3,12 @@
English: [docs/en/pf-guide.md](../en/pf-guide.md).
Este é o guia do "o que dá para rodar hoje". Ele segue a
[matriz de features](feature-matrix.md): o escopo do **M2 (núcleo de
filtragem: pass/block/quick/keep state/tabelas/anchors + habilitar/
desabilitar)** tem evidência em VM; tudo além disso é COMPILES-untested ou
PENDING. Instalação/build ficam em [install.md](install.md) e
[build.md](build.md).
[matriz de features](feature-matrix.md): o núcleo de filtragem
(pass/block/quick/keep state/tabelas/anchors), a família de NAT, o scrub, o
roteamento forçado, os pools, o `divert`, o `pflog` e as filas do OpenBSD
8.0 têm todos **evidência em VM** — os 15 gates do lab estão verdes. O que
*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
@@ -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`
> 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
> serial exatamente por isso). Num host remoto, libere a interface de
> 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
```pf
# /etc/pf.conf — apenas escopo M2. Os nomes de interface aqui são os da
# bancada (mgmt/lan/wan); troque pelos seus (eth0, enp1s0, …).
# /etc/pf.conf. Os nomes de interface aqui são os da bancada
# (mgmt/lan/wan); troque pelos seus (eth0, enp1s0, …).
# 1) mantenha você mesmo alcançável (faça isso primeiro, sempre)
pass in quick on mgmt
@@ -59,14 +60,18 @@ tudo isso offline — mas o *comportamento no kernel* da maioria das opções
`set` ainda não foi verificado em gate; trate como "carrega e reza" até a
matriz mudar.
## O que evitar até os milestones chegarem
**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 |
|---|---|---|
| `nat-to`, `rdr-to` | — | **Verificado em lab** (gates `05-nat`, `06-rdr`; este último baixa 2 MB por um redirect e compara byte a byte). `nat-to (egress)` expande o *grupo* de interfaces do OpenBSD; num host multi-NIC prefira a forma explícita `nat-to (<iface>)`. O `binat-to` é expandido pelo pfctl num par `nat-to … static-port` + `rdr-to` (gate `07-binat`). |
| `scrub (min-ttl, max-mss, df, random-id)`, `set reassemble` | — | **Verificado em lab** (gate `09-scrub.sh`): `min-ttl` e `max-mss` reescrevem de fato pacotes em trânsito. || `divert-to`, tag `divert` | — | **Verificado em lab** (gate `11-divert`): o pacote é entregue a um socket `AF_DIVERT` (`PF_DIVERT=32`) atendido pelo `tools/pdivert`; sem listener é descartado, exatamente como o `divs_noport` do BSD. |
| `route-to`, `dup-to`, `reply-to` | — | **Verificado em lab** (gates `08-routeto.sh`, `13-replyto.sh`): o gateway forçado é usado em vez do egress normal, `dup-to` copia mantendo o fluxo, e `reply-to` empurra as respostas de um fluxo para o enlace do gateway. `fail-to` não é palavra-chave no OpenBSD 8.0 (removido upstream). |
| `queue` / `queue on` / `pfctl -s queue` | M5 | Faz parse e carrega. A fila cai no fallback priq da camada `ifq` de compat; **nenhuma limitação de banda acontece até o qdisc `pfq` chegar**. |
| `queue` / `queue on` / `pfctl -s queue` | — | **Verificado em lab** (gate `15-queue.sh`): 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` é *garantia sob contenção*, não teto - o pf.conf 8.0 não tem palavra-chave de upperlimit. |
| `log`, `log all`, `block log`, `pflogd` | — | **Verificado em lab** (gate `12-pflog`): rode `pflogd /tmp/x.pcap` (lê `/dev/pflog`); o arquivo é um pcap `DLT_PFLOG` (117) legível por tcpdump/wireshark. |
| `route-label …` | nunca (não-objetivo) | Rejeitado na camada de ioctl. |
| política por container (netns) | fora de escopo | O PF é uma instância global em `init_net`; network namespaces **não** enxergam o PF. |
+123
View File
@@ -0,0 +1,123 @@
# Screencasts / Vídeos da demonstração
> PT-BR: esta página · EN: [docs/en/screencast.md](../en/screencast.md)
Sete vídeos por idioma, com **narração de voz feminina** (MiniMax), **legendas
queimadas**, **diagrama de rede do lab** e um **filme completo** com transições
entre as fases. 1920×1080 @ 30fps, H.264/MP4 + AAC — prontos para subir no
YouTube.
Cada vídeo é a gravação de uma **sessão shell real**: uma conexão SSH de
verdade ao firewall Alpine, com os comandos digitados tecla a tecla e a saída
do kernel chegando em tempo real. Nada é simulado.
## As fases
| # | 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 |
| 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` |
| 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 |
| 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 transferência de 2 MB pela classe |
Mais `pf-on-linux-full.mp4`: o filme completo, com as sete fases emendadas por
transições (crossfade sobre um cartão de capítulo).
## Vozes MiniMax
| Idioma | Voz | Id |
|--------|-----|-----|
| Inglês (en_US) | Confident Woman | `English_ConfidentWoman` |
| Português (pt_BR) | Confident Woman | `Portuguese_ConfidentWoman` |
O áudio é gerado pelo MiniMax TTS, **um arquivo por segmento** de fala. Como
cada segmento corresponde a uma batida da sessão, a voz começa exatamente
quando a ação correspondente aparece na tela.
## Como regravar
```sh
# tudo: áudio -> sessões -> vídeos -> filme completo
./scripts/screencast/build-all.sh
# uma fase só
./scripts/screencast/build-all.sh 04-roteamento
```
Etapas individuais:
```sh
python3 scripts/screencast/make_audio.py --lang pt # áudio MiniMax
python3 scripts/screencast/record.py --plan scripts/screencast/plans/04-roteamento.json \\
--out out/screencast/caps/04-roteamento.cast --pace --ensure-pf
python3 scripts/screencast/assemble.py --phase 04-roteamento --lang pt
python3 scripts/screencast/join.py --lang pt
```
### Como o pipeline funciona
```
planos/*.json --record.py--> caps/*.cast --render_video.py--> vídeo mudo
(comandos) (pty real) (asciicast) (pyte + PIL) |
|
narracao/*.json --make_audio.py--> vo/<fase>.<lang>/*.mp3 ----------+
(MiniMax TTS) |
assemble.py: mix + legendas
|
join.py: transições
```
Pontos que importam:
- **Sessão real.** `record.py` abre um pty, faz SSH na VM e digita os comandos
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.
- **Pacing.** A gravação segura a tela enquanto a voz fala: cada batida dura no
máximo o trecho de áudio que a acompanha (o maior entre EN e PT), para uma
única captura servir as duas versões.
- **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 é desenhado sobre o terminal,
e o caminho em discussão fica destacado (eth1/eth2/eth3).
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu, a chave
`MINIMAX_API_KEY` no ambiente, e o lab de VMs no ar.
## Onde ficam os arquivos
| Caminho | Conteúdo |
|---------|----------|
| `out/screencast/en/`, `out/screencast/pt/` | os vídeos finais |
| `out/screencast/caps/` | as capturas asciicast das sessões |
| `out/screencast/vo/` | o áudio gerado pelo MiniMax |
| `scripts/screencast/plans/` | o roteiro de comandos de cada fase |
| `scripts/screencast/narration/` | os textos da narração (EN e PT) |
## Texto de descrição (YouTube)
```
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,
empacotado como módulo (pf.ko) e controlado pelo pfctl original do OpenBSD
sobre a ABI de ioctls do /dev/pf.
O lab é real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
módulo, tráfego real entre cliente, firewall e servidor.
Fases:
0. O ambiente de testes - as 4 VMs e o diagrama de rede
1. Implementação no kernel - das fontes OpenBSD ao pf.ko
2. Ativação - insmod, /dev/pf, pfctl -e, pfctl -s info
3. Regras básicas - tabelas, anchors, keep state, block drop
4. Roteamento - route-to e reply-to, com o caminho real no diagrama
5. Balanceamento - pools round-robin, source-hash e dup-to
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
Suíte automatizada: 15 gates, todos verdes.
Código e documentação: https://git.labexposed.com/lgcosta/pf-linux-port
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
```
+62 -22
View File
@@ -65,6 +65,21 @@ O `vm-create.sh` termina validando `ssh pf0 'ip -br a'` e deixa todas as VMs
ligadas. Os aliases de ssh do laptop (`pf0|cli|srv|gw2`, ProxyJump via deb)
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
```sh
@@ -82,30 +97,55 @@ e sai com código diferente de zero se algo falhou.
## 5. O que cada gate prova hoje
| Gate | Cenário (verificado com tráfego real, não com código de saída) |
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` |
| `02-block.sh` | `block drop in quick proto icmp` → o ping do cli falha; `block return-rst in quick proto tcp … port 8080` → o `nc` do cli para srv:8080 é recusado (RST), checado pelo código de saída do nc |
| `03-tables.sh` | `table <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 |
| `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 |
| `01-state.sh` | `keep state` em um ping de trânsito; o estado aparece no `pfctl -ss` |
| `02-block.sh` | `block drop` mata o ping; `block return-rst` devolve RST ao cliente |
| `03-tables.sh` | uma tabela `pfr` bloqueia um host; um `-T delete` ao vivo libera o mesmo fluxo |
| `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,
08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (tolerância
de ±15% no rate do iperf), 13-pflog, 14-soak (reload sob tráfego + ciclo de
rmmod + kmemleak). IPv6 é smoke test para state/block/nat66/scrub. O
`bench-basic.sh` (planejado, ainda não está na árvore) é informativo; não
existe gate numérico de performance.
## 6. Status atual da suíte (2026-11-11, com honestidade)
Os gates 01–04 do M2 existem e rodam, mas **ainda não estão todos verdes** —
o caminho de dados (modelo netisr/workqueue) e a visibilidade de estados
estão sendo corrigidos no trabalho do gate M2. A última rodada capturada
(`out/test-logs/`) mostra o estado misto: ping de trânsito OK / listagem de
estado falhando no 01, icmp bloqueado OK no 02, resultados parciais no 03, 04
abortado. Trate "VERIFIED-in-VM" na
[matriz de features](feature-matrix.md) como reservado a gates que passam
numa rodada completa e limpa; nada além do M1 é declarado VERIFIED até lá.
Os screencasts ([screencast.md](screencast.md)) mostram boa parte desses
gates sendo executados, com o diagrama de rede do lab desenhado sobre o
terminal.
## 7. Inspeção manual
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/md5.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/md5.o: $(deps_compat/md5.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_if.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_if.o: $(deps_compat/pfk_if.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ifq.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ifq.o: $(deps_compat/pfk_ifq.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip4.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ip4.o: $(deps_compat/pfk_ip4.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip6.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ip6.o: $(deps_compat/pfk_ip6.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_kern.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_kern.o: $(deps_compat/pfk_kern.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_mbuf.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_mbuf.o: $(deps_compat/pfk_mbuf.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_pcb.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/net/inet6_hashtables.h \
compat/pfk_pcb.o: $(deps_compat/pfk_pcb.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_rtable.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_rtable.o: $(deps_compat/pfk_rtable.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_xmit.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
compat/pfk_xmit.o: $(deps_compat/pfk_xmit.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/radix.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/radix.o: $(deps_compat/radix.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/sha2.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/sha2.o: $(deps_compat/sha2.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/siphash.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/siphash.o: $(deps_compat/siphash.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/subr_tree.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/subr_tree.o: $(deps_compat/subr_tree.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/toeplitz.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/crypto/hash.h \
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/linux/crypto.h \
+37 -6
View File
@@ -217,10 +217,14 @@ pfk_pfq_enqueue(void *arg, struct mbuf *m)
struct pfk_pfq *pfq = arg;
if (!pfq)
return NULL;
if (ifq_enqueue(&pfq->ifq, m) != 0)
return NULL;
return m;
return m; /* caller keeps ownership */
/*
* ifq_enqueue takes ownership of m (the scheduler may drop it); we then
* drain immediately, so nothing is returned for the caller to send.
*/
ifq_enqueue(&pfq->ifq, m);
ifq_start(&pfq->ifq);
return NULL;
}
static struct mbuf *
@@ -303,7 +307,13 @@ ifq_attach(struct ifqueue *ifq, const struct ifq_ops *ops, void *opsarg)
if (!opsarg && ops->ifqop_alloc)
opsarg = ops->ifqop_alloc(0, NULL);
ifq->ifq_ops = ops;
/*
* The port's own helpers use ifq_softc; the staged OpenBSD schedulers
* (hfsc/fq_codel) read ifq->ifq_q. Keep both in sync or the back ends
* dereference a NULL scheduler state.
*/
ifq->ifq_softc = opsarg;
ifq->ifq_q = opsarg;
}
/*
@@ -336,19 +346,35 @@ ifq_destroy(struct ifqueue *ifq)
ifq->ifq_ops->ifqop_free(0, ifq->ifq_softc);
ifq->ifq_ops = NULL;
ifq->ifq_softc = NULL;
ifq->ifq_q = NULL;
}
/*
* ifqop_enq returns the packet the scheduler decided to DROP (possibly the one
* just offered), and NULL when the packet was accepted - it is NOT an error
* code. Freeing the returned mbuf is this layer's job.
*/
int
ifq_enqueue(struct ifqueue *ifq, struct mbuf *m)
{
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_enq)
struct mbuf *dm;
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_enq) {
m_freem(m);
return EINVAL;
}
if (ifq->ifq_maxlen && ifq->ifq_len >= ifq->ifq_maxlen) {
ifq->ifq_errors++;
ifq->ifq_qdrops++;
m_freem(m);
return ENOBUFS;
}
return ifq->ifq_ops->ifqop_enq(ifq, m) ? 0 : ENOMEM;
dm = ifq->ifq_ops->ifqop_enq(ifq, m);
if (dm) {
ifq->ifq_qdrops++;
m_freem(dm);
}
return 0;
}
struct mbuf *
@@ -402,9 +428,12 @@ void
ifq_start(struct ifqueue *ifq)
{
struct mbuf *m;
int n;
n = 0;
ifq->ifq_oactive = 1;
while ((m = ifq_dequeue(ifq)) != NULL) {
n++;
if (pfk_ifq_transmit(ifq, m))
break; /* device busy: packet kept */
}
@@ -423,6 +452,8 @@ pf_kifq_input(struct pfk_ifnet *ifp, struct mbuf *m)
if (ifp == NULL || m == NULL)
return 0;
if (ifp == NULL)
return 0;
ifq = &ifp->if_snd;
if (ifq->ifq_ops == NULL || ifq->ifq_ops == ifq_priq_ops)
return 0; /* no scheduler: fast path */
+15 -2
View File
@@ -47,8 +47,21 @@ pfk_printk_compat(const char *fmt, ...)
void
splassert_fail(int expected, int actual, const char *func)
{
pr_err("splassert: %s: expected %d, got %d\n", func, expected, actual);
BUG();
/*
* PFLINUX: OpenBSD's splassert() checks a per-CPU spl level, so
* "assert unlocked" really means "this context did not take the lock".
* The port replaced spl with rw-semas plus work queues, where
* rw_status() is a global counter: an assert-unlocked in a work-queue
* task fires whenever *another* CPU happens to hold the lock, which is
* not a bug (the task never acquires pf_lock itself) and was turning a
* harmless race into a kernel BUG() that wedged the module in
* "Unloading". Report it once instead of killing the host; the
* structural invariant the BSD assert protects - purge never runs while
* holding pf_lock - is upheld by the call sites (pf_purge_* are only
* reached from systqmp tasks and timer work).
*/
pr_warn_ratelimited("pfk: splassert: %s: expected %d, got %d\n",
func, expected, actual);
}
/* ---------------- addlog (OpenBSD kern.log) ---------------- */
+51
View File
@@ -0,0 +1,51 @@
/* $OpenBSD: fq_codel.h,v 1.4 2020/06/18 23:29:59 dlg Exp $ */
/*
* Copyright (c) 2017 Mike Belopuhov
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef _NET_FQ_CODEL_H_
#define _NET_FQ_CODEL_H_
/* compatible with hfsc_pktcntr */
struct fqcodel_pktcntr {
uint64_t packets;
uint64_t bytes;
};
struct fqcodel_stats {
struct fqcodel_pktcntr xmit_cnt;
struct fqcodel_pktcntr drop_cnt;
uint32_t qlength;
uint32_t qlimit;
uint32_t flows;
uint32_t _unused; /* padding */
uint32_t target;
uint32_t interval;
/* the values below are used to calculate standard deviation */
uint64_t delaysum; /* sum of delays, us */
uint64_t delaysumsq;/* sum of squared delays, us */
};
#ifdef _KERNEL
extern const struct ifq_ops * const ifq_fqcodel_ops;
extern const struct pfq_ops * const pfq_fqcodel_ops;
#endif /* _KERNEL */
#endif /* _NET_FQ_CODEL_H_ */
+122
View File
@@ -0,0 +1,122 @@
/* $OpenBSD: hfsc.h,v 1.15 2026/03/19 14:59:05 sthen Exp $ */
/*
* Copyright (c) 2012-2013 Henning Brauer <henning@openbsd.org>
* Copyright (c) 1997-1999 Carnegie Mellon University. All Rights Reserved.
*
* Permission to use, copy, modify, and distribute this software and
* its documentation is hereby granted (including for commercial or
* for-profit use), provided that both the copyright notice and this
* permission notice appear in all copies of the software, derivative
* works, or modified versions, and any portions thereof.
*
* THIS SOFTWARE IS EXPERIMENTAL AND IS KNOWN TO HAVE BUGS, SOME OF
* WHICH MAY HAVE SERIOUS CONSEQUENCES. CARNEGIE MELLON PROVIDES THIS
* SOFTWARE IN ITS ``AS IS'' CONDITION, AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* Carnegie Mellon encourages (but does not require) users of this
* software to return any improvements or extensions that they make,
* and to grant Carnegie Mellon the rights to redistribute these
* changes without encumbrance.
*/
#ifndef _HFSC_H_
#define _HFSC_H_
/* hfsc class flags */
#define HFSC_DEFAULTCLASS 0x1000 /* default class */
struct hfsc_pktcntr {
u_int64_t packets;
u_int64_t bytes;
};
#define PKTCNTR_INC(cntr, len) \
do { (cntr)->packets++; (cntr)->bytes += len; } while (0)
struct hfsc_sc {
u_int64_t m1; /* slope of the first segment in bits/sec */
u_int64_t m2; /* slope of the second segment in bits/sec */
u_int d; /* the x-projection of the first segment in msec */
};
/* special class handles */
#define HFSC_ROOT_CLASS 0x10000
#define HFSC_DEFAULT_CLASSES 64
#define HFSC_MAX_CLASSES 65535
/* service curve types */
#define HFSC_REALTIMESC 1
#define HFSC_LINKSHARINGSC 2
#define HFSC_UPPERLIMITSC 4
#define HFSC_DEFAULTSC (HFSC_REALTIMESC|HFSC_LINKSHARINGSC)
struct hfsc_class_stats {
struct hfsc_pktcntr xmit_cnt;
struct hfsc_pktcntr drop_cnt;
u_int qlength;
u_int qlimit;
u_int period;
u_int class_id;
u_int32_t class_handle;
struct hfsc_sc rsc;
struct hfsc_sc fsc;
struct hfsc_sc usc; /* upper limit service curve */
u_int64_t total; /* total work in bytes */
u_int64_t cumul; /* cumulative work in bytes
done by real-time criteria */
u_int64_t d; /* deadline */
u_int64_t e; /* eligible time */
u_int64_t vt; /* virtual time */
u_int64_t f; /* fit time for upper-limit */
/* info helpful for debugging */
u_int64_t initvt; /* init virtual time */
u_int64_t vtoff; /* cl_vt_ipoff */
u_int64_t cvtmax; /* cl_maxvt */
u_int64_t myf; /* cl_myf */
u_int64_t cfmin; /* cl_mincf */
u_int64_t cvtmin; /* cl_mincvt */
u_int64_t myfadj; /* cl_myfadj */
u_int64_t vtadj; /* cl_vtadj */
u_int64_t cur_time;
u_int32_t machclk_freq;
u_int vtperiod; /* vt period sequence no */
u_int parentperiod; /* parent's vt period seqno */
int nactive; /* number of active children */
/* red and rio related info */
int qtype;
/* struct redstats red[3]; */
};
#ifdef _KERNEL
struct ifnet;
struct ifqueue;
struct pf_queuespec;
struct hfsc_if;
extern const struct ifq_ops * const ifq_hfsc_ops;
extern const struct pfq_ops * const pfq_hfsc_ops;
#define HFSC_ENABLED(ifq) ((ifq)->ifq_ops == ifq_hfsc_ops)
#define HFSC_DEFAULT_QLIMIT 50
void hfsc_initialize(void);
#endif /* _KERNEL */
#endif /* _HFSC_H_ */
+17 -69
View File
@@ -1214,62 +1214,18 @@ struct pfk_pflog_stat {
/*
* Pieces the staged scheduler back ends (fq_codel(4)/hfsc(4) ports) need from
* OpenBSD <net/if_var.h>, <net/if_fq_codel.h> and <net/if_hfsc.h>.
* Scheduler back-end ABI: the very headers OpenBSD ships (net/hfsc.h,
* net/fq_codel.h), vendored into the port so that pfctl and pf.ko agree on the
* stats layouts and on the class-handle/service-curve constants. (Hand-rolled
* approximations of these structs made DIOCGETQSTATS fail with EINVAL, because
* pfctl's sizeof(struct hfsc_class_stats) did not match the kernel's.)
*/
#ifndef nsecuptime
#define nsecuptime() getnsecuptime()
#endif
struct fqcodel_pktcntr { uint64_t packets; uint64_t bytes; };
struct hfsc_pktcntr { uint64_t packets; uint64_t bytes; };
struct fqcodel_stats {
struct fqcodel_pktcntr xmit_cnt;
struct fqcodel_pktcntr drop_cnt;
uint32_t target, interval, tc_interval_drop;
uint32_t rcvtime, mtu, qlimit, qlength;
uint64_t delaysum, delaysumsq;
uint8_t ecn, limit, flows, lscroll, flags;
};
/* ifq scheduler hand-off (see pfsrc/ifq.c): enter returns the scheduler's
* queue when it owns the interface queue, NULL otherwise. */
void *ifq_q_enter(struct ifqueue *, const struct ifq_ops *);
void ifq_q_leave(struct ifqueue *, void *);
int pfk_ifq_transmit(struct ifqueue *, struct mbuf *);
int pf_kifq_input(struct pfk_ifnet *, struct mbuf *);
/*
* OpenBSD <net/if_hfsc.h> pieces the staged hfsc(4) back end needs.
* PFLINUX: the port keeps the BSD scheduler sources verbatim, so the
* user-visible service curve, the class-stats record and the class-table
* limits live here instead of in a separate kernel header.
*/
struct hfsc_sc { /* user-supplied service curve */
u_int64_t m1; /* slope1 (bytes/sec) */
u_int64_t d; /* ordinate of the knee (us) */
u_int64_t m2; /* slope2 (bytes/sec) */
};
struct hfsc_class_stats {
u_int class_id;
u_int32_t class_handle;
struct hfsc_sc rsc, fsc, usc;
u_int64_t total, cumul;
u_int64_t d, e, vt, f;
u_int64_t initvt, vtperiod, parentperiod;
int nactive;
u_int64_t vtoff, cvtmax, myf, cfmin, cvtmin, myfadj, vtadj;
u_int period;
struct hfsc_pktcntr xmit_cnt, drop_cnt;
u_int64_t cur_time;
u_int64_t machclk_freq;
int qlength;
int qlimit;
int qtype;
};
#include <net/hfsc.h>
#include <net/fq_codel.h>
/* OpenBSD's global nanoseconds-per-clocktick (HFSC_CLK_PER_TICK). The port
* has no clock_rate machinery, so derive it from the kernel's HZ. */
@@ -1277,18 +1233,13 @@ struct hfsc_class_stats {
#define tick_nsec (1000000000LL / HZ)
#endif
/*
* Class-handle namespace. pf queue ids are u_int16_t, so the root class gets
* a high bit that can never collide with a queue handle (a collision made
* hfsc_pf_addqueue() see the root class as a duplicate and refuse the queue
* with EBUSY). HFSC_DEFAULTCLASS must equal PFQS_DEFAULT because hfsc.c
* receives the pf queue flags verbatim.
*/
#define HFSC_ROOT_CLASS 0x80000000u
#define HFSC_DEFAULTCLASS 0x1000
#define HFSC_MAX_CLASSES 64
#define HFSC_DEFAULT_CLASSES 8
#define HFSC_DEFAULT_QLIMIT 1000
/* ifq scheduler hand-off (see pfsrc/ifq.c): enter returns the scheduler's
* queue state when it owns the interface queue, NULL otherwise. */
void *ifq_q_enter(struct ifqueue *, const struct ifq_ops *);
void ifq_q_leave(struct ifqueue *, void *);
int pfk_ifq_transmit(struct ifqueue *, struct mbuf *);
int pf_kifq_input(struct pfk_ifnet *, struct mbuf *);
/*
* PFLINUX notes on the scheduler back ends:
@@ -1299,16 +1250,13 @@ struct hfsc_class_stats {
* are kept as markers of the critical regions.
* - KERNEL_ASSERT_LOCKED(): the schedulers are only reached from ifq/pf paths
* that already hold the queue mutex; asserted structurally, not by spl.
* - HFSC_ENABLED(): the scheduler owns the interface queue once it attached
* its private state through ifq_q_enter().
* - HFSC_ENABLED(), PKTCNTR_INC(), the class-handle constants and the stats
* layouts all come from the vendored net/hfsc.h / net/fq_codel.h above, so
* kernel and pfctl cannot drift apart.
*/
#define splnet() 0
#define splx(s) ((void)(s))
#define KERNEL_ASSERT_LOCKED() do { } while (0)
#define HFSC_ENABLED(_ifq) ((_ifq)->ifq_q != NULL)
#define PKTCNTR_INC(_c, _len) do { (_c)->packets++; (_c)->bytes += (_len); } \
while (0)
#endif /* _PFK_H */
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/divert_sock.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/divert_sock.o: $(deps_pfsrc/divert_sock.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/fq_codel.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/fq_codel.o: $(deps_pfsrc/fq_codel.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/hfsc.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/hfsc.o: $(deps_pfsrc/hfsc.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf.o: $(deps_pfsrc/pf.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_if.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_if.o: $(deps_pfsrc/pf_if.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ioctl.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_ioctl.o: $(deps_pfsrc/pf_ioctl.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_lb.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_lb.o: $(deps_pfsrc/pf_lb.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_norm.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_norm.o: $(deps_pfsrc/pf_norm.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_osfp.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_osfp.o: $(deps_pfsrc/pf_osfp.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ruleset.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_ruleset.o: $(deps_pfsrc/pf_ruleset.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_syncookies.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_syncookies.o: $(deps_pfsrc/pf_syncookies.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_table.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_table.o: $(deps_pfsrc/pf_table.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_hooks.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_hooks.o: $(deps_pfsrc/pfk_hooks.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_mod.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_mod.o: $(deps_pfsrc/pfk_mod.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_pflog.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_pflog.o: $(deps_pfsrc/pfk_pflog.o)
+10 -1
View File
@@ -481,7 +481,16 @@ hfsc_pf_qstats(struct pf_queuespec *q, void *ubuf, int *nbytes)
if (hif == NULL)
return (EBADF);
if ((cl = hfsc_clh2cph(hif, q->qid)) == NULL) {
/*
* A root queue's class is created with handle HFSC_ROOT_CLASS | qid
* (see hfsc_pf_addqueue), so looking the queue up by its plain pf qid
* only finds non-root classes: fall back to the root handle, otherwise
* "pfctl -s queue" fails with EINVAL on the first (root) queue.
*/
if ((cl = hfsc_clh2cph(hif, q->qid)) == NULL &&
(q->flags & PFQS_ROOTCLASS))
cl = hfsc_clh2cph(hif, HFSC_ROOT_CLASS | q->qid);
if (cl == NULL) {
ifq_q_leave(&ifp->if_snd, hif);
return (EINVAL);
}
+13 -2
View File
@@ -115,11 +115,22 @@ pfk_pflog_pull(struct mbuf *m, char *buf, size_t want)
{
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
size_t n;
int off;
if (skb == NULL)
return 0;
n = min_t(size_t, want, skb->len);
if (skb_copy_bits(skb, 0, buf, n))
/*
* BSD logs from the network header; pf may already have advanced the
* mbuf/skb data pointer to the transport header while parsing, so
* anchor explicitly at the IP header instead of at skb->data.
*/
off = skb_network_offset(skb);
if (off < 0)
off = 0;
if (off > skb->len)
return 0;
n = min_t(size_t, want, skb->len - off);
if (skb_copy_bits(skb, off, buf, n))
return 0;
return n;
}
-84
View File
@@ -1,84 +0,0 @@
{"version": 2, "width": 80, "height": 24, "timestamp": 1794449787, "env": {"SHELL": null, "TERM": "dumb"}}
[1.414508, "o", "\u001b[1m===============================================================\u001b[0m\r\n\u001b[1m PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel\u001b[0m\r\n\u001b[1m===============================================================\u001b[0m\r\n\r\nLab: 3 Alpine VMs under libvirt.\r\n pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)\r\n pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20\r\n\r\n\u001b[1m--- 1. our kernel, the OpenBSD engine as a module ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# uname -r\u001b[0m\r\n"]
[1.655866, "o", "7.3.4\r\n"]
[2.058621, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf ' || echo 'pf: not loaded yet'\u001b[0m\r\n"]
[2.276696, "o", "pf: not loaded yet\r\n"]
[2.680328, "o", "\u001b[1;36mpftest-pf0# insmod /opt/pf/pf.ko && dmesg | tail -3\u001b[0m\r\n"]
[2.917691, "o", "[ 146.252272] NET: Registered PF_IUCV protocol family\r\n[ 146.252362] pfk: netfilter hooks registered (10, netisr wq data path)\r\n[ 146.252423] pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)\r\n"]
[3.320802, "o", "\r\n\u001b[1m--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s info | head -4\u001b[0m\r\n"]
[3.598837, "o", "Status: Disabled for 0 days 00:02:26 Debug: err\r\n\r\nState Table Total Rate\r\n current entries 0 \r\n"]
[4.00249, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1\u001b[0m\r\n"]
[4.274878, "o", "Status: Enabled Debug: err\r\npf enabled\r\n"]
[4.678074, "o", "\r\n\u001b[1m--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---\u001b[0m\r\n"]
[4.678128, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-pf.conf\u001b[0m\r\n"]
[4.954849, "o", "# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)\r\nset block-policy return\r\ntable <trusted> { 10.0.1.0/24 }\r\n\r\n# transit traffic: stateful, table-driven\r\npass in quick on ingress proto icmp from <trusted> keep state\r\npass out quick on egress proto icmp keep state\r\n\r\n# deny a specific destination\r\nblock drop in quick proto icmp to 10.0.9.99\r\n\r\n# everything else\r\npass in quick\r\npass out quick\r\n"]
[5.358199, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'\u001b[0m\r\n"]
[5.630704, "o", "rules loaded\r\n"]
[6.034552, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr\u001b[0m\r\n"]
[6.322794, "o", "pass in quick on ingress proto icmp from <trusted> to any\r\npass out quick on egress proto icmp all\r\nblock drop in quick inet proto icmp from any to 10.0.9.99\r\npass in quick all flags S/SA\r\npass out quick all flags S/SA\r\n"]
[6.725786, "o", "\r\n"]
[6.72584, "o", "\u001b[1m--- 4. transit traffic is allowed and stateful ---\u001b[0m\r\n\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[7.977099, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.339/0.351/0.363 ms\r\n"]
[8.379918, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[8.634572, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5449 0:0\r\nall icmp 10.0.1.10:5449 -> 10.0.2.20:8 0:0\r\n"]
[9.036709, "o", "\r\n\u001b[1m--- 5. tables are live objects ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -t trusted -T show\u001b[0m\r\n"]
[9.298627, "o", " 10.0.1.0/24\r\n"]
[9.701487, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s Tables\u001b[0m\r\n"]
[9.974937, "o", "trusted\r\n"]
[10.378208, "o", "\r\n\u001b[1m--- 6. the drop rule actually drops ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat > /tmp/demo-block.conf <<EOF\r\nblock drop in quick proto icmp to 10.0.2.20\r\npass in quick\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[11.033956, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'\u001b[0m\r\n"]
[11.310724, "o", "block rules loaded\r\n"]
[11.714107, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[14.969152, "o", "\r\n--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 0 packets received, 100% packet loss\r\n"]
[15.372543, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr -vv | head -6\u001b[0m\r\n"]
[15.66699, "o", "@0 block drop in quick inet proto icmp from any to 10.0.2.20\r\n [ Evaluations: 6 Packets: 2 Bytes: 168 States: 0 ]\r\n [ Inserted: uid 0 pid 1687 State Creations: 0 ]\r\n@1 pass in quick all flags S/SA\r\n [ Evaluations: 1 Packets: 28 Bytes: 9296 States: 1 ]\r\n [ Inserted: uid 0 pid 1687 State Creations: 1 ]\r\n"]
[16.07016, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[16.338781, "o", "back to the stateful ruleset\r\n"]
[16.742479, "o", "\r\n\u001b[1m--- 7. NAT: the source is translated on the way out, restored on the way back ---\u001b[0m\r\n"]
[16.742537, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-nat.conf <<EOF\r\npass out quick on egress from 10.0.1.0/24 to any nat-to (eth2)\r\npass in quick on ingress proto icmp keep state\r\npass out quick on egress proto icmp keep state\r\nEOF\u001b[0m\r\n"]
[17.422636, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'\u001b[0m\r\n"]
[17.686794, "o", "nat rules loaded\r\n"]
[18.090339, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[19.341141, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.276/0.317/0.359 ms\r\n"]
[19.744749, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[20.010971, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5461 0:0\r\nall icmp 10.0.2.1:43040 (10.0.1.10:5461) -> 10.0.2.20:8 0:0\r\n"]
[20.414328, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[20.690817, "o", "back to the stateful ruleset\r\n"]
[21.094035, "o", "\r\n\u001b[1m--- 8. pflog: logged packets reach userspace (/dev/pflog) ---\u001b[0m\r\n"]
[21.094088, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-log.conf <<EOF\r\npass in quick log proto icmp from 10.0.1.10 keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[21.762242, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'\u001b[0m\r\n"]
[22.034943, "o", "log rule loaded\r\n"]
[22.438523, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'\u001b[0m\r\n"]
[23.681454, "o", "pflogd collecting 2 records\r\n"]
[24.084864, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
[25.333003, "o", "2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.352/0.364/0.376 ms\r\n"]
[27.737616, "o", "\u001b[1;36mpftest-pf0# cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap\u001b[0m\r\n"]
[28.026778, "o", "pflogd: 2 records\r\n-rw------- 1 root root 324 Nov 12 02:16 /tmp/pflog.pcap\r\npcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):\r\n d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00\r\n ff ff 00 00 75 00 00 00\r\n"]
[28.430241, "o", "\u001b[1;36mpftest-pf0# pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
[29.114632, "o", "\r\n\u001b[1m--- 9. route-to: force the next hop away from the normal path ---\u001b[0m\r\n"]
[29.114687, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-route.conf <<EOF\r\npass in quick proto icmp from 10.0.1.10 to 10.0.2.20 route-to { 10.0.3.2 } keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[29.778259, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'\u001b[0m\r\n"]
[30.050898, "o", "route-to rule loaded\r\n"]
[30.45427, "o", "\u001b[1;36mpftest-pf0# (tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1\u001b[0m\r\n"]
[32.089063, "o", "\u001b[1;36mpftest-cli# ping -c1 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
[34.31667, "o", "--- 10.0.2.20 ping statistics ---\r\n1 packets transmitted, 0 packets received, 100% packet loss\r\n"]
[36.72139, "o", "\u001b[1;36mpftest-pf0# echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\\|^listening\\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td\u001b[0m\r\n"]
[37.006878, "o", "forced egress (eth3, toward the gateway):\r\n02:16:59.958272 52:54:00:aa:00:04 > 52:54:00:dd:00:02, ethertype IPv4 (0x0800), length 98: 10.0.1.10 > 10.0.2.20: ICMP echo request, id 5473, seq 0, length 64\r\nnormal egress (eth2):\r\n0\r\n"]
[37.410476, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
[38.094888, "o", "\r\n\u001b[1m--- 10. divert-to: the packet goes to userspace and comes back ---\u001b[0m\r\n"]
[38.095122, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-divert.conf\u001b[0m\r\n"]
[38.35866, "o", "pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to \"10.0.1.1\" port 10000 keep state\r\npass out quick\r\n"]
[38.763007, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'\u001b[0m\r\n"]
[39.022814, "o", "divert rule loaded\r\n"]
[39.42606, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert\u001b[0m\r\n"]
[40.654519, "o", "1794\r\n"]
[41.056785, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.1.1 | tail -3\u001b[0m\r\n"]
[42.284778, "o", "--- 10.0.1.1 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.177/0.224/0.271 ms\r\n"]
[42.691426, "o", "\u001b[1;36mpftest-pf0# head -3 /tmp/pdivert.log\u001b[0m\r\n"]
[42.942601, "o", "pdivert: listening on divert port 10000\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\n"]
[43.344696, "o", " (pdivert flipped the last payload byte and fixed the ICMP checksum)\r\n\r\n\u001b[1m--- 11. shutdown ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'\u001b[0m\r\n"]
[43.626606, "o", "pf disabled\r\npf disabled\r\n"]
[44.028882, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf '\u001b[0m\r\n"]
[44.286597, "o", "pf 552960 0\r\n"]
[44.688511, "o", "\r\n\u001b[1mdemo over - port, gates and docs:\u001b[0m\r\n\u001b[1m https://git.labexposed.com/lgcosta/pf-linux-port\u001b[0m\r\n"]
[44.688631, "o", "\r\n"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-161
View File
@@ -1,161 +0,0 @@
#!/usr/bin/env python3
"""
PF-on-Linux demo recorder -> asciicast v2 (.cast)
Every byte shown comes from the REAL lab (Alpine VMs under libvirt, kernel
7.3.4 built from source with our pf.ko). Only the pacing is scripted.
Usage:
./scripts/demo-record.py [--out out/demo-pf-linux.cast] [--quick]
Play back with: asciinema play out/demo-pf-linux.cast
Upload with: asciinema upload out/demo-pf-linux.cast
"""
import argparse
import json
import os
import shlex
import subprocess
import sys
import time
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEB = os.environ.get("DEB", "root@192.168.1.242")
PF0 = os.environ.get("PF0_IP", "192.168.122.8")
CLI = os.environ.get("CLI_IP", "10.0.1.10")
SRV = os.environ.get("SRV_IP", "10.0.2.20")
KEY = os.path.join(os.environ.get("HOME", "/root"), ".ssh/pflab_ed")
CONF = """# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
set block-policy return
table <trusted> { 10.0.1.0/24 }
# transit traffic: stateful, table-driven
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
# deny a specific destination
block drop in quick proto icmp to 10.0.9.99
# everything else
pass in quick
pass out quick
"""
DIVERT_CONF = """pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
pass out quick
"""
def sh(cmd, timeout=40):
p = subprocess.run(cmd, shell=True, capture_output=True, text=True,
timeout=timeout)
return (p.stdout or "") + (p.stderr or "")
def on_pf0(cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
f"-i {KEY} root@{PF0} {shlex.quote(cmd)}", timeout)
def on_vm(ip, cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
f"-i {KEY} root@{ip} {shlex.quote(cmd)}", timeout)
def on_deb(cmd, timeout=40):
return sh(f"ssh -o ConnectTimeout=8 {DEB} {shlex.quote(cmd)}", timeout)
def stage():
"""put the demo ruleset + pdivert on pf0 (through the lab host)"""
with open("/tmp/demo-pf.conf", "w") as f:
f.write(CONF)
with open("/tmp/demo-divert.conf", "w") as f:
f.write(DIVERT_CONF)
sh(f"scp -q /tmp/demo-pf.conf /tmp/demo-divert.conf "
f"{os.path.join(REPO, 'tools/out/pdivert')} {DEB}:/tmp/", timeout=90)
on_deb(f"scp -q -o StrictHostKeyChecking=accept-new -i {KEY} "
f"/tmp/demo-pf.conf /tmp/demo-divert.conf /tmp/pdivert "
f"root@{PF0}:/tmp/ 2>&1", timeout=90)
SCENARIO = [
("local", "echo '=== PF on Linux — OpenBSD PF 8.0 engine running inside the Linux 7.3.4 kernel ==='"),
("local", "echo 'lab: 3 Alpine VMs under libvirt (pf0 = firewall, cli/srv = transit endpoints)'"),
("pf0", "uname -r"),
("pf0", "lsmod | grep '^pf ' || echo 'pf: not loaded'"),
("pf0", "insmod /opt/pf/pf.ko && dmesg | tail -3"),
("pf0", "/opt/pf/pfctl -s info | head -4"),
("pf0", "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"),
("pf0", "cat /tmp/demo-pf.conf"),
("pf0", "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded' && /opt/pf/pfctl -sr"),
("pf0", "/opt/pf/pfctl -sr -vv | head -8"),
("cli", f"ping -c2 -W2 {SRV} | tail -3"),
("pf0", "/opt/pf/pfctl -ss"),
("pf0", "/opt/pf/pfctl -t trusted -T show"),
("pf0", "/opt/pf/pfctl -s Tables"),
("cli", "ping -c2 -W2 10.0.9.99 | tail -3"),
("pf0", "/opt/pf/pfctl -sr -vv | head -6"),
("pf0", "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"),
("pf0", "(setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"),
("cli", "ping -c2 -W2 10.0.1.1 | tail -3"),
("pf0", "head -3 /tmp/pdivert.log"),
("pf0", "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"),
]
def record(out_path, quick=False):
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
events = []
t = 0.0
def emit(text, delay):
nonlocal t
t += delay
events.append([round(t, 3), "o", text])
header = {
"version": 2, "width": 100, "height": 26,
"timestamp": int(time.time()),
"title": "PF on Linux — OpenBSD PF engine inside the Linux kernel (lab demo)",
"env": {"SHELL": "/bin/sh", "TERM": "xterm-256color"},
}
runners = {
"pf0": lambda c: on_pf0(c),
"cli": lambda c: on_vm(CLI, c),
"srv": lambda c: on_vm(SRV, c),
"local": lambda c: sh(c),
}
labels = {"pf0": "pftest-pf0", "cli": "pftest-cli", "srv": "pftest-srv",
"local": "lab-host"}
stage()
with open(out_path, "w") as f:
f.write(json.dumps(header) + "\n")
for kind, cmd in SCENARIO:
emit(f"\x1b[1;36m{labels[kind]}\x1b[0m:\x1b[1;37m {cmd}\x1b[0m\r\n",
0.35)
try:
out = runners[kind](cmd)
except subprocess.TimeoutExpired:
out = "[timeout]\r\n"
except Exception as e: # noqa: BLE001
out = f"[error: {e}]\r\n"
out = out.replace("\r\n", "\n").replace("\n", "\r\n")
emit(out or "(no output)\r\n", min(1.4, 0.3 + len(out) / 5000.0))
with open(out_path, "a") as f:
for ev in events:
f.write(json.dumps(ev) + "\n")
print(f"WROTE {out_path}: {len(events)} events, {t:.1f}s")
print("play: asciinema play " + out_path)
print("upload: asciinema upload " + out_path)
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(REPO, "out",
"demo-pf-linux.cast"))
ap.add_argument("--quick", action="store_true")
a = ap.parse_args()
record(a.out, a.quick)
-182
View File
@@ -1,182 +0,0 @@
#!/usr/bin/env bash
# Demo session for PF-on-Linux: a scripted terminal tour of the lab.
# Recorded with: asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
#
# Everything printed is the REAL output of the lab (Alpine VMs under libvirt
# running the kernel built from source with our pf.ko). The VM management
# addresses are resolved from the hypervisor (they are DHCP), the lab
# topology (10.0.1.0/24, 10.0.2.0/24, ...) is fixed.
set -u
DEB=${DEB:-root@192.168.1.242}
CLI_IP=${CLI_IP:-10.0.1.10}
SRV_IP=${SRV_IP:-10.0.2.20}
KEY=${KEY:-$HOME/.ssh/pflab_ed}
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
# management address of a lab VM (DHCP, so ask the hypervisor)
mgmt() {
ssh -o ConnectTimeout=8 "$DEB" "virsh domifaddr $1 | awk '/vnet/{print \$4}' | cut -d/ -f1" \
2>/dev/null | tr -d '\r' | head -1
}
PF0_MGMT=$(mgmt pftest-pf0)
CLI_MGMT=$(mgmt pftest-cli)
[ -n "$PF0_MGMT" ] || PF0_MGMT=192.168.122.8
[ -n "$CLI_MGMT" ] || CLI_MGMT=192.168.122.80
# run a command on a lab VM (through the hypervisor)
onvm() {
local ip=$1; shift
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
-o ProxyJump=root@192.168.1.242 -i "$KEY" root@"$ip" "$@" 2>&1
}
p0() {
cyan "pftest-pf0# $*"
onvm "$PF0_MGMT" "$@"
sleep 0.4
}
vmc() {
cyan "pftest-cli# $*"
onvm "$CLI_MGMT" "$@"
sleep 0.4
}
CONF=/tmp/demo-pf.conf
DIV=/tmp/demo-divert.conf
cat > "$CONF" <<'CONF'
# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
set block-policy return
table <trusted> { 10.0.1.0/24 }
# transit traffic: stateful, table-driven
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
# deny a specific destination
block drop in quick proto icmp to 10.0.9.99
# everything else
pass in quick
pass out quick
CONF
cat > "$DIV" <<CONF
pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
pass out quick
CONF
# the pf0 interface towards the server (used by the NAT pool)
EGRESS_IF=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
[ -n "$EGRESS_IF" ] || EGRESS_IF=eth2
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \
tools/out/pdivert tools/out/pflogd "root@$PF0_MGMT:/tmp/" 2>/dev/null
bold "==============================================================="
bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel"
bold "==============================================================="
echo
echo "Lab: 3 Alpine VMs under libvirt."
echo " pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)"
echo " pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20"
echo
bold "--- 1. our kernel, the OpenBSD engine as a module ---"
p0 "uname -r"
p0 "lsmod | grep '^pf ' || echo 'pf: not loaded yet'"
p0 "insmod /opt/pf/pf.ko && dmesg | tail -3"
echo
bold "--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---"
p0 "/opt/pf/pfctl -s info | head -4"
p0 "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"
echo
bold "--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---"
p0 "cat /tmp/demo-pf.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'"
p0 "/opt/pf/pfctl -sr"
echo
bold "--- 4. transit traffic is allowed and stateful ---"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4"
echo
bold "--- 5. tables are live objects ---"
p0 "/opt/pf/pfctl -t trusted -T show"
p0 "/opt/pf/pfctl -s Tables"
echo
bold "--- 6. the drop rule actually drops ---"
p0 "cat > /tmp/demo-block.conf <<EOF
block drop in quick proto icmp to $SRV_IP
pass in quick
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -sr -vv | head -6"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
echo
bold "--- 7. NAT: the source is translated on the way out, restored on the way back ---"
p0 "cat > /tmp/demo-nat.conf <<EOF
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_IF)
pass in quick on ingress proto icmp keep state
pass out quick on egress proto icmp keep state
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
echo
bold "--- 8. pflog: logged packets reach userspace (/dev/pflog) ---"
p0 "cat > /tmp/demo-log.conf <<EOF
pass in quick log proto icmp from $CLI_IP keep state
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'"
p0 "chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
sleep 2
p0 "cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap"
p0 "pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
echo
bold "--- 9. route-to: force the next hop away from the normal path ---"
p0 "cat > /tmp/demo-route.conf <<EOF
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { 10.0.3.2 } keep state
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'"
p0 "(tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1"
vmc "ping -c1 -W2 $SRV_IP | tail -2"
sleep 2
p0 "echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\|^listening\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
echo
bold "--- 10. divert-to: the packet goes to userspace and comes back ---"
p0 "cat /tmp/demo-divert.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"
p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"
vmc "ping -c2 -W2 10.0.1.1 | tail -3"
p0 "head -3 /tmp/pdivert.log"
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
echo
bold "--- 11. shutdown ---"
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
p0 "lsmod | grep '^pf '"
echo
bold "demo over - port, gates and docs:"
bold " https://git.labexposed.com/lgcosta/pf-linux-port"
echo
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Assemble the final MP4s: silent video track + MiniMax narration + subtitles.
Each narration segment was recorded as its own audio file, generated by
MiniMax with a female voice for the language of the track. The segments are
placed at the moment their beat starts on screen, so the voice, the caption
and the terminal action always agree.
./scripts/screencast/assemble.py --phase 04-roteamento --lang en
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(os.path.dirname(HERE))
CAPS = os.path.join(REPO, "out", "screencast", "caps")
VO = os.path.join(REPO, "out", "screencast", "vo")
OUT = os.path.join(REPO, "out", "screencast")
ART = os.path.join(OUT, "art")
# which phases draw the lab diagram over the terminal
DIAGRAM_PHASES = {"00-ambiente", "04-roteamento", "05-balanceamento"}
# phases that show the module/host instead of the firewall VM
HOST_PHASES = {"01-implementacao"}
PHASE_ORDER = ["00-ambiente", "01-implementacao", "02-ativacao", "03-regras",
"04-roteamento", "05-balanceamento", "06-qos"]
def run(cmd):
subprocess.run(cmd, check=True)
def probe(path):
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "csv=p=0", path],
capture_output=True, text=True).stdout.strip()
return float(out or 0)
def audio_duration(path):
return probe(path)
def make_video(phase, lang):
"""Render the silent video (terminal + diagram + burned-in subtitles)."""
cast = os.path.join(CAPS, f"{phase}.cast")
nar = os.path.join(HERE, "narration", f"{phase}.{lang}.json")
if not os.path.exists(cast):
raise SystemExit(f"[assemble] missing capture: {cast}")
art = None
if os.path.isdir(ART):
imgs = sorted(f for f in os.listdir(ART) if f.endswith((".jpg", ".png")))
if imgs:
art = os.path.join(ART, imgs[0])
outdir = os.path.join(OUT, lang)
os.makedirs(outdir, exist_ok=True)
silent = os.path.join(outdir, f"{phase}.silent.mp4")
index = PHASE_ORDER.index(phase) + 1
cmd = [sys.executable, os.path.join(HERE, "render_video.py"),
"--cast", cast, "--narration", nar, "--out", silent,
"--lang", lang, "--phase-no", str(index),
"--phase-total", str(len(PHASE_ORDER))]
if phase in DIAGRAM_PHASES:
cmd.append("--diagram")
if art:
cmd += ["--art", art]
run(cmd)
return silent
def mux_audio(phase, lang, silent):
"""Lay every narration segment on the timeline and add it to the video."""
meta = json.load(open(os.path.join(CAPS, f"{phase}.json")))
beats = {b["id"]: b for b in meta["beats"]}
nar = json.load(open(os.path.join(HERE, "narration", f"{phase}.{lang}.json")))
vodir = os.path.join(VO, f"{phase}.{lang}")
segments = []
for i, seg in enumerate(nar["segments"]):
beat = beats.get(seg["beat"])
audio = os.path.join(vodir, f"{i:02d}-{seg['beat']}.mp3")
if beat is None or not os.path.exists(audio):
sys.stderr.write(f"[assemble] missing audio: {audio}\n")
continue
segments.append((beat["start"], audio))
tmp = os.path.join(OUT, "_tmp")
os.makedirs(tmp, exist_ok=True)
if not segments:
out = os.path.join(tmp, f"{phase}.{lang}.m4v")
run(["ffmpeg", "-y", "-loglevel", "error", "-i", silent,
"-c:v", "copy", "-an", out])
return out
vdur = probe(silent)
inputs = []
filters = []
# input 0 is the video; the narration files are inputs 1..N
for k, (start, audio) in enumerate(segments):
inputs += ["-i", audio]
delay = int(start * 1000)
# pad so every segment reaches the end of the video, then the mix is
# trimmed to the video length
filters.append(f"[{k + 1}:a]adelay={delay}|{delay},apad[aud{k}]")
mix_in = "".join(f"[aud{k}]" for k in range(len(segments)))
filters.append(f"{mix_in}amix=inputs={len(segments)}:normalize=0,"
f"atrim=0:{vdur:.3f},asetpts=N/SR/TB[mixed]")
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-i", silent] + inputs + \
["-filter_complex", ";".join(filters),
"-map", "0:v", "-map", "[mixed]", "-c:v", "copy", "-c:a", "aac",
"-b:a", "160k", "-ar", "44100", "-ac", "2",
os.path.join(tmp, f"{phase}.{lang}.m4v")]
run(cmd)
return os.path.join(tmp, f"{phase}.{lang}.m4v")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--phase", required=True)
ap.add_argument("--lang", required=True, choices=["en", "pt"])
ap.add_argument("--keep-silent", action="store_true")
args = ap.parse_args()
phase, lang = args.phase, args.lang
silent = make_video(phase, lang)
muxed = mux_audio(phase, lang, silent)
outdir = os.path.join(OUT, lang)
os.makedirs(outdir, exist_ok=True)
final = os.path.join(outdir, f"{phase}.mp4")
run(["ffmpeg", "-y", "-loglevel", "error", "-i", muxed,
"-c", "copy", "-movflags", "+faststart", final])
if not args.keep_silent:
os.remove(silent)
os.remove(muxed)
dur = probe(final)
size = os.path.getsize(final) / 1e6
print(f"[assemble] {final} {dur:.1f}s {size:.1f}MB")
if __name__ == "__main__":
main()
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Build every screencast: capture -> narration -> video -> full movie.
#
# ./scripts/screencast/build-all.sh # everything
# ./scripts/screencast/build-all.sh 04-roteamento
set -uo pipefail
cd "$(dirname "$0")/../.."
REPO=$(pwd)
PHASES=("$@")
[ ${#PHASES[@]} -eq 0 ] && PHASES=(00-ambiente 01-implementacao 02-ativacao 03-regras \
04-roteamento 05-balanceamento 06-qos)
echo "== narration audio =="
for lang in en pt; do
python3 scripts/screencast/make_audio.py --lang "$lang" | tail -1
done
echo "== sessions =="
for p in "${PHASES[@]}"; do
# the activation phase shows the module being loaded, so it is the one
# that must not have it preloaded
extra=""
[ "$p" = "02-ativacao" ] || extra="--ensure-pf"
python3 scripts/screencast/record.py --plan "scripts/screencast/plans/$p.json" \
--out "out/screencast/caps/$p.cast" --pace $extra
done
echo "== videos =="
for lang in en pt; do
for p in "${PHASES[@]}"; do
python3 scripts/screencast/assemble.py --phase "$p" --lang "$lang" || echo "FALHOU: $p $lang"
done
done
echo "== full movie =="
for lang in en pt; do
python3 scripts/screencast/join.py --lang "$lang" || echo "FALHOU: join $lang"
done
echo
echo "== resultado =="
for f in out/screencast/en/*.mp4 out/screencast/pt/*.mp4; do
[ -e "$f" ] || continue
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f")
a=$(ffprobe -v error -select_streams a -show_entries stream=codec_name -of csv=p=0 "$f")
printf '%-52s %6.1fs audio=%s\n' "$f" "$d" "${a:-none}"
done
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
Join the phases into one continuous video per language, with transitions.
Each phase fades into a short "chapter" card that names it, and the card
fades into the next phase - one continuous stream, no hard cuts, so the
viewer always knows where they are.
./scripts/screencast/join.py --lang en
"""
import argparse
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(os.path.dirname(HERE))
OUT = os.path.join(REPO, "out", "screencast")
ART = os.path.join(OUT, "art")
PHASE_ORDER = ["00-ambiente", "01-implementacao", "02-ativacao", "03-regras",
"04-roteamento", "05-balanceamento", "06-qos"]
TITLES = {
"00-ambiente": ("The lab environment", "O ambiente de testes",
"Four Alpine VMs under libvirt", "Quatro VMs Alpine sob libvirt"),
"01-implementacao": ("Implementation in the kernel", "Implementacao no kernel",
"OpenBSD 8.0 sources + a Linux compatibility layer",
"Fontes do OpenBSD 8.0 + camada de compatibilidade Linux"),
"02-ativacao": ("Activation", "Ativacao",
"insmod pf.ko, then pfctl over the OpenBSD ioctl ABI",
"insmod pf.ko e o pfctl pela ABI de ioctls do OpenBSD"),
"03-regras": ("Basic rules", "Regras basicas",
"Tables, anchors, keep state, block drop",
"Tabelas, anchors, keep state, block drop"),
"04-roteamento": ("Routing", "Roteamento",
"route-to and reply-to", "route-to e reply-to"),
"05-balanceamento": ("Load balancing", "Balanceamento",
"Pools round-robin, source-hash, dup-to",
"Pools round-robin, source-hash, dup-to"),
"06-qos": ("Quality of service", "Qualidade de servico",
"hfsc queues in the data path", "Filas hfsc no caminho de dados"),
}
def run(cmd):
subprocess.run(cmd, check=True)
def probe(path):
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "csv=p=0", path],
capture_output=True, text=True).stdout.strip()
return float(out or 0)
def chapter_card(phase, lang, out_path, seconds=1.8):
"""A short card naming the phase; it is the pivot of the crossfade."""
title = TITLES[phase][0 if lang == "en" else 1]
sub = TITLES[phase][2 if lang == "en" else 3]
num = PHASE_ORDER.index(phase) + 1
art = None
if os.path.isdir(ART):
imgs = sorted(f for f in os.listdir(ART) if f.endswith((".jpg", ".png")))
if imgs:
art = os.path.join(ART, imgs[0])
script = os.path.join(OUT, "_chapter.py")
with open(script, "w") as f:
f.write(f"""
import os, sys
from PIL import Image, ImageDraw, ImageFont
W, H = 1920, 1080
BG = (12, 16, 22); ACCENT = (88, 168, 255); FG = (240, 246, 252); MUTED = (140, 150, 165)
mono = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf"
sans = "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf"
sansb = "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf"
art = {art!r}
img = Image.new("RGB", (W, H), BG)
if art and os.path.exists(art):
img = Image.blend(img, Image.open(art).resize((W, H)).convert("RGB"), 0.5)
d = ImageDraw.Draw(img, "RGBA")
d.rectangle([0, 0, W, 8], fill=ACCENT)
d.text((140, 430), {title!r}, font=ImageFont.truetype(sansb, 62), fill=FG)
d.text((140, 520), {sub!r}, font=ImageFont.truetype(sans, 30), fill=MUTED)
d.text((140, 586), "PHASE %d OF %d" % ({num}, {len(PHASE_ORDER)}),
font=ImageFont.truetype(mono, 24), fill=ACCENT)
d.text((140, 640), "PF on Linux - OpenBSD PF 8.0 no kernel Linux",
font=ImageFont.truetype(mono, 24), fill=MUTED)
img.save(sys.argv[1])
""")
png = out_path + ".png"
run([sys.executable, script, png])
have = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True).stdout
venc = (["-c:v", "libx264", "-preset", "veryfast", "-crf", "20", "-profile:v", "high"]
if "libx264" in have else
["-c:v", "libopenh264", "-b:v", "6M", "-profile:v", "high"])
run(["ffmpeg", "-y", "-loglevel", "error", "-loop", "1", "-i", png,
"-t", str(seconds), "-r", "30"] + venc + ["-pix_fmt", "yuv420p", out_path])
os.remove(png)
os.remove(script)
return out_path
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--lang", required=True, choices=["en", "pt"])
ap.add_argument("--out")
ap.add_argument("--fade", type=float, default=0.7)
args = ap.parse_args()
lang = args.lang
tmp = os.path.join(OUT, "_tmp")
os.makedirs(tmp, exist_ok=True)
segs = [] # (path, duration)
for phase in PHASE_ORDER:
src = os.path.join(OUT, lang, f"{phase}.mp4")
if not os.path.exists(src):
sys.stderr.write(f"[join] missing {src}, skipping\n")
continue
segs.append((src, probe(src)))
card = chapter_card(phase, lang, os.path.join(tmp, f"card-{phase}.mp4"))
segs.append((card, probe(card)))
if not segs:
raise SystemExit("[join] nothing to join")
inputs = []
for path, _ in segs:
inputs += ["-i", path]
# sequential xfade: each segment fades into the next
filters = []
prev = "0:v"
offset = segs[0][1] - args.fade
for i in range(1, len(segs)):
filters.append(f"[{prev}][{i}:v]xfade=transition=fade:"
f"duration={args.fade}:offset={offset:.3f}[v{i}]")
prev = f"v{i}"
offset += segs[i][1] - args.fade
# The audio has to follow the same timeline: with xfade the segments
# overlap by the fade duration, so each one starts `fade` earlier than
# the plain sum would suggest.
audio_at = []
acc = 0.0
for i, (_, dur) in enumerate(segs):
audio_at.append(max(0.0, acc - args.fade * i))
acc += dur
# Only the phase videos carry narration; the chapter cards are silent.
# Their input indices are the even ones in `inputs`.
phase_idx = [i for i in range(len(segs)) if i % 2 == 0]
afilters = []
labels = []
for k, i in enumerate(phase_idx):
ms = int(audio_at[i] * 1000)
afilters.append(f"[{i}:a]adelay={ms}|{ms}[a{k}]")
labels.append(f"[a{k}]")
afilters.append("".join(labels) +
f"amix=inputs={len(phase_idx)}:normalize=0,"
f"atrim=0:{audio_at[-1] + segs[-1][1]:.3f},"
f"asetpts=N/SR/TB[amix]")
filters += afilters
amap = "[amix]"
out = args.out or os.path.join(OUT, lang, "pf-on-linux-full.mp4")
have = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True).stdout
if "libx264" in have:
venc = ["-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
"-profile:v", "high", "-level", "4.0"]
elif "libopenh264" in have:
venc = ["-c:v", "libopenh264", "-b:v", "6M", "-profile:v", "high"]
else:
venc = ["-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "30", "-row-mt", "1",
"-cpu-used", "4"]
# the phases already carry their narration; the cards are silent
run(["ffmpeg", "-y", "-loglevel", "error"] + inputs +
["-filter_complex", ";".join(filters), "-map", f"[{prev}]",
"-map", amap] + venc +
["-pix_fmt", "yuv420p", "-movflags", "+faststart", out])
print(f"[join] {out} {probe(out):.1f}s")
if __name__ == "__main__":
main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Generate the narration audio for every screencast phase with MiniMax TTS.
Female voices, one per language:
en -> English_ConfidentWoman
pt -> Portuguese_ConfidentWoman
Each *segment* of a narration script becomes its own audio file, so the
renderer can start the voice exactly when the matching beat appears on
screen and burn the same sentence as a subtitle.
./scripts/screencast/make_audio.py --lang en
./scripts/screencast/make_audio.py --lang pt --phase 04-roteamento
"""
import argparse
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
HERE = os.path.dirname(os.path.abspath(__file__))
NARR = os.path.join(HERE, "narration")
VO_DIR = os.path.join(HERE, "..", "..", "out", "screencast", "vo")
VOICES = {
"en": {"voice_id": "English_ConfidentWoman", "language_boost": "English"},
"pt": {"voice_id": "Portuguese_ConfidentWoman", "language_boost": "Portuguese"},
}
# MiniMax occasionally stumbles on the punctuation of long sentences; keeping
# the text plain (no dashes, no slashes) makes the prosody predictable.
MODEL = "speech-02-hd"
API = "https://api.minimax.io/v1/t2a_v2"
def decode_audio(payload):
"""MiniMax returns the audio as a hex string (it starts with the ASCII
bytes of the ID3 header, 49443304). Older/other endpoints use base64,
so both are handled."""
if len(payload) > 8 and all(c in "0123456789abcdefABCDEF" for c in payload[:64]):
return bytes.fromhex(payload)
data = payload.replace("-", "+").replace("_", "/")
data += "=" * (-len(data) % 4)
return base64.b64decode(data, validate=False)
def tts(text, voice, lang, out_path, retries=3):
body = {
"model": MODEL,
"text": text,
"stream": False,
"voice_setting": {
"voice_id": voice["voice_id"],
"speed": 0.97, "vol": 1.0, "pitch": 0, "emotion": "neutral",
},
"audio_setting": {"sample_rate": 44100, "bitrate": 128000,
"format": "mp3", "channel": 1},
"language_boost": voice["language_boost"],
}
key = os.environ.get("MINIMAX_API_KEY")
if not key:
raise SystemExit("MINIMAX_API_KEY is not set")
for attempt in range(retries):
req = urllib.request.Request(
API, data=json.dumps(body).encode(),
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=120) as r:
data = json.loads(r.read())
if data.get("base_resp", {}).get("status_code") != 0:
raise RuntimeError(str(data.get("base_resp")))
audio = data.get("data", {}).get("audio")
if not audio:
raise RuntimeError("no audio in response")
with open(out_path, "wb") as f:
f.write(decode_audio(audio))
return True
except Exception as exc: # noqa: BLE001
sys.stderr.write(f"[tts] attempt {attempt + 1} failed: {exc}\n")
time.sleep(2 + attempt * 3)
return False
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--lang", required=True, choices=["en", "pt"])
ap.add_argument("--phase", help="only this phase")
ap.add_argument("--force", action="store_true")
args = ap.parse_args()
voice = VOICES[args.lang]
files = sorted(f for f in os.listdir(NARR) if f.endswith(f".{args.lang}.json"))
if args.phase:
files = [f for f in files if f.startswith(args.phase + ".")]
total = 0
for fn in files:
phase = fn.split(".")[0]
script = json.load(open(os.path.join(NARR, fn)))
outdir = os.path.abspath(os.path.join(VO_DIR, f"{phase}.{args.lang}"))
os.makedirs(outdir, exist_ok=True)
for i, seg in enumerate(script["segments"]):
out = os.path.join(outdir, f"{i:02d}-{seg['beat']}.mp3")
if os.path.exists(out) and not args.force:
print(f"skip {phase}.{args.lang} seg{i} ({seg['beat']})")
total += 1
continue
ok = tts(seg["text"], voice, args.lang, out)
size = os.path.getsize(out) if os.path.exists(out) else 0
print(f"{'ok ' if ok else 'FAIL'} {phase}.{args.lang} seg{i} "
f"({seg['beat']}) {size} bytes")
total += 1
print(f"[audio] {total} segments processed for lang={args.lang}")
if __name__ == "__main__":
main()
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Write the narration scripts for every screencast phase, in English and in
Brazilian Portuguese.
Each segment belongs to one *beat* of the recorded session (see
scripts/screencast/plans/*.json). The renderer places the segment's audio at
the moment that beat starts on screen, and burns the same text as a subtitle,
so the voice, the caption and the terminal action always agree.
A segment may also carry:
highlight - nodes of the lab diagram to light up while it is spoken
diagram_caption - one-line caption shown inside the diagram panel
"""
import json
import os
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "narration")
os.makedirs(OUT, exist_ok=True)
EN_VOICE = "English_ConfidentWoman"
PT_VOICE = "Portuguese_ConfidentWoman"
PHASES = {}
# ------------------------------------------------------------------ 00 ----
PHASES["00-ambiente"] = {
"en": {
"title": "The lab environment",
"subtitle": "Four Alpine VMs under libvirt, running a kernel built from source",
"segments": [
{"beat": "b1", "text": "Welcome to the PF on Linux demonstration. Before we look at the firewall itself, let me show you the environment where everything runs. These are four virtual machines managed by libvirt, all of them Alpine Linux, and the machine you are looking at is the firewall: a kernel we compiled from source, version seven point three point four, with our port of the OpenBSD packet filter built in as a module."},
{"beat": "b2", "text": "The firewall has four interfaces. The management interface on the virtual network the hypervisor uses to reach us. Then three transit networks: ten point zero point one, where the client lives; ten point zero point two, where the server lives; and ten point zero point three, a second gateway we use to demonstrate routing decisions.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "three transit networks, four interfaces"},
{"beat": "b3", "text": "The OpenBSD tools are installed on this machine exactly as they ship: the original pfctl, a pflog daemon and our divert tool. The module is not loaded yet, which is where we will start.", "highlight": ["pf0"], "diagram_caption": "pfctl, pflogd and pdivert, straight from OpenBSD"},
{"beat": "b4", "text": "And from the firewall we can reach the other machines directly. The client is on ten point zero point one point ten, the server on ten point zero point two point twenty, and the second gateway on ten point zero point three point two. Every command in this demonstration is real traffic crossing real interfaces.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "cli, srv and gw2 reachable from pf0"},
],
},
"pt": {
"title": "O ambiente de testes",
"subtitle": "Quatro VMs Alpine sob libvirt, rodando um kernel compilado do fonte",
"segments": [
{"beat": "b1", "text": "Bem-vindo à demonstração do PF on Linux. Antes de falar do firewall em si, deixa eu mostrar o ambiente onde tudo acontece. São quatro máquinas virtuais administradas pelo libvirt, todas com Alpine Linux, e a máquina que você está vendo é o firewall: um kernel que compilamos do fonte, versão sete ponto três ponto quatro, com o nosso port do packet filter do OpenBSD compilado dentro como módulo."},
{"beat": "b2", "text": "O firewall tem quatro interfaces. A interface de gerência, na rede virtual que o hypervisor usa para falar com a gente. E depois três redes de trânsito: dez ponto zero ponto um, onde mora o cliente; dez ponto zero ponto dois, onde mora o servidor; e dez ponto zero ponto três, um segundo gateway que vamos usar para demonstrar decisões de roteamento.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "três redes de transito, quatro interfaces"},
{"beat": "b3", "text": "As ferramentas do OpenBSD estão instaladas nesta máquina exatamente como são distribuídas: o pfctl original, um daemon de pflog e a nossa ferramenta de divert. O módulo ainda não está carregado, e é por aí que a gente começa.", "highlight": ["pf0"], "diagram_caption": "pfctl, pflogd e pdivert originais do OpenBSD"},
{"beat": "b4", "text": "E do firewall a gente alcança as outras máquinas diretamente. O cliente está em dez ponto zero ponto um ponto dez, o servidor em dez ponto zero ponto dois ponto vinte, e o segundo gateway em dez ponto zero ponto três ponto dois. Todo comando desta demonstração é tráfego real cruzando interfaces reais.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "cli, srv e gw2 alcançáveis a partir do pf0"},
],
},
}
# ------------------------------------------------------------------ 01 ----
PHASES["01-implementacao"] = {
"en": {
"title": "Implementation in the kernel",
"subtitle": "OpenBSD 8.0 sources plus a Linux compatibility layer equals pf.ko",
"segments": [
{"beat": "b1", "text": "This is the heart of the work. We did not rewrite the firewall. We took the OpenBSD eight point zero sources, nearly twenty seven thousand lines of C, and we kept them almost intact. On top of that we wrote a compatibility layer, about seven thousand lines, that translates the BSD kernel interfaces into Linux ones."},
{"beat": "b2", "text": "The result is a normal Linux kernel module. Here modinfo shows us the license, the description and the vermagic, which tells us exactly which kernel this object was compiled against."},
{"beat": "b3", "text": "The vermagic line matters: it is the kernel version, the compiler flags and the module signature. Load the wrong one and the kernel refuses it. Everything you will see in the next minutes is running inside that module."},
{"beat": "b4", "text": "This file is what we load into the firewall. A single artifact, built out of tree, carrying the entire OpenBSD packet filter."},
],
},
"pt": {
"title": "Implementação no kernel",
"subtitle": "Fontes do OpenBSD 8.0 mais uma camada de compatibilidade Linux resultam em pf.ko",
"segments": [
{"beat": "b1", "text": "Aqui está o coração do trabalho. A gente não reescreveu o firewall. Pegamos as fontes do OpenBSD oito ponto zero, quase vinte e sete mil linhas de C, e mantivemos praticamente intactas. Em cima disso escrevemos uma camada de compatibilidade, cerca de sete mil linhas, que traduz as interfaces de kernel do BSD para as do Linux."},
{"beat": "b2", "text": "O resultado é um módulo de kernel Linux comum. Aqui o modinfo mostra a licença, a descrição e o vermagic, que nos diz exatamente com qual kernel este objeto foi compilado."},
{"beat": "b3", "text": "A linha do vermagic importa: é a versão do kernel, as flags do compilador e a assinatura do módulo. Carregar o arquivo errado e o kernel recusa. Tudo que você vai ver nos próximos minutos roda dentro desse módulo."},
{"beat": "b4", "text": "Este é o arquivo que carregamos no firewall. Um único artefato, compilado fora da árvore do kernel, carregando o packet filter inteiro do OpenBSD."},
],
},
}
# ------------------------------------------------------------------ 02 ----
PHASES["02-ativacao"] = {
"en": {
"title": "Activation",
"subtitle": "insmod pf.ko, then pfctl talks to it over the OpenBSD ioctl ABI",
"segments": [
{"beat": "b1", "text": "First we make sure nothing is loaded, so the activation is honest. No module, no firewall."},
{"beat": "b2", "text": "Now we insert it. The kernel accepts the module and the log shows our initialization message. This is the OpenBSD engine running inside Linux."},
{"beat": "b3", "text": "The device appears, owned by root with mode six hundred, exactly like the BSD original, and the module shows up in the list of loaded modules."},
{"beat": "b4", "text": "pfctl is the original OpenBSD tool. We enable the firewall and ask for its status. The ioctl interface, the statistics table, the rule counters, all of it is the OpenBSD ABI, unchanged."},
{"beat": "b5", "text": "The ruleset is still empty, and that is exactly right: with no rules, pf blocks everything and returns. The engine is armed and waiting for instructions."},
],
},
"pt": {
"title": "Ativação",
"subtitle": "insmod pf.ko e o pfctl conversa com ele pela ABI de ioctls do OpenBSD",
"segments": [
{"beat": "b1", "text": "Primeiro a gente garante que não tem nada carregado, para a ativação ser honesta. Sem módulo, sem firewall."},
{"beat": "b2", "text": "Agora inserimos. O kernel aceita o módulo e o log mostra a nossa mensagem de inicialização. Este é o motor do OpenBSD rodando dentro do Linux."},
{"beat": "b3", "text": "O dispositivo aparece, do root, com modo seiscentos, exatamente como no BSD original, e o módulo aparece na lista de módulos carregados."},
{"beat": "b4", "text": "O pfctl é a ferramenta original do OpenBSD. Habilitamos o firewall e pedimos o status. A interface de ioctl, a tabela de estatísticas, os contadores das regras: tudo é a ABI do OpenBSD, sem alteração."},
{"beat": "b5", "text": "O ruleset ainda está vazio, e isso está certo: sem regras, o pf bloqueia tudo e devolve. O motor está armado e esperando instruções."},
],
},
}
# ------------------------------------------------------------------ 03 ----
PHASES["03-regras"] = {
"en": {
"title": "Basic rules",
"subtitle": "Tables, anchors, keep state and block drop",
"segments": [
{"beat": "b1", "text": "Now we write a real pf dot conf. This is the original OpenBSD syntax: a table with the trusted network, an anchor, and the usual quick rules with keep state. pfctl parses it and the kernel accepts it."},
{"beat": "b2", "text": "pfctl shows us what the kernel understood: the rules as they were compiled, the list of tables, and the contents of the trusted table."},
{"beat": "b3", "text": "Let the traffic through. From the client we ping the server, and the firewall creates a state. You can see it in the state table: the five tuple, the timeout, the counters."},
{"beat": "b4", "text": "Now the same thing with a drop. We replace the pass rule with a block drop rule for the client network, and reload."},
{"beat": "b5", "text": "The ping is dead. One hundred percent packet loss. And the rule counters show the packets that were evaluated and the ones that were blocked. This is a stateful firewall behaving exactly like the original."},
{"beat": "b6", "text": "Finally, anchors. We load a ruleset into the anchor called demo, and pfctl lists the rules that live inside it, separately from the main ruleset."},
],
},
"pt": {
"title": "Regras básicas",
"subtitle": "Tabelas, anchors, keep state e block drop",
"segments": [
{"beat": "b1", "text": "Agora escrevemos um pf ponto conf de verdade. Esta é a sintaxe original do OpenBSD: uma tabela com a rede confiável, um anchor, e as regras quick de sempre com keep state. O pfctl faz o parsing e o kernel aceita."},
{"beat": "b2", "text": "O pfctl nos mostra o que o kernel entendeu: as regras como foram compiladas, a lista de tabelas e o conteúdo da tabela trusted."},
{"beat": "b3", "text": "Deixa o tráfego passar. Do cliente a gente dá ping no servidor, e o firewall cria um estado. Você vê isso na tabela de estados: a tupla de cinco campos, o timeout, os contadores."},
{"beat": "b4", "text": "Agora a mesma coisa com um drop. Trocamos a regra de pass por uma regra block drop para a rede do cliente, e recarregamos."},
{"beat": "b5", "text": "O ping morreu. Cem por cento de perda. E os contadores da regra mostram os pacotes avaliados e os bloqueados. Este é um firewall stateful se comportando exatamente como o original."},
{"beat": "b6", "text": "Por fim, os anchors. Carregamos um ruleset no anchor chamado demo, e o pfctl lista as regras que moram dentro dele, separadamente do ruleset principal."},
],
},
}
# ------------------------------------------------------------------ 04 ----
PHASES["04-roteamento"] = {
"en": {
"title": "Routing",
"subtitle": "route-to forces the next hop, reply-to forces the way back",
"segments": [
{"beat": "b1", "text": "Now the routing features. This rule matches traffic from the client to the server and sends it through the second gateway, ten point zero point three point two, instead of the normal path. pfctl shows the rule exactly as the kernel stored it.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "route-to: normal path is eth2, forced path is eth3"},
{"beat": "b2", "text": "To prove where the packet actually went, we capture on both interfaces at the same time: eth three, towards the gateway, and eth two, towards the server."},
{"beat": "b3", "text": "And now the client pings the server.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "the request leaves through eth3, to the gateway"},
{"beat": "b4", "text": "The evidence is in the captures. The request appears on eth three, on the way to the gateway, and it never appears on eth two. The firewall decided the next hop, not the routing table.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "1 request on eth3, 0 on eth2"},
{"beat": "b5", "text": "The other half of the problem is the way back. reply to forces the reply of a flow to leave through a different gateway than the request came in, which is how you build asymmetric routing.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "reply-to: the answer leaves through another gateway"},
{"beat": "b6", "text": "We capture eth three again and also eth one, the link where the client is."},
{"beat": "b7", "text": "The replies show up on eth three, on the gateway link, and not on the client link. The request and the reply now take different paths, exactly as commanded.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "reply on eth3, nothing on the client link"},
],
},
"pt": {
"title": "Roteamento",
"subtitle": "route-to força o próximo salto, reply-to força o caminho de volta",
"segments": [
{"beat": "b1", "text": "Agora as funcionalidades de roteamento. Esta regra casa com o tráfego do cliente para o servidor e o manda pelo segundo gateway, dez ponto zero ponto três ponto dois, em vez do caminho normal. O pfctl mostra a regra exatamente como o kernel guardou.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "route-to: o caminho normal e' eth2; o forcado, eth3"},
{"beat": "b2", "text": "Para provar por onde o pacote realmente passou, capturamos nas duas interfaces ao mesmo tempo: eth três, rumo ao gateway, e eth dois, rumo ao servidor."},
{"beat": "b3", "text": "E agora o cliente dá ping no servidor.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "o pedido sai por eth3, rumo ao gateway"},
{"beat": "b4", "text": "A prova está nas capturas. O pedido aparece na eth três, indo para o gateway, e nunca aparece na eth dois. Quem decidiu o próximo salto foi o firewall, não a tabela de rotas.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "1 pedido em eth3, 0 em eth2"},
{"beat": "b5", "text": "A outra metade do problema é o caminho de volta. O reply to força a resposta de um fluxo a sair por um gateway diferente do por onde o pedido entrou, que é assim que se constrói roteamento assimétrico.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "reply-to: a resposta sai por um gateway diferente"},
{"beat": "b6", "text": "Capturamos a eth três de novo e também a eth um, o link onde está o cliente."},
{"beat": "b7", "text": "As respostas aparecem na eth três, no link do gateway, e não no link do cliente. O pedido e a resposta agora seguem caminhos diferentes, exatamente como foi pedido.", "highlight": ["cli", "pf0", "gw2"], "diagram_caption": "resposta em eth3, nada no link do cliente"},
],
},
}
# ------------------------------------------------------------------ 05 ----
PHASES["05-balanceamento"] = {
"en": {
"title": "Load balancing",
"subtitle": "NAT pools in round-robin and source-hash, plus dup-to",
"segments": [
{"beat": "b1", "text": "Load balancing in pf is done with pools. This rule translates the source of the client network into a pool of two addresses, the two gateways of the firewall, and selects between them round robin.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: round-robin"},
{"beat": "b2", "text": "We capture on the interface towards the server to see which source address each packet carries."},
{"beat": "b3", "text": "Six packets from the client."},
{"beat": "b4", "text": "And the capture shows the truth: three packets went out with one address, three with the other. The pool is really rotating between its members, and the server sees a balanced load.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "3 packets with each source address"},
{"beat": "b5", "text": "Sometimes you do not want rotation. source hash keeps a given origin always mapped to the same pool member, which is what you want when a session has to be sticky.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: source-hash"},
{"beat": "b6", "text": "Four more packets, and now every single one carries the same source address. The mapping is stable per origin.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "every packet with the same source"},
{"beat": "b7", "text": "And dup to is the mirror case: the packet keeps its normal path, and a copy is delivered to the second gateway. We count the requests on both interfaces: one on the normal path, one on the mirrored path. The packet was duplicated, not moved.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "dup-to: the normal path and the mirrored copy"},
],
},
"pt": {
"title": "Balanceamento",
"subtitle": "Pools de NAT em round-robin e source-hash, mais o dup-to",
"segments": [
{"beat": "b1", "text": "O balanceamento no pf é feito com pools. Esta regra traduz a origem da rede do cliente para um pool de dois endereços, os dois gateways do firewall, e escolhe entre eles em round robin.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: round-robin"},
{"beat": "b2", "text": "Capturamos na interface que vai para o servidor para ver qual endereço de origem cada pacote carrega."},
{"beat": "b3", "text": "Seis pacotes vindo do cliente."},
{"beat": "b4", "text": "E a captura mostra a verdade: três pacotes saíram com um endereço, três com o outro. O pool está realmente rodando entre os membros, e o servidor enxerga a carga balanceada.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "3 pacotes com cada endereço de origem"},
{"beat": "b5", "text": "Às vezes você não quer rotação. O source hash mantém uma origem sempre mapeada para o mesmo membro do pool, que é o que se quer quando a sessão precisa ser sticky.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: source-hash"},
{"beat": "b6", "text": "Mais quatro pacotes, e agora todos carregam o mesmo endereço de origem. O mapeamento é estável por origem.", "highlight": ["cli", "pf0", "srv"], "diagram_caption": "todos os pacotes com a mesma origem"},
{"beat": "b7", "text": "E o dup to é o caso espelho: o pacote mantém o caminho normal e uma cópia é entregue no segundo gateway. Contamos os pedidos nas duas interfaces: um no caminho normal, um no caminho espelhado. O pacote foi duplicado, não movido.", "highlight": ["cli", "pf0", "srv", "gw2"], "diagram_caption": "dup-to: o caminho normal e a cópia espelhada"},
],
},
}
# ------------------------------------------------------------------ 06 ----
PHASES["06-qos"] = {
"en": {
"title": "Quality of service",
"subtitle": "OpenBSD 8.0 queues: hfsc classes attached to the interface",
"segments": [
{"beat": "b1", "text": "The last piece is quality of service. This is the OpenBSD eight point zero queue syntax: a root class with ten megabits of bandwidth, a leaf class with two megabits as the default, and a rule that steers the traffic from the client to that leaf class."},
{"beat": "b2", "text": "pfctl shows the queues with their statistics, and the rule set shows the queue attached to the rule."},
{"beat": "b3", "text": "Now we move real traffic through the queue. On the server we prepare two megabytes and listen on port ninety ninety five."},
{"beat": "b4", "text": "And the client downloads it, through the firewall, through the class."},
{"beat": "b5", "text": "Two million bytes transferred, and look at the queue counters: the hfsc class counted the packets and the bytes. The scheduler is in the data path, not just in the configuration."},
],
},
"pt": {
"title": "Qualidade de serviço",
"subtitle": "Filas do OpenBSD 8.0: classes hfsc anexadas à interface",
"segments": [
{"beat": "b1", "text": "A última peça é qualidade de serviço. Esta é a sintaxe de filas do OpenBSD oito ponto zero: uma classe raiz com dez megabits de banda, uma classe folha com dois megabits como default, e uma regra que direciona o tráfego do cliente para essa classe folha."},
{"beat": "b2", "text": "O pfctl mostra as filas com as estatísticas, e o ruleset mostra a fila anexada à regra."},
{"beat": "b3", "text": "Agora movemos tráfego de verdade pela fila. No servidor preparamos dois megabytes e escutamos na porta noventa e noventa e cinco."},
{"beat": "b4", "text": "E o cliente baixa isso, passando pelo firewall, passando pela classe."},
{"beat": "b5", "text": "Dois milhões de bytes transferidos, e olha os contadores das filas: a classe hfsc contou os pacotes e os bytes. O escalonador está no caminho de dados, não só na configuração."},
],
},
}
# the diagram is drawn over these phases
DIAGRAM_PHASES = {"00-ambiente", "04-roteamento", "05-balanceamento"}
for phase, langs in PHASES.items():
for lang, data in langs.items():
out = {
"phase": phase, "lang": lang,
"voice": EN_VOICE if lang == "en" else PT_VOICE,
"title": data["title"], "subtitle": data["subtitle"],
"segments": data["segments"],
}
with open(os.path.join(OUT, f"{phase}.{lang}.json"), "w") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"wrote {sum(len(v) for v in PHASES.values())} narration scripts to {OUT}")
@@ -0,0 +1,43 @@
{
"phase": "00-ambiente",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "The lab environment",
"subtitle": "Four Alpine VMs under libvirt, running a kernel built from source",
"segments": [
{
"beat": "b1",
"text": "Welcome to the PF on Linux demonstration. Before we look at the firewall itself, let me show you the environment where everything runs. These are four virtual machines managed by libvirt, all of them Alpine Linux, and the machine you are looking at is the firewall: a kernel we compiled from source, version seven point three point four, with our port of the OpenBSD packet filter built in as a module."
},
{
"beat": "b2",
"text": "The firewall has four interfaces. The management interface on the virtual network the hypervisor uses to reach us. Then three transit networks: ten point zero point one, where the client lives; ten point zero point two, where the server lives; and ten point zero point three, a second gateway we use to demonstrate routing decisions.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "three transit networks, four interfaces"
},
{
"beat": "b3",
"text": "The OpenBSD tools are installed on this machine exactly as they ship: the original pfctl, a pflog daemon and our divert tool. The module is not loaded yet, which is where we will start.",
"highlight": [
"pf0"
],
"diagram_caption": "pfctl, pflogd and pdivert, straight from OpenBSD"
},
{
"beat": "b4",
"text": "And from the firewall we can reach the other machines directly. The client is on ten point zero point one point ten, the server on ten point zero point two point twenty, and the second gateway on ten point zero point three point two. Every command in this demonstration is real traffic crossing real interfaces.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "cli, srv and gw2 reachable from pf0"
}
]
}
@@ -0,0 +1,43 @@
{
"phase": "00-ambiente",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "O ambiente de testes",
"subtitle": "Quatro VMs Alpine sob libvirt, rodando um kernel compilado do fonte",
"segments": [
{
"beat": "b1",
"text": "Bem-vindo à demonstração do PF on Linux. Antes de falar do firewall em si, deixa eu mostrar o ambiente onde tudo acontece. São quatro máquinas virtuais administradas pelo libvirt, todas com Alpine Linux, e a máquina que você está vendo é o firewall: um kernel que compilamos do fonte, versão sete ponto três ponto quatro, com o nosso port do packet filter do OpenBSD compilado dentro como módulo."
},
{
"beat": "b2",
"text": "O firewall tem quatro interfaces. A interface de gerência, na rede virtual que o hypervisor usa para falar com a gente. E depois três redes de trânsito: dez ponto zero ponto um, onde mora o cliente; dez ponto zero ponto dois, onde mora o servidor; e dez ponto zero ponto três, um segundo gateway que vamos usar para demonstrar decisões de roteamento.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "três redes de transito, quatro interfaces"
},
{
"beat": "b3",
"text": "As ferramentas do OpenBSD estão instaladas nesta máquina exatamente como são distribuídas: o pfctl original, um daemon de pflog e a nossa ferramenta de divert. O módulo ainda não está carregado, e é por aí que a gente começa.",
"highlight": [
"pf0"
],
"diagram_caption": "pfctl, pflogd e pdivert originais do OpenBSD"
},
{
"beat": "b4",
"text": "E do firewall a gente alcança as outras máquinas diretamente. O cliente está em dez ponto zero ponto um ponto dez, o servidor em dez ponto zero ponto dois ponto vinte, e o segundo gateway em dez ponto zero ponto três ponto dois. Todo comando desta demonstração é tráfego real cruzando interfaces reais.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "cli, srv e gw2 alcançáveis a partir do pf0"
}
]
}
@@ -0,0 +1,25 @@
{
"phase": "01-implementacao",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Implementation in the kernel",
"subtitle": "OpenBSD 8.0 sources plus a Linux compatibility layer equals pf.ko",
"segments": [
{
"beat": "b1",
"text": "This is the heart of the work. We did not rewrite the firewall. We took the OpenBSD eight point zero sources, nearly twenty seven thousand lines of C, and we kept them almost intact. On top of that we wrote a compatibility layer, about seven thousand lines, that translates the BSD kernel interfaces into Linux ones."
},
{
"beat": "b2",
"text": "The result is a normal Linux kernel module. Here modinfo shows us the license, the description and the vermagic, which tells us exactly which kernel this object was compiled against."
},
{
"beat": "b3",
"text": "The vermagic line matters: it is the kernel version, the compiler flags and the module signature. Load the wrong one and the kernel refuses it. Everything you will see in the next minutes is running inside that module."
},
{
"beat": "b4",
"text": "This file is what we load into the firewall. A single artifact, built out of tree, carrying the entire OpenBSD packet filter."
}
]
}
@@ -0,0 +1,25 @@
{
"phase": "01-implementacao",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "Implementação no kernel",
"subtitle": "Fontes do OpenBSD 8.0 mais uma camada de compatibilidade Linux resultam em pf.ko",
"segments": [
{
"beat": "b1",
"text": "Aqui está o coração do trabalho. A gente não reescreveu o firewall. Pegamos as fontes do OpenBSD oito ponto zero, quase vinte e sete mil linhas de C, e mantivemos praticamente intactas. Em cima disso escrevemos uma camada de compatibilidade, cerca de sete mil linhas, que traduz as interfaces de kernel do BSD para as do Linux."
},
{
"beat": "b2",
"text": "O resultado é um módulo de kernel Linux comum. Aqui o modinfo mostra a licença, a descrição e o vermagic, que nos diz exatamente com qual kernel este objeto foi compilado."
},
{
"beat": "b3",
"text": "A linha do vermagic importa: é a versão do kernel, as flags do compilador e a assinatura do módulo. Carregar o arquivo errado e o kernel recusa. Tudo que você vai ver nos próximos minutos roda dentro desse módulo."
},
{
"beat": "b4",
"text": "Este é o arquivo que carregamos no firewall. Um único artefato, compilado fora da árvore do kernel, carregando o packet filter inteiro do OpenBSD."
}
]
}
@@ -0,0 +1,29 @@
{
"phase": "02-ativacao",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Activation",
"subtitle": "insmod pf.ko, then pfctl talks to it over the OpenBSD ioctl ABI",
"segments": [
{
"beat": "b1",
"text": "First we make sure nothing is loaded, so the activation is honest. No module, no firewall."
},
{
"beat": "b2",
"text": "Now we insert it. The kernel accepts the module and the log shows our initialization message. This is the OpenBSD engine running inside Linux."
},
{
"beat": "b3",
"text": "The device appears, owned by root with mode six hundred, exactly like the BSD original, and the module shows up in the list of loaded modules."
},
{
"beat": "b4",
"text": "pfctl is the original OpenBSD tool. We enable the firewall and ask for its status. The ioctl interface, the statistics table, the rule counters, all of it is the OpenBSD ABI, unchanged."
},
{
"beat": "b5",
"text": "The ruleset is still empty, and that is exactly right: with no rules, pf blocks everything and returns. The engine is armed and waiting for instructions."
}
]
}
@@ -0,0 +1,29 @@
{
"phase": "02-ativacao",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "Ativação",
"subtitle": "insmod pf.ko e o pfctl conversa com ele pela ABI de ioctls do OpenBSD",
"segments": [
{
"beat": "b1",
"text": "Primeiro a gente garante que não tem nada carregado, para a ativação ser honesta. Sem módulo, sem firewall."
},
{
"beat": "b2",
"text": "Agora inserimos. O kernel aceita o módulo e o log mostra a nossa mensagem de inicialização. Este é o motor do OpenBSD rodando dentro do Linux."
},
{
"beat": "b3",
"text": "O dispositivo aparece, do root, com modo seiscentos, exatamente como no BSD original, e o módulo aparece na lista de módulos carregados."
},
{
"beat": "b4",
"text": "O pfctl é a ferramenta original do OpenBSD. Habilitamos o firewall e pedimos o status. A interface de ioctl, a tabela de estatísticas, os contadores das regras: tudo é a ABI do OpenBSD, sem alteração."
},
{
"beat": "b5",
"text": "O ruleset ainda está vazio, e isso está certo: sem regras, o pf bloqueia tudo e devolve. O motor está armado e esperando instruções."
}
]
}
@@ -0,0 +1,33 @@
{
"phase": "03-regras",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Basic rules",
"subtitle": "Tables, anchors, keep state and block drop",
"segments": [
{
"beat": "b1",
"text": "Now we write a real pf dot conf. This is the original OpenBSD syntax: a table with the trusted network, an anchor, and the usual quick rules with keep state. pfctl parses it and the kernel accepts it."
},
{
"beat": "b2",
"text": "pfctl shows us what the kernel understood: the rules as they were compiled, the list of tables, and the contents of the trusted table."
},
{
"beat": "b3",
"text": "Let the traffic through. From the client we ping the server, and the firewall creates a state. You can see it in the state table: the five tuple, the timeout, the counters."
},
{
"beat": "b4",
"text": "Now the same thing with a drop. We replace the pass rule with a block drop rule for the client network, and reload."
},
{
"beat": "b5",
"text": "The ping is dead. One hundred percent packet loss. And the rule counters show the packets that were evaluated and the ones that were blocked. This is a stateful firewall behaving exactly like the original."
},
{
"beat": "b6",
"text": "Finally, anchors. We load a ruleset into the anchor called demo, and pfctl lists the rules that live inside it, separately from the main ruleset."
}
]
}
@@ -0,0 +1,33 @@
{
"phase": "03-regras",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "Regras básicas",
"subtitle": "Tabelas, anchors, keep state e block drop",
"segments": [
{
"beat": "b1",
"text": "Agora escrevemos um pf ponto conf de verdade. Esta é a sintaxe original do OpenBSD: uma tabela com a rede confiável, um anchor, e as regras quick de sempre com keep state. O pfctl faz o parsing e o kernel aceita."
},
{
"beat": "b2",
"text": "O pfctl nos mostra o que o kernel entendeu: as regras como foram compiladas, a lista de tabelas e o conteúdo da tabela trusted."
},
{
"beat": "b3",
"text": "Deixa o tráfego passar. Do cliente a gente dá ping no servidor, e o firewall cria um estado. Você vê isso na tabela de estados: a tupla de cinco campos, o timeout, os contadores."
},
{
"beat": "b4",
"text": "Agora a mesma coisa com um drop. Trocamos a regra de pass por uma regra block drop para a rede do cliente, e recarregamos."
},
{
"beat": "b5",
"text": "O ping morreu. Cem por cento de perda. E os contadores da regra mostram os pacotes avaliados e os bloqueados. Este é um firewall stateful se comportando exatamente como o original."
},
{
"beat": "b6",
"text": "Por fim, os anchors. Carregamos um ruleset no anchor chamado demo, e o pfctl lista as regras que moram dentro dele, separadamente do ruleset principal."
}
]
}
@@ -0,0 +1,67 @@
{
"phase": "04-roteamento",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Routing",
"subtitle": "route-to forces the next hop, reply-to forces the way back",
"segments": [
{
"beat": "b1",
"text": "Now the routing features. This rule matches traffic from the client to the server and sends it through the second gateway, ten point zero point three point two, instead of the normal path. pfctl shows the rule exactly as the kernel stored it.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "route-to: normal path is eth2, forced path is eth3"
},
{
"beat": "b2",
"text": "To prove where the packet actually went, we capture on both interfaces at the same time: eth three, towards the gateway, and eth two, towards the server."
},
{
"beat": "b3",
"text": "And now the client pings the server.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "the request leaves through eth3, to the gateway"
},
{
"beat": "b4",
"text": "The evidence is in the captures. The request appears on eth three, on the way to the gateway, and it never appears on eth two. The firewall decided the next hop, not the routing table.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "1 request on eth3, 0 on eth2"
},
{
"beat": "b5",
"text": "The other half of the problem is the way back. reply to forces the reply of a flow to leave through a different gateway than the request came in, which is how you build asymmetric routing.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "reply-to: the answer leaves through another gateway"
},
{
"beat": "b6",
"text": "We capture eth three again and also eth one, the link where the client is."
},
{
"beat": "b7",
"text": "The replies show up on eth three, on the gateway link, and not on the client link. The request and the reply now take different paths, exactly as commanded.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "reply on eth3, nothing on the client link"
}
]
}
@@ -0,0 +1,67 @@
{
"phase": "04-roteamento",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "Roteamento",
"subtitle": "route-to força o próximo salto, reply-to força o caminho de volta",
"segments": [
{
"beat": "b1",
"text": "Agora as funcionalidades de roteamento. Esta regra casa com o tráfego do cliente para o servidor e o manda pelo segundo gateway, dez ponto zero ponto três ponto dois, em vez do caminho normal. O pfctl mostra a regra exatamente como o kernel guardou.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "route-to: o caminho normal e' eth2; o forcado, eth3"
},
{
"beat": "b2",
"text": "Para provar por onde o pacote realmente passou, capturamos nas duas interfaces ao mesmo tempo: eth três, rumo ao gateway, e eth dois, rumo ao servidor."
},
{
"beat": "b3",
"text": "E agora o cliente dá ping no servidor.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "o pedido sai por eth3, rumo ao gateway"
},
{
"beat": "b4",
"text": "A prova está nas capturas. O pedido aparece na eth três, indo para o gateway, e nunca aparece na eth dois. Quem decidiu o próximo salto foi o firewall, não a tabela de rotas.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "1 pedido em eth3, 0 em eth2"
},
{
"beat": "b5",
"text": "A outra metade do problema é o caminho de volta. O reply to força a resposta de um fluxo a sair por um gateway diferente do por onde o pedido entrou, que é assim que se constrói roteamento assimétrico.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "reply-to: a resposta sai por um gateway diferente"
},
{
"beat": "b6",
"text": "Capturamos a eth três de novo e também a eth um, o link onde está o cliente."
},
{
"beat": "b7",
"text": "As respostas aparecem na eth três, no link do gateway, e não no link do cliente. O pedido e a resposta agora seguem caminhos diferentes, exatamente como foi pedido.",
"highlight": [
"cli",
"pf0",
"gw2"
],
"diagram_caption": "resposta em eth3, nada no link do cliente"
}
]
}
@@ -0,0 +1,68 @@
{
"phase": "05-balanceamento",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Load balancing",
"subtitle": "NAT pools in round-robin and source-hash, plus dup-to",
"segments": [
{
"beat": "b1",
"text": "Load balancing in pf is done with pools. This rule translates the source of the client network into a pool of two addresses, the two gateways of the firewall, and selects between them round robin.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: round-robin"
},
{
"beat": "b2",
"text": "We capture on the interface towards the server to see which source address each packet carries."
},
{
"beat": "b3",
"text": "Six packets from the client."
},
{
"beat": "b4",
"text": "And the capture shows the truth: three packets went out with one address, three with the other. The pool is really rotating between its members, and the server sees a balanced load.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "3 packets with each source address"
},
{
"beat": "b5",
"text": "Sometimes you do not want rotation. source hash keeps a given origin always mapped to the same pool member, which is what you want when a session has to be sticky.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: source-hash"
},
{
"beat": "b6",
"text": "Four more packets, and now every single one carries the same source address. The mapping is stable per origin.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "every packet with the same source"
},
{
"beat": "b7",
"text": "And dup to is the mirror case: the packet keeps its normal path, and a copy is delivered to the second gateway. We count the requests on both interfaces: one on the normal path, one on the mirrored path. The packet was duplicated, not moved.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "dup-to: the normal path and the mirrored copy"
}
]
}
@@ -0,0 +1,68 @@
{
"phase": "05-balanceamento",
"lang": "pt",
"voice": "Portuguese_ConfidentWoman",
"title": "Balanceamento",
"subtitle": "Pools de NAT em round-robin e source-hash, mais o dup-to",
"segments": [
{
"beat": "b1",
"text": "O balanceamento no pf é feito com pools. Esta regra traduz a origem da rede do cliente para um pool de dois endereços, os dois gateways do firewall, e escolhe entre eles em round robin.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: round-robin"
},
{
"beat": "b2",
"text": "Capturamos na interface que vai para o servidor para ver qual endereço de origem cada pacote carrega."
},
{
"beat": "b3",
"text": "Seis pacotes vindo do cliente."
},
{
"beat": "b4",
"text": "E a captura mostra a verdade: três pacotes saíram com um endereço, três com o outro. O pool está realmente rodando entre os membros, e o servidor enxerga a carga balanceada.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "3 pacotes com cada endereço de origem"
},
{
"beat": "b5",
"text": "Às vezes você não quer rotação. O source hash mantém uma origem sempre mapeada para o mesmo membro do pool, que é o que se quer quando a sessão precisa ser sticky.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "pool { 10.0.2.1, 10.0.3.1 }: source-hash"
},
{
"beat": "b6",
"text": "Mais quatro pacotes, e agora todos carregam o mesmo endereço de origem. O mapeamento é estável por origem.",
"highlight": [
"cli",
"pf0",
"srv"
],
"diagram_caption": "todos os pacotes com a mesma origem"
},
{
"beat": "b7",
"text": "E o dup to é o caso espelho: o pacote mantém o caminho normal e uma cópia é entregue no segundo gateway. Contamos os pedidos nas duas interfaces: um no caminho normal, um no caminho espelhado. O pacote foi duplicado, não movido.",
"highlight": [
"cli",
"pf0",
"srv",
"gw2"
],
"diagram_caption": "dup-to: o caminho normal e a cópia espelhada"
}
]
}
@@ -0,0 +1,29 @@
{
"phase": "06-qos",
"lang": "en",
"voice": "English_ConfidentWoman",
"title": "Quality of service",
"subtitle": "OpenBSD 8.0 queues: hfsc classes attached to the interface",
"segments": [
{
"beat": "b1",
"text": "The last piece is quality of service. This is the OpenBSD eight point zero queue syntax: a root class with ten megabits of bandwidth, a leaf class with two megabits as the default, and a rule that steers the traffic from the client to that leaf class."
},
{
"beat": "b2",
"text": "pfctl shows the queues with their statistics, and the rule set shows the queue attached to the rule."
},
{
"beat": "b3",
"text": "Now we move real traffic through the queue. On the server we prepare two megabytes and listen on port ninety ninety five."
},
{
"beat": "b4",
"text": "And the client downloads it, through the firewall, through the class."
},
{
"beat": "b5",
"text": "Two million bytes transferred, and look at the queue counters: the hfsc class counted the packets and the bytes. The scheduler is in the data path, not just in the configuration."
}
]
}
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more