Files
pf-linux-port/docs/pt-BR/caveats.md
T
lgcosta 0a8ba69340 docs: record the stress findings and the 16-gate suite
caveats §13 documents the capacity ceiling the stress test found: the
workqueue backlog grows with the offered rate and OOM-panics a 1 GB guest
above ~1 Gbit/s. Gate count, suite status and the doc indexes move to
16/16 green.
2026-11-12 20:11:12 -03:00

13 KiB
Raw Blame History

Ressalvas — divergências do comportamento do OpenBSD

English: docs/en/caveats.md.

Estas são as formas pelas quais este port difere de rodar o mesmo ruleset no OpenBSD 8.0, ou ainda não tem forma de produção. Tudo aqui tem base no código (module/pfsrc/pfk_hooks.c, module/pfsrc/pfk_mod.c, docs/porting-notes.md) e em observações da bancada; nada é especulação.

Caminho de dados

1. O PF roda em contexto de processo via workqueue (modelo netisr), não no caminho rápido do pacote

O OpenBSD avalia o pf_test() na CPU que recebeu o pacote, em IPL_NET, com uma lock de leitura NET que não dorme. O Linux nos dá contexto de softirq e locks da compat sleepáveis (rw-semaphores), então o hook nunca chama o pf_test() inline: ele furta a skb (NF_STOLEN) para uma workqueue ordenada (pfk_netisr, WQ_MEM_RECLAIM), roda o engine em contexto de processo e então reinjeta (netif_rx() na entrada, dst->output() na saída/trânsito).

Consequências, todas observáveis:

  • Vereditos são assíncronos. Qualquer regra encadeada depois do nosso hook na mesma chain netfilter (outras regras iptables/nft em prioridades maiores) não vê o pacote até o passe reinjetado chegar. Contadores e intercalação de log mudam na mesma medida.
  • Latência + vazão: cada pacote paga um ida-e-volta pela workqueue, serializado num único worker ordenado. Os gates funcionais são a régua atual; o bench-basic.sh planejado (ainda não está na árvore) é só informativo.
  • Reordenação: a fila ordenada preserva a ordem de chegada dos itens enfileirados, mas o stack em volta (RPS, multi-NAPI) ainda pode nos entregar pacotes cuja ordem original de conclusão difere da ordem de reinjeção.

2. A proteção contra reentrada é um tag por direção no skb->mark

O sobrevivente reinjetado carrega skb->mark |= PFK_DONE_IN (0x50460010) ou PFK_DONE_OUT (0x50460020); nossos hooks aceitam qualquer pacote já marcado para aquela direção em vez de re-enfileirar (senão PF_IN → netif_rx → PF_IN… entraria em loop). O skb->mark é um campo compartilhado e gravável de 32 bits: qualquer subsistema que o sobrescreva — meta mark set do nft, CONNMARK --restore, filtros de tc, alguns stacks de VPN — pode (a) limpar o nosso tag e fazer o pacote ser re-enfileirado pelo PF de novo, ou (b) acidentalmente ligar esses bits e deixar o pacote pular um passe do PF. Regra prática num host com PF-on-Linux: não use mangling de fwmark no mesmo tráfego; se precisar, mascare os bits do tag do PF no seu espaço de mark.

3. Sem lock NET no caminho de dados (interino do M2)

O pf_test() roda hoje sem a lock de leitura NET da compat (comentário em pfk_hooks.c): com o modelo de workqueue, um escritor de controle parqueado (mudança de ruleset, ioctl de purge) esfomeava o lado dos pacotes a ponto de ser contraproducente. A mutação do ruleset e a caminhada de pacotes rodam, portanto, concorrentemente, protegidas pelo que o próprio pf.c do upstream usa (PF_LOCK/padrões tipo RCU) — um desvio interino deliberado, sendo retrabalhado junto com o modelo de locks no M5+. Até lá: espere correção em regime estável, não sob reload concorrente agressivo; o soak de reload sob tráfego é um gate aberto.

4. Fail-open sob pressão de memória

O item da fila é um kmalloc(GFP_ATOMIC) em softirq. Se falhar, o hook retorna NF_ACCEPT — o pacote passa por fora do PF. É uma escolha deliberada de disponibilidade (espelha "cai numa fila cheia = bypass do qdisc"), mas é um trade-off de segurança que vale conhecer: os vereditos do PF são best-effort exatamente quando o host está sem memória atômica.

5. A ordem dos hooks vs conntrack varia por direção

  • Entrada (PREROUTING, NF_IP_PRI_FIRST): o PF roda antes do defrag e do conntrack — como projetado (spec §4.2).
  • Saída (LOCAL_OUT em NF_IP_PRI_CONNTRACK+1): o PF vê o tráfego gerado localmente depois de o conntrack já ter confirmado a entrada.
  • Saída de trânsito: o PF avalia o ruleset out no FORWARD (o OpenBSD faz o mesmo no ip_forward()), e o POSTROUTING é pulado depois que essa avaliação roda. nat-to/rdr-to foram validados contra essa ordem (gates 05/06).
  • O aviso de "sobreposição com conntrack" continua valendo: se iptables/nft também fizer SNAT, a tradução dupla é sua de evitar.

Plano de controle / ciclo de vida

6. O unload está endurecido e verificado sob soak

O rmmod pf desregistra /dev/pf, zera pf_status.running sob a lock NET, deleta e sincroniza os dois timers de purge (eles se rearmam sozinhos — um travamento real de rmmod foi observado e corrigido parando as fontes antes de drenar a workqueue), desregistra os hooks e drena o pfk_netisr. A correção está na árvore, mas o ciclo completo sob tráfego + estados vivos (gate de soak T18) ainda não rodou verde. Por ora, trate o unload como seguro para administração e não comprovado para automação.

Notas de comportamento verificadas hoje

7. pfctl -s states lista estados reais, incluindo traduções

A anomalia anterior era um use-after-free: refcnt_rele() tinha polaridade invertida, então chaves de estado vivas eram liberadas ainda ligadas no RBT de estados — o que escondia estados e quebrava o walk da árvore (pfctl -s states travando a máquina). Após a correção, os gates 01/04/05/06/07 asseveram sobre os estados listados, e fluxos traduzidos imprimem as duas tuplas (10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8).

8. Ainda são necessários consumidores

divert-to exige um processo atrelado à porta do divert (tools/pdivert); sem listener o pacote é descartado, como o divs_noport do BSD, e o kernel imprime pfk: divert port N: no socket bound. log exige tools/pflogd 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).

9. Filas: caminho verificado; a semântica de modelagem é do HFSC, não um bug

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 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.

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)

Estes são desvios deliberados e marcados no código; importam ao raciocinar sobre a semântica das regras:

  • Match pelo dono do socket (user/pid em regras): só uid — o struct sock do Linux não guarda egid/cpid, então o gid degrada para (gid_t)-1 e o pid para NO_PID.
  • Checksums: o offload do driver não é confiável na entrada (decisão do M1) — checksums TCP são recalculados em software; os pseudo-checksums de saída são feitos no caminho próprio do port.
  • Opções IP / record-route / timestamps: não processadas no caminho de saída do PF; redirecionamentos ICMP não são gerados pelo código de rota do PF (o FIB do Linux emite).
  • Labels de rota: não-objetivo — a camada de ioctl rejeita regras com label; rtable_iterate() sempre retorna NULL (sem iteração de uRPF multipath).
  • Grupos de interface: o Linux não tem; matches group … se comportam como um grupo inexistente no OpenBSD.
  • Skbs GSO/multi-segmento: tratadas por linearização na entrada do shim mbuf (custo de performance documentado no spec §10; os gates vão quantificar).
  • Instância única: um mundo PF em init_net, como o OpenBSD tem um kernel. Containers/namespaces não são visíveis ao PF e não podem ser filtrados por ele neste design.

Pipeline dos screencasts (2026-11-12)

10. A geração de vídeo do MiniMax não está habilitada nesta conta

Todos os modelos de vídeo do MiniMax (T2V-01, T2V-01-Director, MiniMax-Hailuo-02) respondem com o erro de API 2068 — "The current Token Plan tier does not support this model". A narração (TTS) e a arte do cartão de título funcionam normalmente; só os modelos de vídeo estão bloqueados. Por isso as transições entre fases são renderizadas localmente com ffmpeg (um crossfade sobre um cartão de capítulo) em vez de clipes gerados por IA. Mudar isso exige só subir o tier da conta — o ponto de extensão é scripts/screencast/join.py, que constrói os cartões.

11. A narração é uma trilha separada, não uma dublagem do vídeo

Cada fase é gravada uma vez e serve aos dois idiomas: a sessão de terminal é neutra em relação ao idioma, e as duas versões narradas diferem apenas na trilha de áudio e nas legendas queimadas. O gravador segura cada batida na tela pelo tempo que o segmento de narração mais longo (EN ou PT) precisa, então a voz e a ação no terminal ficam em sincronia nos dois.

12. O que os vídeos não mostram

Os vídeos são uma demonstração, não uma rodada de teste: a suíte de gates do lab (scripts/vm-run-tests.sh) é a autoridade para PASS/FAIL, e os logs dela ficam em out/test-logs/. Os screencasts mostram os mesmos comandos, mas um quadro de vídeo não é um veredito.

Achados de capacidade (2026-11-12, a partir de scripts/stress/run-stress.sh)

13. O backlog da workqueue cresce com a taxa oferecida: acima de ~1 Gbit/s a VM dá OOM-panic

Esta é a consequência mais dura da §1 e ela é reproduzível, não teórica. O PF rouba cada skb para uma única workqueue ordenada e roda o engine em contexto de processo, então o número de pacotes em voo só é limitado pela memória que o backlog consome.

Medido no lab (guest Alpine, 1 GB de RAM, 4 fluxos TCP por um ruleset stateful):

Taxa oferecida (por fluxo × 4) Resultado
50 Mbit/s (200 Mbit/s no total) estável
100 Mbit/s (400 Mbit/s no total) estável
250 Mbit/s (1 Gbit/s no total) kernel panic: "System is deadlocked on memory"

O panic é um OOM sem processo matável; o slab naquele momento mostra kmalloc-8k ≈ 50 MB, skbuff_head_cache ≈ 18 MB e files_cache ≈ 40 MB — ou seja, o backlog comeu o 1 GB. A VM reinicia (watchdog de 120 s) e todas as conexões que passavam pelo firewall caem junto.

Consequências práticas:

  • Limite a taxa que você aponta para o PF e dimensione a máquina para o backlog: a memória necessária cresce com a taxa oferecida, não com o número de fluxos concorrentes.
  • Por isso o teste de stress (tests/vm/16-stress.sh) roda as fases de tráfego a uma taxa limitada (padrão: 100 Mbit/s por fluxo = 400 Mbit/s no total) — o regime que este port suporta — e reporta o teto separadamente, em vez de dar OOM no firewall sob teste.
  • Os gates funcionais (01-15) não são afetados: eles movem megabytes, não gigabits por segundo.

O que resolveria isso é um fast path de verdade (avaliar pf_test() inline em softirq, como o OpenBSD faz) ou uma fila limitada com política de drop — as duas são mudanças de design fora do escopo deste port.

O que não é ressalva

ARP e outros EtherTypes não-IPv4/IPv6 nunca entram nos hooks netfilter de IPv4/IPv6 — passam pela máquina intactos, exatamente como o PF no OpenBSD ignora não-IP. O /dev/pf é 0600 somente-root por projeto (equivalente ao padrão do pfctl no OpenBSD da era securelevel).