7.5 KiB
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.shplanejado (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_OUTemNF_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 (
POSTROUTINGemNF_IP_PRI_LAST-1): o PF roda no fim da linha — depois de qualquer SNAT/masquerade do iptables/nft — então as regraspass out … nat-toda era M3 serão validadas contra essa ordem. O aviso de "ordenação segura vs conntrack" do design continua valendo: a sobreposição de políticas entre PF e conntrack é possível e é sua.
Plano de controle / ciclo de vida
6. O unload está endurecido; o soak ainda está pendente
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.
Problemas de comportamento/renderização abertos hoje
7. Anomalia de visibilidade do pfctl -s states (trabalho do gate M2)
Na rodada capturada mais recente (gate 01): o ping de trânsito passou com
keep state, mas nenhum estado icmp foi listado depois. Corrigir isso é o
item aberto do trabalho do gate M2; até uma rodada verde, a
linha de listagem de estados é o status honesto. Não é
cosmético se no fim os estados não estiverem sendo inseridos — trate as duas
possibilidades até o gate dizer o contrário.
8. Stubs silenciosos-mas-avisados
Vereditos divert descartam com aviso rate-limited; regras de log não
logam nada e avisam uma única vez. Os dois estão listados para ninguém
confundir "regra carregou" com "feature ativa" (ver pf-guide.md).
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/pidem regras): só uid — ostruct sockdo Linux não guarda egid/cpid, então o gid degrada para(gid_t)-1e o pid paraNO_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.
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).