diff --git a/docs/en/caveats.md b/docs/en/caveats.md index 14c6c84..6922aaa 100644 --- a/docs/en/caveats.md +++ b/docs/en/caveats.md @@ -70,40 +70,46 @@ exactly when the host is out of atomic memory. conntrack — as designed (spec §4.2). - Output (`LOCAL_OUT` at `NF_IP_PRI_CONNTRACK+1`): PF sees locally generated traffic **after** conntrack has already committed the entry. -- Transit egress (`POSTROUTING` at `NF_IP_PRI_LAST-1`): PF runs at the very - end — after any iptables/nft SNAT/masquerade — so `pass out … nat-to`-era - rules (M3) will be validated against this ordering. The "unregister-safe - ordering vs conntrack" warning from the design still applies: overlapping - policy between PF and conntrack is possible and yours to manage. +- Transit egress: PF evaluates the **out** ruleset at `FORWARD` (OpenBSD does + the same in `ip_forward()`), and `POSTROUTING` is skipped once that pass is + done. `nat-to`/`rdr-to` were validated against this ordering (gates 05/06). +- The "overlapping policy with conntrack" warning from the design still + applies: if iptables/nft SNAT also runs, PF's translation happens at the + layer it is hooked into, and double translation is yours to avoid. ## Control plane / lifecycle -### 6. Unload is hardened; the soak is still pending +### 6. Unload is hardened and soak-verified `rmmod pf` deregisters `/dev/pf`, zeroes `pf_status.running` under the NET lock, deletes **and syncs** both purge timers (they self-rearm — a real rmmod hang was observed and fixed by stopping sources before flushing the -workqueue), unregisters hooks and flushes `pfk_netisr`. That fix is in-tree, -but the full cycle under traffic + live states (T18/soak gate) has **not** -been run to green yet. For now treat unload as admin-safe, automation- -unproven. +workqueue), unregisters hooks and flushes `pfk_netisr`. Gate `14-soak.sh` +drives ICMP+TCP traffic, checks `dmesg` for faults, then runs +`rmmod` → `insmod` → `rmmod` and compares the slab footprint before and +after: clean unload, successful reload, no growth. Unload is therefore +verified under traffic; hot-reload automation (reload while a burst is in +flight) is still not a supported workflow. -## Behavioral / rendering issues open today +## Behavioral notes verified today -### 7. `pfctl -s states` visibility anomaly (M2 gate work) +### 7. `pfctl -s states` lists real states, including translations -In the most recent captured run (gate 01): transit ping **passed** under -`keep state`, but no icmp state was listed afterwards. Fixing this is the -open item of the M2 gate work; until a green run lands, the -[state-listing row](feature-matrix.md) is the honest status. Not cosmetic if -it turns out states are not being inserted — treat both possibilities until -the gate says otherwise. +The earlier anomaly was a use-after-free: `refcnt_rele()` had inverted +polarity, so live state keys were freed while still linked in the state +RBT — that both hid states and crashed the tree walk (`pfctl -s states` +hanging the box). After the fix, gates 01/04/05/06/07 assert on the listed +states, and translated flows print both tuples (`10.0.2.1:59335 +(10.0.1.10:4380) -> 10.0.2.20:8`). -### 8. Silent-but-warned stubs +### 8. Consumers still required for two features -`divert` verdicts drop with a rate-limited warning; `log` rules log nothing -with a one-time warning. Both are listed so nobody mistakes "rules loaded" -for "features active" (see [pf-guide.md](pf-guide.md)). +`divert-to` needs a process bound to the divert port (`tools/pdivert`); with +no listener the packet is dropped, exactly like BSD's `divs_noport`, and the +kernel prints `pfk: divert port N: no socket bound`. `log` needs +`tools/pflogd` reading `/dev/pflog`; without it the queue fills to its bound +(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)). ## Compat-layer deviations (from `docs/porting-notes.md`, M1 record) diff --git a/docs/pt-BR/caveats.md b/docs/pt-BR/caveats.md index 29fc389..b54b3b4 100644 --- a/docs/pt-BR/caveats.md +++ b/docs/pt-BR/caveats.md @@ -72,15 +72,16 @@ PF são best-effort exatamente quando o host está sem memória atômica. 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 (`POSTROUTING` em `NF_IP_PRI_LAST-1`): o PF roda no fim - da linha — depois de qualquer SNAT/masquerade do iptables/nft — então as - regras `pass out … nat-to` da 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. +- 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; o soak ainda está pendente +### 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 — @@ -90,22 +91,25 @@ 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 +## Notas de comportamento verificadas hoje -### 7. Anomalia de visibilidade do `pfctl -s states` (trabalho do gate M2) +### 7. `pfctl -s states` lista estados reais, incluindo traduções -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](feature-matrix.md) é 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. +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. Stubs silenciosos-mas-avisados +### 8. Ainda são necessários consumidores -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](pf-guide.md)). +`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](pf-guide.md)). ## Desvios da camada de compat (de `docs/porting-notes.md`, registro do M1) diff --git a/tests/vm/08-routeto.sh b/tests/vm/08-routeto.sh new file mode 100755 index 0000000..c2c33f9 --- /dev/null +++ b/tests/vm/08-routeto.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# M4 gate: route-to / dup-to — forced forwarding path (pf_route). +# +# Lab: cli (10.0.1.10) pings srv (10.0.2.20). Without the rule pf0 forwards it +# straight out eth2. With `route-to { 10.0.3.2 }` the packet must be pushed to +# gw2 instead - proof: gw2 captures it and the server does not see it. +# Then `dup-to { 10.0.3.2 }`: the ORIGINAL path keeps working (server sees the +# packet) AND gw2 receives a copy. +set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh + +pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true' +pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null + +GW2_DEV=$(gw2 "ip route get $CLI_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2") +[ -n "$GW2_DEV" ] || GW2_DEV=eth1 + +capture_gw2() { gw2 "(tcpdump -ni $GW2_DEV -c 4 -e icmp > /tmp/gw2.td 2>&1 &)" 2>/dev/null; } +capture_srv() { srv "(tcpdump -ni eth1 -c 4 -e icmp > /tmp/srv.td 2>&1 &)" 2>/dev/null; } +sleep 1 + +# --- route-to: the packet is diverted to gw2 and never reaches the server --- +cat > /tmp/g08a.conf </dev/null 2>&1" +sleep 2 +gw2 'cat /tmp/gw2.td' | grep -q "$SRV_IP" && ok "route-to delivered the packet to gw2" \ + || bad "gw2 did not see the routed packet" +srv 'cat /tmp/srv.td' | grep -q "$CLI_IP" && bad "server saw the packet (route not forced)" \ + || ok "normal egress bypassed" + +# --- dup-to: original path still works and gw2 gets a copy --- +cat > /tmp/g08b.conf </dev/null 2>&1" && ok "dup-to did not break the flow" \ + || bad "dup-to broke the transit ping" +sleep 2 +srv 'cat /tmp/srv.td' | grep -q "$CLI_IP" && ok "original path delivered" || bad "server missed the packet" +gw2 'cat /tmp/gw2.td' | grep -q "$SRV_IP" && ok "duplicate reached gw2" || bad "no duplicate at gw2" + +pf0 '/opt/pf/pfctl -d' >/dev/null +finish