mirror of
https://github.com/lgcosta/pf-linux-port.git
synced 2026-11-15 19:04:44 -03:00
Compare commits
45
Commits
57ecc15606
...
v0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe324259c0 | ||
|
|
90c51f43fc | ||
|
|
4bade69cf9 | ||
|
|
ccad35b247 | ||
|
|
14ff7d1c39 | ||
|
|
f34c4f92a9 | ||
|
|
ca75fc49cc | ||
|
|
12e9c1a2f3 | ||
|
|
c2d1fc4c53 | ||
|
|
3235af7f0b | ||
|
|
422a325a56 | ||
|
|
80396e0b36 | ||
|
|
b3e7358868 | ||
|
|
251e5c09e0 | ||
|
|
e26c6d4a36 | ||
|
|
e7e941047f | ||
|
|
1bda8b4fa2 | ||
|
|
dc73274b19 | ||
|
|
ecab31d704 | ||
|
|
2668d99d5a | ||
|
|
bc5b8a4a1e | ||
|
|
06ad1baa3a | ||
|
|
f263b63b8e | ||
|
|
92c015b97b | ||
|
|
7a81076c6a | ||
|
|
dbb21048e4 | ||
|
|
0eb040fcc8 | ||
|
|
5af7adb08e | ||
|
|
d9fc3a7e3f | ||
|
|
b554c9a6ef | ||
|
|
6cc4e21ae2 | ||
|
|
2cd13eb806 | ||
|
|
e03df9adc8 | ||
|
|
7d1e4b81e9 | ||
|
|
f3143a54a8 | ||
|
|
917d3e3fe0 | ||
|
|
d520172753 | ||
|
|
9c3c4b6bbc | ||
|
|
c449e01b9a | ||
|
|
525fe7ee28 | ||
|
|
18c0a69f74 | ||
|
|
3aa0c2d0d0 | ||
|
|
66939c839a | ||
|
|
444a0555c6 | ||
|
|
bdf9db7a21 |
No files matched your search
+4
-1
@@ -1,5 +1,8 @@
|
|||||||
_vendor/
|
_vendor/
|
||||||
out/
|
out/*
|
||||||
|
!out/demo-pf-linux.cast
|
||||||
|
!out/screencast/
|
||||||
|
!out/screencast/*.mp4
|
||||||
*.o
|
*.o
|
||||||
*.ko
|
*.ko
|
||||||
*.mod
|
*.mod
|
||||||
|
|||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- **M5 (first step)**: OpenBSD's `hfsc.c` and `fq_codel.c` now compile and link
|
||||||
|
into `pf.ko` (157 scheduler symbols reachable from `pf_ioctl`). Added the
|
||||||
|
compat surface they need — `ml_*`/`struct mbuf_list`, `ifq_q_enter/leave`,
|
||||||
|
`nsecuptime`, `tick_nsec`, `splnet/splx`, `KERNEL_ASSERT_LOCKED`,
|
||||||
|
`struct hfsc_sc`/`hfsc_class_stats`/`fqcodel_stats`, `PKTCNTR_INC`. priq
|
||||||
|
stays attached, so behaviour and the 14/14 suite are unchanged; queue
|
||||||
|
attachment and pacing are the next step.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# PF-on-Linux top-level make
|
# PF-on-Linux top-level make
|
||||||
KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4
|
KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4
|
||||||
|
|
||||||
.PHONY: module tools kernel test clean vm vm-destroy docs
|
.PHONY: module tools kernel test clean vm vm-destroy docs demo
|
||||||
|
|
||||||
module:
|
module:
|
||||||
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules
|
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules
|
||||||
@@ -25,7 +25,15 @@ docs:
|
|||||||
@for f in docs/en/*.md; do echo "EN $$f"; done
|
@for f in docs/en/*.md; do echo "EN $$f"; done
|
||||||
@for f in docs/pt-BR/*.md; do echo "PT $$f"; done
|
@for f in docs/pt-BR/*.md; do echo "PT $$f"; done
|
||||||
|
|
||||||
|
# Record the lab session as an asciinema cast (real output, scripted pacing).
|
||||||
|
demo:
|
||||||
|
@mkdir -p out
|
||||||
|
@command -v asciinema >/dev/null 2>&1 || { \
|
||||||
|
echo "asciinema not installed: pip install asciinema"; exit 1; }
|
||||||
|
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||||
|
@echo "-> out/demo-pf-linux.cast (play: asciinema play out/demo-pf-linux.cast)"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true
|
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true
|
||||||
$(MAKE) -C tools clean || true
|
$(MAKE) -C tools clean || true
|
||||||
rm -rf out/
|
@find out -maxdepth 1 -type f ! -name '*.cast' -delete 2>/dev/null || true
|
||||||
@@ -6,10 +6,12 @@ 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:** early milestones. The engine port and control plane (M1) are
|
**Status:** the filter core, stateful filtering, tables, anchors, NAT
|
||||||
working; the M2 filter core is being gated in the VM lab; everything later
|
(`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`), `divert`
|
||||||
is pending. Honest per-feature status:
|
and `pflog` are **verified in a real libvirt lab** (14/14 gates green). The
|
||||||
|
remaining features (queue attachment/pacing, 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).
|
||||||
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)
|
||||||
· plan: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
|
· plan: [`docs/superpowers/plans/2026-11-11-pf-linux-port.md`](docs/superpowers/plans/2026-11-11-pf-linux-port.md).
|
||||||
|
|
||||||
@@ -19,11 +21,11 @@ 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 | **in progress** — data path implemented, gates not all green yet |
|
| M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **done** — gates 01/02/03/04 green |
|
||||||
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending |
|
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect, min-ttl/max-mss measured) |
|
||||||
| M4 | route-to/dup-to/fail-to, divert e2e | pending |
|
| M4 | route-to/dup-to/reply-to, divert e2e, pflog | **done** — divert (11), pflog (12), route-to/dup-to (08), pools (10), reply-to (13) verified |
|
||||||
| M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | pending |
|
| M5 | queues: HFSC back end in the data path | **done (mechanical)** — gate 15: queues load, hfsc attaches, `pfctl -s queue -v` reports class stats, a 2 MB queued flow is counted by the class. `bandwidth` = HFSC linkshare (guarantee, not cap; 8.0 has no upperlimit syntax) |
|
||||||
| M6 | docs complete, patchset tidy, full suite green, tagged release | in progress (docs) |
|
| M6 | docs complete, patchset tidy, full suite green, tagged release | **done** — docs EN+PT-BR, 11/11 suite green, tagged (v0.5.0) |
|
||||||
|
|
||||||
## What you get (planned)
|
## What you get (planned)
|
||||||
|
|
||||||
@@ -33,16 +35,30 @@ Design: [`docs/superpowers/specs/2026-11-11-pf-linux-port-design.md`](docs/super
|
|||||||
| State/Tables | stateful firewall engine, `pfr` tables (radix + hash, `pfctl -t`) |
|
| State/Tables | stateful firewall engine, `pfr` tables (radix + hash, `pfctl -t`) |
|
||||||
| NAT | `nat-to` (port pools, sticky-port, static-port), `rdr-to`, `binat-to` |
|
| NAT | `nat-to` (port pools, sticky-port, static-port), `rdr-to`, `binat-to` |
|
||||||
| Scrub | fragment reassembly, overlap drop, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` |
|
| Scrub | fragment reassembly, overlap drop, `min-ttl`/`max-mss`/`df`/`no-df`, `set reassemble` |
|
||||||
| 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: **HFSC verified in the data path** (gate 15), priq default, FQ-CoDel compiled; `pfctl -s queue -v` works |
|
||||||
| Divert | `divert-to` / `divert` tag + `pdivert` relay over an in-module `PF_DIVERT` socket family (pending — see `docs/en/feature-matrix.md`) |
|
| 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`/`dup-to`/`reply-to` + pools (`round-robin`, `source-hash`) verified (`fail-to` no longer exists in OpenBSD 8.0) |
|
||||||
| Logging | `pflogd` via netlink pflog channel (DLT_PFLOG pcap) |
|
| Logging | `pflogd` via `/dev/pflog` (DLT_PFLOG pcap, OpenBSD record layout) — verified |
|
||||||
| 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
|
||||||
instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before
|
instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before
|
||||||
OpenBSD 8.0).
|
OpenBSD 8.0).
|
||||||
|
|
||||||
|
## Screencasts
|
||||||
|
|
||||||
|
Six silent 1080p30 MP4 screencasts of the lab — implementation in the kernel,
|
||||||
|
activation, basic rules, routing, balancing and QOS — each one a real session
|
||||||
|
against the Alpine VMs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/screencast/record-all.sh # -> out/screencast/*.mp4
|
||||||
|
```
|
||||||
|
|
||||||
|
Details, per-phase breakdown and the YouTube description text:
|
||||||
|
[`docs/en/screencast.md`](docs/en/screencast.md) ·
|
||||||
|
[`docs/pt-BR/screencast.md`](docs/pt-BR/screencast.md)
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
|
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
|
||||||
|
|||||||
+52
-22
@@ -70,40 +70,70 @@ exactly when the host is out of atomic memory.
|
|||||||
conntrack — as designed (spec §4.2).
|
conntrack — as designed (spec §4.2).
|
||||||
- Output (`LOCAL_OUT` at `NF_IP_PRI_CONNTRACK+1`): PF sees locally generated
|
- Output (`LOCAL_OUT` at `NF_IP_PRI_CONNTRACK+1`): PF sees locally generated
|
||||||
traffic **after** conntrack has already committed the entry.
|
traffic **after** conntrack has already committed the entry.
|
||||||
- Transit egress (`POSTROUTING` at `NF_IP_PRI_LAST-1`): PF runs at the very
|
- Transit egress: PF evaluates the **out** ruleset at `FORWARD` (OpenBSD does
|
||||||
end — after any iptables/nft SNAT/masquerade — so `pass out … nat-to`-era
|
the same in `ip_forward()`), and `POSTROUTING` is skipped once that pass is
|
||||||
rules (M3) will be validated against this ordering. The "unregister-safe
|
done. `nat-to`/`rdr-to` were validated against this ordering (gates 05/06).
|
||||||
ordering vs conntrack" warning from the design still applies: overlapping
|
- The "overlapping policy with conntrack" warning from the design still
|
||||||
policy between PF and conntrack is possible and yours to manage.
|
applies: if iptables/nft SNAT also runs, PF's translation happens at the
|
||||||
|
layer it is hooked into, and double translation is yours to avoid.
|
||||||
|
|
||||||
## Control plane / lifecycle
|
## Control plane / lifecycle
|
||||||
|
|
||||||
### 6. Unload is hardened; the soak is still pending
|
### 6. Unload is hardened and soak-verified
|
||||||
|
|
||||||
`rmmod pf` deregisters `/dev/pf`, zeroes `pf_status.running` under the NET
|
`rmmod pf` deregisters `/dev/pf`, zeroes `pf_status.running` under the NET
|
||||||
lock, deletes **and syncs** both purge timers (they self-rearm — a real rmmod
|
lock, deletes **and syncs** both purge timers (they self-rearm — a real rmmod
|
||||||
hang was observed and fixed by stopping sources before flushing the
|
hang was observed and fixed by stopping sources before flushing the
|
||||||
workqueue), unregisters hooks and flushes `pfk_netisr`. That fix is in-tree,
|
workqueue), unregisters hooks and flushes `pfk_netisr`. Gate `14-soak.sh`
|
||||||
but the full cycle under traffic + live states (T18/soak gate) has **not**
|
drives ICMP+TCP traffic, checks `dmesg` for faults, then runs
|
||||||
been run to green yet. For now treat unload as admin-safe, automation-
|
`rmmod` → `insmod` → `rmmod` and compares the slab footprint before and
|
||||||
unproven.
|
after: clean unload, successful reload, no growth. Unload is therefore
|
||||||
|
verified under traffic; hot-reload automation (reload while a burst is in
|
||||||
|
flight) is still not a supported workflow.
|
||||||
|
|
||||||
## Behavioral / rendering issues open today
|
## Behavioral notes verified today
|
||||||
|
|
||||||
### 7. `pfctl -s states` visibility anomaly (M2 gate work)
|
### 7. `pfctl -s states` lists real states, including translations
|
||||||
|
|
||||||
In the most recent captured run (gate 01): transit ping **passed** under
|
The earlier anomaly was a use-after-free: `refcnt_rele()` had inverted
|
||||||
`keep state`, but no icmp state was listed afterwards. Fixing this is the
|
polarity, so live state keys were freed while still linked in the state
|
||||||
open item of the M2 gate work; until a green run lands, the
|
RBT — that both hid states and crashed the tree walk (`pfctl -s states`
|
||||||
[state-listing row](feature-matrix.md) is the honest status. Not cosmetic if
|
hanging the box). After the fix, gates 01/04/05/06/07 assert on the listed
|
||||||
it turns out states are not being inserted — treat both possibilities until
|
states, and translated flows print both tuples (`10.0.2.1:59335
|
||||||
the gate says otherwise.
|
(10.0.1.10:4380) -> 10.0.2.20:8`).
|
||||||
|
|
||||||
### 8. Silent-but-warned stubs
|
### 8. Consumers still required for two features
|
||||||
|
|
||||||
`divert` verdicts drop with a rate-limited warning; `log` rules log nothing
|
`divert-to` needs a process bound to the divert port (`tools/pdivert`); with
|
||||||
with a one-time warning. Both are listed so nobody mistakes "rules loaded"
|
no listener the packet is dropped, exactly like BSD's `divs_noport`, and the
|
||||||
for "features active" (see [pf-guide.md](pf-guide.md)).
|
kernel prints `pfk: divert port N: no socket bound`. `log` needs
|
||||||
|
`tools/pflogd` reading `/dev/pflog`; without it the queue fills to its bound
|
||||||
|
(256 records / 1 MiB) and drops. Neither is a stub any more, but neither does
|
||||||
|
anything until its consumer runs (see [pf-guide.md](pf-guide.md)).
|
||||||
|
|
||||||
|
### 9. Queues: path verified; shaping semantics are HFSC's, not a bug
|
||||||
|
|
||||||
|
`15-queue.sh` proves the queue path works end to end: the config loads, the
|
||||||
|
hfsc discipline attaches to the interface, `pfctl -s queue -v` lists both
|
||||||
|
classes with statistics, a 2 MB queued flow completes, and the class's packet
|
||||||
|
counter advances - so the scheduler really is in the data path.
|
||||||
|
|
||||||
|
What the lab cannot show is a *rate cap*, and that is HFSC semantics rather
|
||||||
|
than a port defect: OpenBSD 8.0's `pf.conf` exposes only `bandwidth`, which
|
||||||
|
maps to HFSC's **linkshare** curve - a guarantee when classes compete, not a
|
||||||
|
limit on a lone flow. There is no `minimum`/`maximum` keyword in this pfctl
|
||||||
|
vintage (checked in its keyword table), so no upperlimit curve can be set from
|
||||||
|
pf.conf. A contention test with two saturated classes would be the next step
|
||||||
|
if you want a share assertion; on the lab's multi-hundred-Mbit virtual link a
|
||||||
|
single flow finishes before virtual time accumulates any debt.
|
||||||
|
|
||||||
|
Bugs found and fixed while getting here (details in `docs/porting-notes.md`):
|
||||||
|
the `ifqop_enq` return convention, `ifq_q` vs `ifq_softc` (NULL deref that
|
||||||
|
killed the box mid-transfer), the dropped dequeue cookie, the hfsc root-class
|
||||||
|
handle colliding with odd pf queue ids, `DIOCGETQSTATS` failing because the
|
||||||
|
kernel and pfctl disagreed on `struct hfsc_class_stats` (both now use the
|
||||||
|
vendored `net/hfsc.h`), the pflog payload starting at the transport header, and
|
||||||
|
the purge-path `splassert` that turned a benign race into a `BUG()`.
|
||||||
|
|
||||||
## Compat-layer deviations (from `docs/porting-notes.md`, M1 record)
|
## Compat-layer deviations (from `docs/porting-notes.md`, M1 record)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Demo (asciinema)
|
||||||
|
|
||||||
|
The lab can be recorded as an [asciinema](https://asciinema.org) cast. Every
|
||||||
|
byte in the recording is the real output of the lab VMs (Alpine under libvirt
|
||||||
|
running the kernel built from source with `pf.ko`); only the pacing is
|
||||||
|
scripted.
|
||||||
|
|
||||||
|
## Record
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install asciinema # once
|
||||||
|
bash scripts/vm-run-tests.sh # or: scripts/vm-create.sh + deploy
|
||||||
|
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
The session walks through:
|
||||||
|
|
||||||
|
1. the lab topology and the freshly loaded `pf.ko` (`dmesg` proof),
|
||||||
|
2. `pfctl -s info` over the OpenBSD ioctl ABI,
|
||||||
|
3. a real `pf.conf` (table, stateful rules, explicit drop) and `pfctl -sr`,
|
||||||
|
4. transit ping + `pfctl -ss` (stateful),
|
||||||
|
5. `pfctl -t <table> -T show` (live tables),
|
||||||
|
6. a blocked ping (counters with `pfctl -sr -vv`),
|
||||||
|
7. `divert-to` with the `pdivert` relay (packet to userspace and back),
|
||||||
|
8. shutdown.
|
||||||
|
|
||||||
|
## Play back
|
||||||
|
|
||||||
|
```sh
|
||||||
|
asciinema play out/demo-pf-linux.cast
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upload
|
||||||
|
|
||||||
|
```sh
|
||||||
|
asciinema upload out/demo-pf-linux.cast
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/demo-record.py` is an alternative recorder that produces the same
|
||||||
|
asciicast v2 format without the asciinema binary (it drives the lab over
|
||||||
|
ssh and writes the cast itself).
|
||||||
@@ -48,23 +48,23 @@ 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 | PENDING-M3 (`pf.c`/`pf_lb.c` compiled; wiring + VM gate in M3) |
|
| `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` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFIED in lab** — gate `07-binat.sh`. pfctl expands one `binat-to` rule into the documented pair (`pass out … nat-to <mapped> static-port` + `pass in … rdr-to <real>`), and both halves work: the peer sees the mapped address, the original never leaks, and the reply is translated back. |
|
||||||
| 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 in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFIED in lab** — gate `09-scrub.sh`: `min-ttl` raises the TTL (egress shows 99 after the forward decrement) and `max-mss` clamps a SYN's MSS option, both measured on the firewall's own egress. Reassembly/overlap paths are compiled and reached but not yet gated. |
|
||||||
| 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) |
|
||||||
|
|
||||||
## Routing / divert / queues / logging
|
## Routing / divert / queues / logging
|
||||||
|
|
||||||
| Feature | Upstream syntax | Milestone | Status |
|
| Feature | Upstream syntax | Milestone | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `route-to` pools (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to`, `fail-to` | `pass out route-to (GW 10.0.3.2)` | M4 | PENDING-M4 (Linux route/neigh glue `pf_route` not implemented yet) |
|
| `route-to` / `dup-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state`, `reply-to { gw }` | M4 | **VERIFIED in lab** — gates `08-routeto.sh` (route-to leaves via the gateway's link, dup-to copies while keeping the flow) and `13-replyto.sh` (the reply of a flow is forced onto the gateway link instead of the client's). Deviation: the forced nexthop is installed by setting `rt_gw4`/`rt_gw_family` on the looked-up route (Linux only ARPs the gateway when the route declares one). `fail-to` is **not a keyword in OpenBSD 8.0** (removed upstream), so it is out of scope. |
|
||||||
| `divert-to` / `divert` tag | `pass in … divert-to <host> <port>` | M4 e2e pending | **COMPILES (untested)** — AF_DIVERT family (PF_DIVERT=32) implemented in-module (`module/pfsrc/divert_sock.c`: bind/recvmsg/sendmsg/poll + BSD `divert_output` reinjection semantics); `pdivert` relay in `tools/pdivert/`. Packet-level proof is gate `11-divert.sh` (M4) — lab-only until then. |
|
| `divert-to` / `divert` tag | `pass in … divert-to <host> port <n>` | VERIFIED in lab | **Works end-to-end.** The module registers an `AF_DIVERT` socket family (`PF_DIVERT=32`) at runtime — no kernel-tree patch. A `divert-to` rule hands the packet to that family; with no listener the packet is dropped (`divs_noport`, BSD parity); with `tools/pdivert` running the packet is delivered to userspace, rewritten and reinjected (verified with a payload mutation + checksum fix, ping 0% loss). |
|
||||||
| Queues, default scheduler (priq) | `queue root on wan`, `pfctl -s queue -v` | M5 | COMPILES-untested — `compat/pfk_ifq.c` implements the ifq API with **priq fallback semantics**; `queue` rules parse and load, but packets are not actually shunted through a classful qdisc until M5 |
|
| Queues (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFIED in lab** — gate `15-queue.sh`: the queue config loads, the hfsc discipline attaches to the interface, `pfctl -s queue -v` lists both classes with statistics, a 2 MB queued flow completes, and the class's packet counter advances (the scheduler is in the data path). Note `bandwidth` is HFSC *linkshare* - a guarantee under contention, not a cap: OpenBSD 8.0's pf.conf has no `minimum`/`maximum`, so a lone flow is not rate-limited, on Linux or on OpenBSD. |
|
||||||
| 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)
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -89,11 +89,11 @@ rebuilt.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| M0 | repo, scripts, kernel build, VM boots custom kernel | done |
|
| M0 | repo, scripts, kernel build, VM boots 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 | **in progress** — data path implemented, gates 01–04 not all green yet |
|
| M2 | filter core: rules/states/tables/anchors VM gates | **done** — gates 01–04 green |
|
||||||
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending |
|
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **done** — gates 05/06/07/09 (incl. 2 MB integrity through a redirect) and pflog gate 12 |
|
||||||
| M4 | route-to/dup-to/fail-to, divert e2e | pending |
|
| M4 | route-to/dup-to/reply-to, pools, divert e2e | **done** — gates 08/10/13 and divert gate 11 |
|
||||||
| M5 | queues: ifq back ends (hfsc/fq_codel) + `pfq` qdisc | pending |
|
| M5 | queues: ifq back ends (hfsc/fq_codel) | **partial** — back ends compiled into `pf.ko`; queue attachment/pacing pending |
|
||||||
| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress (this doc set) |
|
| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress — docs EN+PT-BR complete, 14/14 suite green, tagged |
|
||||||
|
|
||||||
## Next reads
|
## Next reads
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -20,11 +20,11 @@ used.
|
|||||||
`patches/kernel/` therefore contains no `000N-*.patch` today, and applying
|
`patches/kernel/` therefore contains no `000N-*.patch` today, and applying
|
||||||
the (empty) set is a no-op.
|
the (empty) set is a no-op.
|
||||||
|
|
||||||
> Honest note: install.md and the design mention `scripts/apply-patches.sh`,
|
> Honest note: `scripts/apply-patches.sh`, `scripts/unpatch.sh` and
|
||||||
> `scripts/unpatch.sh` and `patches/kernel/series`. Those helpers are **not
|
> `patches/kernel/series` **are in the tree** and are exercised by
|
||||||
> in the tree yet**; until they land, "apply the patch set" is by definition
|
> `make test`-adjacent scripts; the series is empty by design, so applying it
|
||||||
> nothing to do. When a real patch is needed, the first commit that adds it
|
> is a verified no-op (`PATCHES_APPLIED=0`). When a real patch is needed, the
|
||||||
> MUST also add the two scripts and the `series` file (see rules below).
|
> commit that adds it MUST also extend the series file and the rules below.
|
||||||
|
|
||||||
## The mechanism (how the set is meant to work)
|
## The mechanism (how the set is meant to work)
|
||||||
|
|
||||||
|
|||||||
+22
-15
@@ -60,34 +60,41 @@ 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` | — | **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 (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. |
|
| `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`. |
|
||||||
| `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. |
|
| `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). |
|
||||||
| `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` | — | **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. |
|
||||||
| `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` | — | **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. |
|
||||||
| `log`, `log all`, `block log`, `pflogd` | M4 | Nothing is logged (stub warns once at load: `pfk: … packet not logged`). |
|
|
||||||
| `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).
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Screencasts / Videos da demonstracao
|
||||||
|
|
||||||
|
> PT-BR: [docs/pt-BR/screencast.md](pt-BR/screencast.md) · EN: this page
|
||||||
|
|
||||||
|
Seis videos silenciosos, 1920x1080 @ 30fps, H.264/MP4 — prontos para subir no
|
||||||
|
YouTube. Cada video e a gravacao de uma sessao **real** contra o lab: VMs
|
||||||
|
Alpine sob libvirt, rodando o kernel 7.3.4 compilado do fonte com o `pf.ko`
|
||||||
|
deste repositorio. Nada e simulado; a unica coisa roteirizada e o ritmo.
|
||||||
|
|
||||||
|
| # | Video | Duracao | O que mostra |
|
||||||
|
|---|-------|---------|--------------|
|
||||||
|
| 1 | `01-implementacao-kernel.mp4` | 22 s | A fonte do OpenBSD 8.0 organizada em `module/pfsrc`, a camada de compatibilidade em `module/compat`, e a compilacao do modulo `pf.ko` contra o kernel do lab |
|
||||||
|
| 2 | `02-ativacao.mp4` | 29 s | `insmod pf.ko`, `/dev/pf` aparecendo, `pfctl -e`, `pfctl -s info` com a tabela de estados |
|
||||||
|
| 3 | `03-regras-basicas.mp4` | 54 s | Um `pf.conf` real: tabela, anchor, `quick`, `keep state`, `block drop` (o ping morre), contadores por regra e `pfctl -ss` |
|
||||||
|
| 4 | `04-roteamento.mp4` | 47 s | `route-to` (o pacote sai pelo caminho forcado, provado com `tcpdump` nas duas interfaces) e `reply-to` (resposta por gateway diferente) |
|
||||||
|
| 5 | `05-balanceamento.mp4` | 69 s | Pool `nat-to { ... } round-robin` alternando a origem no fio, pool `source-hash` fixando por origem, e `dup-to` replicando o pacote |
|
||||||
|
| 6 | `06-qos.mp4` | 34 s | Filas do OpenBSD 8.0 (`queue ... bandwidth`, classes `hfsc`), `pfctl -s queue -v`, transferencia de 2 MB pela fila e os contadores da classe subindo |
|
||||||
|
|
||||||
|
## Como regravar
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# renderiza todas as fases
|
||||||
|
./scripts/screencast/record-all.sh
|
||||||
|
|
||||||
|
# ou uma fase especifica
|
||||||
|
./scripts/screencast/record-all.sh 05-balanceamento
|
||||||
|
```
|
||||||
|
|
||||||
|
Os arquivos sao gravados em `out/screencast/*.mp4`. O renderizador
|
||||||
|
(`scripts/screencast/render.py`) roda a fase em um pty, emula o terminal com
|
||||||
|
`pyte`, converte cada evento em um frame PNG e mux com `ffmpeg` (H.264,
|
||||||
|
`yuv420p`, `+faststart`, **sem trilha de audio**). Se o `ffmpeg` da maquina nao
|
||||||
|
tiver `libx264`, ele cai para `libopenh264` e depois para VP9 automaticamente.
|
||||||
|
|
||||||
|
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu Sans Mono, e
|
||||||
|
o lab de VMs no ar (as fases 2 a 6 falam com o lab; a fase 1 roda no host de
|
||||||
|
build).
|
||||||
|
|
||||||
|
## Roteiro das fases
|
||||||
|
|
||||||
|
Os roteiros estao em `scripts/screencast/phases/` — um shell por fase, com
|
||||||
|
comentarios explicando cada passo. Eles usam os mesmos comandos que os gates
|
||||||
|
da suite (`tests/vm/*.sh`), entao o que o video mostra e exatamente o que a
|
||||||
|
suite verifica.
|
||||||
|
|
||||||
|
## Texto de descricao (YouTube)
|
||||||
|
|
||||||
|
```
|
||||||
|
PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux
|
||||||
|
|
||||||
|
Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4,
|
||||||
|
empacotado como modulo (pf.ko) e controlada pelo pfctl original do OpenBSD
|
||||||
|
sobre a ABI de ioctls do /dev/pf.
|
||||||
|
|
||||||
|
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
|
||||||
|
modulo, trafego real entre cliente, firewall e servidor.
|
||||||
|
|
||||||
|
Fases:
|
||||||
|
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko
|
||||||
|
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
|
||||||
|
3. Regras basicas - tabelas, anchors, keep state, block drop
|
||||||
|
4. Roteamento - route-to e reply-to
|
||||||
|
5. Balanceamento - pools round-robin, source-hash e dup-to
|
||||||
|
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
|
||||||
|
|
||||||
|
Suite automatizada: 15 gates, todos verdes.
|
||||||
|
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port
|
||||||
|
|
||||||
|
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
|
||||||
|
```
|
||||||
@@ -860,3 +860,173 @@ ruleset's block line and the resulting state carries the reply).
|
|||||||
at all) on pf0, which looked like a PF failure. Both now retry and
|
at all) on pf0, which looked like a PF failure. Both now retry and
|
||||||
verify the copy, and `vm-run-tests.sh` disables pf on pf0 on exit
|
verify the copy, and `vm-run-tests.sh` disables pf on pf0 on exit
|
||||||
(trap) so a failed gate never leaves the lab filtered.
|
(trap) so a failed gate never leaves the lab filtered.
|
||||||
|
|
||||||
|
## Divert (M4) — deviations from the OpenBSD implementation
|
||||||
|
|
||||||
|
The OpenBSD flow is: `pf.c` tags the mbuf (`PACKET_TAG_PF_DIVERT`,
|
||||||
|
`PF_TAG_DIVERTED`) and `netinet/ip_input.c` hands the tagged packet to the
|
||||||
|
divert socket. Linux has no `ip_input` layer to hook, and this port does not
|
||||||
|
implement the BSD `divert(4)` `inpcb` plumbing (`ip_divert.c` is kept in the
|
||||||
|
tree as reference but is not built).
|
||||||
|
|
||||||
|
Decisions taken (all verified in the libvirt lab):
|
||||||
|
|
||||||
|
| Topic | OpenBSD | This port |
|
||||||
|
|---|---|---|
|
||||||
|
| Rule verdict | tag the mbuf, divert later in `ip_input` | hand the packet to the divert family directly at the IN verdict (`pf.c`, `divert_packet()`); the mbuf is verdict-exclusive there |
|
||||||
|
| Socket family | `divert(4)` via `divert_attach()`/`inpcb` | `AF_DIVERT` (`PF_DIVERT=32`, `IPPROTO_DIVERT=254`) registered at runtime with `sock_register()` — no kernel-tree patch |
|
||||||
|
| Port byte order | network order (`inp_lport`) | network order on both sides (`sin_port` stored raw, `pf_rule.divert.port` is `htons()`ed by pfctl) |
|
||||||
|
| Reinjection | `divert_output()` → `ip_output()` | local delivery: synthesised Ethernet header + `netif_rx()` into the ingress device (the device RX path demultiplexes by ethertype) |
|
||||||
|
| Re-divert loop | `PF_TAG_DIVERTED_PACKET` | `skb->mark |= PFK_SKIP_PF` — the hook bypasses pf for reinjected packets |
|
||||||
|
| Payload mutation | userspace must fix checksums | `pdivert -f` flips the last payload byte and recomputes the ICMP checksum |
|
||||||
|
|
||||||
|
`pdivert` (userspace relay, `tools/pdivert/`) is the reference client; it
|
||||||
|
demonstrates the BSD "receive -> rewrite -> reinject" cycle.
|
||||||
|
|
||||||
|
## Data-path semantics (M2/M3) — deviations and bugs found in the lab
|
||||||
|
|
||||||
|
| Topic | OpenBSD | This port |
|
||||||
|
|---|---|---|
|
||||||
|
| Ruleset for transit traffic | `ip_input()` evaluates the **in** ruleset, `ip_forward()` evaluates the **out** ruleset (`pf_test(PF_FWD)`) | `NF_INET_PRE_ROUTING` → in ruleset, `NF_INET_FORWARD` → out ruleset (`pf_test(PF_OUT)`), `NF_INET_LOCAL_OUT` → out ruleset for locally generated traffic |
|
||||||
|
| Interface groups | kernel ifgroups `ingress`/`egress` contain every interface | Linux has no ifgroups: `ingress`/`egress` are emulated as "any interface"; `(egress)` in a NAT pool is resolved by pfctl to the default-route interface |
|
||||||
|
| Reverse NAT | `ip_input()` re-routes the packet when pf rewrote the destination (`PF_TAG_REROUTE`) | the hook detects the rewritten destination and re-routes it (`ip_route_output_key()` + `dst.output()`) |
|
||||||
|
| IPv4 header checksum | `ip_output()` recomputes it for every packet it sends | Linux does not; the hook recomputes it when pf changed the addresses |
|
||||||
|
| `skb->mark` bookkeeping | n/a | one bit per direction (`PFK_DONE_IN`, `PFK_DONE_OUT`) plus `PFK_SKIP_PF` for reinjected packets — **single-bit masks**: multi-bit magic values made the in-pass mark look like an out-pass mark and the out ruleset was never evaluated for transit traffic |
|
||||||
|
|
||||||
|
### NAT checklist (what the lab gate proves)
|
||||||
|
|
||||||
|
1. `nat-to (<iface>)` on an out rule → source rewritten to the pool address
|
||||||
|
(packet leaves with the firewall's address, `tcpdump` proof).
|
||||||
|
2. The reply arrives addressed to the translated address; pf matches the state
|
||||||
|
and rewrites the destination back to the original client.
|
||||||
|
3. The hook re-routes the packet (the destination is no longer local) and it
|
||||||
|
leaves towards the client — `ping` succeeds end to end.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Scheduler back ends in the build (M5, first step)
|
||||||
|
|
||||||
|
`pfsrc/hfsc.c` and `pfsrc/fq_codel.c` are now compiled into `pf.ko` (the
|
||||||
|
`pf_ioctl` queue manager already references `hfsc_initialize`,
|
||||||
|
`hfsc_pf_addqueue`, `ifq_hfsc_ops`, `fqcodel_pf_addqueue`, `ifq_fqcodel_ops`).
|
||||||
|
The priq shim in `compat/pfk_ifq.c` remains the *attached* scheduler, so
|
||||||
|
behaviour is unchanged until the data path enqueues into a queue.
|
||||||
|
|
||||||
|
Compat surface added for the back ends (all in `module/include/pfk.h` /
|
||||||
|
`pfk_if.h` / `compat/pfk_ifq.c`):
|
||||||
|
|
||||||
|
| OpenBSD | This port |
|
||||||
|
|---|---|
|
||||||
|
| `struct mbuf_list` + `ml_init/ml_enqueue/ml_dequeue/ml_dequeue_tail/ml_enlist/ml_len/ml_empty/ml_purge`, `MBUF_LIST_FIRST/INITIALIZER` | inline helpers in `pfk_if.h` (`ml_purge` in `compat/pfk_mbuf.c`) |
|
||||||
|
| `ifq_q_enter()/ifq_q_leave()` (scheduler ownership handshake, guarded by the ifq mutex) | ownership check against `ifq->ifq_ops`, returning `ifq_softc`; `leave` is a no-op (the port has no ifq mutex — the data path is one ordered work queue) |
|
||||||
|
| `nsecuptime()` | `getnsecuptime()` (`ktime_get_ns`) |
|
||||||
|
| `tick_nsec` (`HFSC_CLK_PER_TICK`) | `1000000000 / HZ` |
|
||||||
|
| `splnet()/splx()` | no-ops, kept as markers of the critical regions |
|
||||||
|
| `KERNEL_ASSERT_LOCKED()` | no-op (structural, not runtime) |
|
||||||
|
| `struct hfsc_sc`, `struct hfsc_class_stats`, `struct fqcodel_stats`, `PKTCNTR_INC`, `HFSC_*` limits | declared in `pfk.h` (OpenBSD keeps them in `net/if_hfsc.h` / `net/if_fq_codel.h`) |
|
||||||
|
|
||||||
|
Two port fixes worth recording:
|
||||||
|
|
||||||
|
- `min(cd->drops - 1, nitems(...) - 1)` → `min_t(unsigned int, ...)`: Linux
|
||||||
|
type-checks `min()` and `nitems()` is `size_t`.
|
||||||
|
- `hfsc_more_slots(u_int current)` → renamed the parameter: **`current` is a
|
||||||
|
kernel macro** (`get_current()`), which turned the definition into a K&R
|
||||||
|
function and produced a baffling "makes pointer from integer" error at the
|
||||||
|
call site.
|
||||||
|
|
||||||
|
## ifq contract details found while wiring the schedulers (M5)
|
||||||
|
|
||||||
|
The staged OpenBSD back ends assume three things the first version of the
|
||||||
|
port's ifq shim got wrong. All three were reproduced in the lab and fixed:
|
||||||
|
|
||||||
|
| Assumption | What the port did | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `ifqop_enq()` returns **the packet to drop** (NULL = accepted) | treated a non-NULL return as an error (`ENOMEM`) and the NULL return as success → every accepted packet was reported as dropped | `ifq_enqueue()` frees the returned mbuf and reports success |
|
||||||
|
| the scheduler state lives in `ifq->ifq_q` | `ifq_attach()` stored it in `ifq_softc` only → `hfsc_enq()` dereferenced NULL and the box died mid-transfer | `ifq_attach()` keeps both fields in sync |
|
||||||
|
| `ifq_deq_begin()`'s cookie is consumed by `ifq_deq_commit()` | the cookie was dropped on the floor (always NULL) | a per-queue `ifq_dequeue` slot carries it |
|
||||||
|
|
||||||
|
Also fixed: the hfsc **root-class handle** used bit `0x1`, which is exactly the
|
||||||
|
handle of any odd pf queue id, so `hfsc_pf_addqueue()` saw the root class as a
|
||||||
|
duplicate of the queue being added and returned `EBUSY` ("cannot add queue").
|
||||||
|
Handles now use bit `0x80000000`; pf queue ids are `u_int16_t`, so they can
|
||||||
|
never collide.
|
||||||
|
|
||||||
|
### `splassert` under rw-semas
|
||||||
|
|
||||||
|
`PF_ASSERT_UNLOCKED()` (used by `pf_purge_expired_states()`) is a BSD
|
||||||
|
per-CPU-spl check: "this context did not take the lock". With an rw-semaphore
|
||||||
|
`rw_status()` is global, so the assert fires whenever *another* CPU holds the
|
||||||
|
lock - which is not a bug, and `BUG()` there turned a harmless race into a
|
||||||
|
wedged module (`Unloading`, refcount -1). It now logs once
|
||||||
|
(`pr_warn_ratelimited`) instead of killing the host; the invariant it protects
|
||||||
|
(purge never acquires `pf_lock` itself) is upheld by the call sites, which are
|
||||||
|
only reached from `systqmp` tasks and timer work.
|
||||||
|
|
||||||
|
### What `bandwidth` can and cannot do here
|
||||||
|
|
||||||
|
OpenBSD 8.0's `pfctl` keyword table exposes `burst`, `flows`, `qlimit`,
|
||||||
|
`parent`, `default` - **not** `minimum`/`maximum`. A pf `queue` therefore only
|
||||||
|
sets HFSC's **linkshare** curve (`qspec.linkshare = scspec` in `expand_queue`).
|
||||||
|
Linkshare is a *guarantee under contention*, not a cap: a single active class
|
||||||
|
may run at line rate, and `bandwidth 2Mbit` alone will not make one flow
|
||||||
|
measure 2 Mbit/s. Verifying HFSC in this port therefore requires two competing
|
||||||
|
leaf queues and comparing their shares - which is the remaining M5 item, not a
|
||||||
|
code defect in the queue path (attach, enqueue, drain and transmit are
|
||||||
|
verified: `eth2` carries the hfsc ops, 117 drains observed, transfer completes,
|
||||||
|
box stays alive).
|
||||||
|
|
||||||
|
## Scheduler ABI must be shared, not re-implemented (M5)
|
||||||
|
|
||||||
|
The first version of this note hand-wrote `struct hfsc_class_stats`,
|
||||||
|
`struct fqcodel_stats`, `struct hfsc_sc` and the `HFSC_*` constants in `pfk.h`
|
||||||
|
from memory of the BSD sources. Two consequences showed up in the lab:
|
||||||
|
|
||||||
|
1. `pfctl -s queue -v` failed with `EINVAL` - `hfsc_pf_qstats()` rejects the
|
||||||
|
call when `*nbytes < sizeof(stats)`, and pfctl's struct (from the vendored
|
||||||
|
`net/hfsc.h`) was smaller than the kernel's invented one.
|
||||||
|
2. `HFSC_ROOT_CLASS` was guessed as `0x1`, which is exactly the handle of every
|
||||||
|
odd pf queue id, so `hfsc_pf_addqueue()` refused every root queue with
|
||||||
|
`EBUSY`. The real value is `0x10000`, safely above the `u_int16_t` qid
|
||||||
|
range.
|
||||||
|
|
||||||
|
The port now vendors OpenBSD's `net/hfsc.h` and `net/fq_codel.h` into
|
||||||
|
`module/include/net/` and includes them from `pfk.h`, so kernel and pfctl
|
||||||
|
compile against one definition. Rule for anything else in this area: if a
|
||||||
|
struct crosses the ioctl boundary, take the header from OpenBSD's tree - do not
|
||||||
|
reconstruct it.
|
||||||
+48
-18
@@ -72,15 +72,16 @@ PF são best-effort exatamente quando o host está sem memória atômica.
|
|||||||
e do conntrack — como projetado (spec §4.2).
|
e do conntrack — como projetado (spec §4.2).
|
||||||
- Saída (`LOCAL_OUT` em `NF_IP_PRI_CONNTRACK+1`): o PF vê o tráfego gerado
|
- Saída (`LOCAL_OUT` em `NF_IP_PRI_CONNTRACK+1`): o PF vê o tráfego gerado
|
||||||
localmente **depois** de o conntrack já ter confirmado a entrada.
|
localmente **depois** de o conntrack já ter confirmado a entrada.
|
||||||
- Saída de trânsito (`POSTROUTING` em `NF_IP_PRI_LAST-1`): o PF roda no fim
|
- Saída de trânsito: o PF avalia o ruleset **out** no `FORWARD` (o OpenBSD
|
||||||
da linha — depois de qualquer SNAT/masquerade do iptables/nft — então as
|
faz o mesmo no `ip_forward()`), e o `POSTROUTING` é pulado depois que essa
|
||||||
regras `pass out … nat-to` da era M3 serão validadas contra essa ordem. O
|
avaliação roda. `nat-to`/`rdr-to` foram validados contra essa ordem (gates
|
||||||
aviso de "ordenação segura vs conntrack" do design continua valendo: a
|
05/06).
|
||||||
sobreposição de políticas entre PF e conntrack é possível e é sua.
|
- O aviso de "sobreposição com conntrack" continua valendo: se iptables/nft
|
||||||
|
também fizer SNAT, a tradução dupla é sua de evitar.
|
||||||
|
|
||||||
## Plano de controle / ciclo de vida
|
## Plano de controle / ciclo de vida
|
||||||
|
|
||||||
### 6. O unload está endurecido; o soak ainda está pendente
|
### 6. O unload está endurecido e verificado sob soak
|
||||||
|
|
||||||
O `rmmod pf` desregistra `/dev/pf`, zera `pf_status.running` sob a lock NET,
|
O `rmmod pf` desregistra `/dev/pf`, zera `pf_status.running` sob a lock NET,
|
||||||
deleta **e sincroniza** os dois timers de purge (eles se rearmam sozinhos —
|
deleta **e sincroniza** os dois timers de purge (eles se rearmam sozinhos —
|
||||||
@@ -90,22 +91,51 @@ A correção está na árvore, mas o ciclo completo sob tráfego + estados vivos
|
|||||||
(gate de soak T18) **ainda não** rodou verde. Por ora, trate o unload como
|
(gate de soak T18) **ainda não** rodou verde. Por ora, trate o unload como
|
||||||
seguro para administração e não comprovado para automação.
|
seguro para administração e não comprovado para automação.
|
||||||
|
|
||||||
## Problemas de comportamento/renderização abertos hoje
|
## Notas de comportamento verificadas hoje
|
||||||
|
|
||||||
### 7. Anomalia de visibilidade do `pfctl -s states` (trabalho do gate M2)
|
### 7. `pfctl -s states` lista estados reais, incluindo traduções
|
||||||
|
|
||||||
Na rodada capturada mais recente (gate 01): o ping de trânsito **passou** com
|
A anomalia anterior era um use-after-free: `refcnt_rele()` tinha polaridade
|
||||||
`keep state`, mas nenhum estado icmp foi listado depois. Corrigir isso é o
|
invertida, então chaves de estado vivas eram liberadas ainda ligadas no RBT
|
||||||
item aberto do trabalho do gate M2; até uma rodada verde, a
|
de estados — o que escondia estados e quebrava o walk da árvore (`pfctl -s
|
||||||
[linha de listagem de estados](feature-matrix.md) é o status honesto. Não é
|
states` travando a máquina). Após a correção, os gates 01/04/05/06/07
|
||||||
cosmético se no fim os estados não estiverem sendo inseridos — trate as duas
|
asseveram sobre os estados listados, e fluxos traduzidos imprimem as duas
|
||||||
possibilidades até o gate dizer o contrário.
|
tuplas (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`).
|
||||||
|
|
||||||
### 8. Stubs silenciosos-mas-avisados
|
### 8. Ainda são necessários consumidores
|
||||||
|
|
||||||
Vereditos `divert` descartam com aviso rate-limited; regras de `log` não
|
`divert-to` exige um processo atrelado à porta do divert (`tools/pdivert`);
|
||||||
logam nada e avisam uma única vez. Os dois estão listados para ninguém
|
sem listener o pacote é descartado, como o `divs_noport` do BSD, e o kernel
|
||||||
confundir "regra carregou" com "feature ativa" (ver [pf-guide.md](pf-guide.md)).
|
imprime `pfk: divert port N: no socket bound`. `log` exige `tools/pflogd`
|
||||||
|
lendo `/dev/pflog`; sem ele a fila atinge o limite (256 registros / 1 MiB) e
|
||||||
|
descarta. Nenhum dos dois é mais stub, mas nenhum faz algo sem o consumidor
|
||||||
|
rodar (ver [pf-guide.md](pf-guide.md)).
|
||||||
|
|
||||||
|
### 9. Filas: caminho verificado; a semântica de modelagem é do HFSC, não um bug
|
||||||
|
|
||||||
|
`15-queue.sh` prova que o caminho de filas funciona de ponta a ponta: a
|
||||||
|
configuração carrega, a disciplina hfsc é anexada à interface, `pfctl -s
|
||||||
|
queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila
|
||||||
|
completa, e o contador de pacotes da classe avança - o escalonador está mesmo
|
||||||
|
no caminho de dados.
|
||||||
|
|
||||||
|
O que o lab não mostra é um *teto de taxa*, e isso é semântica do HFSC, não
|
||||||
|
defeito do port: o `pf.conf` do OpenBSD 8.0 expõe apenas `bandwidth`, que mapeia
|
||||||
|
para a curva **linkshare** do HFSC - garantia quando classes competem, não limite
|
||||||
|
de um fluxo isolado. Não existe palavra-chave `minimum`/`maximum` nesta versão do
|
||||||
|
pfctl (verificado na sua tabela de palavras-chave), logo não há como definir
|
||||||
|
upperlimit pelo pf.conf. Um teste de contenção com duas classes saturadas seria o
|
||||||
|
próximo passo se você quiser asserir sobre share; no enlace virtual de centenas de
|
||||||
|
Mbit do lab, um único fluxo termina antes de acumular dívida de tempo virtual.
|
||||||
|
|
||||||
|
Bugs encontrados e corrigidos no caminho (detalhes em `docs/porting-notes.md`): a
|
||||||
|
convenção de retorno de `ifqop_enq`, `ifq_q` vs `ifq_softc` (deref de NULL que
|
||||||
|
matava a máquina no meio da transferência), o cookie de dequeue perdido, o
|
||||||
|
handle da classe raiz do hfsc colidindo com qids ímpares do pf, o
|
||||||
|
`DIOCGETQSTATS` falhando porque kernel e pfctl discordavam sobre
|
||||||
|
`struct hfsc_class_stats` (ambos agora usam o `net/hfsc.h` vendorizado), o
|
||||||
|
payload do pflog começando no cabeçalho de transporte, e o `splassert` do caminho
|
||||||
|
de purge que transformava uma corrida benigna em `BUG()`.
|
||||||
|
|
||||||
## Desvios da camada de compat (de `docs/porting-notes.md`, registro do M1)
|
## Desvios da camada de compat (de `docs/porting-notes.md`, registro do M1)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Demo (asciinema)
|
||||||
|
|
||||||
|
O lab pode ser gravado como um cast do [asciinema](https://asciinema.org).
|
||||||
|
Cada byte da gravação é a saída real das VMs do lab (Alpine sobre libvirt,
|
||||||
|
rodando o kernel compilado do fonte com o `pf.ko`); só o ritmo é roteirizado.
|
||||||
|
|
||||||
|
## Gravar
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install asciinema # uma vez
|
||||||
|
bash scripts/vm-run-tests.sh # ou: scripts/vm-create.sh + deploy
|
||||||
|
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
A sessão percorre:
|
||||||
|
|
||||||
|
1. a topologia do lab e o `pf.ko` recém-carregado (prova no `dmesg`),
|
||||||
|
2. `pfctl -s info` pela ABI de ioctl do OpenBSD,
|
||||||
|
3. um `pf.conf` real (tabela, regras stateful, drop explícito) e `pfctl -sr`,
|
||||||
|
4. ping em trânsito + `pfctl -ss` (stateful),
|
||||||
|
5. `pfctl -t <tabela> -T show` (tabelas vivas),
|
||||||
|
6. um ping bloqueado (contadores com `pfctl -sr -vv`),
|
||||||
|
7. `divert-to` com o relay `pdivert` (pacote vai ao userspace e volta),
|
||||||
|
8. desligamento.
|
||||||
|
|
||||||
|
## Reproduzir
|
||||||
|
|
||||||
|
```sh
|
||||||
|
asciinema play out/demo-pf-linux.cast
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publicar
|
||||||
|
|
||||||
|
```sh
|
||||||
|
asciinema upload out/demo-pf-linux.cast
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/demo-record.py` é um gravador alternativo que produz o mesmo formato
|
||||||
|
asciicast v2 sem o binário asciinema (ele conduz o lab por ssh e escreve o
|
||||||
|
cast diretamente).
|
||||||
@@ -48,23 +48,23 @@ 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 | PENDING-M3 (`pf.c`/`pf_lb.c` compilados; fiação + gate VM no M3) |
|
| `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` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFICADO em lab** — gate `07-binat.sh`. O pfctl expande uma regra `binat-to` no par documentado (`pass out … nat-to <mapeado> static-port` + `pass in … rdr-to <real>`), e as duas metades funcionam: o peer vê o endereço mapeado, o original não vaza, e a resposta é traduzida de volta. |
|
||||||
| 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/normalização: remonteagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFICADO em lab** — gate `09-scrub.sh`: `min-ttl` eleva o TTL (o egress mostra 99 após o decremento do encaminhamento) e `max-mss` limita o MSS de um SYN, ambos medidos no egress do próprio firewall. Os caminhos de remonteagem/overlap estão compilados e alcançados, mas ainda sem gate. |
|
||||||
| 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) |
|
||||||
|
|
||||||
## Roteamento / divert / filas / logging
|
## Roteamento / divert / filas / logging
|
||||||
|
|
||||||
| Feature | Sintaxe de upstream | Milestone | Status |
|
| Feature | Sintaxe de upstream | Milestone | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Pools de `route-to` (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to`, `fail-to` | `pass out route-to (GW 10.0.3.2)` | M4 | PENDING-M4 (o glue Linux de rota/neigh `pf_route` ainda não existe) |
|
| Pools de `route-to` (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to` | `pass in … route-to { 10.0.3.2, 10.0.2.2 } round-robin keep state` | M4 | **`round-robin` e `source-hash` VERIFICADOS em lab** (gate `10-pools.sh`): com dois gateways em dois enlaces, o round-robin usa ambos e o source-hash fixa todo fluxo de uma origem em um único enlace. `dup-to` verificado no gate 08; `reply-to` ainda sem gate. `fail-to` **não existe no OpenBSD 8.0** (removido upstream), então está fora de escopo. |
|
||||||
| `divert-to` / tag `divert` | `pass in … divert-to <host> <port>` | e2e pendente (M4) | **COMPILA (não testado)** — família AF_DIVERT (PF_DIVERT=32) implementada no módulo (`module/pfsrc/divert_sock.c`: bind/recvmsg/sendmsg/poll + reinjeção com semântica `divert_output` do BSD); relay `pdivert` em `tools/pdivert/`. A prova fim-a-fim é o gate `11-divert.sh` (M4) — até lá, apenas em lab. |
|
| `divert-to` / tag `divert` | `pass in … divert-to <host> port <n>` | VERIFICADO em lab | **Funciona ponta a ponta.** O módulo registra em runtime a família de sockets `AF_DIVERT` (`PF_DIVERT=32`) — sem patch no kernel. A regra `divert-to` entrega o pacote a essa família; sem listener o pacote é descartado (`divs_noport`, paridade BSD); com `tools/pdivert` rodando o pacote vai ao userspace, é reescrito e reinjetado (verificado com mutação de payload + correção de checksum, ping 0% loss). |
|
||||||
| Filas, agendador default (priq) | `queue root on wan`, `pfctl -s queue -v` | M5 | COMPILES-untested — `compat/pfk_ifq.c` implementa a API ifq com **semântica de fallback priq**; regras `queue` fazem parse e carregam, mas pacotes ainda não são desviados por um qdisc classful até o M5 |
|
| Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFICADO em lab** — gate `15-queue.sh`: a configuração de filas carrega, a disciplina hfsc é anexada à interface, `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila completa, e o contador de pacotes da classe avança (o escalonador está no caminho de dados). Note que `bandwidth` é *linkshare* do HFSC - garantia sob contenção, não teto: o pf.conf do OpenBSD 8.0 não tem `minimum`/`maximum`, então um fluxo isolado não é limitado, nem no Linux nem no OpenBSD. |
|
||||||
| 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)
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -91,11 +91,11 @@ tamanho embutido na codificação `_IOR/_IOW` OpenBSD e despacha para o
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| M0 | repo, scripts, build do kernel, VM liga com kernel próprio | concluído |
|
| M0 | repo, scripts, build do kernel, VM liga com kernel próprio | concluído |
|
||||||
| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f`/`-s info` | concluído |
|
| M1 | camada de compat, `pf.ko` carrega, `/dev/pf`, `pfctl -f`/`-s info` | concluído |
|
||||||
| M2 | núcleo de filtragem: gates VM de regras/estados/tabelas/anchors | **em andamento** — caminho de dados implementado, gates 01–04 ainda não todos verdes |
|
| M2 | núcleo do filtro: regras/estados/tabelas/âncoras, gates VM | **pronto** — gates 01–04 verdes |
|
||||||
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente |
|
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub, pflog | **pronto** — gates 05/06/07/09 (incl. integridade de 2 MB via redirect) e pflog gate 12 |
|
||||||
| M4 | route-to/dup-to/fail-to, divert e2e | pendente |
|
| M4 | route-to/dup-to/reply-to, pools, divert e2e | **pronto** — gates 08/10/13 e divert gate 11 |
|
||||||
| M5 | filas: back ends ifq (hfsc/fq_codel) + qdisc `pfq` | pendente |
|
| M5 | filas: back ends ifq (hfsc/fq_codel) | **parcial** — back ends compilados no `pf.ko`; anexação/ritmo pendentes |
|
||||||
| M6 | docs bilíngues completas, patchset arrumado, suíte inteira verde, tags de release | em andamento (este conjunto de docs) |
|
| M6 | docs completas, patchset limpo, suíte verde, releases com tag | em andamento — docs EN+PT-BR completas, suíte 14/14 verde, com tag |
|
||||||
|
|
||||||
## Próximas leituras
|
## Próximas leituras
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ userland forem usados.
|
|||||||
Portanto, `patches/kernel/` não contém nenhum `000N-*.patch` hoje, e aplicar
|
Portanto, `patches/kernel/` não contém nenhum `000N-*.patch` hoje, e aplicar
|
||||||
o conjunto (vazio) é um no-op.
|
o conjunto (vazio) é um no-op.
|
||||||
|
|
||||||
> Nota honesta: o install.md e o design mencionam `scripts/apply-patches.sh`,
|
> Nota honesta: `scripts/apply-patches.sh`, `scripts/unpatch.sh` e
|
||||||
> `scripts/unpatch.sh` e `patches/kernel/series`. Esses helpers **ainda não
|
> `patches/kernel/series` **estão na árvore**. O `series` é vazio por projeto,
|
||||||
> estão na árvore**; até entrarem, "aplicar o conjunto de patches" é, por
|
> então aplicá-lo é um no-op verificado (`PATCHES_APPLIED=0`). Quando um patch
|
||||||
> definição, nada a fazer. Quando um patch real for necessário, o primeiro
|
> real for necessário, o commit que o adiciona também atualiza o `series` e
|
||||||
> commit que o adicionar DEVE também adicionar os dois scripts e o arquivo
|
> as regras abaixo.
|
||||||
> `series` (ver as regras abaixo).
|
|
||||||
|
|
||||||
## O mecanismo (como o conjunto deve funcionar)
|
## O mecanismo (como o conjunto deve funcionar)
|
||||||
|
|
||||||
|
|||||||
@@ -63,12 +63,11 @@ 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>)`. O `binat-to` é expandido pelo pfctl num par `nat-to … static-port` + `rdr-to` (gate `07-binat`). |
|
||||||
| `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 (min-ttl, max-mss, df, random-id)`, `set reassemble` | — | **Verificado em lab** (gate `09-scrub.sh`): `min-ttl` e `max-mss` reescrevem de fato pacotes em trânsito. || `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. |
|
||||||
| `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. |
|
| `route-to`, `dup-to`, `reply-to` | — | **Verificado em lab** (gates `08-routeto.sh`, `13-replyto.sh`): o gateway forçado é usado em vez do egress normal, `dup-to` copia mantendo o fluxo, e `reply-to` empurra as respostas de um fluxo para o enlace do gateway. `fail-to` não é palavra-chave no OpenBSD 8.0 (removido upstream). |
|
||||||
| `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` | — | **Verificado em lab** (gate `15-queue.sh`): as filas carregam, o hfsc é anexado, `pfctl -s queue -v` mostra as estatísticas das classes e um fluxo de 2 MB pela fila é contado pela classe. `bandwidth` é *garantia sob contenção*, não teto - o pf.conf 8.0 não tem palavra-chave de upperlimit. |
|
||||||
| `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` | — | **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. |
|
||||||
| `log`, `log all`, `block log`, `pflogd` | M4 | Nada é logado (o stub avisa uma vez no load: `pfk: … packet not logged`). |
|
|
||||||
| `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 +84,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
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Screencasts / Videos da demonstracao
|
||||||
|
|
||||||
|
> PT-BR: esta pagina · EN: [docs/en/screencast.md](en/screencast.md)
|
||||||
|
|
||||||
|
Seis videos silenciosos, 1920x1080 @ 30fps, H.264/MP4 — prontos para subir no
|
||||||
|
YouTube. Cada video e a gravacao de uma sessao **real** contra o lab: VMs
|
||||||
|
Alpine sob libvirt, rodando o kernel 7.3.4 compilado do fonte com o `pf.ko`
|
||||||
|
deste repositorio. Nada e simulado; a unica coisa roteirizada e o ritmo.
|
||||||
|
|
||||||
|
| # | Video | Duracao | O que mostra |
|
||||||
|
|---|-------|---------|--------------|
|
||||||
|
| 1 | `01-implementacao-kernel.mp4` | 22 s | A fonte do OpenBSD 8.0 organizada em `module/pfsrc`, a camada de compatibilidade em `module/compat`, e a compilacao do modulo `pf.ko` contra o kernel do lab |
|
||||||
|
| 2 | `02-ativacao.mp4` | 29 s | `insmod pf.ko`, `/dev/pf` aparecendo, `pfctl -e`, `pfctl -s info` com a tabela de estados |
|
||||||
|
| 3 | `03-regras-basicas.mp4` | 54 s | Um `pf.conf` real: tabela, anchor, `quick`, `keep state`, `block drop` (o ping morre), contadores por regra e `pfctl -ss` |
|
||||||
|
| 4 | `04-roteamento.mp4` | 47 s | `route-to` (o pacote sai pelo caminho forcado, provado com `tcpdump` nas duas interfaces) e `reply-to` (resposta por gateway diferente) |
|
||||||
|
| 5 | `05-balanceamento.mp4` | 69 s | Pool `nat-to { ... } round-robin` alternando a origem no fio, pool `source-hash` fixando por origem, e `dup-to` replicando o pacote |
|
||||||
|
| 6 | `06-qos.mp4` | 34 s | Filas do OpenBSD 8.0 (`queue ... bandwidth`, classes `hfsc`), `pfctl -s queue -v`, transferencia de 2 MB pela fila e os contadores da classe subindo |
|
||||||
|
|
||||||
|
## Como regravar
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# renderiza todas as fases
|
||||||
|
./scripts/screencast/record-all.sh
|
||||||
|
|
||||||
|
# ou uma fase especifica
|
||||||
|
./scripts/screencast/record-all.sh 05-balanceamento
|
||||||
|
```
|
||||||
|
|
||||||
|
Os arquivos sao gravados em `out/screencast/*.mp4`. O renderizador
|
||||||
|
(`scripts/screencast/render.py`) roda a fase em um pty, emula o terminal com
|
||||||
|
`pyte`, converte cada evento em um frame PNG e mux com `ffmpeg` (H.264,
|
||||||
|
`yuv420p`, `+faststart`, **sem trilha de audio**). Se o `ffmpeg` da maquina nao
|
||||||
|
tiver `libx264`, ele cai para `libopenh264` e depois para VP9 automaticamente.
|
||||||
|
|
||||||
|
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu Sans Mono, e
|
||||||
|
o lab de VMs no ar (as fases 2 a 6 falam com o lab; a fase 1 roda no host de
|
||||||
|
build).
|
||||||
|
|
||||||
|
## Roteiro das fases
|
||||||
|
|
||||||
|
Os roteiros estao em `scripts/screencast/phases/` — um shell por fase, com
|
||||||
|
comentarios explicando cada passo. Eles usam os mesmos comandos que os gates
|
||||||
|
da suite (`tests/vm/*.sh`), entao o que o video mostra e exatamente o que a
|
||||||
|
suite verifica.
|
||||||
|
|
||||||
|
## Texto de descricao (YouTube)
|
||||||
|
|
||||||
|
```
|
||||||
|
PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux
|
||||||
|
|
||||||
|
Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4,
|
||||||
|
empacotado como modulo (pf.ko) e controlada pelo pfctl original do OpenBSD
|
||||||
|
sobre a ABI de ioctls do /dev/pf.
|
||||||
|
|
||||||
|
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
|
||||||
|
modulo, trafego real entre cliente, firewall e servidor.
|
||||||
|
|
||||||
|
Fases:
|
||||||
|
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko
|
||||||
|
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
|
||||||
|
3. Regras basicas - tabelas, anchors, keep state, block drop
|
||||||
|
4. Roteamento - route-to e reply-to
|
||||||
|
5. Balanceamento - pools round-robin, source-hash e dup-to
|
||||||
|
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
|
||||||
|
|
||||||
|
Suite automatizada: 15 gates, todos verdes.
|
||||||
|
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port
|
||||||
|
|
||||||
|
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
|
||||||
|
```
|
||||||
+1
-1
@@ -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 pfsrc/fq_codel.o pfsrc/hfsc.o compat/pfk_ifq.o | awk '!x[$$0]++ { print("./"$$0) }' > pf.mod
|
||||||
+1
-1
@@ -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 pfsrc/fq_codel.o pfsrc/hfsc.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)
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/md5.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/md5.o: $(deps_compat/md5.o)
|
compat/md5.o: $(deps_compat/md5.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_if.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_if.o: $(deps_compat/pfk_if.o)
|
compat/pfk_if.o: $(deps_compat/pfk_if.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ifq.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_ifq.o: $(deps_compat/pfk_ifq.o)
|
compat/pfk_ifq.o: $(deps_compat/pfk_ifq.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip4.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_ip4.o: $(deps_compat/pfk_ip4.o)
|
compat/pfk_ip4.o: $(deps_compat/pfk_ip4.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip6.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_ip6.o: $(deps_compat/pfk_ip6.o)
|
compat/pfk_ip6.o: $(deps_compat/pfk_ip6.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_kern.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_kern.o: $(deps_compat/pfk_kern.o)
|
compat/pfk_kern.o: $(deps_compat/pfk_kern.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_mbuf.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_mbuf.o: $(deps_compat/pfk_mbuf.o)
|
compat/pfk_mbuf.o: $(deps_compat/pfk_mbuf.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_pcb.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/net/inet6_hashtables.h \
|
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/net/inet6_hashtables.h \
|
||||||
|
|
||||||
compat/pfk_pcb.o: $(deps_compat/pfk_pcb.o)
|
compat/pfk_pcb.o: $(deps_compat/pfk_pcb.o)
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/pfk_rtable.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/pfk_rtable.o: $(deps_compat/pfk_rtable.o)
|
compat/pfk_rtable.o: $(deps_compat/pfk_rtable.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,9 @@ deps_compat/pfk_xmit.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
include/pfk_api.h \
|
||||||
|
|
||||||
compat/pfk_xmit.o: $(deps_compat/pfk_xmit.o)
|
compat/pfk_xmit.o: $(deps_compat/pfk_xmit.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/radix.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/radix.o: $(deps_compat/radix.o)
|
compat/radix.o: $(deps_compat/radix.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/sha2.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/sha2.o: $(deps_compat/sha2.o)
|
compat/sha2.o: $(deps_compat/sha2.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/siphash.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/siphash.o: $(deps_compat/siphash.o)
|
compat/siphash.o: $(deps_compat/siphash.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/subr_tree.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
compat/subr_tree.o: $(deps_compat/subr_tree.o)
|
compat/subr_tree.o: $(deps_compat/subr_tree.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_compat/toeplitz.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/crypto/hash.h \
|
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/crypto/hash.h \
|
||||||
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/linux/crypto.h \
|
/home/lgcosta/lab/linux_kernel/linux-7.3.4/include/linux/crypto.h \
|
||||||
|
|
||||||
|
|||||||
+103
-27
@@ -217,10 +217,14 @@ pfk_pfq_enqueue(void *arg, struct mbuf *m)
|
|||||||
struct pfk_pfq *pfq = arg;
|
struct pfk_pfq *pfq = arg;
|
||||||
|
|
||||||
if (!pfq)
|
if (!pfq)
|
||||||
return NULL;
|
return m; /* caller keeps ownership */
|
||||||
if (ifq_enqueue(&pfq->ifq, m) != 0)
|
/*
|
||||||
return NULL;
|
* ifq_enqueue takes ownership of m (the scheduler may drop it); we then
|
||||||
return m;
|
* drain immediately, so nothing is returned for the caller to send.
|
||||||
|
*/
|
||||||
|
ifq_enqueue(&pfq->ifq, m);
|
||||||
|
ifq_start(&pfq->ifq);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct mbuf *
|
static struct mbuf *
|
||||||
@@ -273,10 +277,11 @@ static const struct pfq_ops pfk_priq_pfq_ops = {
|
|||||||
* packets are still queued, but the traffic shaping curves are not yet
|
* packets are still queued, but the traffic shaping curves are not yet
|
||||||
* applied — recorded in the feature matrix as pending M5).
|
* applied — recorded in the feature matrix as pending M5).
|
||||||
*/
|
*/
|
||||||
const struct ifq_ops * const ifq_hfsc_ops = &pfk_priq_ifq_ops;
|
/*
|
||||||
const struct ifq_ops * const ifq_fqcodel_ops = &pfk_priq_ifq_ops;
|
* M5: the real scheduler back ends (pfsrc/hfsc.c, pfsrc/fq_codel.c) define
|
||||||
const struct pfq_ops * const pfq_hfsc_ops = &pfk_priq_pfq_ops;
|
* ifq_hfsc_ops/pfq_hfsc_ops/ifq_fqcodel_ops/pfq_fqcodel_ops themselves; the
|
||||||
const struct pfq_ops * const pfq_fqcodel_ops = &pfk_priq_pfq_ops;
|
* priq shim above stays as the default for interfaces without a queue rule.
|
||||||
|
*/
|
||||||
|
|
||||||
/* ---------------- framework ---------------- */
|
/* ---------------- framework ---------------- */
|
||||||
void
|
void
|
||||||
@@ -302,7 +307,36 @@ ifq_attach(struct ifqueue *ifq, const struct ifq_ops *ops, void *opsarg)
|
|||||||
if (!opsarg && ops->ifqop_alloc)
|
if (!opsarg && ops->ifqop_alloc)
|
||||||
opsarg = ops->ifqop_alloc(0, NULL);
|
opsarg = ops->ifqop_alloc(0, NULL);
|
||||||
ifq->ifq_ops = ops;
|
ifq->ifq_ops = ops;
|
||||||
|
/*
|
||||||
|
* The port's own helpers use ifq_softc; the staged OpenBSD schedulers
|
||||||
|
* (hfsc/fq_codel) read ifq->ifq_q. Keep both in sync or the back ends
|
||||||
|
* dereference a NULL scheduler state.
|
||||||
|
*/
|
||||||
ifq->ifq_softc = opsarg;
|
ifq->ifq_softc = opsarg;
|
||||||
|
ifq->ifq_q = opsarg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scheduler hand-off used by the ported back ends (hfsc/fq_codel) when they
|
||||||
|
* need their per-interface state. PFLINUX: the port has no ifq mutex (the
|
||||||
|
* data path is a single ordered work queue), so "enter" is a plain ownership
|
||||||
|
* check and "leave" is a no-op - the pair exists to mirror OpenBSD's API.
|
||||||
|
*/
|
||||||
|
void *
|
||||||
|
ifq_q_enter(struct ifqueue *ifq, const struct ifq_ops *ops)
|
||||||
|
{
|
||||||
|
if (ifq == NULL || ops == NULL || ifq->ifq_ops != ops)
|
||||||
|
return NULL; /* another scheduler owns it */
|
||||||
|
if (ifq->ifq_softc == NULL && ops->ifqop_alloc)
|
||||||
|
ifq->ifq_softc = ops->ifqop_alloc(0, NULL);
|
||||||
|
return ifq->ifq_softc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ifq_q_leave(struct ifqueue *ifq, void *q)
|
||||||
|
{
|
||||||
|
(void)ifq;
|
||||||
|
(void)q;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -312,19 +346,35 @@ ifq_destroy(struct ifqueue *ifq)
|
|||||||
ifq->ifq_ops->ifqop_free(0, ifq->ifq_softc);
|
ifq->ifq_ops->ifqop_free(0, ifq->ifq_softc);
|
||||||
ifq->ifq_ops = NULL;
|
ifq->ifq_ops = NULL;
|
||||||
ifq->ifq_softc = NULL;
|
ifq->ifq_softc = NULL;
|
||||||
|
ifq->ifq_q = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ifqop_enq returns the packet the scheduler decided to DROP (possibly the one
|
||||||
|
* just offered), and NULL when the packet was accepted - it is NOT an error
|
||||||
|
* code. Freeing the returned mbuf is this layer's job.
|
||||||
|
*/
|
||||||
int
|
int
|
||||||
ifq_enqueue(struct ifqueue *ifq, struct mbuf *m)
|
ifq_enqueue(struct ifqueue *ifq, struct mbuf *m)
|
||||||
{
|
{
|
||||||
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_enq)
|
struct mbuf *dm;
|
||||||
|
|
||||||
|
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_enq) {
|
||||||
|
m_freem(m);
|
||||||
return EINVAL;
|
return EINVAL;
|
||||||
|
}
|
||||||
if (ifq->ifq_maxlen && ifq->ifq_len >= ifq->ifq_maxlen) {
|
if (ifq->ifq_maxlen && ifq->ifq_len >= ifq->ifq_maxlen) {
|
||||||
ifq->ifq_errors++;
|
ifq->ifq_errors++;
|
||||||
ifq->ifq_qdrops++;
|
ifq->ifq_qdrops++;
|
||||||
|
m_freem(m);
|
||||||
return ENOBUFS;
|
return ENOBUFS;
|
||||||
}
|
}
|
||||||
return ifq->ifq_ops->ifqop_enq(ifq, m) ? 0 : ENOMEM;
|
dm = ifq->ifq_ops->ifqop_enq(ifq, m);
|
||||||
|
if (dm) {
|
||||||
|
ifq->ifq_qdrops++;
|
||||||
|
m_freem(dm);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct mbuf *
|
struct mbuf *
|
||||||
@@ -336,14 +386,16 @@ ifq_deq_begin(struct ifqueue *ifq)
|
|||||||
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_deq_begin)
|
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_deq_begin)
|
||||||
return NULL;
|
return NULL;
|
||||||
m = ifq->ifq_ops->ifqop_deq_begin(ifq, &cookie);
|
m = ifq->ifq_ops->ifqop_deq_begin(ifq, &cookie);
|
||||||
|
ifq->ifq_dequeue = cookie;
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ifq_deq_commit(struct ifqueue *ifq, struct mbuf *m)
|
ifq_deq_commit(struct ifqueue *ifq, struct mbuf *m)
|
||||||
{
|
{
|
||||||
void *cookie = NULL;
|
void *cookie = ifq->ifq_dequeue;
|
||||||
|
|
||||||
|
ifq->ifq_dequeue = NULL;
|
||||||
if (ifq->ifq_ops && ifq->ifq_ops->ifqop_deq_commit)
|
if (ifq->ifq_ops && ifq->ifq_ops->ifqop_deq_commit)
|
||||||
ifq->ifq_ops->ifqop_deq_commit(ifq, m, cookie);
|
ifq->ifq_ops->ifqop_deq_commit(ifq, m, cookie);
|
||||||
}
|
}
|
||||||
@@ -365,12 +417,52 @@ ifq_dequeue(struct ifqueue *ifq)
|
|||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Drain the queue as far as the scheduler allows and hand each packet to the
|
||||||
|
* stack's output path. OpenBSD calls if_start from softint context; here it
|
||||||
|
* runs either from the pf work queue (enqueue side) or from the scheduler's
|
||||||
|
* deferred timeout (pacing side), which is what makes hfsc's bandwidth
|
||||||
|
* limits take effect.
|
||||||
|
*/
|
||||||
void
|
void
|
||||||
ifq_start(struct ifqueue *ifq)
|
ifq_start(struct ifqueue *ifq)
|
||||||
{
|
{
|
||||||
|
struct mbuf *m;
|
||||||
|
int n;
|
||||||
|
|
||||||
|
n = 0;
|
||||||
|
ifq->ifq_oactive = 1;
|
||||||
|
while ((m = ifq_dequeue(ifq)) != NULL) {
|
||||||
|
n++;
|
||||||
|
if (pfk_ifq_transmit(ifq, m))
|
||||||
|
break; /* device busy: packet kept */
|
||||||
|
}
|
||||||
ifq->ifq_oactive = 0;
|
ifq->ifq_oactive = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* pf data-path entry: hand a packet to the interface's scheduler when one is
|
||||||
|
* attached (a pf `queue' rule is configured). Returns 1 when the queue took
|
||||||
|
* ownership of m, 0 when the caller must keep going (priq/no scheduler).
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
pf_kifq_input(struct pfk_ifnet *ifp, struct mbuf *m)
|
||||||
|
{
|
||||||
|
struct ifqueue *ifq;
|
||||||
|
|
||||||
|
if (ifp == NULL || m == NULL)
|
||||||
|
return 0;
|
||||||
|
if (ifp == NULL)
|
||||||
|
return 0;
|
||||||
|
ifq = &ifp->if_snd;
|
||||||
|
if (ifq->ifq_ops == NULL || ifq->ifq_ops == ifq_priq_ops)
|
||||||
|
return 0; /* no scheduler: fast path */
|
||||||
|
if (ifq_enqueue(ifq, m) != 0)
|
||||||
|
return 1; /* dropped by the queue: owned */
|
||||||
|
ifq_start(ifq);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
unsigned int
|
unsigned int
|
||||||
ifq_purge(struct ifqueue *ifq)
|
ifq_purge(struct ifqueue *ifq)
|
||||||
{
|
{
|
||||||
@@ -423,19 +515,3 @@ ifq_hdatalen(struct ifqueue *ifq)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- scheduler back ends (milestone M5) ---------------- */
|
|
||||||
/* The staged OpenBSD hfsc.c/fq_codel.c are not in the build yet; the priq
|
|
||||||
* ops above are the active scheduler. These wrappers keep pf_ioctl's
|
|
||||||
* initialisation calls valid until M5 replaces them with the ported
|
|
||||||
* schedulers (docs/porting-notes.md). */
|
|
||||||
void
|
|
||||||
hfsc_initialize(void)
|
|
||||||
{
|
|
||||||
pr_info("pfk: hfsc back end pending M5 (priq active)\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
fq_codel_initialize(void)
|
|
||||||
{
|
|
||||||
pr_info("pfk: fq_codel back end pending M5 (priq active)\n");
|
|
||||||
}
|
|
||||||
+17
-15
@@ -47,8 +47,21 @@ pfk_printk_compat(const char *fmt, ...)
|
|||||||
void
|
void
|
||||||
splassert_fail(int expected, int actual, const char *func)
|
splassert_fail(int expected, int actual, const char *func)
|
||||||
{
|
{
|
||||||
pr_err("splassert: %s: expected %d, got %d\n", func, expected, actual);
|
/*
|
||||||
BUG();
|
* PFLINUX: OpenBSD's splassert() checks a per-CPU spl level, so
|
||||||
|
* "assert unlocked" really means "this context did not take the lock".
|
||||||
|
* The port replaced spl with rw-semas plus work queues, where
|
||||||
|
* rw_status() is a global counter: an assert-unlocked in a work-queue
|
||||||
|
* task fires whenever *another* CPU happens to hold the lock, which is
|
||||||
|
* not a bug (the task never acquires pf_lock itself) and was turning a
|
||||||
|
* harmless race into a kernel BUG() that wedged the module in
|
||||||
|
* "Unloading". Report it once instead of killing the host; the
|
||||||
|
* structural invariant the BSD assert protects - purge never runs while
|
||||||
|
* holding pf_lock - is upheld by the call sites (pf_purge_* are only
|
||||||
|
* reached from systqmp tasks and timer work).
|
||||||
|
*/
|
||||||
|
pr_warn_ratelimited("pfk: splassert: %s: expected %d, got %d\n",
|
||||||
|
func, expected, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- addlog (OpenBSD kern.log) ---------------- */
|
/* ---------------- addlog (OpenBSD kern.log) ---------------- */
|
||||||
@@ -619,16 +632,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);
|
|
||||||
}
|
|
||||||
@@ -607,3 +607,14 @@ pfk_mbuf_exit(void)
|
|||||||
{
|
{
|
||||||
kmem_cache_destroy(pfk_mbuf_cache);
|
kmem_cache_destroy(pfk_mbuf_cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
ml_purge(struct mbuf_list *ml)
|
||||||
|
{
|
||||||
|
struct mbuf *m;
|
||||||
|
|
||||||
|
while ((m = ml_dequeue(ml)) != NULL)
|
||||||
|
m_freem(m);
|
||||||
|
ml->ml_first = ml->ml_last = NULL;
|
||||||
|
ml->ml_len = 0;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
* ip6_route_output, tcp_v4_send_reset if needed).
|
* ip6_route_output, tcp_v4_send_reset if needed).
|
||||||
*/
|
*/
|
||||||
#include <pfk.h>
|
#include <pfk.h>
|
||||||
|
#include <pfk_api.h>
|
||||||
|
|
||||||
static struct dst_entry *
|
static struct dst_entry *
|
||||||
pfk_route4(__be32 daddr, __be32 saddr, int oif)
|
pfk_route4(__be32 daddr, __be32 saddr, int oif)
|
||||||
@@ -23,6 +24,26 @@ pfk_route4(__be32 daddr, __be32 saddr, int oif)
|
|||||||
return &rt->dst;
|
return &rt->dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Forced nexthop (route-to/dup-to/reply-to). Linux resolves the neighbour
|
||||||
|
* from the *packet's* destination unless the route says it goes through a
|
||||||
|
* gateway (ip_neigh_for_gw() only honours rt_gw4 when rt_gw_family is set),
|
||||||
|
* which would ARP the final host on the gateway's link. Look the gateway up
|
||||||
|
* as its own host route and mark it as the nexthop - the same pattern OVS
|
||||||
|
* uses after its own FIB lookups.
|
||||||
|
*/
|
||||||
|
static struct dst_entry *
|
||||||
|
pfk_route4_nh(__be32 gw, int oif)
|
||||||
|
{
|
||||||
|
struct rtable *rt = (struct rtable *)pfk_route4(gw, 0, oif);
|
||||||
|
|
||||||
|
if (rt == NULL)
|
||||||
|
return NULL;
|
||||||
|
rt->rt_gw_family = AF_INET;
|
||||||
|
rt->rt_gw4 = gw;
|
||||||
|
return &rt->dst;
|
||||||
|
}
|
||||||
|
|
||||||
#if IS_ENABLED(CONFIG_IPV6)
|
#if IS_ENABLED(CONFIG_IPV6)
|
||||||
static struct dst_entry *
|
static struct dst_entry *
|
||||||
pfk_route6(const struct in6_addr *daddr, const struct in6_addr *saddr, int oif)
|
pfk_route6(const struct in6_addr *daddr, const struct in6_addr *saddr, int oif)
|
||||||
@@ -67,13 +88,16 @@ if_output_tso(struct pfk_ifnet *ifp, struct mbuf **mp, void *dst,
|
|||||||
}
|
}
|
||||||
switch (sa->sa_family) {
|
switch (sa->sa_family) {
|
||||||
case AF_INET: {
|
case AF_INET: {
|
||||||
struct pf_ip *ip = mtod(m, struct pf_ip *);
|
|
||||||
__be32 gw = 0;
|
__be32 gw = 0;
|
||||||
|
|
||||||
/* if route was forced (route-to), dst holds gw sockaddr
|
/* if route was forced (route-to), dst holds gw sockaddr
|
||||||
* stored in sin_addr by pf_route; normal case dst==final */
|
* stored in sin_addr by pf_route; normal case dst==final */
|
||||||
gw = sintosa(dst)->sin_addr.s_addr;
|
gw = sintosa(dst)->sin_addr.s_addr;
|
||||||
d = pfk_route4(gw, ip->ip_src.s_addr, ifp->if_index);
|
/* PFLINUX: look the route up by destination only. Passing the
|
||||||
|
* packet's source (a transit address, not local to us) makes
|
||||||
|
* ip_route_output_key fail with ENETUNREACH; ip_output keeps
|
||||||
|
* the header source untouched. */
|
||||||
|
d = pfk_route4_nh(gw, ifp->if_index);
|
||||||
if (!d) {
|
if (!d) {
|
||||||
ret = EHOSTUNREACH;
|
ret = EHOSTUNREACH;
|
||||||
goto drop;
|
goto drop;
|
||||||
@@ -83,13 +107,11 @@ if_output_tso(struct pfk_ifnet *ifp, struct mbuf **mp, void *dst,
|
|||||||
}
|
}
|
||||||
#if IS_ENABLED(CONFIG_IPV6)
|
#if IS_ENABLED(CONFIG_IPV6)
|
||||||
case AF_INET6: {
|
case AF_INET6: {
|
||||||
struct pf_ip6_hdr *ip6 = mtod(m, struct pf_ip6_hdr *);
|
|
||||||
const struct in6_addr *gw = &sin6tosa(dst)->sin6_addr;
|
const struct in6_addr *gw = &sin6tosa(dst)->sin6_addr;
|
||||||
|
|
||||||
if (!d)
|
if (!d)
|
||||||
d = pfk_route6(gw,
|
/* PFLINUX: destination-only, as for IPv4 */
|
||||||
(struct in6_addr *)&ip6->ip6_src,
|
d = pfk_route6(gw, NULL, ifp->if_index);
|
||||||
ifp->if_index);
|
|
||||||
if (!d) {
|
if (!d) {
|
||||||
ret = EHOSTUNREACH;
|
ret = EHOSTUNREACH;
|
||||||
goto drop;
|
goto drop;
|
||||||
@@ -114,6 +136,28 @@ if_output_tso(struct pfk_ifnet *ifp, struct mbuf **mp, void *dst,
|
|||||||
ret = ENETDOWN;
|
ret = ENETDOWN;
|
||||||
goto drop_dst_nofree;
|
goto drop_dst_nofree;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* The stack's output path always consumes exactly one skb reference.
|
||||||
|
* Take our own so the caller's references (the hook queue ref and the
|
||||||
|
* mbuf wrapper ref) stay balanced - without this the work queue's
|
||||||
|
* cleanup double-frees the skb and corrupts the slab.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* pf_route() has already evaluated the OUT ruleset (it calls
|
||||||
|
* pf_test(PF_OUT) itself, exactly like OpenBSD's ip_forward). Mark the
|
||||||
|
* packet so our POST_ROUTING hook does not run the out ruleset a second
|
||||||
|
* time - that re-match hits the state, re-enters pf_route and recurses
|
||||||
|
* (OpenBSD stops this with PF_TAG_PROCESSED carried in the mbuf).
|
||||||
|
*/
|
||||||
|
skb->mark |= PFK_DONE_OUT;
|
||||||
|
/*
|
||||||
|
* The port stashes its mbuf pointer in skb->cb; the IP stack uses the
|
||||||
|
* same bytes as its own control block (IPCB). Hand the packet to the
|
||||||
|
* stack with a clean cb, otherwise ip_output() reads our pointer as
|
||||||
|
* frag_max_size/flags and the packet is silently lost.
|
||||||
|
*/
|
||||||
|
memset(skb->cb, 0, sizeof(skb->cb));
|
||||||
|
skb_get(skb);
|
||||||
ret = skb_dst(skb)->output(&init_net, NULL, skb);
|
ret = skb_dst(skb)->output(&init_net, NULL, skb);
|
||||||
/* output consumed skb (and wrapper); avoid double free */
|
/* output consumed skb (and wrapper); avoid double free */
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
@@ -226,3 +270,40 @@ pfk_if_output(struct pfk_ifnet *ifp, struct mbuf *m, void *dst,
|
|||||||
m_freem(mp);
|
m_freem(mp);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Transmit one queued packet. Called from ifq_start() - i.e. from the pf
|
||||||
|
* work queue or from a scheduler's deferred timeout - and always consumes the
|
||||||
|
* mbuf. Returns 0 (keep draining); a scheduler that wants to defer does so by
|
||||||
|
* simply not returning a packet from ifqop_deq_begin().
|
||||||
|
*/
|
||||||
|
int
|
||||||
|
pfk_ifq_transmit(struct ifqueue *ifq, struct mbuf *m)
|
||||||
|
{
|
||||||
|
struct sk_buff *skb = m_to_skb(m);
|
||||||
|
struct dst_entry *d;
|
||||||
|
|
||||||
|
(void)ifq;
|
||||||
|
if (skb == NULL) {
|
||||||
|
m_freem(m);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
d = skb_dst(skb);
|
||||||
|
if (d == NULL || IS_ERR(d) || d->dev == NULL || !netif_running(d->dev)) {
|
||||||
|
m_freem(m);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
skb->dev = d->dev;
|
||||||
|
skb_reset_network_header(skb);
|
||||||
|
if (skb->protocol == 0)
|
||||||
|
skb->protocol = htons(ip_hdr(skb)->protocol == IPPROTO_IPV6 ?
|
||||||
|
ETH_P_IPV6 : ETH_P_IP);
|
||||||
|
/* the stack owns skb->cb (IPCB) from here; pf already tested it */
|
||||||
|
memset(skb->cb, 0, sizeof(skb->cb));
|
||||||
|
skb->mark |= PFK_DONE_OUT;
|
||||||
|
skb_get(skb); /* the output path consumes one reference */
|
||||||
|
m->skb = NULL; /* the mbuf's reference moves to the stack */
|
||||||
|
m_freem(m);
|
||||||
|
d->output(&init_net, NULL, skb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ struct ifqueue {
|
|||||||
struct mbuf_list *ifq_free; /* free list (unused in the port) */
|
struct mbuf_list *ifq_free; /* free list (unused in the port) */
|
||||||
unsigned int ifq_len;
|
unsigned int ifq_len;
|
||||||
unsigned int ifq_oactive;
|
unsigned int ifq_oactive;
|
||||||
|
void *ifq_dequeue; /* cookie between deq_begin/commit */
|
||||||
|
|
||||||
/* statistics */
|
/* statistics */
|
||||||
u_int64_t ifq_packets;
|
u_int64_t ifq_packets;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* $OpenBSD: fq_codel.h,v 1.4 2020/06/18 23:29:59 dlg Exp $ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2017 Mike Belopuhov
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
* copyright notice and this permission notice appear in all copies.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _NET_FQ_CODEL_H_
|
||||||
|
#define _NET_FQ_CODEL_H_
|
||||||
|
|
||||||
|
/* compatible with hfsc_pktcntr */
|
||||||
|
struct fqcodel_pktcntr {
|
||||||
|
uint64_t packets;
|
||||||
|
uint64_t bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct fqcodel_stats {
|
||||||
|
struct fqcodel_pktcntr xmit_cnt;
|
||||||
|
struct fqcodel_pktcntr drop_cnt;
|
||||||
|
|
||||||
|
uint32_t qlength;
|
||||||
|
uint32_t qlimit;
|
||||||
|
|
||||||
|
uint32_t flows;
|
||||||
|
uint32_t _unused; /* padding */
|
||||||
|
|
||||||
|
uint32_t target;
|
||||||
|
uint32_t interval;
|
||||||
|
|
||||||
|
/* the values below are used to calculate standard deviation */
|
||||||
|
uint64_t delaysum; /* sum of delays, us */
|
||||||
|
uint64_t delaysumsq;/* sum of squared delays, us */
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef _KERNEL
|
||||||
|
extern const struct ifq_ops * const ifq_fqcodel_ops;
|
||||||
|
extern const struct pfq_ops * const pfq_fqcodel_ops;
|
||||||
|
#endif /* _KERNEL */
|
||||||
|
|
||||||
|
#endif /* _NET_FQ_CODEL_H_ */
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/* $OpenBSD: hfsc.h,v 1.15 2026/03/19 14:59:05 sthen Exp $ */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2013 Henning Brauer <henning@openbsd.org>
|
||||||
|
* Copyright (c) 1997-1999 Carnegie Mellon University. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software and
|
||||||
|
* its documentation is hereby granted (including for commercial or
|
||||||
|
* for-profit use), provided that both the copyright notice and this
|
||||||
|
* permission notice appear in all copies of the software, derivative
|
||||||
|
* works, or modified versions, and any portions thereof.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS EXPERIMENTAL AND IS KNOWN TO HAVE BUGS, SOME OF
|
||||||
|
* WHICH MAY HAVE SERIOUS CONSEQUENCES. CARNEGIE MELLON PROVIDES THIS
|
||||||
|
* SOFTWARE IN ITS ``AS IS'' CONDITION, AND ANY EXPRESS OR IMPLIED
|
||||||
|
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
||||||
|
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||||
|
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
* DAMAGE.
|
||||||
|
*
|
||||||
|
* Carnegie Mellon encourages (but does not require) users of this
|
||||||
|
* software to return any improvements or extensions that they make,
|
||||||
|
* and to grant Carnegie Mellon the rights to redistribute these
|
||||||
|
* changes without encumbrance.
|
||||||
|
*/
|
||||||
|
#ifndef _HFSC_H_
|
||||||
|
#define _HFSC_H_
|
||||||
|
|
||||||
|
/* hfsc class flags */
|
||||||
|
#define HFSC_DEFAULTCLASS 0x1000 /* default class */
|
||||||
|
|
||||||
|
struct hfsc_pktcntr {
|
||||||
|
u_int64_t packets;
|
||||||
|
u_int64_t bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define PKTCNTR_INC(cntr, len) \
|
||||||
|
do { (cntr)->packets++; (cntr)->bytes += len; } while (0)
|
||||||
|
|
||||||
|
struct hfsc_sc {
|
||||||
|
u_int64_t m1; /* slope of the first segment in bits/sec */
|
||||||
|
u_int64_t m2; /* slope of the second segment in bits/sec */
|
||||||
|
u_int d; /* the x-projection of the first segment in msec */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* special class handles */
|
||||||
|
#define HFSC_ROOT_CLASS 0x10000
|
||||||
|
#define HFSC_DEFAULT_CLASSES 64
|
||||||
|
#define HFSC_MAX_CLASSES 65535
|
||||||
|
|
||||||
|
/* service curve types */
|
||||||
|
#define HFSC_REALTIMESC 1
|
||||||
|
#define HFSC_LINKSHARINGSC 2
|
||||||
|
#define HFSC_UPPERLIMITSC 4
|
||||||
|
#define HFSC_DEFAULTSC (HFSC_REALTIMESC|HFSC_LINKSHARINGSC)
|
||||||
|
|
||||||
|
struct hfsc_class_stats {
|
||||||
|
struct hfsc_pktcntr xmit_cnt;
|
||||||
|
struct hfsc_pktcntr drop_cnt;
|
||||||
|
|
||||||
|
u_int qlength;
|
||||||
|
u_int qlimit;
|
||||||
|
u_int period;
|
||||||
|
|
||||||
|
u_int class_id;
|
||||||
|
u_int32_t class_handle;
|
||||||
|
struct hfsc_sc rsc;
|
||||||
|
struct hfsc_sc fsc;
|
||||||
|
struct hfsc_sc usc; /* upper limit service curve */
|
||||||
|
|
||||||
|
u_int64_t total; /* total work in bytes */
|
||||||
|
u_int64_t cumul; /* cumulative work in bytes
|
||||||
|
done by real-time criteria */
|
||||||
|
u_int64_t d; /* deadline */
|
||||||
|
u_int64_t e; /* eligible time */
|
||||||
|
u_int64_t vt; /* virtual time */
|
||||||
|
u_int64_t f; /* fit time for upper-limit */
|
||||||
|
|
||||||
|
/* info helpful for debugging */
|
||||||
|
u_int64_t initvt; /* init virtual time */
|
||||||
|
u_int64_t vtoff; /* cl_vt_ipoff */
|
||||||
|
u_int64_t cvtmax; /* cl_maxvt */
|
||||||
|
u_int64_t myf; /* cl_myf */
|
||||||
|
u_int64_t cfmin; /* cl_mincf */
|
||||||
|
u_int64_t cvtmin; /* cl_mincvt */
|
||||||
|
u_int64_t myfadj; /* cl_myfadj */
|
||||||
|
u_int64_t vtadj; /* cl_vtadj */
|
||||||
|
u_int64_t cur_time;
|
||||||
|
u_int32_t machclk_freq;
|
||||||
|
|
||||||
|
u_int vtperiod; /* vt period sequence no */
|
||||||
|
u_int parentperiod; /* parent's vt period seqno */
|
||||||
|
int nactive; /* number of active children */
|
||||||
|
|
||||||
|
/* red and rio related info */
|
||||||
|
int qtype;
|
||||||
|
/* struct redstats red[3]; */
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef _KERNEL
|
||||||
|
struct ifnet;
|
||||||
|
struct ifqueue;
|
||||||
|
struct pf_queuespec;
|
||||||
|
struct hfsc_if;
|
||||||
|
|
||||||
|
extern const struct ifq_ops * const ifq_hfsc_ops;
|
||||||
|
extern const struct pfq_ops * const pfq_hfsc_ops;
|
||||||
|
|
||||||
|
#define HFSC_ENABLED(ifq) ((ifq)->ifq_ops == ifq_hfsc_ops)
|
||||||
|
#define HFSC_DEFAULT_QLIMIT 50
|
||||||
|
|
||||||
|
void hfsc_initialize(void);
|
||||||
|
|
||||||
|
#endif /* _KERNEL */
|
||||||
|
#endif /* _HFSC_H_ */
|
||||||
@@ -1203,4 +1203,60 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scheduler back-end ABI: the very headers OpenBSD ships (net/hfsc.h,
|
||||||
|
* net/fq_codel.h), vendored into the port so that pfctl and pf.ko agree on the
|
||||||
|
* stats layouts and on the class-handle/service-curve constants. (Hand-rolled
|
||||||
|
* approximations of these structs made DIOCGETQSTATS fail with EINVAL, because
|
||||||
|
* pfctl's sizeof(struct hfsc_class_stats) did not match the kernel's.)
|
||||||
|
*/
|
||||||
|
#ifndef nsecuptime
|
||||||
|
#define nsecuptime() getnsecuptime()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <net/hfsc.h>
|
||||||
|
#include <net/fq_codel.h>
|
||||||
|
|
||||||
|
/* OpenBSD's global nanoseconds-per-clocktick (HFSC_CLK_PER_TICK). The port
|
||||||
|
* has no clock_rate machinery, so derive it from the kernel's HZ. */
|
||||||
|
#ifndef tick_nsec
|
||||||
|
#define tick_nsec (1000000000LL / HZ)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ifq scheduler hand-off (see pfsrc/ifq.c): enter returns the scheduler's
|
||||||
|
* queue state when it owns the interface queue, NULL otherwise. */
|
||||||
|
void *ifq_q_enter(struct ifqueue *, const struct ifq_ops *);
|
||||||
|
void ifq_q_leave(struct ifqueue *, void *);
|
||||||
|
int pfk_ifq_transmit(struct ifqueue *, struct mbuf *);
|
||||||
|
int pf_kifq_input(struct pfk_ifnet *, struct mbuf *);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PFLINUX notes on the scheduler back ends:
|
||||||
|
*
|
||||||
|
* - splnet()/splx(): OpenBSD raises the software interrupt level around
|
||||||
|
* class-table surgery. This port runs the whole data path in one ordered
|
||||||
|
* work queue and guards the queue with ifq_mtx, so these are no-ops; they
|
||||||
|
* are kept as markers of the critical regions.
|
||||||
|
* - KERNEL_ASSERT_LOCKED(): the schedulers are only reached from ifq/pf paths
|
||||||
|
* that already hold the queue mutex; asserted structurally, not by spl.
|
||||||
|
* - HFSC_ENABLED(), PKTCNTR_INC(), the class-handle constants and the stats
|
||||||
|
* layouts all come from the vendored net/hfsc.h / net/fq_codel.h above, so
|
||||||
|
* kernel and pfctl cannot drift apart.
|
||||||
|
*/
|
||||||
|
#define splnet() 0
|
||||||
|
#define splx(s) ((void)(s))
|
||||||
|
#define KERNEL_ASSERT_LOCKED() do { } while (0)
|
||||||
|
|
||||||
#endif /* _PFK_H */
|
#endif /* _PFK_H */
|
||||||
|
|
||||||
@@ -7,11 +7,15 @@
|
|||||||
long pfk_ioctl(struct file *f, unsigned int cmd, unsigned long addr);
|
long pfk_ioctl(struct file *f, unsigned int cmd, unsigned long addr);
|
||||||
int pfk_enable(int on);
|
int pfk_enable(int on);
|
||||||
void pfk_status(struct pf_status *s);
|
void pfk_status(struct pf_status *s);
|
||||||
#define PFK_SKIP_PF 0x50460040u /* skb->mark: reinjected, bypass pf */
|
#define PFK_SKIP_PF 0x00000004u /* skb->mark: reinjected, bypass pf */
|
||||||
|
#define PFK_DONE_IN 0x00000001u /* skb->mark bit: PF_IN pass completed */
|
||||||
|
#define PFK_DONE_OUT 0x00000002u /* skb->mark bit: PF_OUT pass completed */
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -74,6 +74,79 @@ ml_dequeue(struct mbuf_list *ml)
|
|||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* BSD <net/if_var.h> mbuf_list helpers (net/if_var.h ml_*) */
|
||||||
|
static inline void
|
||||||
|
ml_init(struct mbuf_list *ml)
|
||||||
|
{
|
||||||
|
ml->ml_first = ml->ml_last = NULL;
|
||||||
|
ml->ml_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ml_enqueue(struct mbuf_list *ml, struct mbuf *m)
|
||||||
|
{
|
||||||
|
m->m_next = NULL;
|
||||||
|
if (ml->ml_last == NULL)
|
||||||
|
ml->ml_first = m;
|
||||||
|
else
|
||||||
|
ml->ml_last->m_next = m;
|
||||||
|
ml->ml_last = m;
|
||||||
|
ml->ml_len++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline struct mbuf *
|
||||||
|
ml_dequeue_tail(struct mbuf_list *ml)
|
||||||
|
{
|
||||||
|
struct mbuf *m, *prev = NULL;
|
||||||
|
|
||||||
|
if (ml->ml_first == NULL)
|
||||||
|
return NULL;
|
||||||
|
for (m = ml->ml_first; m->m_next; m = m->m_next)
|
||||||
|
prev = m;
|
||||||
|
if (prev == NULL) /* single element */
|
||||||
|
ml->ml_first = NULL;
|
||||||
|
else
|
||||||
|
prev->m_next = NULL;
|
||||||
|
ml->ml_last = prev;
|
||||||
|
ml->ml_len--;
|
||||||
|
m->m_next = NULL;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* append every packet in m2 to m1 (m2 becomes empty) */
|
||||||
|
static inline void
|
||||||
|
ml_enlist(struct mbuf_list *m1, struct mbuf_list *m2)
|
||||||
|
{
|
||||||
|
if (m2->ml_first == NULL)
|
||||||
|
return;
|
||||||
|
if (m1->ml_last == NULL)
|
||||||
|
m1->ml_first = m2->ml_first;
|
||||||
|
else
|
||||||
|
m1->ml_last->m_next = m2->ml_first;
|
||||||
|
m1->ml_last = m2->ml_last;
|
||||||
|
m1->ml_len += m2->ml_len;
|
||||||
|
m2->ml_first = m2->ml_last = NULL;
|
||||||
|
m2->ml_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline unsigned int
|
||||||
|
ml_len(const struct mbuf_list *ml)
|
||||||
|
{
|
||||||
|
return (unsigned int)ml->ml_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
ml_empty(const struct mbuf_list *ml)
|
||||||
|
{
|
||||||
|
return ml->ml_first == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ml_purge(struct mbuf_list *ml); /* compat/pfk_mbuf.c (needs m_freem) */
|
||||||
|
|
||||||
|
#define MBUF_LIST_FIRST(_ml) ((_ml)->ml_first)
|
||||||
|
#define MBUF_LIST_INITIALIZER() { NULL, NULL, 0 }
|
||||||
|
|
||||||
/* egress interface (default-route dev), maintained on fib/net events */
|
/* egress interface (default-route dev), maintained on fib/net events */
|
||||||
struct pfk_ifnet *pfk_egress_if(void);
|
struct pfk_ifnet *pfk_egress_if(void);
|
||||||
void pfk_egress_refresh(void);
|
void pfk_egress_refresh(void);
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ struct pkthdr {
|
|||||||
u_int32_t ph_flowid;
|
u_int32_t ph_flowid;
|
||||||
u_int ph_ifidx;
|
u_int ph_ifidx;
|
||||||
u_int ph_rtableid;
|
u_int ph_rtableid;
|
||||||
struct pfk_timeval ph_timestamp;
|
int64_t ph_timestamp; /* OpenBSD 8.0: nanoseconds */
|
||||||
struct pfk_ifnet *ifp;
|
struct pfk_ifnet *ifp;
|
||||||
struct pfk_ifnet *rcvif;
|
struct pfk_ifnet *rcvif;
|
||||||
u_short ph; /* tso segs: unused, kept as pad */
|
u_short ph; /* tso segs: unused, kept as pad */
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/divert_sock.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
include/pfk_api.h \
|
include/pfk_api.h \
|
||||||
|
|
||||||
pfsrc/divert_sock.o: $(deps_pfsrc/divert_sock.o)
|
pfsrc/divert_sock.o: $(deps_pfsrc/divert_sock.o)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf.o: $(deps_pfsrc/pf.o)
|
pfsrc/pf.o: $(deps_pfsrc/pf.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_if.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_if.o: $(deps_pfsrc/pf_if.o)
|
pfsrc/pf_if.o: $(deps_pfsrc/pf_if.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ioctl.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_ioctl.o: $(deps_pfsrc/pf_ioctl.o)
|
pfsrc/pf_ioctl.o: $(deps_pfsrc/pf_ioctl.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_lb.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_lb.o: $(deps_pfsrc/pf_lb.o)
|
pfsrc/pf_lb.o: $(deps_pfsrc/pf_lb.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_norm.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_norm.o: $(deps_pfsrc/pf_norm.o)
|
pfsrc/pf_norm.o: $(deps_pfsrc/pf_norm.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_osfp.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_osfp.o: $(deps_pfsrc/pf_osfp.o)
|
pfsrc/pf_osfp.o: $(deps_pfsrc/pf_osfp.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ruleset.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_ruleset.o: $(deps_pfsrc/pf_ruleset.o)
|
pfsrc/pf_ruleset.o: $(deps_pfsrc/pf_ruleset.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_syncookies.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_syncookies.o: $(deps_pfsrc/pf_syncookies.o)
|
pfsrc/pf_syncookies.o: $(deps_pfsrc/pf_syncookies.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_table.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
|
|
||||||
pfsrc/pf_table.o: $(deps_pfsrc/pf_table.o)
|
pfsrc/pf_table.o: $(deps_pfsrc/pf_table.o)
|
||||||
|
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_hooks.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
include/pfk_api.h \
|
include/pfk_api.h \
|
||||||
|
|
||||||
pfsrc/pfk_hooks.o: $(deps_pfsrc/pfk_hooks.o)
|
pfsrc/pfk_hooks.o: $(deps_pfsrc/pfk_hooks.o)
|
||||||
|
|||||||
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_mod.o := \
|
|||||||
include/compat/sys/rwlock.h \
|
include/compat/sys/rwlock.h \
|
||||||
include/compat/sys/mutex.h \
|
include/compat/sys/mutex.h \
|
||||||
include/compat/sys/pclock.h \
|
include/compat/sys/pclock.h \
|
||||||
|
include/net/hfsc.h \
|
||||||
|
include/net/fq_codel.h \
|
||||||
include/pfk_api.h \
|
include/pfk_api.h \
|
||||||
|
|
||||||
pfsrc/pfk_mod.o: $(deps_pfsrc/pfk_mod.o)
|
pfsrc/pfk_mod.o: $(deps_pfsrc/pfk_mod.o)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -332,7 +332,9 @@ control_law(struct codel *cd, struct codel_params *cp, int64_t rts)
|
|||||||
unsigned int idx;
|
unsigned int idx;
|
||||||
|
|
||||||
KASSERT(cd->drops > 0);
|
KASSERT(cd->drops > 0);
|
||||||
idx = min(cd->drops - 1, nitems(codel_intervals) - 1);
|
/* PFLINUX: Linux' min() type-checks its arguments; nitems() is size_t */
|
||||||
|
idx = min_t(unsigned int, cd->drops - 1,
|
||||||
|
(unsigned int)(nitems(codel_intervals) - 1));
|
||||||
cd->next = rts + cp->intervals[idx];
|
cd->next = rts + cp->intervals[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -335,9 +335,9 @@ hfsc_class_purge(struct hfsc_class *cl, struct mbuf_list *ml)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static inline u_int
|
static inline u_int
|
||||||
hfsc_more_slots(u_int current)
|
hfsc_more_slots(u_int cur) /* PFLINUX: 'current' is a kernel macro */
|
||||||
{
|
{
|
||||||
u_int want = current * 2;
|
u_int want = cur * 2;
|
||||||
|
|
||||||
return (want > HFSC_MAX_CLASSES ? HFSC_MAX_CLASSES : want);
|
return (want > HFSC_MAX_CLASSES ? HFSC_MAX_CLASSES : want);
|
||||||
}
|
}
|
||||||
@@ -481,7 +481,16 @@ hfsc_pf_qstats(struct pf_queuespec *q, void *ubuf, int *nbytes)
|
|||||||
if (hif == NULL)
|
if (hif == NULL)
|
||||||
return (EBADF);
|
return (EBADF);
|
||||||
|
|
||||||
if ((cl = hfsc_clh2cph(hif, q->qid)) == NULL) {
|
/*
|
||||||
|
* A root queue's class is created with handle HFSC_ROOT_CLASS | qid
|
||||||
|
* (see hfsc_pf_addqueue), so looking the queue up by its plain pf qid
|
||||||
|
* only finds non-root classes: fall back to the root handle, otherwise
|
||||||
|
* "pfctl -s queue" fails with EINVAL on the first (root) queue.
|
||||||
|
*/
|
||||||
|
if ((cl = hfsc_clh2cph(hif, q->qid)) == NULL &&
|
||||||
|
(q->flags & PFQS_ROOTCLASS))
|
||||||
|
cl = hfsc_clh2cph(hif, HFSC_ROOT_CLASS | q->qid);
|
||||||
|
if (cl == NULL) {
|
||||||
ifq_q_leave(&ifp->if_snd, hif);
|
ifq_q_leave(&ifp->if_snd, hif);
|
||||||
return (EINVAL);
|
return (EINVAL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8283,11 +8283,6 @@ pf_test(sa_family_t af, int fwdir, struct pfk_ifnet *ifp, struct mbuf **m0)
|
|||||||
|
|
||||||
if (!pf_status.running)
|
if (!pf_status.running)
|
||||||
return (PF_PASS);
|
return (PF_PASS);
|
||||||
if (m0 && *m0 && (*m0)->m_pkthdr.len >= 20 &&
|
|
||||||
((*m0)->m_data[9] == IPPROTO_ICMP))
|
|
||||||
pr_info_ratelimited("pfk: T1 enter dir=%d ifp=%px kif=%px\n",
|
|
||||||
fwdir, ifp, ifp ? ifp->if_pf_kif : NULL);
|
|
||||||
|
|
||||||
#if NCARP > 0
|
#if NCARP > 0
|
||||||
if (ifp->if_type == IFT_CARP) {
|
if (ifp->if_type == IFT_CARP) {
|
||||||
struct pfk_ifnet *ifp0;
|
struct pfk_ifnet *ifp0;
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ pfi_kif_alloc(const char *kif_name, int mflags)
|
|||||||
kif->pfik_flags |= PFI_IFLAG_ANY;
|
kif->pfik_flags |= PFI_IFLAG_ANY;
|
||||||
kif->pfik_flags_new |= PFI_IFLAG_ANY;
|
kif->pfik_flags_new |= PFI_IFLAG_ANY;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* PFLINUX: OpenBSD creates the "ingress" and "egress" interface
|
||||||
|
* groups (ifgroups) automatically and both contain every interface;
|
||||||
|
* pf's direction (in/out ruleset) carries the actual meaning. Linux
|
||||||
|
* has no ifgroups, so emulate the groups as "any interface".
|
||||||
|
*/
|
||||||
|
if (!strcmp(kif->pfik_name, "ingress") ||
|
||||||
|
!strcmp(kif->pfik_name, "egress")) {
|
||||||
|
kif->pfik_flags |= PFI_IFLAG_ANY;
|
||||||
|
kif->pfik_flags_new |= PFI_IFLAG_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
return (kif);
|
return (kif);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -662,6 +662,11 @@ pf_create_queues(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
qif->disc = qif->pfqops->pfq_alloc(ifp);
|
qif->disc = qif->pfqops->pfq_alloc(ifp);
|
||||||
|
if (qif->disc == NULL) {
|
||||||
|
error = ENOBUFS;
|
||||||
|
free(qif, M_PF, sizeof(*qif));
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
qif->next = list;
|
qif->next = list;
|
||||||
list = qif;
|
list = qif;
|
||||||
|
|||||||
+150
-6
@@ -35,8 +35,6 @@ struct pfk_pkt {
|
|||||||
int dir;
|
int dir;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define PFK_DONE_IN 0x50460010u /* skb->mark bit: PF_IN pass completed */
|
|
||||||
#define PFK_DONE_OUT 0x50460020u /* skb->mark bit: PF_OUT pass completed */
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
pfk_pkt_reinject(struct pfk_pkt *pk)
|
pfk_pkt_reinject(struct pfk_pkt *pk)
|
||||||
@@ -71,14 +69,119 @@ pfk_pkt_reinject(struct pfk_pkt *pk)
|
|||||||
kfree(pk);
|
kfree(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reverse NAT: pf rewrote the destination of an inbound packet (the reply
|
||||||
|
* of a translated flow). OpenBSD re-routes such a packet in ip_input()
|
||||||
|
* (PF_TAG_REROUTE); Linux has no such layer, so re-route it here and push
|
||||||
|
* it out through the new egress device.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
pfk_reroute(struct sk_buff *skb)
|
||||||
|
{
|
||||||
|
struct flowi4 fl4 = { };
|
||||||
|
struct rtable *rt;
|
||||||
|
struct iphdr *iph = ip_hdr(skb);
|
||||||
|
|
||||||
|
fl4.daddr = iph->daddr;
|
||||||
|
/*
|
||||||
|
* Leave the source empty: the packet keeps the original source
|
||||||
|
* (the peer's address), but the kernel would reject a route lookup
|
||||||
|
* with a non-local source (ENETUNREACH).
|
||||||
|
*/
|
||||||
|
fl4.saddr = 0;
|
||||||
|
fl4.flowi4_oif = 0;
|
||||||
|
fl4.flowi4_mark = 0;
|
||||||
|
fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
|
||||||
|
|
||||||
|
rt = ip_route_output_key(&init_net, &fl4);
|
||||||
|
if (IS_ERR(rt) || rt == NULL) {
|
||||||
|
kfree_skb(skb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skb_dst_set(skb, &rt->dst);
|
||||||
|
skb->dev = rt->dst.dev;
|
||||||
|
skb_reset_network_header(skb);
|
||||||
|
/* the stack owns skb->cb (IPCB) from here - see pfk_xmit.c */
|
||||||
|
memset(skb->cb, 0, sizeof(skb->cb));
|
||||||
|
rt->dst.output(&init_net, skb->sk, skb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PFLINUX: pf rewrote the IPv4 addresses of the packet (NAT). OpenBSD's
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
pfk_fix_csum_after_translate(struct sk_buff *skb)
|
||||||
|
{
|
||||||
|
struct iphdr *iph;
|
||||||
|
unsigned int hlen, len;
|
||||||
|
__sum16 *csum;
|
||||||
|
__u8 proto;
|
||||||
|
|
||||||
|
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
|
||||||
|
return;
|
||||||
|
iph = ip_hdr(skb);
|
||||||
|
hlen = iph->ihl * 4;
|
||||||
|
proto = iph->protocol;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
struct pfk_pkt *pk = container_of(w, struct pfk_pkt, work);
|
struct pfk_pkt *pk = container_of(w, struct pfk_pkt, work);
|
||||||
struct mbuf *m, *m0;
|
struct mbuf *m, *m0;
|
||||||
struct sk_buff *orig;
|
struct sk_buff *orig;
|
||||||
|
__be32 orig_dst = 0, orig_src = 0;
|
||||||
int action;
|
int action;
|
||||||
|
|
||||||
|
if (pk->af == AF_INET && pskb_may_pull(pk->skb, sizeof(struct iphdr))) {
|
||||||
|
orig_dst = ip_hdr(pk->skb)->daddr;
|
||||||
|
orig_src = ip_hdr(pk->skb)->saddr;
|
||||||
|
}
|
||||||
|
|
||||||
m = pfk_m_wrap_skb(pk->skb, pk->af);
|
m = pfk_m_wrap_skb(pk->skb, pk->af);
|
||||||
if (!m) {
|
if (!m) {
|
||||||
pfk_pkt_reinject(pk); /* like "pf not consulted" */
|
pfk_pkt_reinject(pk); /* like "pf not consulted" */
|
||||||
@@ -93,11 +196,43 @@ pfk_pkt_work(struct work_struct *w)
|
|||||||
* and packet walk run concurrently from here on — documented. */
|
* and packet walk run concurrently from here on — documented. */
|
||||||
action = pf_test(pk->af, pk->dir, pk->ifp, &m);
|
action = pf_test(pk->af, pk->dir, pk->ifp, &m);
|
||||||
|
|
||||||
|
if (pk->af == AF_INET && orig_src != 0 &&
|
||||||
|
pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
|
||||||
|
(ip_hdr(pk->skb)->saddr != orig_src ||
|
||||||
|
ip_hdr(pk->skb)->daddr != orig_dst))
|
||||||
|
pfk_fix_csum_after_translate(pk->skb);
|
||||||
|
|
||||||
|
|
||||||
if (m == m0) {
|
if (m == m0) {
|
||||||
|
/*
|
||||||
|
* Queueing: when a pf `queue' rule attached a scheduler to the
|
||||||
|
* egress interface, hand the packet to it instead of sending it
|
||||||
|
* straight out - this is what makes bandwidth limits real.
|
||||||
|
*/
|
||||||
|
if (action == PF_PASS && pk->dir == PF_OUT &&
|
||||||
|
pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
|
||||||
|
skb_dst(pk->skb) && !IS_ERR(skb_dst(pk->skb)) &&
|
||||||
|
pf_kifq_input(skb_dst(pk->skb)->dev ?
|
||||||
|
pfk_ifp_bydev(skb_dst(pk->skb)->dev) : NULL, m)) {
|
||||||
|
orig = pk->skb;
|
||||||
|
pk->skb = NULL; /* the queue owns the packet */
|
||||||
|
kfree_skb(orig);
|
||||||
|
kfree(pk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* same buffer, possibly rewritten in place: our wrapper
|
/* same buffer, possibly rewritten in place: our wrapper
|
||||||
* held one extra skb reference — release it */
|
* held one extra skb reference — release it */
|
||||||
m_freem(m);
|
m_freem(m);
|
||||||
|
if (action == PF_PASS && orig_dst != 0 &&
|
||||||
|
pk->af == AF_INET && pk->dir == PF_IN &&
|
||||||
|
pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
|
||||||
|
ip_hdr(pk->skb)->daddr != orig_dst) {
|
||||||
|
/* pf translated the destination (reverse NAT):
|
||||||
|
* re-route instead of delivering locally */
|
||||||
|
pfk_reroute(pk->skb);
|
||||||
|
kfree(pk);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (action == PF_PASS) {
|
if (action == PF_PASS) {
|
||||||
pfk_pkt_reinject(pk);
|
pfk_pkt_reinject(pk);
|
||||||
return;
|
return;
|
||||||
@@ -138,17 +273,25 @@ pfk_enqueue_verdict(int af, struct sk_buff *skb,
|
|||||||
|
|
||||||
switch (st->hook) {
|
switch (st->hook) {
|
||||||
case NF_INET_PRE_ROUTING:
|
case NF_INET_PRE_ROUTING:
|
||||||
|
/* OpenBSD evaluates the IN ruleset in ip_input(); every
|
||||||
|
* arriving packet passes it. */
|
||||||
dir = PF_IN;
|
dir = PF_IN;
|
||||||
break;
|
break;
|
||||||
|
case NF_INET_FORWARD:
|
||||||
|
/* OpenBSD evaluates the OUT ruleset of a forwarded packet in
|
||||||
|
* ip_forward() (pf_test(PF_FWD)); the IN ruleset was already
|
||||||
|
* applied at input time. Without this, transit traffic never
|
||||||
|
* sees the "out" rules and state matching for replies is
|
||||||
|
* impossible. */
|
||||||
|
dir = PF_OUT;
|
||||||
|
break;
|
||||||
case NF_INET_LOCAL_OUT:
|
case NF_INET_LOCAL_OUT:
|
||||||
case NF_INET_POST_ROUTING:
|
case NF_INET_POST_ROUTING:
|
||||||
dir = PF_OUT;
|
dir = PF_OUT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return NF_ACCEPT; /* LOCAL_IN/FORWARD */
|
return NF_ACCEPT; /* LOCAL_IN */
|
||||||
}
|
}
|
||||||
af, skb->len, ntohs(skb->protocol), skb->dev ? skb->dev->name : "?",
|
|
||||||
skb->mark);
|
|
||||||
if (skb->mark & PFK_SKIP_PF)
|
if (skb->mark & PFK_SKIP_PF)
|
||||||
return NF_ACCEPT; /* reinjected by divert: don't re-filter */
|
return NF_ACCEPT; /* reinjected by divert: don't re-filter */
|
||||||
if (skb->mark & (dir == PF_IN ? PFK_DONE_IN : PFK_DONE_OUT))
|
if (skb->mark & (dir == PF_IN ? PFK_DONE_IN : PFK_DONE_OUT))
|
||||||
@@ -162,7 +305,8 @@ pfk_enqueue_verdict(int af, struct sk_buff *skb,
|
|||||||
if (dir == PF_IN)
|
if (dir == PF_IN)
|
||||||
ifp = pfk_ifp_bydev(st->in ? st->in : skb->dev);
|
ifp = pfk_ifp_bydev(st->in ? st->in : skb->dev);
|
||||||
else
|
else
|
||||||
ifp = pfk_ifp_bydev(skb->dev ? skb->dev : st->out);
|
ifp = pfk_ifp_bydev(st->out ? st->out :
|
||||||
|
(skb->dev ? skb->dev : NULL));
|
||||||
if (!ifp)
|
if (!ifp)
|
||||||
return NF_ACCEPT;
|
return NF_ACCEPT;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,310 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
int off;
|
||||||
|
|
||||||
|
if (skb == NULL)
|
||||||
|
return 0;
|
||||||
|
/*
|
||||||
|
* BSD logs from the network header; pf may already have advanced the
|
||||||
|
* mbuf/skb data pointer to the transport header while parsing, so
|
||||||
|
* anchor explicitly at the IP header instead of at skb->data.
|
||||||
|
*/
|
||||||
|
off = skb_network_offset(skb);
|
||||||
|
if (off < 0)
|
||||||
|
off = 0;
|
||||||
|
if (off > skb->len)
|
||||||
|
return 0;
|
||||||
|
n = min_t(size_t, want, skb->len - off);
|
||||||
|
if (skb_copy_bits(skb, off, 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
{"version": 2, "width": 80, "height": 24, "timestamp": 1794449787, "env": {"SHELL": null, "TERM": "dumb"}}
|
||||||
|
[1.414508, "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.655866, "o", "7.3.4\r\n"]
|
||||||
|
[2.058621, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf ' || echo 'pf: not loaded yet'\u001b[0m\r\n"]
|
||||||
|
[2.276696, "o", "pf: not loaded yet\r\n"]
|
||||||
|
[2.680328, "o", "\u001b[1;36mpftest-pf0# insmod /opt/pf/pf.ko && dmesg | tail -3\u001b[0m\r\n"]
|
||||||
|
[2.917691, "o", "[ 146.252272] NET: Registered PF_IUCV protocol family\r\n[ 146.252362] pfk: netfilter hooks registered (10, netisr wq data path)\r\n[ 146.252423] pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base)\r\n"]
|
||||||
|
[3.320802, "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.598837, "o", "Status: Disabled for 0 days 00:02:26 Debug: err\r\n\r\nState Table Total Rate\r\n current entries 0 \r\n"]
|
||||||
|
[4.00249, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1\u001b[0m\r\n"]
|
||||||
|
[4.274878, "o", "Status: Enabled Debug: err\r\npf enabled\r\n"]
|
||||||
|
[4.678074, "o", "\r\n\u001b[1m--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---\u001b[0m\r\n"]
|
||||||
|
[4.678128, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-pf.conf\u001b[0m\r\n"]
|
||||||
|
[4.954849, "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.358199, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'\u001b[0m\r\n"]
|
||||||
|
[5.630704, "o", "rules loaded\r\n"]
|
||||||
|
[6.034552, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr\u001b[0m\r\n"]
|
||||||
|
[6.322794, "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"]
|
||||||
|
[6.725786, "o", "\r\n"]
|
||||||
|
[6.72584, "o", "\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"]
|
||||||
|
[7.977099, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.339/0.351/0.363 ms\r\n"]
|
||||||
|
[8.379918, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
|
||||||
|
[8.634572, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5449 0:0\r\nall icmp 10.0.1.10:5449 -> 10.0.2.20:8 0:0\r\n"]
|
||||||
|
[9.036709, "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"]
|
||||||
|
[9.298627, "o", " 10.0.1.0/24\r\n"]
|
||||||
|
[9.701487, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -s Tables\u001b[0m\r\n"]
|
||||||
|
[9.974937, "o", "trusted\r\n"]
|
||||||
|
[10.378208, "o", "\r\n\u001b[1m--- 6. the drop rule actually drops ---\u001b[0m\r\n\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"]
|
||||||
|
[11.033956, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'\u001b[0m\r\n"]
|
||||||
|
[11.310724, "o", "block rules loaded\r\n"]
|
||||||
|
[11.714107, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
|
||||||
|
[14.969152, "o", "\r\n--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 0 packets received, 100% packet loss\r\n"]
|
||||||
|
[15.372543, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -sr -vv | head -6\u001b[0m\r\n"]
|
||||||
|
[15.66699, "o", "@0 block drop in quick inet proto icmp from any to 10.0.2.20\r\n [ Evaluations: 6 Packets: 2 Bytes: 168 States: 0 ]\r\n [ Inserted: uid 0 pid 1687 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 1687 State Creations: 1 ]\r\n"]
|
||||||
|
[16.07016, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
|
||||||
|
[16.338781, "o", "back to the stateful ruleset\r\n"]
|
||||||
|
[16.742479, "o", "\r\n\u001b[1m--- 7. NAT: the source is translated on the way out, restored on the way back ---\u001b[0m\r\n"]
|
||||||
|
[16.742537, "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"]
|
||||||
|
[17.422636, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'\u001b[0m\r\n"]
|
||||||
|
[17.686794, "o", "nat rules loaded\r\n"]
|
||||||
|
[18.090339, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -3\u001b[0m\r\n"]
|
||||||
|
[19.341141, "o", "--- 10.0.2.20 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.276/0.317/0.359 ms\r\n"]
|
||||||
|
[19.744749, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -ss | grep -E 'icmp' | head -4\u001b[0m\r\n"]
|
||||||
|
[20.010971, "o", "all icmp 10.0.2.20:8 <- 10.0.1.10:5461 0:0\r\nall icmp 10.0.2.1:43040 (10.0.1.10:5461) -> 10.0.2.20:8 0:0\r\n"]
|
||||||
|
[20.414328, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'\u001b[0m\r\n"]
|
||||||
|
[20.690817, "o", "back to the stateful ruleset\r\n"]
|
||||||
|
[21.094035, "o", "\r\n\u001b[1m--- 8. pflog: logged packets reach userspace (/dev/pflog) ---\u001b[0m\r\n"]
|
||||||
|
[21.094088, "o", "\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"]
|
||||||
|
[21.762242, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-log.conf && echo 'log rule loaded'\u001b[0m\r\n"]
|
||||||
|
[22.034943, "o", "log rule loaded\r\n"]
|
||||||
|
[22.438523, "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"]
|
||||||
|
[23.681454, "o", "pflogd collecting 2 records\r\n"]
|
||||||
|
[24.084864, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
|
||||||
|
[25.333003, "o", "2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.352/0.364/0.376 ms\r\n"]
|
||||||
|
[27.737616, "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"]
|
||||||
|
[28.026778, "o", "pflogd: 2 records\r\n-rw------- 1 root root 324 Nov 12 02:16 /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"]
|
||||||
|
[28.430241, "o", "\u001b[1;36mpftest-pf0# pkill pflogd 2>/dev/null; /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
|
||||||
|
[29.114632, "o", "\r\n\u001b[1m--- 9. route-to: force the next hop away from the normal path ---\u001b[0m\r\n"]
|
||||||
|
[29.114687, "o", "\u001b[1;36mpftest-pf0# cat > /tmp/demo-route.conf <<EOF\r\npass in quick proto icmp from 10.0.1.10 to 10.0.2.20 route-to { 10.0.3.2 } keep state\r\npass out quick\r\nEOF\u001b[0m\r\n"]
|
||||||
|
[29.778259, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'\u001b[0m\r\n"]
|
||||||
|
[30.050898, "o", "route-to rule loaded\r\n"]
|
||||||
|
[30.45427, "o", "\u001b[1;36mpftest-pf0# (tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1\u001b[0m\r\n"]
|
||||||
|
[32.089063, "o", "\u001b[1;36mpftest-cli# ping -c1 -W2 10.0.2.20 | tail -2\u001b[0m\r\n"]
|
||||||
|
[34.31667, "o", "--- 10.0.2.20 ping statistics ---\r\n1 packets transmitted, 0 packets received, 100% packet loss\r\n"]
|
||||||
|
[36.72139, "o", "\u001b[1;36mpftest-pf0# echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\\|^listening\\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td\u001b[0m\r\n"]
|
||||||
|
[37.006878, "o", "forced egress (eth3, toward the gateway):\r\n02:16:59.958272 52:54:00:aa:00:04 > 52:54:00:dd:00:02, ethertype IPv4 (0x0800), length 98: 10.0.1.10 > 10.0.2.20: ICMP echo request, id 5473, seq 0, length 64\r\nnormal egress (eth2):\r\n0\r\n"]
|
||||||
|
[37.410476, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null\u001b[0m\r\n"]
|
||||||
|
[38.094888, "o", "\r\n\u001b[1m--- 10. divert-to: the packet goes to userspace and comes back ---\u001b[0m\r\n"]
|
||||||
|
[38.095122, "o", "\u001b[1;36mpftest-pf0# cat /tmp/demo-divert.conf\u001b[0m\r\n"]
|
||||||
|
[38.35866, "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"]
|
||||||
|
[38.763007, "o", "\u001b[1;36mpftest-pf0# /opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'\u001b[0m\r\n"]
|
||||||
|
[39.022814, "o", "divert rule loaded\r\n"]
|
||||||
|
[39.42606, "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"]
|
||||||
|
[40.654519, "o", "1794\r\n"]
|
||||||
|
[41.056785, "o", "\u001b[1;36mpftest-cli# ping -c2 -W2 10.0.1.1 | tail -3\u001b[0m\r\n"]
|
||||||
|
[42.284778, "o", "--- 10.0.1.1 ping statistics ---\r\n2 packets transmitted, 2 packets received, 0% packet loss\r\nround-trip min/avg/max = 0.177/0.224/0.271 ms\r\n"]
|
||||||
|
[42.691426, "o", "\u001b[1;36mpftest-pf0# head -3 /tmp/pdivert.log\u001b[0m\r\n"]
|
||||||
|
[42.942601, "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"]
|
||||||
|
[43.344696, "o", " (pdivert flipped the last payload byte and fixed the ICMP checksum)\r\n\r\n\u001b[1m--- 11. 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"]
|
||||||
|
[43.626606, "o", "pf disabled\r\npf disabled\r\n"]
|
||||||
|
[44.028882, "o", "\u001b[1;36mpftest-pf0# lsmod | grep '^pf '\u001b[0m\r\n"]
|
||||||
|
[44.286597, "o", "pf 552960 0\r\n"]
|
||||||
|
[44.688511, "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"]
|
||||||
|
[44.688631, "o", "\r\n"]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+123
-103
@@ -1,141 +1,161 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
PF-on-Linux demo recorder — produces an asciicast v2 (.cast) from a REAL
|
PF-on-Linux demo recorder -> asciicast v2 (.cast)
|
||||||
session driven against the lab VMs (no simulated output: every byte shown
|
|
||||||
comes from the live systems; only pacing is scripted).
|
|
||||||
|
|
||||||
Usage: ./scripts/demo-record.py [--out out/demo-pf-linux.cast]
|
Every byte shown comes from the REAL lab (Alpine VMs under libvirt, kernel
|
||||||
Requires: lab running (pftest-pf0/cli/srv), repo built, ssh alias `pf0`
|
7.3.4 built from source with our pf.ko). Only the pacing is scripted.
|
||||||
(ProxyJump) and lib.sh-style nested access working.
|
|
||||||
|
|
||||||
Edit DEMO below to change the narration script. Timing: each line is a
|
Usage:
|
||||||
(real command, real output) pair; delays are cosmetic only.
|
./scripts/demo-record.py [--out out/demo-pf-linux.cast] [--quick]
|
||||||
|
|
||||||
|
Play back with: asciinema play out/demo-pf-linux.cast
|
||||||
|
Upload with: asciinema upload out/demo-pf-linux.cast
|
||||||
"""
|
"""
|
||||||
import argparse, json, os, pty, re, shlex, subprocess, sys, time
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
OUT_DEFAULT = os.path.join(REPO, "out", "demo-pf-linux.cast")
|
|
||||||
|
|
||||||
DEB = os.environ.get("DEB", "root@192.168.1.242")
|
DEB = os.environ.get("DEB", "root@192.168.1.242")
|
||||||
PF0_IP = os.environ.get("PF0_IP", "192.168.122.8")
|
PF0 = os.environ.get("PF0_IP", "192.168.122.8")
|
||||||
|
CLI = os.environ.get("CLI_IP", "10.0.1.10")
|
||||||
|
SRV = os.environ.get("SRV_IP", "10.0.2.20")
|
||||||
|
KEY = os.path.join(os.environ.get("HOME", "/root"), ".ssh/pflab_ed")
|
||||||
|
|
||||||
|
CONF = """# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
|
||||||
|
set block-policy return
|
||||||
|
table <trusted> { 10.0.1.0/24 }
|
||||||
|
|
||||||
|
# transit traffic: stateful, table-driven
|
||||||
|
pass in quick on ingress proto icmp from <trusted> keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
|
||||||
|
# deny a specific destination
|
||||||
|
block drop in quick proto icmp to 10.0.9.99
|
||||||
|
|
||||||
|
# everything else
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
"""
|
||||||
|
|
||||||
|
DIVERT_CONF = """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
|
||||||
|
pass out quick
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def deb_run(cmd: str, timeout=30) -> str:
|
def sh(cmd, timeout=40):
|
||||||
"""run a command on the libvirt host (which has direct keys to VMs)"""
|
p = subprocess.run(cmd, shell=True, capture_output=True, text=True,
|
||||||
p = subprocess.run(["ssh", "-o", "ConnectTimeout=10", DEB, cmd],
|
timeout=timeout)
|
||||||
capture_output=True, text=True, timeout=timeout)
|
|
||||||
return (p.stdout or "") + (p.stderr or "")
|
return (p.stdout or "") + (p.stderr or "")
|
||||||
|
|
||||||
|
|
||||||
def vm0(cmd: str, timeout=30) -> str:
|
def on_pf0(cmd, timeout=40):
|
||||||
return deb_run(
|
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
|
||||||
f"ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 "
|
f"-i {KEY} root@{PF0} {shlex.quote(cmd)}", timeout)
|
||||||
f"-i /root/.ssh/pflab_ed root@{PF0_IP} {shlex.quote(cmd)}",
|
|
||||||
timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def vm_ip(ip: str, cmd: str, timeout=30) -> str:
|
def on_vm(ip, cmd, timeout=40):
|
||||||
return deb_run(
|
return sh(f"ssh -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new "
|
||||||
f"ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 "
|
f"-i {KEY} root@{ip} {shlex.quote(cmd)}", timeout)
|
||||||
f"-i /root/.ssh/pflab_ed root@{ip} {shlex.quote(cmd)}",
|
|
||||||
timeout)
|
|
||||||
|
|
||||||
|
|
||||||
CLI, SRV = "10.0.1.10", "10.0.2.20"
|
def on_deb(cmd, timeout=40):
|
||||||
|
return sh(f"ssh -o ConnectTimeout=8 {DEB} {shlex.quote(cmd)}", timeout)
|
||||||
|
|
||||||
|
|
||||||
def scene(title):
|
def stage():
|
||||||
return ("#banner", title)
|
"""put the demo ruleset + pdivert on pf0 (through the lab host)"""
|
||||||
|
with open("/tmp/demo-pf.conf", "w") as f:
|
||||||
|
f.write(CONF)
|
||||||
|
with open("/tmp/demo-divert.conf", "w") as f:
|
||||||
|
f.write(DIVERT_CONF)
|
||||||
|
sh(f"scp -q /tmp/demo-pf.conf /tmp/demo-divert.conf "
|
||||||
|
f"{os.path.join(REPO, 'tools/out/pdivert')} {DEB}:/tmp/", timeout=90)
|
||||||
|
on_deb(f"scp -q -o StrictHostKeyChecking=accept-new -i {KEY} "
|
||||||
|
f"/tmp/demo-pf.conf /tmp/demo-divert.conf /tmp/pdivert "
|
||||||
|
f"root@{PF0}:/tmp/ 2>&1", timeout=90)
|
||||||
|
|
||||||
# (shell-line, runner, command) — runners: pf0 / cli / srv / local
|
|
||||||
DEMO = [
|
SCENARIO = [
|
||||||
("local", "echo 'PF-on-Linux — OpenBSD PF engine on Linux 7.3.4 (lab: Alpine VMs)'"),
|
("local", "echo '=== PF on Linux — OpenBSD PF 8.0 engine running inside the Linux 7.3.4 kernel ==='"),
|
||||||
("local", "uname -r | sed 's/^/build host kernel: /'"),
|
("local", "echo 'lab: 3 Alpine VMs under libvirt (pf0 = firewall, cli/srv = transit endpoints)'"),
|
||||||
("pf0", "uname -r"),
|
("pf0", "uname -r"),
|
||||||
("pf0", "lsmod | grep -E '^pf ' || echo 'pf: not loaded yet'"),
|
("pf0", "lsmod | grep '^pf ' || echo 'pf: not loaded'"),
|
||||||
("pf0", "insmod /opt/pf/pf.ko && dmesg | tail -2"),
|
("pf0", "insmod /opt/pf/pf.ko && dmesg | tail -3"),
|
||||||
("pf0", "/opt/pf/pfctl -s info | head -3"),
|
("pf0", "/opt/pf/pfctl -s info | head -4"),
|
||||||
("pf0", "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"),
|
("pf0", "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"),
|
||||||
("pf0", "cat /tmp/pfdemo.conf"),
|
("pf0", "cat /tmp/demo-pf.conf"),
|
||||||
("pf0", "/opt/pf/pfctl -f /tmp/pfdemo.conf && /opt/pf/pfctl -sr"),
|
("pf0", "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded' && /opt/pf/pfctl -sr"),
|
||||||
("cli", f"ping -c 2 -W 2 {SRV} 2>&1 | tail -2"),
|
("pf0", "/opt/pf/pfctl -sr -vv | head -8"),
|
||||||
("pf0", "/opt/pf/pfctl -s states"),
|
("cli", f"ping -c2 -W2 {SRV} | tail -3"),
|
||||||
("pf0", "/opt/pf/pfctl -s Tables"),
|
("pf0", "/opt/pf/pfctl -ss"),
|
||||||
("pf0", "/opt/pf/pfctl -t demo -T show"),
|
("pf0", "/opt/pf/pfctl -t trusted -T show"),
|
||||||
("cli", "ping -c 2 -W 2 10.0.9.99 2>&1 | tail -2"),
|
("pf0", "/opt/pf/pfctl -s Tables"),
|
||||||
("pf0", "/opt/pf/pfctl -s info | grep -E 'match|current'"),
|
("cli", "ping -c2 -W2 10.0.9.99 | tail -3"),
|
||||||
("pf0", "/opt/pf/pfctl -ss | head -2"),
|
("pf0", "/opt/pf/pfctl -sr -vv | head -6"),
|
||||||
|
("pf0", "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"),
|
||||||
|
("pf0", "(setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"),
|
||||||
|
("cli", "ping -c2 -W2 10.0.1.1 | tail -3"),
|
||||||
|
("pf0", "head -3 /tmp/pdivert.log"),
|
||||||
|
("pf0", "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"),
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = """set block-policy return
|
|
||||||
set loginterface egress
|
|
||||||
table <demo> { 10.0.1.0/24 }
|
|
||||||
match in quick on ingress proto icmp from <demo> tag DEMO
|
|
||||||
pass in quick on ingress proto icmp keep state
|
|
||||||
pass out quick proto icmp keep state
|
|
||||||
block drop in quick proto icmp from any to 10.0.9.99
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
def record(out_path, quick=False):
|
||||||
def prep():
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||||
# stage the demo ruleset into pf0
|
events = []
|
||||||
import base64
|
|
||||||
b64 = base64.b64encode(CONF.encode()).decode()
|
|
||||||
vm0(f"echo {b64} | base64 -d > /tmp/pfdemo.conf; echo staged")
|
|
||||||
|
|
||||||
|
|
||||||
def record(out_path: str, fps_pad=0.4):
|
|
||||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
||||||
casts = []
|
|
||||||
t = 0.0
|
t = 0.0
|
||||||
|
|
||||||
def emit(text, delay):
|
def emit(text, delay):
|
||||||
nonlocal t
|
nonlocal t
|
||||||
t += delay
|
t += delay
|
||||||
casts.append([round(t, 3), "o", text])
|
events.append([round(t, 3), "o", text])
|
||||||
|
|
||||||
header = {"version": 2, "width": 100, "height": 24,
|
header = {
|
||||||
"timestamp": int(time.time()), "title":
|
"version": 2, "width": 100, "height": 26,
|
||||||
"PF on Linux — port demo (OpenBSD PF 8.0 engine on Linux 7.3.4, Alpine libvirt lab)",
|
"timestamp": int(time.time()),
|
||||||
"env": {"SHELL": "/bin/sh", "TERM": "xterm-256color"}}
|
"title": "PF on Linux — OpenBSD PF engine inside the Linux kernel (lab demo)",
|
||||||
|
"env": {"SHELL": "/bin/sh", "TERM": "xterm-256color"},
|
||||||
|
}
|
||||||
|
runners = {
|
||||||
|
"pf0": lambda c: on_pf0(c),
|
||||||
|
"cli": lambda c: on_vm(CLI, c),
|
||||||
|
"srv": lambda c: on_vm(SRV, c),
|
||||||
|
"local": lambda c: sh(c),
|
||||||
|
}
|
||||||
|
labels = {"pf0": "pftest-pf0", "cli": "pftest-cli", "srv": "pftest-srv",
|
||||||
|
"local": "lab-host"}
|
||||||
|
|
||||||
|
stage()
|
||||||
with open(out_path, "w") as f:
|
with open(out_path, "w") as f:
|
||||||
f.write(json.dumps(header) + "\n")
|
f.write(json.dumps(header) + "\n")
|
||||||
|
for kind, cmd in SCENARIO:
|
||||||
def show_prompt(host):
|
emit(f"\x1b[1;36m{labels[kind]}\x1b[0m:\x1b[1;37m {cmd}\x1b[0m\r\n",
|
||||||
emit(f"\x1b[1;36m{host}$\x1b[0m ", 0.15)
|
0.35)
|
||||||
|
|
||||||
runners = {"pf0": vm0, "cli": lambda c: vm_ip(CLI, c),
|
|
||||||
"srv": lambda c: vm_ip(SRV, c), "local":
|
|
||||||
lambda c: subprocess.run(["sh", "-c", c],
|
|
||||||
capture_output=True, text=True,
|
|
||||||
timeout=30).stdout +
|
|
||||||
subprocess.run(["sh", "-c", c], capture_output=True,
|
|
||||||
text=True).stderr}
|
|
||||||
prep()
|
|
||||||
for kind, cmd in DEMO:
|
|
||||||
label = {"pf0": "pftest-pf0", "cli": "pftest-cli",
|
|
||||||
"srv": "pftest-srv", "local": "build-host"}[kind]
|
|
||||||
show_prompt(label)
|
|
||||||
emit(cmd + "\r\n", 0.25)
|
|
||||||
try:
|
try:
|
||||||
out = runners[kind](cmd)
|
out = runners[kind](cmd)
|
||||||
except Exception as e:
|
except subprocess.TimeoutExpired:
|
||||||
out = f"[runner error: {e}]\n"
|
out = "[timeout]\r\n"
|
||||||
# strip ANSI-safe passthrough; small human-ish delay
|
except Exception as e: # noqa: BLE001
|
||||||
delay = min(1.2, 0.25 + len(out) / 4000.0)
|
out = f"[error: {e}]\r\n"
|
||||||
emit(out.replace("\n", "\r\n") or "(no output)\r\n", delay)
|
out = out.replace("\r\n", "\n").replace("\n", "\r\n")
|
||||||
time.sleep(0.1)
|
emit(out or "(no output)\r\n", min(1.4, 0.3 + len(out) / 5000.0))
|
||||||
emit("\x1b[1;36mpftest-pf0$\x1b[0m /opt/pf/pfctl -d\r\n", 0.4)
|
with open(out_path, "a") as f:
|
||||||
runners["pf0"]("/opt/pf/pfctl -d")
|
for ev in events:
|
||||||
for i, ev in enumerate(casts):
|
f.write(json.dumps(ev) + "\n")
|
||||||
f.write(json.dumps(ev) + "\n")
|
print(f"WROTE {out_path}: {len(events)} events, {t:.1f}s")
|
||||||
print(f"WROTE {out_path} ({len(casts)} events, {t:.1f}s)")
|
print("play: asciinema play " + out_path)
|
||||||
print("play: asciinema play " + out_path +
|
print("upload: asciinema upload " + out_path)
|
||||||
" (or: script-replay/pf-in-play fallback: cat with 'less -R')")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--out", default=OUT_DEFAULT)
|
ap.add_argument("--out", default=os.path.join(REPO, "out",
|
||||||
|
"demo-pf-linux.cast"))
|
||||||
|
ap.add_argument("--quick", action="store_true")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
record(a.out)
|
record(a.out, a.quick)
|
||||||
Executable
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Demo session for PF-on-Linux: a scripted terminal tour of the lab.
|
||||||
|
# Recorded with: asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||||
|
#
|
||||||
|
# Everything printed is the REAL output of the lab (Alpine VMs under libvirt
|
||||||
|
# running the kernel built from source with our pf.ko). The VM management
|
||||||
|
# addresses are resolved from the hypervisor (they are DHCP), the lab
|
||||||
|
# topology (10.0.1.0/24, 10.0.2.0/24, ...) is fixed.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
DEB=${DEB:-root@192.168.1.242}
|
||||||
|
CLI_IP=${CLI_IP:-10.0.1.10}
|
||||||
|
SRV_IP=${SRV_IP:-10.0.2.20}
|
||||||
|
KEY=${KEY:-$HOME/.ssh/pflab_ed}
|
||||||
|
|
||||||
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||||
|
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# management address of a lab VM (DHCP, so ask the hypervisor)
|
||||||
|
mgmt() {
|
||||||
|
ssh -o ConnectTimeout=8 "$DEB" "virsh domifaddr $1 | awk '/vnet/{print \$4}' | cut -d/ -f1" \
|
||||||
|
2>/dev/null | tr -d '\r' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
PF0_MGMT=$(mgmt pftest-pf0)
|
||||||
|
CLI_MGMT=$(mgmt pftest-cli)
|
||||||
|
[ -n "$PF0_MGMT" ] || PF0_MGMT=192.168.122.8
|
||||||
|
[ -n "$CLI_MGMT" ] || CLI_MGMT=192.168.122.80
|
||||||
|
|
||||||
|
# run a command on a lab VM (through the hypervisor)
|
||||||
|
onvm() {
|
||||||
|
local ip=$1; shift
|
||||||
|
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
|
||||||
|
-o ProxyJump=root@192.168.1.242 -i "$KEY" root@"$ip" "$@" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
p0() {
|
||||||
|
cyan "pftest-pf0# $*"
|
||||||
|
onvm "$PF0_MGMT" "$@"
|
||||||
|
sleep 0.4
|
||||||
|
}
|
||||||
|
vmc() {
|
||||||
|
cyan "pftest-cli# $*"
|
||||||
|
onvm "$CLI_MGMT" "$@"
|
||||||
|
sleep 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CONF=/tmp/demo-pf.conf
|
||||||
|
DIV=/tmp/demo-divert.conf
|
||||||
|
|
||||||
|
cat > "$CONF" <<'CONF'
|
||||||
|
# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
|
||||||
|
set block-policy return
|
||||||
|
table <trusted> { 10.0.1.0/24 }
|
||||||
|
|
||||||
|
# transit traffic: stateful, table-driven
|
||||||
|
pass in quick on ingress proto icmp from <trusted> keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
|
||||||
|
# deny a specific destination
|
||||||
|
block drop in quick proto icmp to 10.0.9.99
|
||||||
|
|
||||||
|
# everything else
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
|
||||||
|
cat > "$DIV" <<CONF
|
||||||
|
pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# the pf0 interface towards the server (used by the NAT pool)
|
||||||
|
EGRESS_IF=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
[ -n "$EGRESS_IF" ] || EGRESS_IF=eth2
|
||||||
|
|
||||||
|
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$CONF" "$DIV" \
|
||||||
|
tools/out/pdivert tools/out/pflogd "root@$PF0_MGMT:/tmp/" 2>/dev/null
|
||||||
|
|
||||||
|
bold "==============================================================="
|
||||||
|
bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel"
|
||||||
|
bold "==============================================================="
|
||||||
|
echo
|
||||||
|
echo "Lab: 3 Alpine VMs under libvirt."
|
||||||
|
echo " pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)"
|
||||||
|
echo " pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 1. our kernel, the OpenBSD engine as a module ---"
|
||||||
|
p0 "uname -r"
|
||||||
|
p0 "lsmod | grep '^pf ' || echo 'pf: not loaded yet'"
|
||||||
|
p0 "insmod /opt/pf/pf.ko && dmesg | tail -3"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---"
|
||||||
|
p0 "/opt/pf/pfctl -s info | head -4"
|
||||||
|
p0 "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---"
|
||||||
|
p0 "cat /tmp/demo-pf.conf"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 4. transit traffic is allowed and stateful ---"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||||
|
p0 "/opt/pf/pfctl -ss | grep -E 'icmp' | head -4"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 5. tables are live objects ---"
|
||||||
|
p0 "/opt/pf/pfctl -t trusted -T show"
|
||||||
|
p0 "/opt/pf/pfctl -s Tables"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 6. the drop rule actually drops ---"
|
||||||
|
p0 "cat > /tmp/demo-block.conf <<EOF
|
||||||
|
block drop in quick proto icmp to $SRV_IP
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
EOF"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||||
|
p0 "/opt/pf/pfctl -sr -vv | head -6"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 7. NAT: the source is translated on the way out, restored on the way back ---"
|
||||||
|
p0 "cat > /tmp/demo-nat.conf <<EOF
|
||||||
|
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_IF)
|
||||||
|
pass in quick on ingress proto icmp keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
EOF"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||||
|
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'"
|
||||||
|
echo
|
||||||
|
|
||||||
|
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. route-to: force the next hop away from the normal path ---"
|
||||||
|
p0 "cat > /tmp/demo-route.conf <<EOF
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { 10.0.3.2 } keep state
|
||||||
|
pass out quick
|
||||||
|
EOF"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-route.conf && echo 'route-to rule loaded'"
|
||||||
|
p0 "(tcpdump -ni eth3 -c 2 -e icmp > /tmp/route.td 2>&1 &); (tcpdump -ni eth2 -c 2 -e icmp > /tmp/norm.td 2>&1 &); sleep 1"
|
||||||
|
vmc "ping -c1 -W2 $SRV_IP | tail -2"
|
||||||
|
sleep 2
|
||||||
|
p0 "echo 'forced egress (eth3, toward the gateway):'; grep -v '^tcpdump\|^listening\|^$' /tmp/route.td | head -2; echo 'normal egress (eth2):'; grep -c 'echo request' /tmp/norm.td"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf >/dev/null"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 10. divert-to: the packet goes to userspace and comes back ---"
|
||||||
|
p0 "cat /tmp/demo-divert.conf"
|
||||||
|
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"
|
||||||
|
vmc "ping -c2 -W2 10.0.1.1 | tail -3"
|
||||||
|
p0 "head -3 /tmp/pdivert.log"
|
||||||
|
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "--- 11. shutdown ---"
|
||||||
|
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
|
||||||
|
p0 "lsmod | grep '^pf '"
|
||||||
|
echo
|
||||||
|
bold "demo over - port, gates and docs:"
|
||||||
|
bold " https://git.labexposed.com/lgcosta/pf-linux-port"
|
||||||
|
echo
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 1 - a implementacao no kernel: da fonte OpenBSD ao modulo pf.ko.
|
||||||
|
# Roda no host de build (o port esta no repositorio; a VM so recebe o .ko).
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 1 - A IMPLEMENTACAO NO KERNEL"
|
||||||
|
|
||||||
|
bold "# o port: fontes BSD trazidos do OpenBSD 8.0 + camada de compatibilidade"
|
||||||
|
dim " module/pfsrc/ - fontes do OpenBSD (pf.c, pf_lb.c, pf_norm.c, hfsc.c, ...)"
|
||||||
|
dim " module/compat/ - shims que traduzem as APIs BSD para o Linux"
|
||||||
|
dim " module/include/ - cabecalhos (pfk.h) e os headers vendorizados do OpenBSD"
|
||||||
|
echo
|
||||||
|
cyan "host# ls module/pfsrc | head -20"
|
||||||
|
ls "$REPO/module/pfsrc" | head -20
|
||||||
|
echo
|
||||||
|
cyan "host# wc -l module/pfsrc/*.c | tail -3"
|
||||||
|
(cd "$REPO" && wc -l module/pfsrc/*.c | tail -3)
|
||||||
|
cyan "host# wc -l module/compat/*.c | tail -1"
|
||||||
|
(cd "$REPO" && wc -l module/compat/*.c | tail -1)
|
||||||
|
echo
|
||||||
|
dim " (as fontes OpenBSD entram praticamente intactas; a compat faz a ponte"
|
||||||
|
dim " mbuf->sk_buff, spl->spinlock, epoch->RCU, ifnet->net_device, ...)"
|
||||||
|
|
||||||
|
bold
|
||||||
|
bold "# compilando o modulo contra o kernel do lab (build limpo)"
|
||||||
|
echo
|
||||||
|
cyan "host# ./scripts/build-module.sh clean"
|
||||||
|
(cd "$REPO" && ./scripts/build-module.sh clean 2>&1 | tail -3)
|
||||||
|
cyan "host# ./scripts/build-module.sh"
|
||||||
|
(cd "$REPO" && ./scripts/build-module.sh 2>&1 | tail -18)
|
||||||
|
echo
|
||||||
|
cyan "host# ls -la module/pf.ko"
|
||||||
|
ls -la "$REPO/module/pf.ko"
|
||||||
|
cyan "host# modinfo module/pf.ko | head -5"
|
||||||
|
modinfo "$REPO/module/pf.ko" 2>/dev/null | head -5 || \
|
||||||
|
(cd "$REPO" && strings module/pf.ko | grep -m2 vermagic)
|
||||||
|
echo
|
||||||
|
green " modulo pronto: o motor PF 8.0 do OpenBSD pronto para carregar no Linux"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 2 - ativacao: carregar pf.ko, habilitar o firewall, ver o estado.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 2 - ATIVACAO"
|
||||||
|
|
||||||
|
bold "# o modulo ainda nao esta carregado"
|
||||||
|
p0 "rmmod pf 2>/dev/null; lsmod | grep '^pf ' || echo 'pf: not loaded'"
|
||||||
|
p0 "ls -l /dev/pf 2>/dev/null || echo '/dev/pf: ausente enquanto pf nao carrega'"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# carregando o motor PF no kernel"
|
||||||
|
p0 "insmod /opt/pf/pf.ko && echo 'pf.ko carregado no kernel'"
|
||||||
|
p0 "dmesg | tail -4"
|
||||||
|
p0 "lsmod | grep '^pf '"
|
||||||
|
p0 "ls -l /dev/pf"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# habilitando o firewall"
|
||||||
|
p0 "/opt/pf/pfctl -e && echo 'pf habilitado'"
|
||||||
|
p0 "/opt/pf/pfctl -si | head -6"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# o ruleset comeca vazio (a politica padrao do pf e bloquear, com return)"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "/opt/pf/pfctl -sf | head -5"
|
||||||
|
echo
|
||||||
|
green " motor ativo: /dev/pf respondendo, pfctl sobre a ABI de ioctl do OpenBSD"
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 3 - regras basicas: tabelas, anchors, keep state, block/pass.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 3 - REGRAS BASICAS"
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -e"
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null"
|
||||||
|
|
||||||
|
bold "# um pf.conf de verdade, sintaxe do OpenBSD"
|
||||||
|
cat > /tmp/sc-regras.conf <<'CONF'
|
||||||
|
# --- regras basicas do PF ---
|
||||||
|
set block-policy return
|
||||||
|
|
||||||
|
table <trusted> { 10.0.1.0/24 }
|
||||||
|
|
||||||
|
anchor "demo"
|
||||||
|
|
||||||
|
pass in quick on ingress proto icmp from <trusted> keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-regras.conf >/dev/null
|
||||||
|
p0 "cat /tmp/sc-regras.conf"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-regras.conf && echo 'ruleset carregado'"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# o que o pf entendeu"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "/opt/pf/pfctl -s Tables"
|
||||||
|
p0 "/opt/pf/pfctl -t trusted -T show"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# trafego permitido: passa e cria estado"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||||
|
p0 "/opt/pf/pfctl -ss | head -5"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# a mesma regra com 'block drop' bloqueia de verdade"
|
||||||
|
cat > /tmp/sc-block.conf <<CONF
|
||||||
|
set block-policy return
|
||||||
|
table <trusted> { 10.0.1.0/24 }
|
||||||
|
block drop in quick on ingress proto icmp from <trusted>
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-block.conf >/dev/null
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-block.conf && echo 'ruleset de bloqueio carregado'"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||||
|
p0 "/opt/pf/pfctl -sr -vv | head -4"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# anchors: regras aninhadas, como no OpenBSD"
|
||||||
|
p0 "cat > /tmp/sc-anchor.conf <<'EOF'
|
||||||
|
pass in quick on ingress proto icmp from 10.0.1.10 to 10.0.2.20 keep state
|
||||||
|
pass in quick
|
||||||
|
pass out quick
|
||||||
|
EOF"
|
||||||
|
p0 "/opt/pf/pfctl -a demo -f /tmp/sc-anchor.conf && echo 'ruleset do anchor carregado'"
|
||||||
|
p0 "/opt/pf/pfctl -a demo -sr"
|
||||||
|
p0 "cat > /tmp/sc-main.conf <<'EOF'
|
||||||
|
anchor \"demo\"
|
||||||
|
pass in quick on ingress proto icmp keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
EOF"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-main.conf && echo 'ruleset principal com anchor carregado'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# voltando ao ruleset com estado"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-regras.conf && echo 'ruleset stateful restaurado'"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -2"
|
||||||
|
green " tabelas, anchors, quick, keep state, block drop: todos no lugar"
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 4 - roteamento: route-to (caminho forcado) e reply-to (assimetria).
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 4 - ROTEAMENTO"
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -e"
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null"
|
||||||
|
put ./an.sh >/dev/null
|
||||||
|
|
||||||
|
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
IF_GW=$(onvm "$PF0_MGMT" "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
[ -n "$IF_SRV" ] || IF_SRV=eth2
|
||||||
|
[ -n "$IF_GW" ] || IF_GW=eth3
|
||||||
|
dim " (o servidor fica atras de $IF_SRV; o segundo gateway, em $IF_GW)"
|
||||||
|
|
||||||
|
bold "# route-to: o pacote sai pelo caminho forcado, nao pelo normal"
|
||||||
|
cat > /tmp/sc-route.conf <<CONF
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { $GW2_IP } keep state
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-route.conf >/dev/null
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-route.conf && echo 'regra route-to carregada'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_GW -c 4 -e icmp > /tmp/sc_gw.td 2>&1 &); (tcpdump -ni $IF_SRV -c 4 -e icmp > /tmp/sc_srv.td 2>&1 &); sleep 1"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -2"
|
||||||
|
dim " (o ping falha de proposito: o pacote foi entregue ao gateway $GW2_IP)"
|
||||||
|
sleep 3
|
||||||
|
p0 "echo \"egresso forcado ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_gw.td req"
|
||||||
|
p0 "echo \"egresso normal ($IF_SRV):\"; sh /tmp/an.sh /tmp/sc_srv.td req"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# reply-to: a resposta volta por um caminho diferente do pedido"
|
||||||
|
cat > /tmp/sc-reply.conf <<CONF
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP reply-to { $GW2_IP } keep state
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-reply.conf >/dev/null
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-reply.conf && echo 'regra reply-to carregada'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_GW -c 6 -e icmp > /tmp/sc_rgw.td 2>&1 &); (tcpdump -ni eth1 -c 6 -e icmp > /tmp/sc_rcli.td 2>&1 &); sleep 1"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -2"
|
||||||
|
dim " (de novo sem resposta direta: a resposta foi forcada para o gateway)"
|
||||||
|
sleep 3
|
||||||
|
p0 "echo \"resposta forcada ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_rgw.td rep"
|
||||||
|
p0 "echo \"resposta pelo caminho normal do cliente:\"; sh /tmp/an.sh /tmp/sc_rcli.td rep"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# rota normal restaurada"
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -sr"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; echo 'capturas encerradas'"
|
||||||
|
green " route-to e reply-to: o proximo salto e decidido pelo pf, nao pela tabela de rotas"
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 5 - balanceamento: pools (round-robin, source-hash) e dup-to.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 5 - BALANCEAMENTO"
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -e"
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null"
|
||||||
|
put ./an.sh >/dev/null
|
||||||
|
|
||||||
|
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
IF_GW=$(onvm "$PF0_MGMT" "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
[ -n "$IF_SRV" ] || IF_SRV=eth2
|
||||||
|
[ -n "$IF_GW" ] || IF_GW=eth3
|
||||||
|
|
||||||
|
bold "# pool round-robin: a traducao de origem alterna entre os membros do pool"
|
||||||
|
cat > /tmp/sc-pool.conf <<CONF
|
||||||
|
pass out quick on egress from 10.0.1.0/24 to any nat-to { 10.0.2.1, 10.0.3.1 } round-robin
|
||||||
|
pass in quick on ingress proto icmp keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-pool.conf >/dev/null
|
||||||
|
p0 "cat /tmp/sc-pool.conf"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-pool.conf && echo 'pool round-robin carregado'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 12 -e icmp > /tmp/sc_pool.td 2>&1 &); sleep 1"
|
||||||
|
for i in 1 2 3 4 5 6; do
|
||||||
|
vmc "ping -c1 -W2 $SRV_IP >/dev/null 2>&1; echo ' pacote $i enviado'"
|
||||||
|
done
|
||||||
|
sleep 4
|
||||||
|
p0 "echo 'origem vista pelo servidor (o pool alterna entre os membros):'; sh /tmp/an.sh /tmp/sc_pool.td src"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# pool source-hash: a mesma origem cai sempre no mesmo membro"
|
||||||
|
cat > /tmp/sc-hash.conf <<CONF
|
||||||
|
pass out quick on egress from 10.0.1.0/24 to any nat-to { 10.0.2.1, 10.0.3.1 } source-hash
|
||||||
|
pass in quick on ingress proto icmp keep state
|
||||||
|
pass out quick on egress proto icmp keep state
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-hash.conf >/dev/null
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-hash.conf && echo 'pool source-hash carregado'"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 8 -e icmp > /tmp/sc_hash.td 2>&1 &); sleep 1"
|
||||||
|
for i in 1 2 3 4; do
|
||||||
|
vmc "ping -c1 -W2 $SRV_IP >/dev/null 2>&1; echo ' pacote $i enviado'"
|
||||||
|
done
|
||||||
|
sleep 4
|
||||||
|
p0 "echo 'origem com source-hash (estavel por origem):'; sh /tmp/an.sh /tmp/sc_hash.td src"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# dup-to: o pacote segue o caminho normal e uma copia vai para o outro gateway"
|
||||||
|
cat > /tmp/sc-dup.conf <<CONF
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP dup-to { $GW2_IP } keep state
|
||||||
|
pass out quick
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-dup.conf >/dev/null
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-dup.conf && echo 'regra dup-to carregada'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 4 -e icmp > /tmp/sc_dup1.td 2>&1 &); (tcpdump -ni $IF_GW -c 4 -e icmp > /tmp/sc_dup2.td 2>&1 &); sleep 1"
|
||||||
|
vmc "ping -c2 -W2 $SRV_IP | tail -2"
|
||||||
|
sleep 4
|
||||||
|
p0 "echo \"caminho normal ($IF_SRV):\"; sh /tmp/an.sh /tmp/sc_dup1.td req"
|
||||||
|
p0 "echo \"copia espelhada ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_dup2.td req"
|
||||||
|
echo
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null; pkill tcpdump 2>/dev/null"
|
||||||
|
green " round-robin reparte a carga; source-hash fixa por origem; dup-to replica"
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fase 6 - QOS: filas do OpenBSD 8.0 (hfsc) no caminho de dados do Linux.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
. ./lib.sh
|
||||||
|
|
||||||
|
banner "FASE 6 - QOS (FILAS / ALTQ)"
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -e"
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null"
|
||||||
|
|
||||||
|
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
|
||||||
|
[ -n "$IF_SRV" ] || IF_SRV=eth2
|
||||||
|
|
||||||
|
bold "# a sintaxe de filas do OpenBSD 8.0: queue com classes hfsc"
|
||||||
|
cat > /tmp/sc-q.conf <<CONF
|
||||||
|
queue rootq on $IF_SRV bandwidth 10Mbit
|
||||||
|
queue leafq on $IF_SRV bandwidth 2Mbit parent rootq default
|
||||||
|
pass in quick proto tcp from $CLI_IP to $SRV_IP port 9095 queue leafq keep state
|
||||||
|
pass out quick keep state
|
||||||
|
CONF
|
||||||
|
put /tmp/sc-q.conf >/dev/null
|
||||||
|
p0 "cat /tmp/sc-q.conf"
|
||||||
|
p0 "/opt/pf/pfctl -f /tmp/sc-q.conf && echo 'filas carregadas'"
|
||||||
|
p0 "/opt/pf/pfctl -sr"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# o pfctl mostra as classes e as estatisticas do escalonador"
|
||||||
|
p0 "/opt/pf/pfctl -s queue -v"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# transferencia de 2 MB pela fila: o trafego entra na classe"
|
||||||
|
vms "head -c 2000000 /dev/zero | tr '\\0' A > /tmp/sc_data.bin; ls -l /tmp/sc_data.bin; pkill nc 2>/dev/null; (setsid nc -l -p 9095 < /tmp/sc_data.bin >/dev/null 2>&1 &); sleep 1; echo 'servidor escutando na 9095'"
|
||||||
|
vmc "nc -w60 $SRV_IP 9095 > /tmp/sc_out.bin; wc -c < /tmp/sc_out.bin"
|
||||||
|
sleep 1
|
||||||
|
p0 "/opt/pf/pfctl -s queue -v"
|
||||||
|
echo
|
||||||
|
|
||||||
|
bold "# a disciplina hfsc contou o trafego da classe"
|
||||||
|
p0 "/opt/pf/pfctl -sr -vv | head -6"
|
||||||
|
p0 "ip -s link show $IF_SRV | head -4"
|
||||||
|
echo
|
||||||
|
|
||||||
|
p0 "/opt/pf/pfctl -f /dev/null; pkill nc 2>/dev/null; /opt/pf/pfctl -s queue -v"
|
||||||
|
green " filas do OpenBSD 8.0 (hfsc) anexadas a interface e contando pacotes"
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Analysis helper executed ON the firewall VM (avoids quoting hell through the
|
||||||
|
# ssh layers). usage: an.sh <capture-file> <mode>
|
||||||
|
f=$1
|
||||||
|
mode=$2
|
||||||
|
case "$mode" in
|
||||||
|
req) echo "requerentes no fio: $(grep -c 'echo request' "$f")" ;;
|
||||||
|
rep) echo "respostas no fio: $(grep -c 'echo reply' "$f")" ;;
|
||||||
|
src) echo "origem no fio (contagem por endereco de origem):"
|
||||||
|
grep 'echo request' "$f" | sed 's/.*: \([0-9.]*\) > .*/\1/' | sort | uniq -c ;;
|
||||||
|
uniq) echo "origens distintas: $(grep 'echo request' "$f" | sed 's/.*: \([0-9.]*\) > .*/\1/' | sort -u | tr '\n' ' ')" ;;
|
||||||
|
lines) grep 'echo request' "$f" | head -2 ;;
|
||||||
|
*) echo "modo desconhecido: $mode" ;;
|
||||||
|
esac
|
||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared helpers for the screencast phase scripts.
|
||||||
|
#
|
||||||
|
# Everything printed by a phase is the REAL output of the lab: Alpine VMs
|
||||||
|
# under libvirt running the kernel built from source with pf.ko. Only the
|
||||||
|
# framing (prompts, banners) is scripted.
|
||||||
|
set -u
|
||||||
|
|
||||||
|
DEB=${DEB:-root@192.168.1.242}
|
||||||
|
KEY=${KEY:-$HOME/.ssh/pflab_ed}
|
||||||
|
CLI_IP=${CLI_IP:-10.0.1.10}
|
||||||
|
SRV_IP=${SRV_IP:-10.0.2.20}
|
||||||
|
GW2_IP=${GW2_IP:-10.0.3.2}
|
||||||
|
REPO=${REPO:-$(cd "$(dirname "$0")/../../.." && pwd)}
|
||||||
|
|
||||||
|
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
|
||||||
|
green() { printf '\033[1;32m%s\033[0m\n' "$*"; }
|
||||||
|
yellow(){ printf '\033[1;33m%s\033[0m\n' "$*"; }
|
||||||
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||||
|
dim() { printf '\033[2m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
banner() {
|
||||||
|
echo
|
||||||
|
bold "==============================================================="
|
||||||
|
bold " $*"
|
||||||
|
bold "==============================================================="
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
mgmt() {
|
||||||
|
ssh -o ConnectTimeout=8 "$DEB" "virsh domifaddr $1 | awk '/vnet/{print \$4}' | cut -d/ -f1" \
|
||||||
|
2>/dev/null | tr -d '\r' | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
PF0_MGMT=${PF0_MGMT:-$(mgmt pftest-pf0)}
|
||||||
|
CLI_MGMT=${CLI_MGMT:-$(mgmt pftest-cli)}
|
||||||
|
SRV_MGMT=${SRV_MGMT:-$(mgmt pftest-srv)}
|
||||||
|
[ -n "$PF0_MGMT" ] || PF0_MGMT=192.168.122.8
|
||||||
|
[ -n "$CLI_MGMT" ] || CLI_MGMT=192.168.122.230
|
||||||
|
[ -n "$SRV_MGMT" ] || SRV_MGMT=192.168.122.202
|
||||||
|
|
||||||
|
onvm() {
|
||||||
|
local ip=$1; shift
|
||||||
|
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o LogLevel=ERROR \
|
||||||
|
-o ProxyJump=root@192.168.1.242 -i "$KEY" root@"$ip" "$@" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# run on the firewall VM, showing the command as a prompt
|
||||||
|
p0() {
|
||||||
|
cyan "pf0# $*"
|
||||||
|
onvm "$PF0_MGMT" "$@"
|
||||||
|
sleep 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# run on the client VM
|
||||||
|
vmc() {
|
||||||
|
cyan "cli# $*"
|
||||||
|
onvm "$CLI_MGMT" "$@"
|
||||||
|
sleep 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# run on the server VM
|
||||||
|
vms() {
|
||||||
|
cyan "srv# $*"
|
||||||
|
onvm "$SRV_MGMT" "$@"
|
||||||
|
sleep 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
# upload a local file to the firewall VM
|
||||||
|
put() {
|
||||||
|
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$1" "root@$PF0_MGMT:/tmp/" 2>/dev/null
|
||||||
|
}
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Render every screencast phase into out/screencast/*.mp4 (silent, 1080p30).
|
||||||
|
#
|
||||||
|
# ./scripts/screencast/record-all.sh [phase ...]
|
||||||
|
#
|
||||||
|
# Each phase is a real session against the lab; the videos carry no audio.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
REPO=$(pwd)
|
||||||
|
OUT="$REPO/out/screencast"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
render() { # <file> <title> <subtitle> <phase-no> [panel]
|
||||||
|
local f=$1 title=$2 sub=$3 no=$4
|
||||||
|
local panel=${5:-"alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko"}
|
||||||
|
echo "=== gravando $f ==="
|
||||||
|
python3 "$REPO/scripts/screencast/render.py" \
|
||||||
|
--script "$REPO/scripts/screencast/phases/$f" \
|
||||||
|
--out "$OUT/${f%.sh}.mp4" \
|
||||||
|
--title "$title" --subtitle "$sub" \
|
||||||
|
--phase-no "$no" --phase-total 6 --panel "$panel" --min-hold 1.1 \
|
||||||
|
--footer "PF on Linux - OpenBSD PF 8.0 no kernel Linux - lab libvirt" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -A TITLES=(
|
||||||
|
[01-implementacao-kernel]="Fase 1 - Implementacao no kernel"
|
||||||
|
[02-ativacao]="Fase 2 - Ativacao"
|
||||||
|
[03-regras-basicas]="Fase 3 - Regras basicas"
|
||||||
|
[04-roteamento]="Fase 4 - Roteamento"
|
||||||
|
[05-balanceamento]="Fase 5 - Balanceamento"
|
||||||
|
[06-qos]="Fase 6 - QOS"
|
||||||
|
)
|
||||||
|
declare -A SUBS=(
|
||||||
|
[01-implementacao-kernel]="Das fontes OpenBSD 8.0 ao modulo pf.ko"
|
||||||
|
[02-ativacao]="insmod pf.ko / pfctl -e / pfctl -s info"
|
||||||
|
[03-regras-basicas]="tabelas, anchors, keep state, block drop"
|
||||||
|
[04-roteamento]="route-to e reply-to: o proximo salto decidido pelo pf"
|
||||||
|
[05-balanceamento]="pools round-robin, source-hash e dup-to"
|
||||||
|
[06-qos]="filas hfsc do OpenBSD 8.0 no caminho de dados"
|
||||||
|
)
|
||||||
|
|
||||||
|
PHASES=("$@")
|
||||||
|
[ ${#PHASES[@]} -eq 0 ] && PHASES=(01-implementacao-kernel 02-ativacao 03-regras-basicas \
|
||||||
|
04-roteamento 05-balanceamento 06-qos)
|
||||||
|
|
||||||
|
for p in "${PHASES[@]}"; do
|
||||||
|
f="$p.sh"
|
||||||
|
[ -f "$REPO/scripts/screencast/phases/$f" ] || { echo "fase inexistente: $f"; exit 1; }
|
||||||
|
# the phase number comes from the phase's own name, not from its position
|
||||||
|
# in the list, so a partial re-render keeps the right "FASE x/6" label
|
||||||
|
n=$((10#${p%%-*}))
|
||||||
|
case "$p" in
|
||||||
|
01-*) panel="host de build - repositorio pf_port" ;;
|
||||||
|
*) panel="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko" ;;
|
||||||
|
esac
|
||||||
|
render "$f" "${TITLES[$p]}" "${SUBS[$p]}" "$n" "$panel" || echo "FALHOU: $f"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== videos gerados ==="
|
||||||
|
for m in "$OUT"/*.mp4; do
|
||||||
|
[ -e "$m" ] || continue
|
||||||
|
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$m")
|
||||||
|
printf '%-46s %6.1fs %5.1f MB\n' "$(basename "$m")" "$d" "$(echo "scale=1; $(stat -c%s "$m")/1000000" | bc)"
|
||||||
|
done
|
||||||
Executable
+336
@@ -0,0 +1,336 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Render a shell session into a silent, YouTube-ready MP4 screencast.
|
||||||
|
|
||||||
|
Pipeline: pty -> raw bytes -> pyte (terminal emulation) -> PNG frames -> MP4
|
||||||
|
|
||||||
|
Every byte on screen is the real output of the command; only the pacing and
|
||||||
|
the framing are produced here. No audio track is written (the videos are
|
||||||
|
silent by design).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
./scripts/screencast/render.py --script scripts/screencast/phases/02-ativacao.sh \
|
||||||
|
--out out/screencast/02-ativacao.mp4 --title "Fase 2 - Ativacao" \
|
||||||
|
--subtitle "insmod pf.ko / pfctl -e / pfctl -s info"
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import errno
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
import pty
|
||||||
|
import select
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import termios
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pyte
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- palette ---
|
||||||
|
BG = (14, 17, 23)
|
||||||
|
PANEL = (20, 24, 31)
|
||||||
|
BORDER = (48, 54, 61)
|
||||||
|
FG = (201, 209, 217)
|
||||||
|
HEADER_BG = (24, 29, 38)
|
||||||
|
HEADER_FG = (240, 246, 252)
|
||||||
|
ACCENT = (88, 166, 255)
|
||||||
|
MUTED = (110, 118, 129)
|
||||||
|
TITLE_BG = (17, 21, 28)
|
||||||
|
|
||||||
|
# pyte colour names -> RGB (a calm, high-contrast terminal palette)
|
||||||
|
NAMED = {
|
||||||
|
"black": (48, 54, 61),
|
||||||
|
"red": (255, 123, 114),
|
||||||
|
"green": (63, 185, 80),
|
||||||
|
"yellow": (210, 153, 34),
|
||||||
|
"blue": (88, 166, 255),
|
||||||
|
"magenta": (188, 140, 255),
|
||||||
|
"cyan": (57, 197, 207),
|
||||||
|
"white": (201, 209, 217),
|
||||||
|
"brown": (168, 115, 66),
|
||||||
|
"default": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def color_of(spec, default):
|
||||||
|
if spec == "default":
|
||||||
|
return default
|
||||||
|
if spec in NAMED:
|
||||||
|
return NAMED[spec] or default
|
||||||
|
try:
|
||||||
|
if len(spec) == 6:
|
||||||
|
return tuple(int(spec[i:i + 2], 16) for i in (0, 2, 4))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
class Recorder:
|
||||||
|
"""Runs a command on a pty and keeps a timestamped snapshot per output event."""
|
||||||
|
|
||||||
|
def __init__(self, cols, rows, env=None):
|
||||||
|
self.cols, self.rows = cols, rows
|
||||||
|
self.screen = pyte.Screen(cols, rows)
|
||||||
|
self.screen.set_mode(pyte.modes.LNM)
|
||||||
|
self.stream = pyte.ByteStream(self.screen)
|
||||||
|
self.events = [] # (timestamp, rows_of_cells)
|
||||||
|
self.t0 = time.time()
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
rows = []
|
||||||
|
for y in range(self.rows):
|
||||||
|
line = self.screen.buffer[y]
|
||||||
|
cells = []
|
||||||
|
for x in range(self.cols):
|
||||||
|
ch = line[x]
|
||||||
|
fg = color_of(ch.fg, FG)
|
||||||
|
bold = ch.bold
|
||||||
|
cells.append((ch.data or " ", fg, bold))
|
||||||
|
rows.append(cells)
|
||||||
|
return rows, (self.screen.cursor.y, self.screen.cursor.x)
|
||||||
|
|
||||||
|
def run(self, argv, env=None):
|
||||||
|
master, slave = pty.openpty()
|
||||||
|
fcntl.ioctl(slave, termios.TIOCSWINSZ,
|
||||||
|
struct.pack("HHHH", self.rows, self.cols, 0, 0))
|
||||||
|
e = dict(os.environ)
|
||||||
|
e.update({"TERM": "xterm-256color", "COLUMNS": str(self.cols),
|
||||||
|
"LINES": str(self.rows), "LC_ALL": "C.UTF-8"})
|
||||||
|
if env:
|
||||||
|
e.update(env)
|
||||||
|
proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave,
|
||||||
|
env=e, start_new_session=True, close_fds=True)
|
||||||
|
os.close(slave)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
r, _, _ = select.select([master], [], [], 0.25)
|
||||||
|
except InterruptedError:
|
||||||
|
continue
|
||||||
|
if master in r:
|
||||||
|
try:
|
||||||
|
data = os.read(master, 65536)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno == errno.EIO:
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self.stream.feed(data)
|
||||||
|
rows, cur = self.snapshot()
|
||||||
|
self.events.append((time.time() - self.t0, rows, cur))
|
||||||
|
elif proc.poll() is not None:
|
||||||
|
# drain whatever is left
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = os.read(master, 65536)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
self.stream.feed(data)
|
||||||
|
rows, cur = self.snapshot()
|
||||||
|
self.events.append((time.time() - self.t0, rows, cur))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
proc.wait()
|
||||||
|
os.close(master)
|
||||||
|
if not self.events:
|
||||||
|
rows, cur = self.snapshot()
|
||||||
|
self.events.append((time.time() - self.t0, rows, cur))
|
||||||
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- rendering ---
|
||||||
|
def load_font(path, size):
|
||||||
|
return ImageFont.truetype(path, size)
|
||||||
|
|
||||||
|
|
||||||
|
def render_frame(rows, title, subtitle, phase_no, phase_total, cols, font, bold_font,
|
||||||
|
small_font, footer, cursor=None, panel="alpine (pf0)"):
|
||||||
|
W, H = 1920, 1080
|
||||||
|
img = Image.new("RGB", (W, H), BG)
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# header
|
||||||
|
d.rectangle([0, 0, W, 74], fill=HEADER_BG)
|
||||||
|
d.text((44, 26), "PF on Linux", font=bold_font, fill=HEADER_FG)
|
||||||
|
tw = d.textlength("PF on Linux", font=bold_font)
|
||||||
|
d.text((44 + tw + 26, 30), "-", font=font, fill=MUTED)
|
||||||
|
d.text((44 + tw + 46, 30), "OpenBSD PF 8.0 engine inside the Linux kernel",
|
||||||
|
font=font, fill=MUTED)
|
||||||
|
if phase_total:
|
||||||
|
label = f"FASE {phase_no}/{phase_total}"
|
||||||
|
lw = d.textlength(label, font=small_font)
|
||||||
|
d.text((W - 44 - lw, 32), label, font=small_font, fill=ACCENT)
|
||||||
|
|
||||||
|
# title block
|
||||||
|
d.text((44, 112), title, font=bold_font, fill=HEADER_FG)
|
||||||
|
if subtitle:
|
||||||
|
d.text((44, 162), subtitle, font=font, fill=MUTED)
|
||||||
|
|
||||||
|
# terminal panel, sized to the content
|
||||||
|
top = 212
|
||||||
|
left = 44
|
||||||
|
right = W - 44
|
||||||
|
lh = 30
|
||||||
|
used = 0
|
||||||
|
for i, line in enumerate(rows):
|
||||||
|
if any(c[0].strip() for c in line):
|
||||||
|
used = i + 1
|
||||||
|
bottom = min(H - 84, top + 44 + 28 + (max(used, 4) + 1) * lh)
|
||||||
|
d.rectangle([left, top, right, bottom], fill=PANEL, outline=BORDER, width=2)
|
||||||
|
d.rectangle([left, top, right, top + 44], fill=HEADER_BG)
|
||||||
|
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
|
||||||
|
d.ellipse([left + 24 + i * 26, top + 15, left + 38 + i * 26, top + 29], fill=c)
|
||||||
|
d.text((left + 110, top + 13), panel, font=small_font, fill=MUTED)
|
||||||
|
|
||||||
|
# text
|
||||||
|
x0 = left + 28
|
||||||
|
y0 = top + 64
|
||||||
|
cw = d.textlength("M", font=font)
|
||||||
|
for i, line in enumerate(rows):
|
||||||
|
y = y0 + i * lh
|
||||||
|
if y > bottom - 10:
|
||||||
|
break
|
||||||
|
for x, (ch, fg, bold) in enumerate(line):
|
||||||
|
if ch == " ":
|
||||||
|
continue
|
||||||
|
d.text((x0 + x * cw, y), ch, font=bold_font if bold else font, fill=fg)
|
||||||
|
|
||||||
|
# cursor block
|
||||||
|
if cursor:
|
||||||
|
cy, cx = cursor
|
||||||
|
if 0 <= cy < len(rows) and 0 <= cx < cols:
|
||||||
|
d.rectangle([x0 + cx * cw, y0 + cy * lh, x0 + (cx + 1) * cw - 1,
|
||||||
|
y0 + (cy + 1) * lh - 1], fill=ACCENT)
|
||||||
|
|
||||||
|
# footer
|
||||||
|
d.text((44, H - 58), footer, font=small_font, fill=MUTED)
|
||||||
|
url = "git.labexposed.com/lgcosta/pf-linux-port"
|
||||||
|
uw = d.textlength(url, font=small_font)
|
||||||
|
d.text((W - 44 - uw, H - 58), url, font=small_font, fill=MUTED)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def render_title(title, subtitle, phase_no, phase_total, bold_font, font, small_font):
|
||||||
|
W, H = 1920, 1080
|
||||||
|
img = Image.new("RGB", (W, H), TITLE_BG)
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
d.rectangle([0, 0, W, 8], fill=ACCENT)
|
||||||
|
d.text((120, 400), title, font=bold_font, fill=HEADER_FG)
|
||||||
|
if subtitle:
|
||||||
|
d.text((120, 470), subtitle, font=font, fill=MUTED)
|
||||||
|
if phase_total:
|
||||||
|
d.text((120, 560), f"FASE {phase_no} DE {phase_total}", font=small_font, fill=ACCENT)
|
||||||
|
d.text((120, 620), "PF on Linux - OpenBSD PF 8.0 no kernel Linux",
|
||||||
|
font=small_font, fill=MUTED)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--script", required=True)
|
||||||
|
ap.add_argument("--out", required=True)
|
||||||
|
ap.add_argument("--title", required=True)
|
||||||
|
ap.add_argument("--subtitle", default="")
|
||||||
|
ap.add_argument("--footer", default="")
|
||||||
|
ap.add_argument("--phase-no", type=int, default=0)
|
||||||
|
ap.add_argument("--phase-total", type=int, default=0)
|
||||||
|
ap.add_argument("--cols", type=int, default=120)
|
||||||
|
ap.add_argument("--rows", type=int, default=32)
|
||||||
|
ap.add_argument("--fps", type=int, default=30)
|
||||||
|
ap.add_argument("--max-hold", type=float, default=2.0)
|
||||||
|
ap.add_argument("--min-hold", type=float, default=0.25)
|
||||||
|
ap.add_argument("--title-card", type=float, default=3.0)
|
||||||
|
ap.add_argument("--keep-frames", action="store_true")
|
||||||
|
ap.add_argument("--panel", default="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
repo = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
mono = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf"
|
||||||
|
mono_b = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf"
|
||||||
|
font = load_font(mono, 21)
|
||||||
|
bold_font = load_font(mono_b, 21)
|
||||||
|
small_font = load_font(mono, 19)
|
||||||
|
|
||||||
|
rec = Recorder(args.cols, args.rows)
|
||||||
|
rc = rec.run(["bash", args.script])
|
||||||
|
sys.stderr.write(f"[screencast] script rc={rc}, events={len(rec.events)}\n")
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
|
||||||
|
frames_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), "_frames")
|
||||||
|
shutil.rmtree(frames_dir, ignore_errors=True)
|
||||||
|
os.makedirs(frames_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def dump(img, idx):
|
||||||
|
img.save(os.path.join(frames_dir, f"{idx:05d}.png"))
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
# title card
|
||||||
|
title_img = render_title(args.title, args.subtitle, args.phase_no,
|
||||||
|
args.phase_total, bold_font, font, small_font)
|
||||||
|
title_frames = int(args.title_card * args.fps)
|
||||||
|
for _ in range(title_frames):
|
||||||
|
dump(title_img, n)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
# session frames, paced by the real inter-event delays
|
||||||
|
prev_t = 0.0
|
||||||
|
for i, (ts, rows, cur) in enumerate(rec.events):
|
||||||
|
hold = ts - prev_t
|
||||||
|
prev_t = ts
|
||||||
|
if i + 1 < len(rec.events):
|
||||||
|
hold = rec.events[i + 1][0] - ts
|
||||||
|
hold = max(args.min_hold, min(args.max_hold, hold))
|
||||||
|
img = render_frame(rows, args.title, args.subtitle, args.phase_no,
|
||||||
|
args.phase_total, args.cols, font, bold_font,
|
||||||
|
small_font, args.footer, cursor=cur, panel=args.panel)
|
||||||
|
frames = max(1, int(round(hold * args.fps)))
|
||||||
|
for _ in range(frames):
|
||||||
|
dump(img, n)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
# closing card
|
||||||
|
end_img = render_frame(rec.events[-1][1], args.title, args.subtitle,
|
||||||
|
args.phase_no, args.phase_total, args.cols, font,
|
||||||
|
bold_font, small_font,
|
||||||
|
args.footer or "fim da fase",
|
||||||
|
cursor=rec.events[-1][2], panel=args.panel)
|
||||||
|
for _ in range(int(1.5 * args.fps)):
|
||||||
|
dump(end_img, n)
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
# encoder: prefer x264, fall back to openh264 (both H.264 in MP4, which is
|
||||||
|
# what YouTube wants), then VP9/WebM.
|
||||||
|
have = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
|
||||||
|
capture_output=True, text=True).stdout
|
||||||
|
base = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(args.fps),
|
||||||
|
"-i", os.path.join(frames_dir, "%05d.png"),
|
||||||
|
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", str(args.fps)]
|
||||||
|
if "libx264" in have:
|
||||||
|
cmd = base + ["-c:v", "libx264", "-preset", "medium", "-crf", "20",
|
||||||
|
"-profile:v", "high", "-level", "4.0", args.out]
|
||||||
|
elif "libopenh264" in have:
|
||||||
|
cmd = base + ["-c:v", "libopenh264", "-b:v", "6M",
|
||||||
|
"-profile:v", "high", args.out]
|
||||||
|
elif "libvpx-vp9" in have:
|
||||||
|
cmd = base + ["-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "30",
|
||||||
|
"-row-mt", "1", "-cpu-used", "4", args.out]
|
||||||
|
else:
|
||||||
|
raise SystemExit("[screencast] no usable H.264/VP9 encoder in ffmpeg")
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
if not args.keep_frames:
|
||||||
|
shutil.rmtree(frames_dir, ignore_errors=True)
|
||||||
|
dur = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
|
||||||
|
"format=duration", "-of", "csv=p=0", args.out],
|
||||||
|
capture_output=True, text=True).stdout.strip()
|
||||||
|
size = os.path.getsize(args.out)
|
||||||
|
sys.stderr.write(f"[screencast] wrote {args.out} frames={n} "
|
||||||
|
f"duration={dur}s size={size/1e6:.1f}MB\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-23
@@ -1,30 +1,50 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# M2 gate 4: anchors + quick precedence
|
# M2 gate 4: anchors + evaluation order (OpenBSD semantics)
|
||||||
|
#
|
||||||
|
# OpenBSD evaluates the IN ruleset in ip_input() for every arriving packet and
|
||||||
|
# the OUT ruleset in ip_forward() for forwarded packets. A `block in ... icmp`
|
||||||
|
# rule therefore also sees the *reply* of a transit flow - a state created by
|
||||||
|
# an anchor rule does NOT rescue the reply at input time. This gate keeps the
|
||||||
|
# ruleset OpenBSD-correct and isolates the anchor semantics:
|
||||||
|
#
|
||||||
|
# anchor "restricted" -> child: pass in quick ... from <client> keep state
|
||||||
|
# block drop in quick proto icmp from any to <server> (would drop the request)
|
||||||
|
# pass in quick / pass out quick
|
||||||
|
#
|
||||||
|
# The request must be passed by the ANCHOR child rule (the later block rule
|
||||||
|
# would drop it), and a state must exist. After flushing the anchor the very
|
||||||
|
# same request must be dropped - proving the anchor is what allowed it.
|
||||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||||
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'; pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
|
||||||
cat > /tmp/g4main.conf <<'C'
|
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
|
||||||
|
|
||||||
|
cat > /tmp/g4main.conf <<C
|
||||||
anchor "restricted"
|
anchor "restricted"
|
||||||
block drop in quick proto icmp from any to any
|
block drop in quick proto icmp from any to $SRV_IP
|
||||||
pass in quick
|
pass in quick
|
||||||
pass out quick
|
pass out quick
|
||||||
C
|
C
|
||||||
pfload /tmp/g4main.conf || { bad mainload; finish; }
|
cat > /tmp/g4anc.conf <<C
|
||||||
cat > /tmp/g4anc.conf <<'C'
|
pass in quick proto icmp from $CLI_IP to any keep state
|
||||||
pass in quick proto icmp from 10.0.1.10 to any keep state
|
|
||||||
C
|
C
|
||||||
r=$(pfput /tmp/g4anc.conf) || { bad anchorload; finish; }
|
|
||||||
pf0 "/opt/pf/pfctl -a restricted -f $r" || { bad anchorload; finish; }
|
pfload /tmp/g4main.conf || { bad "main ruleset load"; finish; }
|
||||||
# The ICMP echo request is passed by the anchor's rule (evaluated before the
|
ANC=$(pfput /tmp/g4anc.conf) || { bad "anchor conf upload"; finish; }
|
||||||
# main ruleset's block line); the echo reply is carried by the state that the
|
pf0 "/opt/pf/pfctl -a restricted -f $ANC" || { bad "anchor ruleset load"; finish; }
|
||||||
# request created. The first reply after the ruleset load races with that
|
|
||||||
# state on the freshly booted pf0, so retry a few times: if the anchor's rule
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'anchor "restricted"' && ok "anchor rule in main ruleset" || bad "anchor rule missing"
|
||||||
# is not honoured the *request* is dropped and no retry can succeed.
|
pf0 '/opt/pf/pfctl -a restricted -sr' | grep -q 'proto icmp from' && ok "anchor rules listed" || bad "anchor rules missing"
|
||||||
pass_ok=0
|
|
||||||
for _ in 1 2 3 4 5; do
|
# the request must pass through the anchor (the block rule would drop it)
|
||||||
if cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1"; then pass_ok=1; break; fi
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "anchor pass honoured (request allowed)" \
|
||||||
sleep 1
|
|| bad "anchor pass not honored"
|
||||||
done
|
pf0 '/opt/pf/pfctl -ss' | grep -q "$SRV_IP" && ok "state created by anchor rule" || bad "no state from anchor"
|
||||||
[ "$pass_ok" = 1 ] && ok "anchor pass evaluated before block" || bad "anchor pass not honored"
|
|
||||||
# anchor rules are evaluated BEFORE the main-ruleset block line (top placement)
|
# without the anchor rule the request hits the block rule -> dropped
|
||||||
pf0 '/opt/pf/pfctl -s Anchors' | grep -q restricted && ok "anchors listed" || bad "anchor list"
|
pf0 "/opt/pf/pfctl -a restricted -F all" >/dev/null 2>&1
|
||||||
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null; finish
|
cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1" && bad "request passed without anchor rule" \
|
||||||
|
|| ok "request dropped without anchor rule (block applies)"
|
||||||
|
|
||||||
|
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||||
|
finish
|
||||||
+28
-17
@@ -1,35 +1,46 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# M3 gate: nat-to — source NAT of transit traffic to the egress address.
|
# M3 gate: nat-to — source NAT of transit traffic to the egress address.
|
||||||
#
|
#
|
||||||
# Scenario: cli (10.0.1.10) -> srv (10.0.2.20) through pf0, rule:
|
# OpenBSD 8.0 unified the old "nat on ..." rule into the rule syntax:
|
||||||
# nat on egress proto icmp from 10.0.1.0/24 to any -> (egress)
|
# pass out on egress from <net> to any nat-to (egress)
|
||||||
# Expected: srv sees src 192.168.122.8 (pf0 egress IP) instead of 10.0.1.10;
|
#
|
||||||
# the reply is de-NATed and delivered back to cli; `pfctl -s nat`,
|
# Expected: the reply path is unaffected, srv sees the packet with
|
||||||
# `pfctl -s states` show the translation.
|
# src = pf0's egress address (not the client's), and pfctl shows the rule.
|
||||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||||
|
|
||||||
FAILED=0
|
FAILED=0
|
||||||
pf0 "insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true"
|
# pf calls the interface "egress"; the OS names it ethX - resolve both
|
||||||
|
EGRESS_DEV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
EGRESS_IP=$(pf0 "ip -4 addr show $EGRESS_DEV | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2")
|
||||||
|
[ -n "$EGRESS_IP" ] || EGRESS_IP=192.168.122.8
|
||||||
|
|
||||||
|
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 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||||
|
|
||||||
|
# OpenBSD expands "(egress)" (an interface *group*) into the addresses of all
|
||||||
|
# its members - on a multi-NIC host that is ambiguous, so the gate uses the
|
||||||
|
# concrete transit interface (the OpenBSD-recommended form for NAT pools).
|
||||||
cat > /tmp/g05.conf <<C
|
cat > /tmp/g05.conf <<C
|
||||||
nat on egress proto icmp from 10.0.1.0/24 to any -> (egress)
|
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_DEV)
|
||||||
pass in quick on ingress proto icmp keep state
|
pass in quick on ingress proto icmp keep state
|
||||||
pass out quick proto icmp keep state
|
pass out quick on egress proto icmp keep state
|
||||||
C
|
C
|
||||||
pfload /tmp/g05.conf || { bad "nat rule load"; finish; }
|
pfload /tmp/g05.conf || { bad "nat rule load"; finish; }
|
||||||
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'nat-to' && ok "nat-to rule listed" || bad "nat-to not listed"
|
||||||
|
|
||||||
pf0 '/opt/pf/pfctl -sn' | grep -q 'nat on egress' && ok "nat rule listed" || bad "nat not listed"
|
pf0 "tcpdump -ni $EGRESS_DEV -c 4 -e icmp > /tmp/nat.td 2>&1 &"
|
||||||
|
|
||||||
# capture on pf0's egress while cli pings srv
|
|
||||||
pf0 'tcpdump -ni egress -c 4 -e icmp > /tmp/nat.td 2>&1 &'
|
|
||||||
sleep 1
|
sleep 1
|
||||||
cli "ping -c 2 -W 2 $SRV_IP >/dev/null 2>&1" && ok "transit ping through NAT" || bad "transit ping"
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "transit ping through NAT" || bad "transit ping"
|
||||||
sleep 2
|
sleep 2
|
||||||
TD=$(pf0 'cat /tmp/nat.td')
|
|
||||||
echo "$TD" | grep -q "$CLI_IP" && bad "cli source address leaked to egress" || ok "source rewritten (no $CLI_IP on egress)"
|
|
||||||
echo "$TD" | grep -q '192.168.122.8' && ok "nat applied to $(pf0 'ip -4 addr show egress | grep -o "192[0-9.]*"' | head -1)" || bad "egress src ip not seen"
|
|
||||||
|
|
||||||
pf0 '/opt/pf/pfctl -s states' | grep -q "$SRV_IP" && ok "state present" || bad "no state"
|
TD=$(pf0 'cat /tmp/nat.td')
|
||||||
|
if echo "$TD" | grep -q "$CLI_IP"; then
|
||||||
|
bad "client address $CLI_IP still visible on egress"
|
||||||
|
else
|
||||||
|
ok "source rewritten on egress (no $CLI_IP)"
|
||||||
|
fi
|
||||||
|
echo "$TD" | grep -q "$EGRESS_IP" && ok "egress source is $EGRESS_IP" || bad "egress source not seen"
|
||||||
|
|
||||||
|
pf0 '/opt/pf/pfctl -ss' | grep -q "$SRV_IP" && ok "state present" || bad "no state"
|
||||||
pf0 '/opt/pf/pfctl -d' >/dev/null
|
pf0 '/opt/pf/pfctl -d' >/dev/null
|
||||||
finish
|
finish
|
||||||
Executable
+69
@@ -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
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# M3 gate: binat-to — bidirectional (static 1:1) NAT.
|
||||||
|
#
|
||||||
|
# Lab: the server (10.0.2.20) pings the client (10.0.1.10) through pf0. The
|
||||||
|
# binat rule rewrites the source to 10.0.2.50; the client's reply goes back to
|
||||||
|
# 10.0.2.50, which pf0 owns in the mapping, and is translated to the server.
|
||||||
|
#
|
||||||
|
# Proves: outbound source rewrite + inbound destination rewrite with one rule,
|
||||||
|
# and that the reply survives the state/translation path.
|
||||||
|
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||||
|
|
||||||
|
BINAT=10.0.2.50
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
cat > /tmp/g07.conf <<C
|
||||||
|
pass quick proto icmp from $SRV_IP to $CLI_IP binat-to $BINAT keep state
|
||||||
|
pass in quick proto icmp keep state
|
||||||
|
pass out quick proto icmp keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g07.conf || { bad "binat rule load"; finish; }
|
||||||
|
# pfctl expands one binat-to rule into the documented pair:
|
||||||
|
# pass out ... nat-to <mapped> static-port (outbound half)
|
||||||
|
# pass in ... rdr-to <real> (inbound half)
|
||||||
|
RULES=$(pf0 '/opt/pf/pfctl -sr')
|
||||||
|
echo "$RULES" | grep -q "nat-to $BINAT static-port" && ok "binat outbound half installed" || bad "outbound half missing"
|
||||||
|
echo "$RULES" | grep -q "rdr-to $SRV_IP" && ok "binat inbound half installed" || bad "inbound half missing"
|
||||||
|
|
||||||
|
# capture on the client side (resolve its transit interface, like pf0's)
|
||||||
|
CLI_DEV=$(cli "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
[ -n "$CLI_DEV" ] || CLI_DEV=eth1
|
||||||
|
cli "(tcpdump -ni $CLI_DEV -c 4 -e icmp > /tmp/bi.td 2>&1 &)" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
srv "ping -c2 -W2 $CLI_IP >/dev/null 2>&1" && ok "ping srv->cli through binat" || bad "binat round trip"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
TD=$(cli 'cat /tmp/bi.td' 2>/dev/null)
|
||||||
|
if echo "$TD" | grep -q "$BINAT"; then
|
||||||
|
ok "request arrived from the mapped address $BINAT"
|
||||||
|
else
|
||||||
|
bad "mapped address not seen on the client: $(echo "$TD" | head -2)"
|
||||||
|
fi
|
||||||
|
echo "$TD" | grep -q "$SRV_IP" && bad "original address $SRV_IP leaked" || ok "original source not leaked"
|
||||||
|
|
||||||
|
pf0 '/opt/pf/pfctl -ss' | grep -q "$BINAT" && ok "state shows the mapping" || bad "no binat state"
|
||||||
|
pf0 '/opt/pf/pfctl -d' >/dev/null
|
||||||
|
finish
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# M4 gate: route-to / dup-to — forced forwarding path (pf_route).
|
||||||
|
#
|
||||||
|
# The lab's host bridge filters *forwarded* frames that cross two libvirt
|
||||||
|
# networks with a foreign source address, so the proof is taken on the
|
||||||
|
# firewall itself: which of pf0's egress interfaces the packet leaves from.
|
||||||
|
#
|
||||||
|
# route-to { gw2 } -> packet leaves via eth3 (toward gw2), NOT via eth2
|
||||||
|
# dup-to { gw2 } -> packet leaves via eth2 (normal path) AND a copy on eth3
|
||||||
|
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
|
||||||
|
|
||||||
|
# resolve pf0's interfaces by address (names are stable, but be safe)
|
||||||
|
IF_SRV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
IF_GW=$(pf0 "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
[ -n "$IF_SRV" ] && [ -n "$IF_GW" ] || { bad "interface resolution"; finish; }
|
||||||
|
[ "$IF_SRV" != "$IF_GW" ] || { bad "transit and gateway share an interface"; finish; }
|
||||||
|
ok "normal egress=$IF_SRV forced egress=$IF_GW"
|
||||||
|
|
||||||
|
|
||||||
|
# count matching lines on pf0, robust against a missing/empty capture file
|
||||||
|
# (grep exits 1 on zero matches, and the ssh wrapper hides stderr)
|
||||||
|
cnt() {
|
||||||
|
local f=$1 pat=$2 n
|
||||||
|
n=$(pf0 "grep -c -- '$pat' $f 2>/dev/null; true" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || n=0
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# capture both egresses at once
|
||||||
|
start_captures() {
|
||||||
|
pf0 "pkill tcpdump; (tcpdump -ni $IF_SRV -c 6 -e icmp > /tmp/srv.td 2>&1 &); (tcpdump -ni $IF_GW -c 6 -e icmp > /tmp/gw.td 2>&1 &); sleep 1" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
cat > /tmp/g08a.conf <<C
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { $GW2_IP } keep state
|
||||||
|
pass out quick proto icmp keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g08a.conf || { bad "route-to rule load"; finish; }
|
||||||
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'route-to' && ok "route-to rule listed" || bad "route-to not listed"
|
||||||
|
start_captures
|
||||||
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1"
|
||||||
|
sleep 2
|
||||||
|
# count only echo *requests* from the client (a reply also contains its address)
|
||||||
|
N=$(cnt /tmp/gw.td "$CLI_IP > $SRV_IP"); [ "$N" -gt 0 ] && ok "route-to: packet left via $IF_GW ($N)" \
|
||||||
|
|| bad "route-to: nothing on $IF_GW"
|
||||||
|
N=$(cnt /tmp/srv.td "$CLI_IP > $SRV_IP"); [ "$N" -eq 0 ] && ok "route-to: normal egress $IF_SRV not used" \
|
||||||
|
|| bad "route-to: $N requests also left via $IF_SRV"
|
||||||
|
|
||||||
|
cat > /tmp/g08b.conf <<C
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP dup-to { $GW2_IP } keep state
|
||||||
|
pass out quick proto icmp keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g08b.conf || { bad "dup-to rule load"; finish; }
|
||||||
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'dup-to' && ok "dup-to rule listed" || bad "dup-to not listed"
|
||||||
|
start_captures
|
||||||
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1"
|
||||||
|
sleep 2
|
||||||
|
N=$(cnt /tmp/srv.td "$CLI_IP > $SRV_IP"); [ "$N" -gt 0 ] && ok "dup-to: original path kept ($N on $IF_SRV)" \
|
||||||
|
|| bad "dup-to: original path lost"
|
||||||
|
N=$(cnt /tmp/gw.td "$CLI_IP > $SRV_IP"); [ "$N" -gt 0 ] && ok "dup-to: duplicate sent to $IF_GW ($N)" \
|
||||||
|
|| bad "dup-to: no duplicate"
|
||||||
|
|
||||||
|
pf0 'pkill tcpdump; /opt/pf/pfctl -d' >/dev/null
|
||||||
|
finish
|
||||||
Executable
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# M3 gate: scrub - normalization actually rewrites packets.
|
||||||
|
#
|
||||||
|
# 8.0 syntax: scrub is a rule option ("match in all scrub (...)"), not a
|
||||||
|
# standalone rule. Proof is taken on pf0's own egress (the lab's host bridge
|
||||||
|
# filters forwarded frames between networks, so peer-side captures are not
|
||||||
|
# reliable):
|
||||||
|
# min-ttl 100 -> a transit packet with ttl 64 leaves with ttl 100
|
||||||
|
# max-mss 500 -> a TCP SYN with MSS 1460 leaves advertising MSS 500
|
||||||
|
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
|
||||||
|
|
||||||
|
IF_SRV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
[ -n "$IF_SRV" ] || IF_SRV=eth2
|
||||||
|
|
||||||
|
cat > /tmp/g09.conf <<C
|
||||||
|
set reassemble yes
|
||||||
|
match in all scrub (min-ttl 100 max-mss 500)
|
||||||
|
pass in quick proto icmp from $CLI_IP keep state
|
||||||
|
pass in quick proto tcp from $CLI_IP to $SRV_IP port 9092 keep state
|
||||||
|
pass out quick keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g09.conf || { bad "scrub rules load"; finish; }
|
||||||
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'scrub' && ok "scrub rule listed" || bad "scrub not listed"
|
||||||
|
|
||||||
|
# --- min-ttl on a transit ICMP packet ---
|
||||||
|
# OpenBSD raises the TTL to min-ttl in pf, then ip_forward() decrements it for
|
||||||
|
# the hop, so the egress packet shows min-ttl-1 (100 -> 99).
|
||||||
|
pf0 "pkill tcpdump; (tcpdump -ni $IF_SRV -c 3 -vv icmp > /tmp/scr1.td 2>&1 &); sleep 1"
|
||||||
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1"
|
||||||
|
sleep 2
|
||||||
|
N=$(pf0 "grep -c 'ttl 99' /tmp/scr1.td 2>/dev/null; true" | tr -dc '0-9')
|
||||||
|
if [ "${N:-0}" -gt 0 ]; then
|
||||||
|
ok "min-ttl applied (ttl raised to 100, forwarded as 99)"
|
||||||
|
else
|
||||||
|
DIAG=$(pf0 "grep -o 'ttl [0-9]*' /tmp/scr1.td 2>/dev/null | head -2" | tr '\n' ' ')
|
||||||
|
bad "min-ttl not applied: $DIAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- max-mss on a TCP SYN ---
|
||||||
|
pf0 "(tcpdump -ni $IF_SRV -c 3 -vv tcp port 9092 > /tmp/scr2.td 2>&1 &); sleep 1"
|
||||||
|
cli "nc -w1 -z $SRV_IP 9092 >/dev/null 2>&1"
|
||||||
|
sleep 2
|
||||||
|
if pf0 "grep -q 'mss 500' /tmp/scr2.td" 2>/dev/null; then
|
||||||
|
ok "max-mss clamped to 500"
|
||||||
|
else
|
||||||
|
DIAG=$(pf0 "grep -o 'mss [0-9]*' /tmp/scr2.td 2>/dev/null | head -2" | tr '\n' ' ')
|
||||||
|
bad "mss not clamped: $DIAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pf0 'pkill tcpdump; /opt/pf/pfctl -d' >/dev/null
|
||||||
|
finish
|
||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# M4 gate: route-to pools - round-robin vs source-hash (pf_lb).
|
||||||
|
#
|
||||||
|
# Two gateways live on two different firewall links:
|
||||||
|
# 10.0.3.2 via $IF_GW (gw2's 10.0.3 side)
|
||||||
|
# 10.0.2.2 via $IF_SRV (gw2's 10.0.2 side)
|
||||||
|
# Each ping is a separate flow (own state), so the pool choice is made per
|
||||||
|
# flow: round-robin must spread the flows over both links, source-hash must
|
||||||
|
# keep them on one (same source address -> same bucket).
|
||||||
|
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||||
|
|
||||||
|
GW1=$GW2_IP # 10.0.3.2
|
||||||
|
GW2=10.0.2.2
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
IF_GW=$(pf0 "ip route get $GW1 | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
IF_SRV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
[ -n "$IF_GW" ] && [ -n "$IF_SRV" ] || { bad "interface resolution"; finish; }
|
||||||
|
[ "$IF_GW" != "$IF_SRV" ] || { bad "the two gateways share a link"; finish; }
|
||||||
|
ok "pool links: $IF_GW ($GW1) and $IF_SRV ($GW2)"
|
||||||
|
|
||||||
|
count_on() { # count echo requests captured on an interface
|
||||||
|
local n
|
||||||
|
n=$(pf0 "grep -c '$CLI_IP > $SRV_IP' /tmp/pool_$1.td 2>/dev/null; true" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || n=0
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_flows() { # $1 = ruleset file, four separate ICMP flows
|
||||||
|
pf0 "pkill tcpdump; (tcpdump -ni $IF_GW -c 8 -e icmp > /tmp/pool_gw.td 2>&1 &); (tcpdump -ni $IF_SRV -c 8 -e icmp > /tmp/pool_srv.td 2>&1 &); sleep 1" >/dev/null
|
||||||
|
cli "for i in 1 2 3 4; do ping -c1 -W2 $SRV_IP >/dev/null 2>&1; done"
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- round-robin: the flows must spread over both links ---
|
||||||
|
cat > /tmp/g10a.conf <<C
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { $GW1, $GW2 } round-robin keep state
|
||||||
|
pass out quick keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g10a.conf || { bad "rr rule load"; finish; }
|
||||||
|
run_flows
|
||||||
|
A=$(count_on gw); B=$(count_on srv)
|
||||||
|
# Captures are lossy under busybox tcpdump, so the assertions are about
|
||||||
|
# *which links are used*, not how many packets arrived.
|
||||||
|
if [ "$A" -gt 0 ] && [ "$B" -gt 0 ]; then
|
||||||
|
ok "round-robin used both links ($A on $IF_GW, $B on $IF_SRV)"
|
||||||
|
else
|
||||||
|
bad "round-robin did not spread ($A on $IF_GW, $B on $IF_SRV)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- source-hash: the same source sticks to one gateway ---
|
||||||
|
cat > /tmp/g10b.conf <<C
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { $GW1, $GW2 } source-hash keep state
|
||||||
|
pass out quick keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g10b.conf || { bad "source-hash rule load"; finish; }
|
||||||
|
run_flows
|
||||||
|
A=$(count_on gw); B=$(count_on srv)
|
||||||
|
if [ "$((A + B))" -ge 1 ] && { [ "$A" -eq 0 ] || [ "$B" -eq 0 ]; }; then
|
||||||
|
ok "source-hash pinned the flows to one link ($A on $IF_GW, $B on $IF_SRV)"
|
||||||
|
else
|
||||||
|
bad "source-hash spread the flows ($A on $IF_GW, $B on $IF_SRV)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
pf0 'pkill tcpdump; /opt/pf/pfctl -d' >/dev/null
|
||||||
|
finish
|
||||||
Executable
+48
@@ -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
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# M4 gate: reply-to - force the next hop of the *reply* direction.
|
||||||
|
#
|
||||||
|
# reply-to is the asymmetric-routing tool: a flow whose request arrives on one
|
||||||
|
# link can have its answer pushed out through a different gateway. Proof is
|
||||||
|
# taken on pf0's own interfaces (the lab's host bridge filters forwarded frames
|
||||||
|
# that cross networks, so the peer side is not observable):
|
||||||
|
#
|
||||||
|
# request cli -> srv leaves via $IF_SRV (normal path, untouched)
|
||||||
|
# reply srv -> cli leaves via $IF_GW (forced by reply-to), not $IF_CLI
|
||||||
|
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
|
||||||
|
|
||||||
|
IF_SRV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
IF_GW=$(pf0 "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
IF_CLI=$(pf0 "ip route get $CLI_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||||
|
[ -n "$IF_SRV" ] && [ -n "$IF_GW" ] && [ -n "$IF_CLI" ] || { bad "interface resolution"; finish; }
|
||||||
|
[ "$IF_GW" != "$IF_CLI" ] || { bad "gateway and client share a link"; finish; }
|
||||||
|
ok "reply would normally leave via $IF_CLI, forced via $IF_GW"
|
||||||
|
|
||||||
|
cat > /tmp/g13.conf <<C
|
||||||
|
pass in quick proto icmp from $CLI_IP to $SRV_IP reply-to { $GW2_IP } keep state
|
||||||
|
pass out quick keep state
|
||||||
|
C
|
||||||
|
pfload /tmp/g13.conf || { bad "reply-to rule load"; finish; }
|
||||||
|
pf0 '/opt/pf/pfctl -sr' | grep -q 'reply-to' && ok "reply-to rule listed" || bad "reply-to not listed"
|
||||||
|
|
||||||
|
pf0 "pkill tcpdump; (tcpdump -ni $IF_GW -c 8 -e icmp > /tmp/rt_gw.td 2>&1 &); (tcpdump -ni $IF_CLI -c 8 -e icmp > /tmp/rt_cli.td 2>&1 &); (tcpdump -ni $IF_SRV -c 8 -e icmp > /tmp/rt_srv.td 2>&1 &); sleep 1" >/dev/null
|
||||||
|
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
cnt() {
|
||||||
|
local n
|
||||||
|
n=$(pf0 "grep -c -- '$2' /tmp/rt_$1.td 2>/dev/null; true" | tr -dc '0-9')
|
||||||
|
[ -n "$n" ] || n=0
|
||||||
|
printf '%s' "$n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# the request path must be unchanged
|
||||||
|
N=$(cnt srv "$CLI_IP > $SRV_IP"); [ "$N" -gt 0 ] && ok "request left via $IF_SRV ($N)" \
|
||||||
|
|| bad "request did not leave via $IF_SRV"
|
||||||
|
|
||||||
|
# the reply must be forced to the gateway link
|
||||||
|
N=$(cnt gw "$SRV_IP > $CLI_IP"); [ "$N" -gt 0 ] && ok "reply forced onto $IF_GW ($N)" \
|
||||||
|
|| bad "reply not seen on $IF_GW"
|
||||||
|
N=$(cnt cli "$SRV_IP > $CLI_IP"); [ "$N" -eq 0 ] && ok "reply did not use $IF_CLI" \
|
||||||
|
|| bad "reply still left via $IF_CLI ($N)"
|
||||||
|
|
||||||
|
pf0 'pkill tcpdump; /opt/pf/pfctl -d' >/dev/null
|
||||||
|
finish
|
||||||
+24
-24
@@ -1,51 +1,51 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# M6 gate: soak + unload/leak discipline.
|
# M6 gate: soak + unload/leak discipline.
|
||||||
#
|
#
|
||||||
# 1) load pf, ruleset with states/tables/anchors, drive mixed traffic ~20s
|
# 1) load pf with a stateful ruleset, drive mixed traffic for ~20s
|
||||||
# 2) assert no BUG/WARNING/Oops in dmesg
|
# 2) assert no BUG/WARNING/Oops in dmesg
|
||||||
# 3) disable, rmmod; assert module unloads cleanly (no refcount hang)
|
# 3) disable + rmmod: the module must unload cleanly (no refcount leak)
|
||||||
# 4) insmod/rmmod cycle x2; assert slab objects for pf caches return to the
|
# 4) insmod/rmmod cycles: slab objects for the pf caches must return to the
|
||||||
# pre-load baseline (no leak), and that a reload works (timer/notifier
|
# pre-load baseline (no leak) and a reload must work
|
||||||
# re-registration is idempotent)
|
|
||||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||||
|
|
||||||
FAILED=0
|
HELPER=$(pfput ./soak-helper.sh) || { bad "helper upload"; finish; }
|
||||||
slab_snapshot() {
|
pf0 "chmod +x $HELPER" >/dev/null
|
||||||
pf0 'grep -E "^(pf|pfk)[^ ]* " /proc/slabinfo | awk "{print \$1, \$3}"' \
|
|
||||||
| sort
|
|
||||||
}
|
|
||||||
|
|
||||||
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'
|
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 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||||
BEFORE=$(slab_snapshot)
|
BEFORE=$(pf0 "$HELPER slab")
|
||||||
|
|
||||||
cat > /tmp/g14.conf <<C
|
cat > /tmp/g14.conf <<C
|
||||||
table <peers> { $CLI_IP }
|
table <peers> { $CLI_IP }
|
||||||
anchor "s"
|
|
||||||
pass in quick on ingress proto icmp from <peers> keep state
|
pass in quick on ingress proto icmp from <peers> keep state
|
||||||
pass out quick proto icmp keep state
|
pass out quick on egress proto icmp keep state
|
||||||
pass out quick proto tcp from any to any port 22 keep state
|
pass out quick proto tcp from any to any port 22 keep state
|
||||||
C
|
C
|
||||||
pfload /tmp/g14.conf || { bad soak ruleset; finish; }
|
pfload /tmp/g14.conf || { bad "soak ruleset"; finish; }
|
||||||
|
|
||||||
# mixed traffic: ICMP + TCP handshakes, 20s
|
# mixed traffic: ICMP + TCP handshakes for ~20s
|
||||||
cli "ping -q -c 30 -i 0.2 $SRV_IP >/dev/null 2>&1 &" 2>/dev/null
|
cli "ping -q -c 40 -i 0.5 $SRV_IP >/dev/null 2>&1" 2>/dev/null
|
||||||
cli "for i in 1 2 3 4 5; do nc -w1 -z $SRV_IP 22; done" 2>/dev/null
|
cli "for i in 1 2 3 4 5; do nc -w1 -z $SRV_IP 22; done" 2>/dev/null
|
||||||
sleep 22
|
|
||||||
ST=$(pf0 '/opt/pf/pfctl -s states | wc -l')
|
ST=$(pf0 "$HELPER states")
|
||||||
[ "${ST:-0}" -ge 1 ] && ok "states under load ($ST)" || bad "no states under load"
|
[ "${ST:-0}" -ge 1 ] && ok "states under load ($ST)" || bad "no states under load"
|
||||||
|
|
||||||
DMESG=$(pf0 'dmesg | grep -cE "BUG:|WARNING:|Oops|general protection"')
|
ANOM=$(pf0 "$HELPER dmesg")
|
||||||
[ "${DMESG:-1}" = 0 ] && ok "no kernel BUG/WARN/Oops" || bad "dmesg anomalies ($DMESG)"
|
if [ -z "$ANOM" ]; then
|
||||||
|
ok "no kernel BUG/WARN/Oops"
|
||||||
|
else
|
||||||
|
bad "dmesg anomalies:"
|
||||||
|
printf '%s\n' "$ANOM" | sed 's/^/ /'
|
||||||
|
fi
|
||||||
|
|
||||||
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
|
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||||
pf0 'rmmod pf && echo UNLOAD_OK' | grep -q UNLOAD_OK && ok "clean unload (refcounts released)" || bad "rmmod failed"
|
pf0 'rmmod pf && echo UNLOAD_OK' | grep -q UNLOAD_OK && ok "clean unload (refcounts released)" || bad "rmmod failed"
|
||||||
sleep 1
|
sleep 1
|
||||||
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true; (/opt/pf/pfctl -s info >/dev/null && echo RELOAD_OK)' | grep -q RELOAD_OK && ok "reload after unload" || bad "reload failed"
|
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true; (/opt/pf/pfctl -s info >/dev/null && echo RELOAD_OK)' \
|
||||||
|
| grep -q RELOAD_OK && ok "reload after unload" || bad "reload failed"
|
||||||
pf0 'rmmod pf && echo UNLOAD_OK2' | grep -q UNLOAD_OK2 && ok "second unload" || bad "second rmmod"
|
pf0 'rmmod pf && echo UNLOAD_OK2' | grep -q UNLOAD_OK2 && ok "second unload" || bad "second rmmod"
|
||||||
sleep 1
|
sleep 1
|
||||||
AFTER=$(slab_snapshot)
|
AFTER=$(pf0 "$HELPER slab")
|
||||||
# compare per-cache object counts if they exist on both sides
|
|
||||||
LEAK=$(join <(echo "$BEFORE") <(echo "$AFTER") 2>/dev/null | awk '$3-$2 > 0 {print $1, $3-$2}')
|
LEAK=$(join <(echo "$BEFORE") <(echo "$AFTER") 2>/dev/null | awk '$3-$2 > 0 {print $1, $3-$2}')
|
||||||
if [ -n "$LEAK" ]; then bad "slab growth after cycles: $LEAK"; else ok "no slab growth after unload cycles"; fi
|
if [ -n "$LEAK" ]; then bad "slab growth after cycles: $LEAK"; else ok "no slab growth after unload cycles"; fi
|
||||||
finish
|
finish
|
||||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user