Files
pf-linux-port/docs/en/pf-guide.md
T
lgcosta dff2185b20 Docs: bring the documentation in line with the code
- Demo docs replaced by the screencast docs (single source of truth): the
  legacy demo-session.sh/demo-record.py and the v0.9 renderer were removed
  instead of being kept as a second convention.
- feature-matrix: stale 'M2-in-progress' vocabulary gone; the rows the 15
  gates prove now say VERIFIED-in-VM and name the gate; af-to is now an
  explicit non-goal row (INET6 0 in this build).
- guide: the queues/divert/pflog sections described a state that no longer
  exists (priq-only, divert dropping packets, pflog logging nothing); they
  now describe the shipped behaviour.
- vm-lab: the 15 real gates with what each proves, 15/15 green, and the
  pf0 -> cli/srv/gw2 ssh setup the screencasts rely on.
- install/build: pdivert and pflogd are built and ported (docs said
  otherwise); module object list now includes hfsc.c, fq_codel.c, pfk_pflog.c;
  the screencast pipeline is an optional build stage.
- pf-guide: the 'M2 scope only' framing is gone; honest gaps listed.
- caveats: three new sections about the screencast pipeline (MiniMax video
  generation blocked by the account tier, narration as a separate track, a
  video frame is not a verdict).
- porting-notes: what the recording taught us.
- README/README.pt-BR: status, milestones, layout and video section updated.

All internal doc links verified.
2026-11-12 11:51:25 -03:00

5.9 KiB

Using PF-on-Linux day to day

Português: docs/pt-BR/pf-guide.md.

This is the "what can I actually run today" guide. It follows the feature matrix: the filtering core (pass/block/quick/ keep state/tables/anchors), the NAT family, scrub, forced routing, pools, divert, pflog and the OpenBSD 8.0 queues all have VM evidence behind them — 15/15 lab gates are green. What is not gate-verified is listed explicitly below. Install/build is covered by install.md and build.md.

Daily loop

sudo insmod module/pf.ko          # creates /dev/pf
sudo pfctl -nf /etc/pf.conf       # parse check (userland only — always safe)
sudo pfctl -f /etc/pf.conf        # load ruleset
sudo pfctl -e                     # ENABLE — traffic starts hitting the ruleset NOW
sudo pfctl -s info                # status + counters
sudo pfctl -s states              # states listing (see caveat below)
sudo pfctl -t blk -T add 10.0.1.10   # live table update
sudo pfctl -a admin -f /etc/pf.d/admin.conf
sudo pfctl -d                     # disable
sudo rmmod pf                     # unload (rule/state flush happens here)

Warning — order matters: -f then -e. Enabling with no matching pass rule for your admin path blocks it — this port follows OpenBSD's default-block policy, and the lab runs have actually cut the test VM's ssh that way (the lab keeps a serial console for exactly this). On a remote host, pass your management interface before you enable anything.

A ruleset that works in this port today

# /etc/pf.conf. Interface names here match the VM lab (mgmt/lan/wan);
# substitute your own (eth0, enp1s0, …).

# 1) keep yourself reachable (do this first, always)
pass in  quick on mgmt
pass out quick

# 2) filter core — proven in the lab: quick, proto/port matching, state, drop
table <blk> persist                       # live blocklist (gate 03 pattern)
block in quick proto tcp from <blk> to any

pass in  quick on lan proto tcp from 10.0.1.0/24 to any port { 80 443 } keep state
block drop in quick on wan proto icmp from any to any

# 3) anchors with precedence (gate 04 pattern)
anchor "admin"

Macros (xif = "lan"), nested anchor "in/*", label, tag, set timeout/limit/state-policy/block-policy/skip and the rest of the OpenBSD 8.0 grammar parse fine — make -C tools test exercises them offline — but the kernel behavior of most set options has not been gate-verified yet; treat them as load-and-pray until the matrix moves them.

Not gate-verified (honest gaps): the IPv6 path, OS fingerprinting, the set options above, syncookies and the FQ-CoDel discipline. af-to is compiled out in this build (INET6 0).

Feature notes — what each syntax does today

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 (<iface>) form. binat-to is expanded by pfctl into a nat-to … static-port + rdr-to pair (gate 07-binat).
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, reply-to — Verified in the lab (gates 08-routeto.sh, 13-replyto.sh): the forced gateway is used instead of the normal egress, dup-to copies while keeping the flow, and reply-to pushes a flow's answers out through the gateway link. fail-to is not a keyword in OpenBSD 8.0 (removed upstream).
queue / queue on / pfctl -s queue — Verified in the lab (gate 15-queue.sh): queues load, hfsc attaches, pfctl -s queue -v reports class statistics and a queued 2 MB flow is counted by the class. bandwidth is a guarantee under contention, not a cap - 8.0's pf.conf has no upperlimit keyword.
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.
route-label … never (non-goal) Rejected at the ioctl layer.
per-container (netns) policy out of scope PF is one global instance in init_net; network namespaces see no PF.

Reading state correctly

pfctl -s states shows the real table, including translations, in OpenBSD's own format: a NAT/redirect state prints both tuples, e.g.

all icmp 10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8   0:0
all tcp  10.0.2.20:80 (10.0.1.1:8080) <- 10.0.1.10:42091  FIN_WAIT_2:FIN_WAIT_2

The value in parentheses is the original address before translation. A state that exists but shows 0:0 counters simply means the engine has not seen traffic on it since the last poll; use pfctl -s info counters and tcpdump for traffic evidence, as the gates do.

Kernel log signals you should know

pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)     ← insmod ok
pfk: netfilter hooks registered (10, netisr wq data path)
pfk: divert port N: no socket bound      # pdivert/pflogd consumer not listening

Unloading stops the purge timers and drains the work queue first (an rmmod hang observed in the lab was fixed that way). The soak gate (14-soak.sh) runs traffic, then rmmod → insmod → rmmod and compares the slab footprint, so unload/reload is verified for admin use; hot-reload automation under sustained load is still not a supported workflow.

Full list of behavioral divergences: caveats.md.