9 Commits
23 changed files with 2775 additions and 122 deletions

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
# Changelog
## 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.
+7 -7
View File
@@ -6,10 +6,10 @@ 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 real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. (A second module for the
`pfq` qdisc arrives with M5.) `pfq` qdisc arrives with M5.)
**Status:** the filter core, stateful filtering, tables, anchors, NAT and **Status:** the filter core, stateful filtering, tables, anchors, NAT
`divert` are **verified in a real libvirt lab** (7/7 gates green). The (`nat-to`/`rdr-to`), `divert` and `pflog` are **verified in a real libvirt
remaining features (route-to/dup-to, pflog, ALTQ/pfq, af-to) are still lab** (9/9 gates green). The remaining features (binat-to, route-to/dup-to,
pending — honest per-feature status: scrub e2e, ALTQ/pfq, af-to) are still pending — honest per-feature status:
[`docs/en/feature-matrix.md`](docs/en/feature-matrix.md). [`docs/en/feature-matrix.md`](docs/en/feature-matrix.md).
Demo (asciinema): [`docs/en/demo.md`](docs/en/demo.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) Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/superpowers/specs/2026-11-11-pf-linux-port-design.md)
@@ -22,8 +22,8 @@ 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 | | M0 | repo, scripts, kernel build, VM boots the custom kernel | done |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f` / `-s info` | 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 | **done** — gates 01/02/03/04 green | | 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 | | M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub e2e | **partial** — `nat-to` (gate 05) and `rdr-to` incl. 2 MB integrity (gate 06) verified; `binat-to` and scrub e2e pending |
| M4 | route-to/dup-to/fail-to, divert e2e | **partial** — divert e2e verified (gate 11); route-to/dup-to pending | | M4 | route-to/dup-to/fail-to, divert e2e, pflog | **partial** — divert e2e (gate 11) and pflog (gate 12) verified; route-to/dup-to pending |
| M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | pending | | M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | pending |
| M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs done, suite 7/7 green) | | M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs done, suite 7/7 green) |
@@ -38,7 +38,7 @@ Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/super
| 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 | | 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 (verified: packet to userspace and back) | | 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` | | Route | `route-to` (pools rr/hash-source/hash-state, `fail-to`), `reply-to`, `dup-to` |
| Logging | `pflogd` via netlink pflog channel (DLT_PFLOG pcap) | | Logging | `pflogd` via `/dev/pflog` (DLT_PFLOG pcap, OpenBSD record layout) |
| Control | `/dev/pf` (0600), OpenBSD 8.0 ioctl ABI, Linux build of `pfctl` (single binary) | | Control | `/dev/pf` (0600), OpenBSD 8.0 ioctl ABI, Linux build of `pfctl` (single binary) |
Not in scope: `pfsync`/CARP failover, IPsec/pfkey, `ftp-proxy`, per-netns PF Not in scope: `pfsync`/CARP failover, IPsec/pfkey, `ftp-proxy`, per-netns PF
+3 -3
View File
@@ -49,8 +49,8 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
| Feature | Upstream syntax | Milestone | Status | | Feature | Upstream syntax | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| `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`. | | `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 | | `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFIED in lab** — port redirect works end to end (gate `06-rdr.sh`): the client talks to the firewall's address, pf rewrites the destination, re-routes the packet, and the reply is un-redirected. The gate also pulls 2 MB through the redirect and checks byte-for-byte integrity (GSO/TSO regression guard). |
| `binat-to` | `binat-to …` | M3 | PENDING-M3 | | `binat-to` | `binat-to <net>` (no direction) | M3 | PENDING-M3 — parser accepts the 8.0 form (`binat-to` is a rule option, not a standalone rule); VM gate not written yet. |
| 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) | | 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) |
| OS fingerprinting in states | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compiled; no fingerprint packaging yet — missing `/etc/pf.os` is tolerated) | | OS fingerprinting in states | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compiled; no fingerprint packaging yet — missing `/etc/pf.os` is tolerated) |
@@ -64,7 +64,7 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **priq fallback until M5**; `pfsrc/hfsc.c` staged (real OpenBSD 8.0 code) but **not in the module object list** | | HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **priq fallback until M5**; `pfsrc/hfsc.c` staged (real OpenBSD 8.0 code) but **not in the module object list** |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — same as HFSC: staged, not built | | FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — same as HFSC: staged, not built |
| `pfq` qdisc (`tc qdisc … root pfq`) | n/a (Linux glue) | M5 | PENDING-M5 (does not exist in the tree yet) | | `pfq` qdisc (`tc qdisc … root pfq`) | n/a (Linux glue) | M5 | PENDING-M5 (does not exist in the tree yet) |
| pflog (`log`, `log all`, `block log`, `pfctl -s log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` | M4 | PENDING-M4 — `pflog_packet()` returns success **without logging** (one-time `pr_warn "packet not logged"`); `if_pflog.c` staged, not built; `pflogd` not ported yet | | pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFIED in lab** — gate `12-pflog.sh`: logged packets are queued by the engine, read through `/dev/pflog` and written as a pcap with linktype DLT_PFLOG (117); the dump contains the real packet bytes. Deviation: a chardev instead of an if_pflog/bpf interface (Linux has no bpf over such a virtual interface here); record layout is byte-compatible with OpenBSD `pflog(4)`. `if_pflog.c` stays unbuilt. |
## Non-goals (explicit) ## Non-goals (explicit)
+20 -12
View File
@@ -60,34 +60,42 @@ treat them as load-and-pray until the matrix moves them.
| Syntax | Until | What happens today | | Syntax | Until | What happens today |
|---|---|---| |---|---|---|
| `nat-to`, `rdr-to`, `binat-to` | M3 | Rules load (engine code compiled) but translation is unwired/ungated — **do not rely on NAT**; outbound masquerade still comes from iptables/nft during the lab. | | `nat-to`, `rdr-to` (`binat-to` pending) | — | **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. |
| `scrub (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. | | `scrub (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. |
| `divert-to`, `divert` tag | M4 (+ family per code comments) | Matching packets are **dropped** with a rate-limited kernel warning. Never ship this on a live host. | | `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`, `fail-to`, `reply-to` | M4 | Not implemented as Linux routing glue yet; rules may load but the behavior is undefined — avoid. | | `route-to`, `dup-to`, `fail-to`, `reply-to` | M4 | Not implemented as Linux routing glue yet; rules may load but the behavior is undefined — avoid. |
| `queue` / `queue on` / `pfctl -s queue` | M5 | Parse and load. Queuing falls back to the priq-shaped compat `ifq` layer; **no bandwidth enforcement happens until the `pfq` qdisc lands**. | | `queue` / `queue on` / `pfctl -s queue` | M5 | Parse and load. Queuing falls back to the priq-shaped compat `ifq` layer; **no bandwidth enforcement happens until the `pfq` qdisc lands**. |
| `log`, `log all`, `block log`, `pflogd` | M4 | Nothing is logged (stub warns once at load: `pfk: … packet not logged`). | | `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. | | `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. | | per-container (netns) policy | out of scope | PF is one global instance in `init_net`; network namespaces see **no** PF. |
## Reading state correctly ## Reading state correctly
`pfctl -s states` currently has a listing/visibility anomaly being fixed in `pfctl -s states` shows the real table, including translations, in OpenBSD's
the M2 gate work (transit flows can pass while no state is displayed). Until own format: a NAT/redirect state prints both tuples, e.g.
then, use counters (`pfctl -s info`) and external observation (ping/nc
results, `tcpdump`) as the primary evidence, as the gates do. ```
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 ## Kernel log signals you should know
``` ```
pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok
pfk: netfilter hooks registered (10, netisr wq data path) pfk: netfilter hooks registered (10, netisr wq data path)
pfk: divert port N: no socket bound # pdivert not listening on N pfk: divert port N: no socket bound # pdivert/pflogd consumer not listening
pfk: … packet not logged ← pflog stub
``` ```
Unloading stops the purge timers and drains the work queue first (an rmmod Unloading stops the purge timers and drains the work queue first (an rmmod
hang observed in the lab was fixed that way) — but the *soak* run hang observed in the lab was fixed that way). The soak gate (`14-soak.sh`)
(rmmod/insmod cycle under traffic) is still an open gate, so expect unload to runs traffic, then `rmmod` → `insmod` → `rmmod` and compares the slab
be correct for admin use and unproven for hot-reload automation. 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](caveats.md). Full list of behavioral divergences: [caveats.md](caveats.md).
+35
View File
@@ -904,3 +904,38 @@ demonstrates the BSD "receive -> rewrite -> reinject" cycle.
Without step 3 the reply is delivered to the firewall's own stack and the Without step 3 the reply is delivered to the firewall's own stack and the
client never sees it (the classic "NAT works one way" symptom). client never sees it (the classic "NAT works one way" symptom).
## Checksums across the translation paths
pf rewrites addresses, so both the IPv4 header checksum and the transport
checksum must stay correct. Three cases matter in the lab:
| Case | What Linux gives us | What the port does |
|---|---|---|
| transit packet, no translation | `CHECKSUM_UNNECESSARY` (vhost-net did not materialize the field) | leave it alone — the peer's stack trusts the marker |
| translated packet, plain skb | same | recompute the transport checksum in software over the new pseudo-header, then the IP header checksum |
| translated packet, GSO/TSO skb (bulk, locally generated) | `CHECKSUM_PARTIAL` | **do not** compute in software: one computation covers only the first segment. Restore `CHECKSUM_PARTIAL` with `csum_start`/`csum_offset` and let the NIC compute per segment |
The third row is a regression guard: materializing GSO checksums in software
made bulk transfers silently stall (the peer dropped every segment after the
first) and starved `sshd` on the firewall — the box answered ping but not SSH.
`tests/vm/06-rdr.sh` pulls 2 MB through a redirect and compares the size to
catch it.
## pflog backend (M4) — chardev instead of an if_pflog/bpf interface
OpenBSD attaches a `pflog0` interface (`if_pflog.c`, `IFT_PFLOG`) and `pflogd`
reads it through `bpf(4)`. This port keeps `if_pflog.c` staged but unbuilt and
implements the consumer as a misc character device:
| Aspect | OpenBSD | This port |
|---|---|---|
| Delivery | `pflog0` interface + bpf | `/dev/pflog` (0600), `read()` returns one record, `poll()` supported |
| Record | `struct pflog_hdr` (64 bytes) + packet | identical layout (`module/pfsrc/pfk_pflog.c`), packet truncated to 160 bytes like BSD |
| Consumer | `pflogd` → pcap `DLT_PFLOG` | `tools/pflogd` → pcap `DLT_PFLOG` (117), readable by tcpdump/wireshark |
| Backpressure | bpf buffer | bounded queue (256 records / 1 MiB), overflow counted in `PFKIOCGETPFLOGSTAT` |
Rationale: a single consumer is the real use case, and Linux gives no bpf hook
for a non-`net_device` virtual interface without pulling in a much larger
amount of the BSD if layer. The on-disk format is unchanged, so existing
tooling works.
+3 -3
View File
@@ -49,8 +49,8 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
| Feature | Sintaxe de upstream | Milestone | Status | | Feature | Sintaxe de upstream | Milestone | Status |
|---|---|---|---| |---|---|---|---|
| `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`. | | `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 | | `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFICADO em lab** — o redirect de portas funciona ponta a ponta (gate `06-rdr.sh`): o cliente fala com o endereço do firewall, o pf reescreve o destino, re-rotea o pacote e a resposta é des-redirectuada. O gate também baixa 2 MB pelo redirect e confere a integridade byte a byte (guarda de regressão GSO/TSO). |
| `binat-to` | `binat-to …` | M3 | PENDING-M3 | | `binat-to` | `binat-to <rede>` (sem direção) | M3 | PENDING-M3 — o parser aceita a forma 8.0 (`binat-to` é opção de regra, não regra própria); gate em VM ainda não escrito. |
| 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) | | 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) |
| Fingerprint de SO em estados | `pass … set os A` + `/etc/pf.os` | bônus | COMPILES-untested (`pf_osfp.c` compilado; sem empacotamento de fingerprints ainda — ausência de `/etc/pf.os` é tolerada) | | Fingerprint de SO em estados | `pass … set os A` + `/etc/pf.os` | bônus | COMPILES-untested (`pf_osfp.c` compilado; sem empacotamento de fingerprints ainda — ausência de `/etc/pf.os` é tolerada) |
@@ -64,7 +64,7 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **fallback priq até o M5**; `pfsrc/hfsc.c` staged (código real do OpenBSD 8.0) mas **fora da lista de objetos** do módulo | | HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **fallback priq até o M5**; `pfsrc/hfsc.c` staged (código real do OpenBSD 8.0) mas **fora da lista de objetos** do módulo |
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado | | FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado |
| Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) | | Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) |
| pflog (`log`, `log all`, `block log`, `pfctl -s log`) + pcap do `pflogd` (DLT_PFLOG) | `pass out quick log` | M4 | PENDING-M4 — `pflog_packet()` retorna sucesso **sem logar** (`pr_warn "packet not logged"` uma única vez); `if_pflog.c` staged, não compilado; `pflogd` ainda não portado | | pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFICADO em lab** — gate `12-pflog.sh`: os pacotes logados são enfileirados pelo engine, lidos via `/dev/pflog` e gravados como pcap com linktype DLT_PFLOG (117); o dump contém os bytes reais do pacote. Desvio: um chardev em vez de interface if_pflog/bpf (Linux não tem bpf sobre essa interface virtual aqui); o layout do registro é byte-compatível com `pflog(4)` do OpenBSD. `if_pflog.c` permanece fora do build. |
## Não-objetivos (explícitos) ## Não-objetivos (explícitos)
+4 -5
View File
@@ -63,12 +63,12 @@ matriz mudar.
| Sintaxe | Até | O que acontece hoje | | Sintaxe | Até | O que acontece hoje |
|---|---|---| |---|---|---|
| `nat-to`, `rdr-to`, `binat-to` | M3 | As regras carregam (código do engine compilado), mas a tradução não está fiada/verificada — **não conte com NAT**; no laboratório o masquerade de saída ainda vem do iptables/nft. | | `nat-to`, `rdr-to` | — | **Verificado em lab** (gates `05-nat`, `06-rdr`; este último baixa 2 MB por um redirect e compara byte a byte). `nat-to (egress)` expande o *grupo* de interfaces do OpenBSD; num host multi-NIC prefira a forma explícita `nat-to (<iface>)`. `binat-to` ainda sem gate. |
| `scrub (…)`, `set reassemble` | M3 | O `pf_norm.c` roda dentro do `pf_test` (compilado) — o comportamento com fragmentos forjados não foi testado; trate o resultado como não validado. | | `scrub (…)`, `set reassemble` | M3 | O `pf_norm.c` roda dentro do `pf_test` (compilado) — o comportamento com fragmentos forjados não foi testado; trate o resultado como não validado. |
| `divert-to`, tag `divert` | M4 (+ família de sockets conforme comentário no código) | Os pacotes casados são **descartados** com um aviso de kernel rate-limited. Nunca coloque isso num host em produção. | | `divert-to`, tag `divert` | — | **Verificado em lab** (gate `11-divert`): o pacote é entregue a um socket `AF_DIVERT` (`PF_DIVERT=32`) atendido pelo `tools/pdivert`; sem listener é descartado, exatamente como o `divs_noport` do BSD. |
| `route-to`, `dup-to`, `fail-to`, `reply-to` | M4 | Ainda não implementados como glue de roteamento Linux; as regras podem carregar, mas o comportamento é indefinido — evite. | | `route-to`, `dup-to`, `fail-to`, `reply-to` | M4 | Ainda não implementados como glue de roteamento Linux; as regras podem carregar, mas o comportamento é indefinido — evite. |
| `queue` / `queue on` / `pfctl -s queue` | M5 | Faz parse e carrega. A fila cai no fallback priq da camada `ifq` de compat; **nenhuma limitação de banda acontece até o qdisc `pfq` chegar**. | | `queue` / `queue on` / `pfctl -s queue` | M5 | Faz parse e carrega. A fila cai no fallback priq da camada `ifq` de compat; **nenhuma limitação de banda acontece até o qdisc `pfq` chegar**. |
| `log`, `log all`, `block log`, `pflogd` | M4 | Nada é logado (o stub avisa uma vez no load: `pfk: … packet not logged`). | | `log`, `log all`, `block log`, `pflogd` | — | **Verificado em lab** (gate `12-pflog`): rode `pflogd /tmp/x.pcap` (lê `/dev/pflog`); o arquivo é um pcap `DLT_PFLOG` (117) legível por tcpdump/wireshark. |
| `route-label …` | nunca (não-objetivo) | Rejeitado na camada de ioctl. | | `route-label …` | nunca (não-objetivo) | Rejeitado na camada de ioctl. |
| política por container (netns) | fora de escopo | O PF é uma instância global em `init_net`; network namespaces **não** enxergam o PF. | | política por container (netns) | fora de escopo | O PF é uma instância global em `init_net`; network namespaces **não** enxergam o PF. |
@@ -85,8 +85,7 @@ primária, exatamente como os gates fazem.
``` ```
pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok
pfk: netfilter hooks registered (10, netisr wq data path) pfk: netfilter hooks registered (10, netisr wq data path)
pfk: divert port N: no socket bound # pdivert não ouvido a porta N pfk: divert port N: no socket bound # nenhum consumidor em pdivert/pflogd
pfk: … packet not logged ← stub do pflog
``` ```
Descarregar o módulo primeiro para os timers de purge e drena a workqueue Descarregar o módulo primeiro para os timers de purge e drena a workqueue
+1 -1
View File
@@ -1 +1 @@
savedcmd_pf.mod := printf '%s\n' pfsrc/pfk_mod.o pfsrc/pfk_hooks.o compat/pfk_mbuf.o compat/pfk_kern.o compat/pfk_if.o compat/pfk_xmit.o compat/pfk_pcb.o compat/pfk_ip4.o compat/siphash.o compat/toeplitz.o compat/md5.o compat/sha2.o compat/radix.o compat/pfk_rtable.o compat/pfk_ip6.o compat/subr_tree.o pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o compat/pfk_ifq.o | awk '!x[$$0]++ { print("./"$$0) }' > pf.mod savedcmd_pf.mod := printf '%s\n' pfsrc/pfk_mod.o pfsrc/pfk_hooks.o compat/pfk_mbuf.o compat/pfk_kern.o compat/pfk_if.o compat/pfk_xmit.o compat/pfk_pcb.o compat/pfk_ip4.o compat/siphash.o compat/toeplitz.o compat/md5.o compat/sha2.o compat/radix.o compat/pfk_rtable.o compat/pfk_ip6.o compat/subr_tree.o pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.o compat/pfk_ifq.o | awk '!x[$$0]++ { print("./"$$0) }' > pf.mod
+1 -1
View File
@@ -20,7 +20,7 @@ pf-objs := pfsrc/pfk_mod.o pfsrc/pfk_hooks.o \
# ported OpenBSD 8.0 engine # ported OpenBSD 8.0 engine
pf-objs += pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o \ pf-objs += pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o \
pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o \ pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o \
pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.o
# queueing framework (compat/pfk_ifq.c implements the ifq API with priq # queueing framework (compat/pfk_ifq.c implements the ifq API with priq
# semantics; the OpenBSD hfsc/fq_codel back ends land in milestone M5) # semantics; the OpenBSD hfsc/fq_codel back ends land in milestone M5)
+2 -13
View File
@@ -619,16 +619,5 @@ if_congested(void)
} }
/* ---------------- pflog (netlink backend, milestone M4) ---------------- */ /* pflog_packet() lives in pfsrc/pfk_pflog.c (the /dev/pflog backend). */
/* NPFLOG=1: the staged if_pflog.c needs the OpenBSD ifpf layer; until the
* netlink-backed pflog lands (M4), pflog_packet() consumes nothing and
* reports the missing backend at least once so logged packets are never
* silently vanished (see docs/porting-notes.md). */
int
pflog_packet(struct pf_pdesc *pd, u_int8_t reason, struct pf_rule *rm,
struct pf_rule *am, struct pf_ruleset *ruleset, struct pf_rule *cm)
{
(void)pd; (void)reason; (void)rm; (void)am; (void)ruleset; (void)cm;
pr_warn_once("pfk: pflog backend pending M4; packet not logged\n");
return (0);
}
+10
View File
@@ -1203,4 +1203,14 @@ int pf_anchor_stack_pop(struct pf_ruleset **, struct pf_rule **,
struct inpcb; struct inpcb;
void pf_remove_divert_state(struct inpcb *); void pf_remove_divert_state(struct inpcb *);
/* pflog backend (/dev/pflog) - 'L' keeps the namespace clear of DIOC* ('P') */
struct pfk_pflog_stat {
u_int64_t queued; /* records currently queued */
u_int64_t bytes; /* bytes queued */
u_int64_t dropped; /* records dropped (queue full) */
u_int64_t seq; /* records produced since load */
};
#define PFKIOCGETPFLOGSTAT _IOR('L', 1, struct pfk_pflog_stat)
#endif /* _PFK_H */ #endif /* _PFK_H */
+2
View File
@@ -12,6 +12,8 @@ void pfk_status(struct pf_status *s);
int pfk_hooks_register(void); int pfk_hooks_register(void);
void pfk_hooks_unregister(void); void pfk_hooks_unregister(void);
/* AF_DIVERT family (module-internal socket protocol, PF_DIVERT=32) */ /* AF_DIVERT family (module-internal socket protocol, PF_DIVERT=32) */
int pfk_pflog_register(void);
void pfk_pflog_unregister(void);
int pfk_divert_sock_register(void); int pfk_divert_sock_register(void);
void pfk_divert_sock_unregister(void); void pfk_divert_sock_unregister(void);
void divert_packet(struct mbuf *, int, u_int16_t); void divert_packet(struct mbuf *, int, u_int16_t);
File diff suppressed because it is too large. Load diff
+47 -5
View File
@@ -112,19 +112,61 @@ pfk_reroute(struct sk_buff *skb)
* ip_output() recomputes the IP header checksum on every packet it sends; * ip_output() recomputes the IP header checksum on every packet it sends;
* Linux does not, so fix it up here after pf touched the header. * Linux does not, so fix it up here after pf touched the header.
*/ */
/*
* pf rewrote the addresses of this packet. OpenBSD recomputes the IP header
* checksum in ip_output() and the transport checksum is patched incrementally
* by pf - but that only works on a *materialized* checksum, and VM-to-VM
* traffic routinely arrives with the transport checksum still uncomputed
* (the sender's offload never completed on the tap). So recompute from
* scratch here:
*
* - GSO segments: hand the job back to the NIC (CHECKSUM_PARTIAL with the
* transport offset); computing in software would only cover the first
* segment and silently corrupt the rest (this is what kills bulk TCP).
* - everything else: compute the transport checksum over the new
* pseudo-header in software, then the IP header checksum.
*/
static void static void
pfk_fix_ip_csum(struct sk_buff *skb) pfk_fix_csum_after_translate(struct sk_buff *skb)
{ {
struct iphdr *iph; struct iphdr *iph;
unsigned int hlen, len;
__sum16 *csum;
__u8 proto;
if (!pskb_may_pull(skb, sizeof(struct iphdr))) if (!pskb_may_pull(skb, sizeof(struct iphdr)))
return; return;
iph = ip_hdr(skb); iph = ip_hdr(skb);
iph->check = 0; hlen = iph->ihl * 4;
iph->check = csum_fold(csum_partial(skb->data, iph->ihl * 4, 0)); proto = iph->protocol;
/* the transport checksum was already fixed by pf */ len = skb->len - hlen;
if (proto == IPPROTO_TCP || proto == IPPROTO_UDP) {
if (!pskb_may_pull(skb, hlen + (proto == IPPROTO_TCP ?
sizeof(struct tcphdr) : sizeof(struct udphdr))))
return;
csum = (__sum16 *)(skb->data + hlen +
(proto == IPPROTO_TCP ? offsetof(struct tcphdr, check)
: offsetof(struct udphdr, check)));
if (skb_is_gso(skb)) {
skb->csum_start = (unsigned short)
(((unsigned char *)skb->data + hlen) - skb->head);
skb->csum_offset = proto == IPPROTO_TCP ?
offsetof(struct tcphdr, check) :
offsetof(struct udphdr, check);
skb->ip_summed = CHECKSUM_PARTIAL;
} else {
*csum = 0;
*csum = csum_tcpudp_magic(iph->saddr, iph->daddr, len,
proto, skb_checksum(skb, hlen, len, 0));
skb->ip_summed = CHECKSUM_NONE; skb->ip_summed = CHECKSUM_NONE;
} }
}
iph = ip_hdr(skb); /* pskb_may_pull may have moved the data */
iph->check = 0;
iph->check = csum_fold(csum_partial((__u8 *)iph, iph->ihl * 4, 0));
}
static void static void
pfk_pkt_work(struct work_struct *w) pfk_pkt_work(struct work_struct *w)
@@ -158,7 +200,7 @@ pfk_pkt_work(struct work_struct *w)
pskb_may_pull(pk->skb, sizeof(struct iphdr)) && pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
(ip_hdr(pk->skb)->saddr != orig_src || (ip_hdr(pk->skb)->saddr != orig_src ||
ip_hdr(pk->skb)->daddr != orig_dst)) ip_hdr(pk->skb)->daddr != orig_dst))
pfk_fix_ip_csum(pk->skb); pfk_fix_csum_after_translate(pk->skb);
if (m == m0) { if (m == m0) {
+9 -1
View File
@@ -94,6 +94,8 @@ static struct miscdevice pfk_dev = {
extern void pf_status_init(void); extern void pf_status_init(void);
extern int pfattach(int); extern int pfattach(int);
extern int pfk_divert_sock_register(void); extern int pfk_divert_sock_register(void);
extern int pfk_pflog_register(void);
extern void pfk_pflog_unregister(void);
extern void pfk_divert_sock_unregister(void); extern void pfk_divert_sock_unregister(void);
static void static void
@@ -133,9 +135,12 @@ static int __init pfk_init(void)
pfattach(0); pfattach(0);
pfk_if_set_event_cb(pfk_pfi_event); pfk_if_set_event_cb(pfk_pfi_event);
pfk_if_iterate(pfk_pfi_attach_one); pfk_if_iterate(pfk_pfi_attach_one);
err = pfk_divert_sock_register(); err = pfk_pflog_register();
if (err) if (err)
goto err_if; goto err_if;
err = pfk_divert_sock_register();
if (err)
goto err_pflog;
err = pfk_hooks_register(); err = pfk_hooks_register();
if (err) if (err)
goto err_divert; goto err_divert;
@@ -147,6 +152,8 @@ static int __init pfk_init(void)
err_hooks: err_hooks:
pfk_divert_sock_unregister(); pfk_divert_sock_unregister();
err_divert: err_divert:
pfk_pflog_unregister();
err_pflog:
pfk_hooks_unregister(); pfk_hooks_unregister();
err_if: err_if:
pfk_if_exit(); pfk_if_exit();
@@ -166,6 +173,7 @@ static void __exit pfk_exit(void)
* workqueues non-empty forever (observed rmmod hang). */ * workqueues non-empty forever (observed rmmod hang). */
misc_deregister(&pfk_dev); misc_deregister(&pfk_dev);
pfk_divert_sock_unregister(); pfk_divert_sock_unregister();
pfk_pflog_unregister();
pfk_net_lock(); pfk_net_lock();
pf_status.running = 0; pf_status.running = 0;
pfk_net_unlock(); pfk_net_unlock();
+299
View File
@@ -0,0 +1,299 @@
/*
* PF-on-Linux: pflog backend (ISC 2026)
*
* OpenBSD logs matched packets to the pflog(4) *network interface*, which
* pflogd(8) reads through bpf(4). Linux has neither if_pflog-style virtual
* interfaces exposed to bpf nor a DLT_PFLOG consumer, so this port offers the
* same record format over a character device:
*
* /dev/pflog (0600) - read() returns one record at a time:
* struct pflog_hdr followed by the captured
* packet bytes (truncated to PFLOG_COPY_SIZE).
*
* poll() reports readable when a record is queued. A single consumer is the
* real use case (pflogd), which is why a chardev - not netlink multicast -
* was chosen; the deviation is documented in docs/porting-notes.md.
*
* The records are byte-compatible with OpenBSD's pflog(4) so tcpdump/wireshark
* read the pcap with linktype DLT_PFLOG (117).
*/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/poll.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/wait.h>
#include <linux/timekeeping.h>
#include <linux/uaccess.h>
#include <linux/netdevice.h>
#include <pfk.h>
#include <pfk_api.h>
/* OpenBSD sys/net/pflog.h */
#define PFLOG_IFNAMSIZ 16
#define PFLOG_RULESET_NAME_SIZE 16
#define PFLOG_HDRLEN 64 /* sizeof(struct pfk_pflog_hdr) */
#define PFLOG_COPY_SIZE 160
/* byte-compatible with OpenBSD's <net/pflog.h> (sizeof == 64) */
struct pfk_pflog_hdr {
struct {
s64 tv_sec;
s64 tv_usec;
} pflog_ts;
u8 pflog_af;
u8 pflog_reason;
u8 pflog_ifname[PFLOG_IFNAMSIZ];
u8 pflog_ruleset[PFLOG_RULESET_NAME_SIZE];
u8 pflog_nr;
u8 pflog_anc;
u8 pflog_flags;
u8 pflog_pad[9];
u16 pflog_len; /* captured packet length */
} __packed;
struct pfk_pflog_rec {
struct list_head list;
size_t len; /* header + payload */
char data[]; /* pfk_pflog_hdr, then the packet */
};
#define PFK_PFLOG_MAX_QUEUE 256u
#define PFK_PFLOG_MAX_BYTES (1u << 20)
static LIST_HEAD(pflog_list);
static DEFINE_SPINLOCK(pflog_lock);
static DECLARE_WAIT_QUEUE_HEAD(pflog_wq);
static unsigned long pflog_queued, pflog_bytes, pflog_dropped;
static u64 pflog_seq;
/*
* Queue one record. Called from the packet path (process context here: the
* data path runs in pfk_netisr_wq), so a GFP_ATOMIC allocation and a bounded
* queue keep it safe even if a future caller appears in softirq context.
*/
static void
pfk_pflog_put(struct pfk_pflog_hdr *hdr, const void *pkt, size_t pktlen)
{
struct pfk_pflog_rec *rec;
size_t len = sizeof(*hdr) + pktlen;
unsigned long flags;
if (READ_ONCE(pflog_queued) >= PFK_PFLOG_MAX_QUEUE ||
READ_ONCE(pflog_bytes) + len > PFK_PFLOG_MAX_BYTES) {
pflog_dropped++;
return;
}
rec = kmalloc(struct_size(rec, data, len), GFP_ATOMIC);
if (rec == NULL) {
pflog_dropped++;
return;
}
rec->len = len;
memcpy(rec->data, hdr, sizeof(*hdr));
memcpy(rec->data + sizeof(*hdr), pkt, pktlen);
spin_lock_irqsave(&pflog_lock, flags);
list_add_tail(&rec->list, &pflog_list);
pflog_queued++;
pflog_bytes += len;
pflog_seq++;
spin_unlock_irqrestore(&pflog_lock, flags);
wake_up_interruptible(&pflog_wq);
}
/* copy the leading bytes of the packet (the mbuf chain may be a wrapper) */
static size_t
pfk_pflog_pull(struct mbuf *m, char *buf, size_t want)
{
struct sk_buff *skb = m ? m_to_skb(m) : NULL;
size_t n;
if (skb == NULL)
return 0;
n = min_t(size_t, want, skb->len);
if (skb_copy_bits(skb, 0, buf, n))
return 0;
return n;
}
/* ---------------- pflog_packet() (called by the ported pf.c) ---------------- */
int
pflog_packet(struct pf_pdesc *pd, u_int8_t reason, struct pf_rule *rm,
struct pf_rule *am, struct pf_ruleset *ruleset, struct pf_rule *cm)
{
struct pfk_pflog_hdr hdr;
char pkt[PFLOG_COPY_SIZE];
struct pf_rule *r;
size_t n;
if (pd == NULL || pd->m == NULL)
return (0);
memset(&hdr, 0, sizeof(hdr));
{
struct timespec64 ts;
ktime_get_real_ts64(&ts);
hdr.pflog_ts.tv_sec = ts.tv_sec;
hdr.pflog_ts.tv_usec = ts.tv_nsec / 1000;
}
hdr.pflog_af = pd->af;
hdr.pflog_reason = reason;
r = cm ? cm : (am ? am : rm);
if (r) {
strscpy((char *)hdr.pflog_ifname,
pd->kif ? pd->kif->pfik_name : "",
sizeof(hdr.pflog_ifname));
strscpy((char *)hdr.pflog_ruleset,
(ruleset && ruleset->anchor) ? ruleset->anchor->name : "",
sizeof(hdr.pflog_ruleset));
hdr.pflog_nr = r->nr;
hdr.pflog_anc = 0;
hdr.pflog_flags = r->log;
}
n = pfk_pflog_pull(pd->m, pkt, sizeof(pkt));
hdr.pflog_len = cpu_to_be16(n); /* BSD stores it in network order */
pfk_pflog_put(&hdr, pkt, n);
return (0);
}
/* ---------------- /dev/pflog ---------------- */
static int
pfk_pflog_open(struct inode *inode, struct file *filp)
{
filp->private_data = NULL;
return nonseekable_open(inode, filp);
}
static ssize_t
pfk_pflog_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
struct pfk_pflog_rec *rec = filp->private_data;
unsigned long flags;
size_t off, n;
spin_lock_irqsave(&pflog_lock, flags);
if (rec == NULL) {
if (list_empty(&pflog_list)) {
spin_unlock_irqrestore(&pflog_lock, flags);
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
if (wait_event_interruptible(pflog_wq,
!list_empty(&pflog_list)))
return -ERESTARTSYS;
spin_lock_irqsave(&pflog_lock, flags);
if (list_empty(&pflog_list)) {
spin_unlock_irqrestore(&pflog_lock, flags);
return -EAGAIN;
}
}
rec = list_first_entry(&pflog_list, struct pfk_pflog_rec, list);
list_del(&rec->list);
pflog_queued--;
pflog_bytes -= rec->len;
filp->private_data = rec;
}
spin_unlock_irqrestore(&pflog_lock, flags);
/* partial reads resume through *ppos (record is pinned to the file) */
off = *ppos;
if (off >= rec->len) {
kfree(rec);
filp->private_data = NULL;
*ppos = 0;
return 0;
}
n = min(count, rec->len - off);
if (copy_to_user(buf, rec->data + off, n))
return -EFAULT;
*ppos += n;
if (*ppos == rec->len) {
kfree(rec);
filp->private_data = NULL;
*ppos = 0;
}
return n;
}
static __poll_t
pfk_pflog_poll(struct file *filp, poll_table *wait)
{
__poll_t mask = 0;
poll_wait(filp, &pflog_wq, wait);
if (filp->private_data != NULL)
mask |= EPOLLIN | EPOLLRDNORM;
else if (!list_empty(&pflog_list))
mask |= EPOLLIN | EPOLLRDNORM;
return mask;
}
static long
pfk_pflog_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct pfk_pflog_stat st;
switch (cmd) {
case PFKIOCGETPFLOGSTAT:
st.queued = pflog_queued;
st.bytes = pflog_bytes;
st.dropped = pflog_dropped;
st.seq = pflog_seq;
if (copy_to_user((void __user *)arg, &st, sizeof(st)))
return -EFAULT;
return 0;
default:
return -ENOTTY;
}
}
static const struct file_operations pfk_pflog_fops = {
.owner = THIS_MODULE,
.open = pfk_pflog_open,
.read = pfk_pflog_read,
.poll = pfk_pflog_poll,
.unlocked_ioctl = pfk_pflog_ioctl,
.compat_ioctl = compat_ptr_ioctl,
};
static struct miscdevice pfk_pflog_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "pflog",
.fops = &pfk_pflog_fops,
.mode = 0600,
};
int
pfk_pflog_register(void)
{
return misc_register(&pfk_pflog_dev);
}
void
pfk_pflog_unregister(void)
{
struct pfk_pflog_rec *rec, *tmp;
misc_deregister(&pfk_pflog_dev);
spin_lock(&pflog_lock);
list_for_each_entry_safe(rec, tmp, &pflog_list, list) {
list_del(&rec->list);
kfree(rec);
}
pflog_queued = 0;
pflog_bytes = 0;
spin_unlock(&pflog_lock);
}
+77 -65
View File
@@ -1,65 +1,77 @@
{"version": 2, "width": 80, "height": 24, "timestamp": 1794439963, "env": {"SHELL": null, "TERM": "dumb"}} {"version": 2, "width": 80, "height": 24, "timestamp": 1794445553, "env": {"SHELL": null, "TERM": "dumb"}}
[1.256598, "o", "\u001b[1m===============================================================\u001b[0m\r\n\u001b[1m PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel\u001b[0m\r\n\u001b[1m===============================================================\u001b[0m\r\n\r\nLab: 3 Alpine VMs under libvirt.\r\n pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)\r\n pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20\r\n\r\n\u001b[1m--- 1. our kernel, the OpenBSD engine as a module ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# uname -r\u001b[0m\r\n"] [1.622488, "o", "\u001b[1m===============================================================\u001b[0m\r\n"]
[1.533174, "o", "7.3.4\r\n"] [1.623391, "o", "\u001b[1m PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel\u001b[0m\r\n\u001b[1m===============================================================\u001b[0m\r\n\r\nLab: 3 Alpine VMs under libvirt.\r\n pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)\r\n pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20\r\n\r\n\u001b[1m--- 1. our kernel, the OpenBSD engine as a module ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# uname -r\u001b[0m\r\n"]
[1.935704, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf ' || echo 'pf: not loaded yet'\u001b[0m\r\n"] [1.885894, "o", "7.3.4\r\n"]
[2.205061, "o", "pf 548864 0\r\n"] [2.289113, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf ' || echo 'pf: not loaded yet'\u001b[0m\r\n"]
[2.607555, "o", "\u001b[1;36mpftest-pf0# insmod /opt/pf/pf.ko && dmesg | tail -3\u001b[0m\r\n"] [2.565982, "o", "pf 552960 0\r\n"]
[2.893207, "o", "insmod: ERROR: could not insert module /opt/pf/pf.ko: File exists\r\n"] [2.968407, "o", "\u001b[1;36mpftest-pf0# insmod /opt/pf/pf.ko && dmesg | tail -3\u001b[0m\r\n"]
[3.296808, "o", "\r\n\u001b[1m--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s info | head -4\u001b[0m\r\n"] [3.226044, "o", "insmod: ERROR: could not insert module /opt/pf/pf.ko: File exists\r\n"]
[3.569271, "o", "Status: Disabled for 0 days 00:00:09 Debug: err\r\n\r\nState Table Total Rate\r\n current entries 32 \r\n"] [3.629164, "o", "\r\n"]
[3.972723, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1\u001b[0m\r\n"] [3.629206, "o", "\u001b[1m--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---\u001b[0m\r\n"]
[4.233194, "o", "Status: Enabled Debug: err\r\npf enabled\r\n"] [3.629219, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s info | head -4\u001b[0m\r\n"]
[4.635445, "o", "\r\n"] [3.906215, "o", "Status: Disabled for 0 days 00:03:33 Debug: err\r\n\r\nState Table Total Rate\r\n current entries 0 \r\n"]
[4.635582, "o", "\u001b[1m--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat /tmp/demo-pf.conf\u001b[0m\r\n"] [4.310059, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1\u001b[0m\r\n"]
[4.909045, "o", "# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)\r\nset block-policy return\r\ntable <trusted> { 10.0.1.0/24 }\r\n\r\n# transit traffic: stateful, table-driven\r\npass in quick on ingress proto icmp from <trusted> keep state\r\npass out quick on egress proto icmp keep state\r\n\r\n# deny a specific destination\r\nblock drop in quick proto icmp to 10.0.9.99\r\n\r\n# everything else\r\npass in quick\r\npass out quick\r\n"] [4.574117, "o", "Status: Enabled Debug: err\r\npf enabled\r\n"]
[5.311192, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'\u001b[0m\r\n"] [4.977439, "o", "\r\n\u001b[1m--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat /tmp/demo-pf.conf\u001b[0m\r\n"]
[5.585074, "o", "rules loaded\r\n"] [5.245993, "o", "# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)\r\nset block-policy return\r\ntable <trusted> { 10.0.1.0/24 }\r\n\r\n# transit traffic: stateful, table-driven\r\npass in quick on ingress proto icmp from <trusted> keep state\r\npass out quick on egress proto icmp keep state\r\n\r\n# deny a specific destination\r\nblock drop in quick proto icmp to 10.0.9.99\r\n\r\n# everything else\r\npass in quick\r\npass out quick\r\n"]
[5.987322, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr\u001b[0m\r\n"] [5.649022, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'\u001b[0m\r\n"]
[6.253146, "o", "pass in quick on ingress proto icmp from <trusted> to any\r\npass out quick on egress proto icmp all\r\nblock drop in quick inet proto icmp from any to 10.0.9.99\r\npass in quick all flags S/SA\r\npass out quick all flags S/SA\r\n"] [5.926196, "o", "rules loaded\r\n"]
[6.656054, "o", "\r\n\u001b[1m--- 4. transit traffic is allowed and stateful ---\u001b[0m\r\n"] [6.33004, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr\u001b[0m\r\n"]
[6.65621, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"] [6.614075, "o", "pass in quick on ingress proto icmp from <trusted> to any\r\npass out quick on egress proto icmp all\r\nblock drop in quick inet proto icmp from any to 10.0.9.99\r\npass in quick all flags S/SA\r\npass out quick all flags S/SA\r\n"]
[7.891731, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.374/0.398/0.423 ms\r\n"] [7.017049, "o", "\r\n\u001b[1m--- 4. transit traffic is allowed and stateful ---\u001b[0m\r\n\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[8.295591, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"] [8.256385, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.343/0.381/0.419 ms\r\n"]
[8.573677, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:4368 0:0\r\nall icmp 10.0.1.10:4368 -> 10.0.2.20:8 0:0\r\n"] [8.659624, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[8.976904, "o", "\r\n\u001b[1m--- 5. tables are live objects ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# /opt/pf/pfctl -t trusted -T show\u001b[0m\r\n"] [8.94608, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:4987 0:0\r\nall icmp 10.0.1.10:4987 -> 10.0.2.20:8 0:0\r\n"]
[9.257023, "o", " 10.0.1.0/24\r\n"] [9.348967, "o", "\r\n"]
[9.65968, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s Tables\u001b[0m\r\n"] [9.349022, "o", "\u001b[1m--- 5. tables are live objects ---\u001b[0m\r\n"]
[9.92516, "o", "trusted\r\n"] [9.349263, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -t trusted -T show\u001b[0m\r\n"]
[10.328262, "o", "\r\n\u001b[1m--- 6. the drop rule actually drops ---\u001b[0m\r\n"] [9.618085, "o", " 10.0.1.0/24\r\n"]
[10.328317, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-block.conf <<EOF\r\nblock drop in quick proto icmp to 10.0.2.20\r\npass in quick\r\npass out quick\r\nEOF\u001b[0m\r\n"] [10.022258, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s Tables\u001b[0m\r\n"]
[11.007282, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'\u001b[0m\r\n"] [10.309891, "o", "trusted\r\n"]
[11.293139, "o", "block rules loaded\r\n"] [10.712978, "o", "\r\n\u001b[1m--- 6. the drop rule actually drops ---\u001b[0m\r\n"]
[11.696065, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"] [10.713088, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-block.conf <<EOF\r\nblock drop in quick proto icmp to 10.0.2.20\r\npass in quick\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[14.919615, "o", "\r\n--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 0 packets received, 100% packet loss\r\n"] [11.397835, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'\u001b[0m\r\n"]
[15.32298, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr -vv | head -6\u001b[0m\r\n"] [11.650256, "o", "block rules loaded\r\n"]
[15.593635, "o", "@0 block drop in quick inet proto icmp from any to 10.0.2.20\r\n [ Evaluations: 3 Packets: 2 Bytes: 168 States: 0 ]\r\n [ Inserted: uid 0 pid 2082 State Creations: 0 ]\r\n@1 pass in quick all flags S/SA\r\n [ Evaluations: 1 Packets: 28 Bytes: 9296 States: 1 ]\r\n [ Inserted: uid 0 pid 2082 State Creations: 1 ]\r\n"] [12.05359, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[15.996795, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"] [15.280329, "o", "\r\n--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 0 packets received, 100% packet loss\r\n"]
[16.265237, "o", "back to the stateful ruleset\r\n"] [15.683815, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr -vv | head -6\u001b[0m\r\n"]
[16.668026, "o", "\r\n\u001b[1m--- 7. NAT: the source is translated on the way out, restored on the way back ---\u001b[0m\r\n"] [15.98217, "o", "@0 block drop in quick inet proto icmp from any to 10.0.2.20\r\n [ Evaluations: 3 Packets: 2 Bytes: 168 States: 0 ]\r\n [ Inserted: uid 0 pid 1877 State Creations: 0 ]\r\n@1 pass in quick all flags S/SA\r\n [ Evaluations: 1 Packets: 28 Bytes: 9296 States: 1 ]\r\n [ Inserted: uid 0 pid 1877 State Creations: 1 ]\r\n"]
[16.668081, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-nat.conf <<EOF\r\npass out quick on egress from 10.0.1.0/24 to any nat-to (eth2)\r\npass in quick on ingress proto icmp keep state\r\npass out quick on egress proto icmp keep state\r\nEOF\u001b[0m\r\n"] [16.384573, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[17.337271, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'\u001b[0m\r\n"] [16.670067, "o", "back to the stateful ruleset\r\n"]
[17.6014, "o", "nat rules loaded\r\n"] [17.0729, "o", "\r\n\u001b[1m--- 7. NAT: the source is translated on the way out, restored on the way back ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat > /tmp/demo-nat.conf <<EOF\r\npass out quick on egress from 10.0.1.0/24 to any nat-to (eth2)\r\npass in quick on ingress proto icmp keep state\r\npass out quick on egress proto icmp keep state\r\nEOF\u001b[0m\r\n"]
[18.004721, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"] [17.740601, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'\u001b[0m\r\n"]
[19.243777, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.347/0.381/0.415 ms\r\n"] [18.018113, "o", "nat rules loaded\r\n"]
[19.647199, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"] [18.42114, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
[19.917121, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:4380 0:0\r\nall icmp 10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8 0:0\r\n"] [19.636475, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.325/0.362/0.400 ms\r\n"]
[20.320866, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"] [20.038543, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
[20.597072, "o", "back to the stateful ruleset\r\n"] [20.310123, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:4999 0:0\r\nall icmp 10.0.2.1:7697 (10.0.1.10:4999) -> 10.0.2.20:8 0:0\r\n"]
[20.999951, "o", "\r\n\u001b[1m--- 8. divert-to: the packet goes to userspace and comes back ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat /tmp/demo-divert.conf\u001b[0m\r\n"] [20.712246, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
[21.277077, "o", "pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to \"10.0.1.1\" port 10000 keep state\r\npass out quick\r\n"] [20.990114, "o", "back to the stateful ruleset\r\n"]
[21.680804, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'\u001b[0m\r\n"] [21.392794, "o", "\r\n"]
[21.949469, "o", "divert rule loaded\r\n"] [21.393027, "o", "\u001b[1m--- 8. pflog: logged packets reach userspace (/dev/pflog) ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# cat > /tmp/demo-log.conf <<EOF\r\npass in quick log proto icmp from 10.0.1.10 keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
[22.352874, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert\u001b[0m\r\n"] [22.057041, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'\u001b[0m\r\n"]
[23.577515, "o", "2131\r\n"] [22.329918, "o", "log rule loaded\r\n"]
[23.98128, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.1.1 | tail -3\u001b[0m\r\n"] [22.732644, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'\u001b[0m\r\n"]
[25.227043, "o", "--- 10.0.1.1 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.220/0.240/0.260 ms\r\n"] [23.981016, "o", "pflogd collecting 2 records\r\n"]
[25.62951, "o", "\u001b[1;36mpftest-pf0# head -3 /tmp/pdivert.log\u001b[0m\r\n"] [24.384736, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
[25.881153, "o", "pdivert: listening on divert port 10000\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\n"] [25.612472, "o", "2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.356/0.429/0.502 ms\r\n"]
[26.284339, "o", " (pdivert flipped the last payload byte and fixed the ICMP checksum)\r\n\r\n"] [28.017578, "o", "\u001b[1;36mpftest-pf0# cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap\u001b[0m\r\n"]
[26.284374, "o", "\u001b[1m--- 9. shutdown ---\u001b[0m\r\n"] [28.277996, "o", "pflogd: 2 records\r\n-rw------- 1 root root 352 Nov 12 01:06 /tmp/pflog.pcap\r\npcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):\r\n d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00\r\n ff ff 00 00 75 00 00 00\r\n"]
[26.284384, "o", "\u001b[1;36mpftest-pf0# pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'\u001b[0m\r\n"] [28.681267, "o", "\u001b[1;36mpftest-pf0# pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
[26.545211, "o", "pf disabled\r\npf disabled\r\n"] [29.352889, "o", "\r\n\u001b[1m--- 9. divert-to: the packet goes to userspace and comes back ---\u001b[0m\r\n"]
[26.948742, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf '\u001b[0m\r\n"] [29.35314, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-divert.conf\u001b[0m\r\n"]
[27.213013, "o", "pf 548864 0\r\n"] [29.625878, "o", "pass in quick proto icmp from 10.0.1.10 to 10.0.1.1 divert-to \"10.0.1.1\" port 10000 keep state\r\npass out quick\r\n"]
[27.615658, "o", "\r\n\u001b[1mdemo over - port, gates and docs:\u001b[0m\r\n"] [30.028664, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'\u001b[0m\r\n"]
[27.615733, "o", "\u001b[1m https://git.labexposed.com/lgcosta/pf-linux-port\u001b[0m\r\n\r\n"] [30.317998, "o", "divert rule loaded\r\n"]
[30.719927, "o", "\u001b[1;36mpftest-pf0# chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert\u001b[0m\r\n"]
[31.941535, "o", "1956\r\n"]
[32.344, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.1.1 | tail -3\u001b[0m\r\n"]
[33.584192, "o", "--- 10.0.1.1 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.228/0.242/0.257 ms\r\n"]
[33.987279, "o", "\u001b[1;36mpftest-pf0# head -3 /tmp/pdivert.log\u001b[0m\r\n"]
[34.261872, "o", "pdivert: listening on divert port 10000\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\npdivert: got 84 bytes 10.0.1.10 -> 10.0.1.1\r\n"]
[34.664226, "o", " (pdivert flipped the last payload byte and fixed the ICMP checksum)\r\n\r\n"]
[34.664382, "o", "\u001b[1m--- 10. shutdown ---\u001b[0m\r\n\u001b[1;36mpftest-pf0# pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'\u001b[0m\r\n"]
[34.938113, "o", "pf disabled\r\npf disabled\r\n"]
[35.340577, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf '\u001b[0m\r\n"]
[35.617928, "o", "pf 552960 0\r\n"]
[36.021187, "o", "\r\n\u001b[1mdemo over - port, gates and docs:\u001b[0m\r\n\u001b[1m https://git.labexposed.com/lgcosta/pf-linux-port\u001b[0m\r\n\r\n"]
+16 -3
View File
@@ -76,7 +76,7 @@ EGRESS_IF=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | c
[ -n "$EGRESS_IF" ] || EGRESS_IF=eth2 [ -n "$EGRESS_IF" ] || EGRESS_IF=eth2
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \ scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \
tools/out/pdivert "root@$PF0_MGMT:/tmp/" 2>/dev/null tools/out/pdivert tools/out/pflogd "root@$PF0_MGMT:/tmp/" 2>/dev/null
bold "===============================================================" bold "==============================================================="
bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel" bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel"
@@ -138,7 +138,20 @@ 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'" p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
echo echo
bold "--- 8. divert-to: the packet goes to userspace and comes back ---" bold "--- 8. pflog: logged packets reach userspace (/dev/pflog) ---"
p0 "cat > /tmp/demo-log.conf <<EOF
pass in quick log proto icmp from $CLI_IP keep state
pass out quick
EOF"
p0 "/opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'"
p0 "chmod +x /tmp/pflogd; rm -f /tmp/pflog.pcap; (setsid /tmp/pflogd -c 2 /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; echo 'pflogd collecting 2 records'"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
sleep 2
p0 "cat /tmp/pflogd.log; ls -l /tmp/pflog.pcap; echo 'pcap header (magic d4c3b2a1, linktype 0x75=DLT_PFLOG):'; od -An -tx1 -N24 /tmp/pflog.pcap"
p0 "pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
echo
bold "--- 9. divert-to: the packet goes to userspace and comes back ---"
p0 "cat /tmp/demo-divert.conf" p0 "cat /tmp/demo-divert.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'" p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"
p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert" p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"
@@ -147,7 +160,7 @@ p0 "head -3 /tmp/pdivert.log"
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)" echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
echo echo
bold "--- 9. shutdown ---" bold "--- 10. shutdown ---"
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'" p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
p0 "lsmod | grep '^pf '" p0 "lsmod | grep '^pf '"
echo echo
+5 -2
View File
@@ -22,15 +22,18 @@ deploy() {
# showed up as a bogus gate failure. Retry and verify the copy. # showed up as a bogus gate failure. Retry and verify the copy.
local i local i
for i in 1 2 3; do for i in 1 2 3; do
local extra=()
[ -x "$REPO/tools/out/pdivert" ] && extra+=("$REPO/tools/out/pdivert")
[ -x "$REPO/tools/out/pflogd" ] && extra+=("$REPO/tools/out/pflogd")
if scp -q -o ProxyJump=root@192.168.1.242 -o StrictHostKeyChecking=accept-new \ if scp -q -o ProxyJump=root@192.168.1.242 -o StrictHostKeyChecking=accept-new \
-i "$HOME/.ssh/pflab_ed" "$REPO/module/pf.ko" \ -i "$HOME/.ssh/pflab_ed" "$REPO/module/pf.ko" \
"$REPO/tools/out/pfctl" "$REPO/tests/confs/"*.conf pf0:/tmp/ 2>/dev/null; then "$REPO/tools/out/pfctl" "${extra[@]}" "$REPO/tests/confs/"*.conf pf0:/tmp/ 2>/dev/null; then
break break
fi fi
sleep 2 sleep 2
done done
# stage onto pf0 /opt/pf via mgmt alias already copied to /tmp # stage onto pf0 /opt/pf via mgmt alias already copied to /tmp
ssh "$DEB" "$SSH 'mkdir -p /opt/pf; mv /tmp/pf.ko /tmp/pfctl /opt/pf/ 2>/dev/null; true'" >/dev/null ssh "$DEB" "$SSH 'mkdir -p /opt/pf; mv /tmp/pf.ko /tmp/pfctl /tmp/pdivert /tmp/pflogd /opt/pf/ 2>/dev/null; chmod +x /opt/pf/pfctl /opt/pf/pdivert /opt/pf/pflogd 2>/dev/null; true'" >/dev/null
ssh "$DEB" "$SSH 'test -s /opt/pf/pf.ko && test -x /opt/pf/pfctl'" >/dev/null 2>&1 || { ssh "$DEB" "$SSH 'test -s /opt/pf/pf.ko && test -x /opt/pf/pfctl'" >/dev/null 2>&1 || {
echo "deploy: pf.ko/pfctl missing on pf0"; return 1; } echo "deploy: pf.ko/pfctl missing on pf0"; return 1; }
} }
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# M3 gate: rdr-to — destination/port redirect.
#
# Lab: the client connects to the firewall's own address 10.0.1.1:8080; pf
# redirects it to the server (10.0.2.20:80, busybox httpd). This exercises
# the inbound translation *and* the re-route of a packet whose destination pf
# rewrote (the packet is no longer local to the firewall).
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
# the runner only resets pf0 between gates - clear any responder a previous
# run (or a manual session) left on the server, or it answers first and this
# gate sees the wrong payload.
srv 'pkill -f '[l]oop.sh' 2>/dev/null; pkill -f '[b]ig.sh' 2>/dev/null; pkill nc 2>/dev/null; true' >/dev/null
sleep 1
# a tiny responder on srv:80 (base64-encoded so it survives the two ssh hops
# without nested quoting problems; detached so ssh returns)
LOOP=$(printf 'while :; do nc -l -p 80 < /tmp/resp.txt >/dev/null 2>&1; done\n' | base64 | tr -d '\n')
# NOTE: never pkill -f a pattern that also appears in this very command line -
# it kills the invoking shell. Use a pidfile and check the listening socket.
srv "echo PF-RDR-OK > /tmp/resp.txt; echo $LOOP | base64 -d > /tmp/loop.sh; [ -f /tmp/loop.pid ] && kill \$(cat /tmp/loop.pid) 2>/dev/null; pkill nc 2>/dev/null; setsid sh /tmp/loop.sh >/dev/null 2>&1 </dev/null & echo \$! > /tmp/loop.pid; sleep 1; netstat -lnt 2>/dev/null | grep -q ':80 ' && echo RESPONDER_UP" | grep -q RESPONDER_UP && ok "responder up on srv:80" || bad "responder start"
cat > /tmp/g06.conf <<C
pass in quick proto tcp from 10.0.1.0/24 to 10.0.1.1 port 8080 rdr-to $SRV_IP port 80 keep state
pass out quick proto tcp keep state
C
pfload /tmp/g06.conf || { bad "rdr rule load"; finish; }
pf0 '/opt/pf/pfctl -sr' | grep -q 'rdr-to' && ok "rdr rule listed" || bad "rdr not listed"
# the client talks to the firewall's address; pf redirects to srv:80
RESP=$(cli 'printf "GET / HTTP/1.0\r\n\r\n" | nc -w3 10.0.1.1 8080' 2>/dev/null)
if echo "$RESP" | grep -q 'PF-RDR-OK'; then
ok "redirect delivered the response"
else
bad "no redirected response"
printf '%s\n' "$RESP" | head -4 | sed 's/^/ /'
fi
# the state must show the translated tuple (server:80 with the original in parens)
pf0 '/opt/pf/pfctl -ss' | grep -E "$SRV_IP:80 .*$CLI_IP" >/dev/null && ok "state shows the translation" || bad "no translated state"
# the reply path must survive the un-redirect
pf0 '/opt/pf/pfctl -s info' | grep -qiE 'match' && ok "counters readable" || bad "counters"
srv '[ -f /tmp/loop.pid ] && kill $(cat /tmp/loop.pid) 2>/dev/null; pkill nc 2>/dev/null; true' >/dev/null
# bulk (GSO/TSO) through the redirect: 2 MB must arrive byte-identical.
# This is the regression that "materialize the checksum in software" introduced:
# computing the transport checksum once for a GSO skb corrupts every segment
# after the first, and the peer silently stalls (sshd mute, transfers hang).
B64=$(printf 'head -c 2000000 /dev/zero | tr "\0" A > /tmp/big.txt\n' | base64 | tr -d '\n')
srv "echo $B64 | base64 -d > /tmp/mkbig.sh; sh /tmp/mkbig.sh; pkill nc 2>/dev/null; sleep 1; nohup setsid nc -l -p 9091 < /tmp/big.txt >/dev/null 2>&1 & sleep 1; netstat -lnt | grep -q 9091 && echo BULK_UP" | grep -q BULK_UP || bad "bulk listener"
pf0 "printf 'pass in quick proto tcp from 10.0.1.0/24 to 10.0.1.1 port 9091 rdr-to $SRV_IP port 9091 keep state\npass out quick proto tcp keep state\n' > /tmp/g06b.conf; /opt/pf/pfctl -f /tmp/g06b.conf && echo BULK_RULE" >/dev/null
GOT=$(cli 'nc -w10 10.0.1.1 9091 | wc -c' | tr -d '[:space:]')
EXP=$(srv 'wc -c < /tmp/big.txt' | tr -d '[:space:]')
if [ "$GOT" = "$EXP" ] && [ "${GOT:-0}" -ge 2000000 ]; then
ok "2 MB through the redirect intact ($GOT bytes)"
else
bad "bulk through redirect: got $GOT want $EXP"
fi
# the firewall must still answer ssh (the GSO path must not wedge the box)
pf0 'echo SSH_ALIVE' | grep -q SSH_ALIVE && ok "pf0 sshd still responsive" || bad "pf0 unreachable after bulk"
srv 'pkill nc 2>/dev/null; true' >/dev/null
pf0 '/opt/pf/pfctl -d' >/dev/null
finish
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# M4 gate: pflog — logged packets must reach userspace.
#
# pflogd reads /dev/pflog (the port's chardev backend) and writes a pcap with
# linktype DLT_PFLOG (117). A `log` rule + ICMP traffic must produce records
# that contain the real packet bytes (checked by searching the client address
# in the capture) and a valid pcap header.
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
pf0 'pkill -f "[p]flogd" 2>/dev/null; rm -f /tmp/pflog.pcap; true'
LOGD=$(pfput ../../tools/out/pflogd) || { bad "pflogd deploy"; finish; }
pf0 "test -c /dev/pflog && echo DEV_OK" | grep -q DEV_OK || { bad "/dev/pflog missing"; finish; }
pf0 "chmod +x $LOGD; (setsid $LOGD -c 4 -f /dev/pflog /tmp/pflog.pcap >/tmp/pflogd.log 2>&1 </dev/null &); sleep 1; pgrep pflogd >/dev/null && echo DAEMON_UP" \
| grep -q DAEMON_UP || { bad "pflogd start"; finish; }
cat > /tmp/g12.conf <<C
pass in quick log proto icmp from $CLI_IP keep state
pass out quick proto icmp keep state
C
pfload /tmp/g12.conf || { bad "log rule load"; finish; }
cli "ping -c3 -W2 $SRV_IP >/dev/null 2>&1"
sleep 2
# pcap header: magic d4c3b2a1 (LE) and network = 117 at offset 20
HDR=$(pf0 "od -An -tx1 -N24 /tmp/pflog.pcap 2>/dev/null | tr -d '[[:space:]]'")
if [ "${HDR:0:8}" = "d4c3b2a1" ]; then ok "pcap magic ok"; else bad "pcap header: $HDR"; fi
if [ "${HDR:40:4}" = "7500" ] || [ "${HDR:40:8}" = "75000000" ]; then
ok "linktype DLT_PFLOG (117)"
else
bad "linktype: ${HDR:40:8}"
fi
SZ=$(pf0 'wc -c < /tmp/pflog.pcap' | tr -d '[:space:]')
if [ "${SZ:-0}" -gt 24 ]; then ok "records captured ($SZ bytes)"; else bad "no records ($SZ)"; fi
# the client address must appear inside a captured packet
if pf0 "od -An -tx1 /tmp/pflog.pcap | tr -d '[[:space:]]' | grep -q 0a00010a"; then
ok "captured payload contains $CLI_IP"
else
bad "client address not in capture"
fi
pf0 'pkill pflogd 2>/dev/null; /opt/pf/pfctl -d' >/dev/null
finish
+10 -1
View File
@@ -57,7 +57,7 @@ GOODTESTS := tests/01_basic.conf tests/02_translations.conf \
tests/03_queues_scrub.conf tests/03_queues_scrub.conf
.PHONY: all pfctl abitest test abi clean .PHONY: all pfctl abitest test abi clean
all: pfctl abitest pdivert all: pfctl abitest pdivert pflogd
pfctl: $(OUT)/pfctl pfctl: $(OUT)/pfctl
@@ -190,3 +190,12 @@ $(PD): pdivert/pdivert.c
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ pdivert/pdivert.c $(CC) $(CFLAGS) $(LDFLAGS) -o $@ pdivert/pdivert.c
clean: clean:
# ---------------------------------------------------------------------------
# pflogd - reads /dev/pflog and writes DLT_PFLOG pcap (self-contained)
LOGD := $(OUT)/pflogd
pflogd: $(LOGD)
$(LOGD): pflogd/pflogd.c
@mkdir -p $(OUT)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ pflogd/pflogd.c
+184
View File
@@ -0,0 +1,184 @@
/*
* pflogd for PF-on-Linux (ISC 2026)
*
* Reads records from /dev/pflog (see module/pfsrc/pfk_pflog.c) and writes a
* pcap file with linktype DLT_PFLOG (117) - the same format OpenBSD's pflogd
* produces, so tcpdump -r / wireshark decode it.
*
* pflogd [-i iface] [-f /dev/pflog] [-p pidfile] [-c count] <file>
*
* A "-" as <file> writes to stdout. Omitting <file> prints a human-readable
* one-line summary per record (useful in the lab gates).
*/
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#define PFLOG_IFNAMSIZ 16
#define PFLOG_RULESET_NAME 16
#define DLT_PFLOG 117
struct pflog_hdr {
int64_t ts_sec;
int64_t ts_usec;
uint8_t af;
uint8_t reason;
uint8_t ifname[PFLOG_IFNAMSIZ];
uint8_t ruleset[PFLOG_RULESET_NAME];
uint8_t nr;
uint8_t anc;
uint8_t flags;
uint8_t pad[9];
uint16_t pktlen; /* network order, as in BSD */
} __attribute__((packed));
struct pcap_file_header {
uint32_t magic;
uint16_t major, minor;
int32_t thiszone;
uint32_t sigfigs, snaplen, network;
};
struct pcap_pkthdr {
uint32_t ts_sec, ts_usec, caplen, len;
};
static volatile int running = 1;
static void onterm(int s) { (void)s; running = 0; }
static const char *reason_name(uint8_t r)
{
static const char *n[] = { "pass", "drop", "scrub", "noscrub",
"nat", "nonat", "rdr", "nordr" };
return r < 8 ? n[r] : "?";
}
int
main(int argc, char **argv)
{
const char *dev = "/dev/pflog", *file = NULL, *pidfile = NULL;
long want = -1;
int fd, out = 1, ch, wrote_header = 0, nrec = 0;
unsigned char buf[sizeof(struct pflog_hdr) + 2048];
while ((ch = getopt(argc, argv, "f:c:p:")) != -1)
switch (ch) {
case 'f': dev = optarg; break;
case 'c': want = atol(optarg); break;
case 'p': pidfile = optarg; break;
default: goto usage;
}
if (optind < argc)
file = argv[optind];
signal(SIGINT, onterm);
signal(SIGTERM, onterm);
fd = open(dev, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "pflogd: open %s: %s\n", dev, strerror(errno));
return 1;
}
if (file && strcmp(file, "-") != 0) {
out = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (out < 0) {
fprintf(stderr, "pflogd: open %s: %s\n", file,
strerror(errno));
return 1;
}
}
if (pidfile) {
FILE *p = fopen(pidfile, "w");
if (p) { fprintf(p, "%d\n", (int)getpid()); fclose(p); }
}
while (running) {
ssize_t n, off = 0;
struct pflog_hdr *h;
/* read one full record: the chardev returns header+payload
* consecutively; a short read just continues where we left */
while (off < (ssize_t)sizeof(*h)) {
n = read(fd, buf + off, sizeof(buf) - off);
if (n <= 0) {
if (n < 0 && errno == EINTR)
goto cont;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
fd_set r;
struct timeval tv = { 0, 500000 };
FD_ZERO(&r); FD_SET(fd, &r);
if (select(fd + 1, &r, NULL, NULL, &tv) <= 0)
goto cont;
continue;
}
goto cont;
}
off += n;
}
h = (struct pflog_hdr *)buf;
{
uint16_t plen = ntohs(h->pktlen);
while (off < (ssize_t)(sizeof(*h) + plen)) {
n = read(fd, buf + off, sizeof(buf) - off);
if (n <= 0)
break;
off += n;
}
if (off < (ssize_t)(sizeof(*h) + plen))
goto cont;
if (file) {
struct pcap_file_header g = {
.magic = 0xa1b2c3d4, .major = 2,
.minor = 4, .thiszone = 0,
.sigfigs = 0, .snaplen = 65535,
.network = DLT_PFLOG,
};
struct pcap_pkthdr ph = {
.ts_sec = (uint32_t)h->ts_sec,
.ts_usec = (uint32_t)h->ts_usec,
.caplen = sizeof(*h) + plen,
.len = sizeof(*h) + plen,
};
if (!wrote_header) {
if (write(out, &g, sizeof(g)) != sizeof(g))
break;
wrote_header = 1;
}
if (write(out, &ph, sizeof(ph)) != sizeof(ph) ||
write(out, buf, ph.caplen) != (ssize_t)ph.caplen)
break;
} else {
printf("%lld.%06lld %s if=%s ruleset=%s nr=%u "
"len=%u\n", (long long)h->ts_sec,
(long long)h->ts_usec, reason_name(h->reason),
(char *)h->ifname, (char *)h->ruleset,
h->nr, plen);
fflush(stdout);
}
nrec++;
if (want > 0 && nrec >= want)
break;
}
cont:
if (want > 0 && nrec >= want)
break;
}
if (file && strcmp(file, "-") != 0)
close(out);
close(fd);
fprintf(stderr, "pflogd: %d records\n", nrec);
return 0;
usage:
fprintf(stderr, "usage: pflogd [-f dev] [-c count] [-p pidfile] [file]\n");
return 2;
}