Files
pf-linux-port/CHANGELOG.md
T

8.2 KiB
Raw Permalink Blame History

Changelog

v0.6.0 — 2026-11-11

Adds scrub, route pools and reply-to verification. Suite: 14 gates, all green.

Capability Gate Proof
reply-to (asymmetric reply path) 13-replyto a flow's reply is forced onto the gateway's link and never uses the client's link (measured on the firewall's own interfaces)
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-to ARPed the final destination on the gateway's link. The port now sets rt_gw4/rt_gw_family on the looked-up route (the same pattern OVS uses).
  • route-to looked the route up with the packet's source, which is not local for transit traffic → ENETUNREACH → silent drop. Destination-only lookup now.
  • if_output_tso reference 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 sshd starves. The port now recomputes in software only for non-GSO skbs and hands CHECKSUM_PARTIAL back to the NIC otherwise (regression guarded by gate 06-rdr).
  • Gate 08 counts 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-to are proved on the firewall's own interfaces rather than on gw2.

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/pflog chardev backend (OpenBSD pflog_hdr layout, 64-byte records, bounded queue, poll, stats ioctl).
  • tools/pflogd — reads /dev/pflog, writes DLT_PFLOG pcap (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_DIVERT socket family registered at runtime (PF_DIVERT = 32, IPPROTO_DIVERT = 254), blocking recvmsg, 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.md records 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->mark bookkeeping 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_FORWARD hook (OpenBSD evaluates it in ip_forward()).
  • refcnt_rele() had inverted polarity → live state keys freed → Oops in the state RBT and pfctl -s states hangs.
  • radix SALEN() read the sa_family byte as a length (Linux sockaddrs have no sa_len) → table entries dropped as duplicates.
  • BSD/Linux LIST_HEAD macro arity clash in compat/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_output does 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 between bind and 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 → EINVAL on load.
  • State iteration started at id 1 (kifs never attached), stoeplitz_init() never called, purge timer re-arming itself at rmmod, rtnl×NET lock 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.