7.9 KiB
Changelog
v0.6.0 — 2026-11-11
Adds scrub and route-pool verification. Suite: 13 gates, all green.
| Capability | Gate | Proof |
|---|---|---|
scrub normalization |
09-scrub |
min-ttl 100 raises the TTL (egress shows 99 after the forward hop decrement); max-mss 500 clamps a SYN's MSS option — measured on the firewall's own egress |
route-to pools |
10-pools |
with two gateways on two links, round-robin uses both, source-hash pins every flow from one source to a single link |
Also: docs/{en,pt-BR}/patches.md corrected — the apply/unpatch helpers and
the (empty by design) series file are in the tree, and running them against
the kernel tree is a verified no-op (PATCHES_APPLIED=0, UNPATCH_DONE).
v0.5.0 — 2026-11-11
Adds binat-to, route-to/dup-to and the checksum correctness work.
Suite: 11 gates, all green in the libvirt lab.
Verified in the lab (new since v0.4.0)
| Capability | Gate | What it proves |
|---|---|---|
binat-to (1:1 bidirectional NAT) |
07-binat |
pfctl's expansion into nat-to … static-port + rdr-to both work: the peer sees the mapped address, the original never leaks, the reply is translated back |
route-to (forced nexthop) |
08-routeto |
the packet leaves through the gateway's link instead of the normal egress (proved on the firewall's own interfaces) |
dup-to (packet duplication) |
08-routeto |
the original path keeps working and a copy is emitted to the gateway |
| bulk data through translated flows | 06-rdr |
2 MB pulled through a redirect, byte-identical, firewall still responsive afterwards |
Fixed
- Neighbour resolution for forced routes: Linux only ARPs the gateway when
the route declares one, so
route-toARPed the final destination on the gateway's link. The port now setsrt_gw4/rt_gw_familyon the looked-up route (the same pattern OVS uses). route-tolooked the route up with the packet's source, which is not local for transit traffic →ENETUNREACH→ silent drop. Destination-only lookup now.if_output_tsoreference imbalance: the stack's output path consumed the hook queue's skb reference, so the work queue's cleanup double-freed the skb and corrupted the slab (observed as the firewall dying mid-test).- GSO/TSO checksums: materializing a transport checksum in software for a
translated skb covers only the first segment; every later segment is then
wrong, bulk transfers stall and the firewall's own
sshdstarves. The port now recomputes in software only for non-GSO skbs and handsCHECKSUM_PARTIALback to the NIC otherwise (regression guarded by gate06-rdr). - Gate
08counts only echo requests (a reply also contains the client address) and tolerates a not-yet-created capture file.
Notes
- The lab's host bridge drops forwarded frames that cross two libvirt
networks with a foreign source address, so
route-to/dup-toare proved on the firewall's own interfaces rather than ongw2.
v0.4.0 — 2026-11-11
Lab-verified milestone release. The full gate suite (9 gates) runs against Alpine VMs under libvirt on the kernel built from source.
Verified in the lab (each has a gate in tests/vm/)
| Capability | Gate | What it proves |
|---|---|---|
Engine loads, /dev/pf, real OpenBSD pfctl over the ioctl ABI |
01 |
insmod + pfctl -s info + transit ping + state created |
block / return / return-rst |
02 |
ICMP dropped, TCP refused |
pfr tables (inline { }, -T add/del/show, -T block) |
03 |
table load, block by table, delete, unblock |
| Anchors and evaluation order | 04 |
anchor child passes the request before the parent block, state created by the anchor rule, request dropped after the anchor is flushed |
nat-to (source NAT) |
05 |
tcpdump proof of the rewritten source, reply de-NATed, state shows both tuples |
rdr-to (port redirect) |
06 |
client talks to the firewall, server answers, 2 MB pulled through the redirect byte-identical, firewall still responsive afterwards |
divert-to + AF_DIVERT + pdivert |
11 |
drop with no listener (BSD divs_noport parity), packet delivered to userspace, rewritten and reinjected |
pflog + pflogd |
12 |
logged packets read from /dev/pflog and written as a DLT_PFLOG pcap containing the real bytes |
| Soak / unload discipline | 14 |
traffic + states with no kernel warnings, clean rmmod, reload, no slab growth |
Added
module/pfsrc/pfk_pflog.c—/dev/pflogchardev backend (OpenBSDpflog_hdrlayout, 64-byte records, bounded queue,poll, stats ioctl).tools/pflogd— reads/dev/pflog, writesDLT_PFLOGpcap (or a readable one-line-per-record summary).tools/pdivert— divert(4) relay with payload mutation + ICMP checksum fix.module/pfsrc/divert_sock.c—AF_DIVERTsocket family registered at runtime (PF_DIVERT = 32,IPPROTO_DIVERT = 254), blockingrecvmsg,poll, BSD-compatible reinjection.scripts/demo-session.sh+scripts/demo-record.py+make demo— asciinema recording of the live lab (out/demo-pf-linux.cast).docs/{en,pt-BR}/: README, install, build, guide, vm-lab, feature-matrix, patches, caveats, demo.docs/porting-notes.mdrecords every deviation.patches/kernel/+scripts/apply-patches.sh/scripts/unpatch.sh— kernel-core patch mechanism, empty series by design (verified: the module needs no core patch).
Fixed (all found by the VM gates, root causes documented)
skb->markbookkeeping used multi-bit values, so the "in pass done" mark also satisfied the "out pass done" test: transit traffic never reached the out ruleset (NAT and egress rules silently ignored).- The out ruleset was never evaluated for forwarded packets: added the
NF_INET_FORWARDhook (OpenBSD evaluates it inip_forward()). refcnt_rele()had inverted polarity → live state keys freed →Oopsin the state RBT andpfctl -s stateshangs.radixSALEN()read thesa_familybyte as a length (Linux sockaddrs have nosa_len) → table entries dropped as duplicates.- BSD/Linux
LIST_HEADmacro arity clash incompat/sys/queue.h. - Reverse NAT: a packet whose destination pf rewrote was delivered to the firewall's own stack instead of being re-routed.
- Checksums across translation: IP header checksum recomputed (OpenBSD's
ip_outputdoes this, Linux does not); transport checksum recomputed in software for plain skbs and handed back to the NIC (CHECKSUM_PARTIAL) for GSO/TSO — the earlier software-only fix corrupted every segment after the first and stalled bulk transfers. divert: NULL dereference on an empty listener list (wedged the VM), port byte order mismatch betweenbindand the queue, L3-only skbs injected into an Ethernet device RX path, re-divert loop (solved with a skip mark).IOC_SIZEBITS= 14 truncated the rule ioctl struct →EINVALon load.- State iteration started at id 1 (kifs never attached),
stoeplitz_init()never called, purge timer re-arming itself atrmmod,rtnl×NETlock order inversion in the interface notifier.
Known gaps
binat-to, route-to/dup-to/fail-to, af-to, scrub end-to-end
(normalization code is ported and compiles; the lab gate is not written),
queues/ALTQ (hfsc.c/fq_codel.c staged, pfq qdisc not implemented),
pfsync/CARP (out of scope). See docs/en/feature-matrix.md.
v0.3.0 — 2026-11-11
M2 data path: netisr-style workqueue, per-direction marks, state/table/anchor gates passing; divert design settled (in-module socket family, no core patch).
v0.2.0 — 2026-11-11
M1: engine compiles into pf.ko (pf.c, pf_ioctl.c with the full ioctl ABI,
pf_norm, pf_if, pf_lb, pf_table, pf_ruleset, pf_osfp, pf_syncookies, radix,
md5/sha, ifq), /dev/pf, Linux-built OpenBSD pfctl talking to it.
v0.1.0 — 2026-11-11
M0: repo, bilingual scaffold, kernel 7.3.4 build, libvirt Alpine lab with custom-kernel VMs (pf0/cli/srv/gw2), build/deploy/test scripts.