diff --git a/docs/en/feature-matrix.md b/docs/en/feature-matrix.md index c887710..1519e37 100644 --- a/docs/en/feature-matrix.md +++ b/docs/en/feature-matrix.md @@ -51,7 +51,7 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`, | `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 port 8080 rdr-to 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 static-port` + `pass in … rdr-to `), 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 on all scrub (max-mss 1380 …)` | M3 | COMPILES-untested (`pf_norm.c` compiled into pf.ko and reached from `pf_test`; scapy gate 08 pending) | +| 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) | ## Routing / divert / queues / logging diff --git a/docs/en/pf-guide.md b/docs/en/pf-guide.md index 3902f51..45abca5 100644 --- a/docs/en/pf-guide.md +++ b/docs/en/pf-guide.md @@ -61,8 +61,7 @@ treat them as load-and-pray until the matrix moves them. | 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 ()` form. `binat-to` is expanded by pfctl into a `nat-to … static-port` + `rdr-to` pair (gate `07-binat`). | -| `scrub (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. | -| `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` | — | **Verified in the lab** (gate `08-routeto.sh`): the forced gateway is used instead of the normal egress, and `dup-to` copies while keeping the flow. `fail-to`/`reply-to` load but have no gate yet. | | `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**. | | `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. | diff --git a/docs/pt-BR/feature-matrix.md b/docs/pt-BR/feature-matrix.md index 32cfdc5..d9f49db 100644 --- a/docs/pt-BR/feature-matrix.md +++ b/docs/pt-BR/feature-matrix.md @@ -51,7 +51,7 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`, | `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 port 8080 rdr-to 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 static-port` + `pass in … rdr-to `), e as duas metades funcionam: o peer vê o endereço mapeado, o original não vaza, e a resposta é traduzida de volta. | -| Scrub/normalize: remontagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match on all scrub (max-mss 1380 …)` | M3 | COMPILES-untested (`pf_norm.c` compilado no pf.ko e alcançado pelo `pf_test`; gate scapy 08 pendente) | +| 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) | ## Roteamento / divert / filas / logging diff --git a/docs/pt-BR/pf-guide.md b/docs/pt-BR/pf-guide.md index 9f156de..da7fe05 100644 --- a/docs/pt-BR/pf-guide.md +++ b/docs/pt-BR/pf-guide.md @@ -64,8 +64,7 @@ matriz mudar. | 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 ()`. O `binat-to` é expandido pelo pfctl num par `nat-to … static-port` + `rdr-to` (gate `07-binat`). | -| `scrub (…)`, `set reassemble` | M3 | O `pf_norm.c` roda dentro do `pf_test` (compilado) — o comportamento com fragmentos forjados não foi testado; trate o resultado como não validado. | -| `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. | +| `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` | — | **Verificado em lab** (gate `08-routeto.sh`): o gateway forçado é usado em vez do egress normal, e `dup-to` copia mantendo o fluxo. `fail-to`/`reply-to` carregam mas ainda sem gate. | | `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**. | | `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. | diff --git a/tests/vm/09-scrub.sh b/tests/vm/09-scrub.sh new file mode 100755 index 0000000..ce55412 --- /dev/null +++ b/tests/vm/09-scrub.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# M3 gate: scrub - normalization actually rewrites packets. +# +# 8.0 syntax: scrub is a rule option ("match in all scrub (...)"), not a +# standalone rule. Proof is taken on pf0's own egress (the lab's host bridge +# filters forwarded frames between networks, so peer-side captures are not +# reliable): +# min-ttl 100 -> a transit packet with ttl 64 leaves with ttl 100 +# max-mss 500 -> a TCP SYN with MSS 1460 leaves advertising MSS 500 +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 + +IF_SRV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2") +[ -n "$IF_SRV" ] || IF_SRV=eth2 + +cat > /tmp/g09.conf < 99). +pf0 "pkill tcpdump; (tcpdump -ni $IF_SRV -c 3 -vv icmp > /tmp/scr1.td 2>&1 &); sleep 1" +cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" +sleep 2 +N=$(pf0 "grep -c 'ttl 99' /tmp/scr1.td 2>/dev/null; true" | tr -dc '0-9') +if [ "${N:-0}" -gt 0 ]; then + ok "min-ttl applied (ttl raised to 100, forwarded as 99)" +else + DIAG=$(pf0 "grep -o 'ttl [0-9]*' /tmp/scr1.td 2>/dev/null | head -2" | tr '\n' ' ') + bad "min-ttl not applied: $DIAG" +fi + +# --- max-mss on a TCP SYN --- +pf0 "(tcpdump -ni $IF_SRV -c 3 -vv tcp port 9092 > /tmp/scr2.td 2>&1 &); sleep 1" +cli "nc -w1 -z $SRV_IP 9092 >/dev/null 2>&1" +sleep 2 +if pf0 "grep -q 'mss 500' /tmp/scr2.td" 2>/dev/null; then + ok "max-mss clamped to 500" +else + DIAG=$(pf0 "grep -o 'mss [0-9]*' /tmp/scr2.td 2>/dev/null | head -2" | tr '\n' ' ') + bad "mss not clamped: $DIAG" +fi + +pf0 'pkill tcpdump; /opt/pf/pfctl -d' >/dev/null +finish