# VM validation lab (libvirt / Alpine) Português: [docs/pt-BR/vm-lab.md](../pt-BR/vm-lab.md). All commands below were read from the scripts in `scripts/` (they are the source of truth); the lab has been created and run during M0–M2 — a full-suite green run is the M2+ gate and is still in progress (see §6). ## 1. Where it lives | Host | Role | |---|---| | laptop (this repo) | builds kernel/module/tools; drives everything over ssh | | **deb** — `root@192.168.1.242` | libvirt hypervisor; runs the 4 `pftest-*` Alpine VMs | The lab never needs NAT to the outside; the only external access is the libvirt `default` network (192.168.122.0/24, mgmt+DHCP) used for cloud-init package installs and test ssh. The existing `sms-vm` on deb is never touched. ## 2. Topology (single source: `lab/ips.env`) | VM | Role | NICs | Kernel | |---|---|---|---| | `pftest-pf0` | filter under test (runs `pf.ko`) | `mgmt` (dhcp, default net), `lan` 10.0.1.1/24, `wan` 10.0.2.1/24, `wan2` 10.0.3.1/24 | stock Alpine via image-extracted direct boot; **or** the self-built 7.3.4 `bzImage` with `--pf0-kernel=` (`root=/dev/vda2 ro console=ttyS0,115200n8`, no initrd) | | `pftest-cli` | client | `mgmt` dhcp (routes disabled), `lan` 10.0.1.10/24, gw 10.0.1.1 (pf0) | Alpine stock | | `pftest-srv` | server | `mgmt` dhcp (routes disabled), `wan` 10.0.2.20/24, gw 10.0.2.1 (pf0), 10.0.3.0/24 via gw2 | Alpine stock | | `pftest-gw2` | second-path gateway (ip_forward=1) | `wan2` 10.0.3.2/24, `wan` 10.0.2.2/24 (no mgmt NIC) | Alpine stock | All VMs get `net.ipv4.ip_forward=1` and `rp_filter=0` via cloud-init, root password `pftest` plus an injected lab keypair (`/root/.ssh/pflab_ed` on deb, copy on the laptop for ProxyJump), and packages `iperf3 tcpdump python3 scapy bash iptables` (Alpine name is `scapy`; the image also needs `iptables` for pf0 masquerade experiments). libvirt networks: `default` (kept as-is, started+autostarted for packages) and isolated bridges `pf-lan` 10.0.1.0/24, `pf-wan` 10.0.2.0/24, `pf-wan2` 10.0.3.0/24 — `forward="none"`, no DHCP, plus host-side bridge IPs `10.0.x.254` on deb so deb can ssh into every guest's data NIC. VM sizing: 2 vCPU / 1 GB each. Disks are per-VM qcow2 **backing** a shared immutable `/var/lib/libvirt/images/alpine-base.qcow2` (synced from `_vendor/images/alpine-3.22-x86_64-cloud.qcow2` by sha256). > Boot note: the Alpine cloud image is not BIOS/UEFI bootable in QEMU as > delivered (empty ESP), so *all* VMs use direct kernel boot from the image's > own extracted kernel; `--pf0-kernel=` only swaps pf0's kernel for the > built one. ## 3. Recreate / deploy / destroy ```sh ./scripts/vm-create.sh # idempotent: tears down and rebuilds pftest-* domains + seeds ./scripts/vm-create.sh --pf0-kernel=out/linux-7.3.4-bzImage # pf0 boots the built kernel (T6) ./scripts/vm-create.sh --recreate-base # force base-image re-sync ./scripts/vm-deploy.sh # laptop→deb ($DEPLOY_DIR)→pf0:/opt/pf: # out/*bzImage, module/*.ko, tools/out/* ./scripts/vm-destroy.sh # domains, disks, seeds, pf-* nets, ssh fragments ./scripts/vm-destroy.sh --purge-base --purge-keys # + base image + lab keypair ``` `vm-create.sh` ends by validating `ssh pf0 'ip -br a'` and leaves all VMs running. Laptop ssh aliases (`pf0|cli|srv|gw2`, ProxyJump via deb) are generated into `~/.ssh/config.d/pflab`. ## 4. Running the suite ```sh ./scripts/vm-run-tests.sh # or: make test ``` Deterministic by design: **before every gate** the runner destroys and restarts `pftest-pf0` (fresh disk delta + no leftover module), waits for ssh on the mgmt DHCP address `192.168.122.8`, re-deploys `pf.ko`/`pfctl`/`*.conf` into the guest, then runs the gate script from the **laptop** (gates talk to the VMs through deb with nested ssh — `tests/vm/lib.sh`). Per-gate output is captured to `out/test-logs/.sh.log`; the runner prints `PASS ` / `FAIL (rc=...)` per test, a summary `PASS=n FAIL=m`, and exits non-zero if anything failed. ## 5. What each gate proves today | Gate | Scenario (asserted with real traffic, not exit codes) | |---|---| | `01-state.sh` | `pass in/out quick proto icmp keep state` → cli pings srv **through** pf0 (transit); pf0 then lists an icmp state via `pfctl -s states` | | `02-block.sh` | `block drop in quick proto icmp` → cli ping fails; `block return-rst in quick proto tcp … port 8080` → cli's `nc` to srv:8080 is refused (RST), checked via nc exit | | `03-tables.sh` | `table { 10.0.1.10 }` + `block in quick from ` → `pfctl -t blk -T show` lists it; cli ping blocked; live `-T delete` unblocks the same flow | | `04-anchors.sh` | anchor rule (`-a restricted -f`) passing icmp is evaluated **before** a later main-ruleset `block` (quick/top-placement); `pfctl -s Anchors` lists the anchor | Planned gates per the plan document (T11–T18): 05-nat, 06-rdr, 07-binat, 08-scrub (scapy), 09-routeto, 10-dup-fail, 11-divert, 12-queues (iperf rate tolerance ±15%), 13-pflog, 14-soak (reload under traffic + rmmod cycle + kmemleak). IPv6 is smoke-only for state/block/nat66/scrub. `bench-basic.sh` (planned, not in the tree yet) is informational; there is no numeric perf gate. ## 6. Current suite status (2026-11-11, honestly) The M2 gates 01–04 exist and run, but **are not all green yet** — the data path (netisr workqueue model) and state visibility are being fixed in the M2 gate work. The latest captured run (`out/test-logs/`) shows the mixed state: transit ping OK / state listing failing on 01, icmp-blocked OK on 02, partial results on 03, 04 aborted. Treat "VERIFIED-in-VM" in the [feature matrix](feature-matrix.md) as reserved for gates that pass in a full clean run; nothing beyond M1 is claimed VERIFIED until then. ## 7. Manual poking ```sh ssh pf0 # via laptop alias (ProxyJump deb) insmod /opt/pf/pf.ko /opt/pf/pfctl -e /opt/pf/pfctl -f /tmp/g1.conf /opt/pf/pfctl -s info ; /opt/pf/pfctl -s states -v dmesg | grep pfk ``` Artifacts of future full runs (log + pcaps) are meant to ship with the Gitea release (spec §7).