From 6cc4e21ae250d5cb435abf944ffd6d5a4e8f211b Mon Sep 17 00:00:00 2001 From: lgcosta Date: Wed, 11 Nov 2026 20:33:27 -0300 Subject: [PATCH] docs+demo: asciinema recording of the lab (stateful filtering, tables, drop, NAT, divert); README/matrix reflect the verified state --- README.md | 18 +++++++++------- docs/en/feature-matrix.md | 2 +- docs/pt-BR/feature-matrix.md | 2 +- scripts/demo-session.sh | 41 ++++++++++++++++++++++++++++-------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 88da7b5..fbc29ef 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ out-of-tree kernel module — the real PF engine (`pf.c`, `pf_norm.c`, real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. (A second module for the `pfq` qdisc arrives with M5.) -**Status:** early milestones. The engine port and control plane (M1) are -working; the M2 filter core is being gated in the VM lab; everything later -is pending. Honest per-feature status: +**Status:** the filter core, stateful filtering, tables, anchors, NAT and +`divert` are **verified in a real libvirt lab** (7/7 gates green). The +remaining features (route-to/dup-to, pflog, ALTQ/pfq, 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). 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). @@ -19,11 +21,11 @@ Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/super |---|---|---| | 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 | **in progress** — data path implemented, gates not all green yet | -| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending | -| M4 | route-to/dup-to/fail-to, divert e2e | pending | +| M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **done** — gates 01/02/03/04 green | +| M3 | NAT (`nat-to`), scrub e2e, pflog | **partial** — `nat-to` verified (gate 05); rdr/binat, scrub e2e and pflog pending | +| M4 | route-to/dup-to/fail-to, divert e2e | **partial** — divert e2e verified (gate 11); route-to/dup-to pending | | M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | pending | -| M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs) | +| M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs done, suite 7/7 green) | ## What you get (planned) @@ -34,7 +36,7 @@ Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/super | 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 | -| Divert | `divert-to` / `divert` tag + `pdivert` relay over an in-module `PF_DIVERT` socket family (pending — see `docs/en/feature-matrix.md`) | +| Divert | `divert-to` / `divert` tag + `pdivert` relay over an in-module `PF_DIVERT` socket family (verified: packet to userspace and back) | | Route | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` | | Logging | `pflogd` via netlink pflog channel (DLT_PFLOG pcap) | | Control | `/dev/pf` (0600), OpenBSD 8.0 ioctl ABI, Linux build of `pfctl` (single binary) | diff --git a/docs/en/feature-matrix.md b/docs/en/feature-matrix.md index acc827d..e815907 100644 --- a/docs/en/feature-matrix.md +++ b/docs/en/feature-matrix.md @@ -48,7 +48,7 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`, | Feature | Upstream syntax | Milestone | Status | |---|---|---|---| -| `nat-to` (port pools, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | PENDING-M3 (`pf.c`/`pf_lb.c` compiled; wiring + VM gate in M3) | +| `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` | `rdr pass on wan proto tcp from any to any port 8080 -> srv port 80` | M3 | PENDING-M3 | | `binat-to` | `binat-to …` | M3 | PENDING-M3 | | 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) | diff --git a/docs/pt-BR/feature-matrix.md b/docs/pt-BR/feature-matrix.md index 3ff86a9..7afb3eb 100644 --- a/docs/pt-BR/feature-matrix.md +++ b/docs/pt-BR/feature-matrix.md @@ -48,7 +48,7 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`, | Feature | Sintaxe de upstream | Milestone | Status | |---|---|---|---| -| `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | PENDING-M3 (`pf.c`/`pf_lb.c` compilados; fiação + gate VM no M3) | +| `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` | `rdr pass on wan proto tcp from any to any port 8080 -> srv port 80` | M3 | PENDING-M3 | | `binat-to` | `binat-to …` | M3 | PENDING-M3 | | 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) | diff --git a/scripts/demo-session.sh b/scripts/demo-session.sh index cd6ef80..06a9d70 100755 --- a/scripts/demo-session.sh +++ b/scripts/demo-session.sh @@ -3,27 +3,49 @@ # 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 ssh aliases -# (pf0/cli/srv) come from ~/.ssh/config.d/pflab. +# 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# $*" - ssh pf0 "$*" 2>&1 | sed 's/^/ /' + onvm "$PF0_MGMT" "$@" sleep 0.4 } vmc() { cyan "pftest-cli# $*" - ssh cli "$*" 2>&1 | sed 's/^/ /' + onvm "$CLI_MGMT" "$@" sleep 0.4 } + CONF=/tmp/demo-pf.conf DIV=/tmp/demo-divert.conf @@ -49,11 +71,12 @@ pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 1000 pass out quick CONF -EGRESS_IF=$(ssh pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" 2>/dev/null | tr -d '\r') +# 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 "$CONF" "$DIV" pf0:/tmp/ 2>/dev/null -scp -q tools/out/pdivert pf0:/tmp/pdivert 2>/dev/null +scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \ + tools/out/pdivert "root@$PF0_MGMT:/tmp/" 2>/dev/null bold "===============================================================" bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel" @@ -83,7 +106,7 @@ echo bold "--- 4. transit traffic is allowed and stateful ---" vmc "ping -c2 -W2 $SRV_IP | tail -3" -p0 "/opt/pf/pfctl -ss" +p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4" echo bold "--- 5. tables are live objects ---" @@ -111,7 +134,7 @@ 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" +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