12 Commits
Author SHA1 Message Date
lgcosta 90c51f43fc M5: queue gate 15 green (hfsc in the data path, pfctl -s queue works); vendor OpenBSD net/hfsc.h + net/fq_codel.h so kernel and pfctl share the ABI; pflog payload anchored; splassert no longer BUGs 2026-11-12 03:10:46 -03:00
lgcosta 4bade69cf9 docs: queue status honest - attach path reached, purge lock assert documented as the blocker (caveats #9) 2026-11-12 01:42:51 -03:00
lgcosta ccad35b247 hfsc: fix class-handle collision (root class used bit 0x1, colliding with odd pf qids -> pfq_addqueue EBUSY); guard against NULL discipline; drop debug prints 2026-11-12 01:40:49 -03:00
lgcosta 14ff7d1c39 ifq: real ifq_start (drain+transmit), dequeue cookie fix, pf_kifq_input hook in the data path; 14/14 suite unchanged 2026-11-12 01:15:26 -03:00
lgcosta f34c4f92a9 M5 step 1: build OpenBSD hfsc/fq_codel back ends into pf.ko (ml_* API, ifq_q handshake, scheduler types); priq still attached, 14/14 suite unchanged 2026-11-12 00:48:56 -03:00
lgcosta ca75fc49cc docs: guide milestone tables synced with the verified state (EN+PT-BR) 2026-11-11 23:59:03 -03:00
lgcosta 12e9c1a2f3 reply-to: gate 13 green (reply forced onto the gateway link); docs/README/CHANGELOG for 14 gates 2026-11-11 23:56:01 -03:00
lgcosta c2d1fc4c53 docs: fail-to is not an OpenBSD 8.0 keyword (removed upstream) - correct all references; reply-to gate added 2026-11-11 23:52:11 -03:00
lgcosta 3235af7f0b pools: gate 10 green (round-robin vs source-hash); docs/README/CHANGELOG for 13 gates 2026-11-11 23:45:24 -03:00
lgcosta 422a325a56 docs: drop leftover line in pt-BR patches note 2026-11-11 23:37:38 -03:00
lgcosta 80396e0b36 docs: patches helpers are in tree (verified PATCHES_APPLIED=0 / UNPATCH_DONE); route pool gate 10 written 2026-11-11 23:37:02 -03:00
lgcosta b3e7358868 scrub: gate 09 green (min-ttl/max-mss measured on egress); matrix + guides updated EN+PT-BR 2026-11-11 23:32:32 -03:00
64 changed files with 4688 additions and 81 deletions

No files matched your search

+27
View File
@@ -1,5 +1,32 @@
# 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.
+7 -8
View File
@@ -8,9 +8,8 @@ real OpenBSD `pfctl` over the `/dev/pf` ioctl ABI. (A second module for the
**Status:** the filter core, stateful filtering, tables, anchors, NAT
(`nat-to`/`rdr-to`/`binat-to`), forced routing (`route-to`/`dup-to`), `divert`
and `pflog` are **verified in a real libvirt lab** (11/11 gates green). The
remaining features (scrub e2e, ALTQ/pfq, af-to, `fail-to`/`reply-to`) are
still 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).
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)
@@ -23,9 +22,9 @@ 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 |
| M1 | compat layer, `pf.ko` loads, `/dev/pf`, `pfctl -f` / `-s info` | done |
| M2 | filter core: rules/states/tables/anchors, VM gates 01–04 | **done** — gates 01/02/03/04 green |
| M3 | NAT (`nat-to`/`rdr-to`/`binat-to`), scrub e2e | **partial** — NAT verified (gate 05), redirect incl. 2 MB integrity (gate 06), `binat-to` (gate 07); scrub e2e pending |
| M4 | route-to/dup-to/fail-to, divert e2e, pflog | **partial** — divert (gate 11), pflog (gate 12), route-to/dup-to (gate 08) verified; `fail-to`/`reply-to` pending |
| M5 | queues: HFSC/FQ-CoDel back ends + `pfq` qdisc | 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/reply-to, divert e2e, pflog | **done** — divert (11), pflog (12), route-to/dup-to (08), pools (10), reply-to (13) verified |
| 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 | **done** — docs EN+PT-BR, 11/11 suite green, tagged (v0.5.0) |
## What you get (planned)
@@ -36,9 +35,9 @@ 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`) |
| 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` |
| 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 (verified: packet to userspace and back) |
| Route | `route-to`/`dup-to` verified; pools (`rr`, `source-hash`), `fail-to`, `reply-to` pending |
| 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 `/dev/pflog` (DLT_PFLOG pcap, OpenBSD record layout) — verified |
| Control | `/dev/pf` (0600), OpenBSD 8.0 ioctl ABI, Linux build of `pfctl` (single binary) |
+24
View File
@@ -111,6 +111,30 @@ kernel prints `pfk: divert port N: no socket bound`. `log` needs
(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)
These are deliberate, marked-in-code divergences of the compat layer; they
+3 -3
View File
@@ -51,16 +51,16 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
| `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` | `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` | `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) |
## Routing / divert / queues / logging
| Feature | Upstream syntax | Milestone | Status |
|---|---|---|---|
| `route-to` / `dup-to` / `fail-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state` | M4 | **`route-to` and `dup-to` VERIFIED in lab** (gate `08-routeto.sh`): the packet leaves through the gateway's link instead of the normal egress (proved on the firewall's own interfaces), and `dup-to` keeps the original path while emitting the copy. `fail-to`/`reply-to` parse and are stored; no gate yet. 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). |
| `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 <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** |
| 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) |
+5 -5
View File
@@ -89,11 +89,11 @@ rebuilt.
|---|---|---|
| M0 | repo, scripts, kernel build, VM boots custom kernel | 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 |
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pending |
| M4 | route-to/dup-to/fail-to, divert e2e | pending |
| M5 | queues: ifq back ends (hfsc/fq_codel) + `pfq` qdisc | pending |
| M6 | docs complete, patchset tidy, full suite green, tagged releases | in progress (this doc set) |
| M2 | filter core: rules/states/tables/anchors VM gates | **done** — gates 01–04 green |
| 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/reply-to, pools, divert e2e | **done** — gates 08/10/13 and divert gate 11 |
| 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 — docs EN+PT-BR complete, 14/14 suite green, tagged |
## Next reads
+5 -5
View File
@@ -20,11 +20,11 @@ used.
`patches/kernel/` therefore contains no `000N-*.patch` today, and applying
the (empty) set is a no-op.
> Honest note: install.md and the design mention `scripts/apply-patches.sh`,
> `scripts/unpatch.sh` and `patches/kernel/series`. Those helpers are **not
> in the tree yet**; until they land, "apply the patch set" is by definition
> nothing to do. When a real patch is needed, the first commit that adds it
> MUST also add the two scripts and the `series` file (see rules below).
> Honest note: `scripts/apply-patches.sh`, `scripts/unpatch.sh` and
> `patches/kernel/series` **are in the tree** and are exercised by
> `make test`-adjacent scripts; the series is empty by design, so applying it
> is a verified no-op (`PATCHES_APPLIED=0`). When a real patch is needed, the
> commit that adds it MUST also extend the series file and the rules below.
## The mechanism (how the set is meant to work)
+3 -4
View File
@@ -61,10 +61,9 @@ treat them as load-and-pray until the matrix moves them.
| Syntax | Until | What happens today |
|---|---|---|
| `nat-to`, `rdr-to`, `binat-to` | — | **Verified in the lab** (gates `05-nat`, `06-rdr`, the latter pulls 2 MB through a redirect and compares it byte for byte). `nat-to (egress)` expands the OpenBSD interface *group*; on a multi-NIC host prefer the explicit `nat-to (<iface>)` form. `binat-to` is expanded by pfctl into a `nat-to … static-port` + `rdr-to` pair (gate `07-binat`). |
| `scrub (…)`, `set reassemble` | M3 | `pf_norm.c` runs inside `pf_test` (compiled) — crafted-fragment behavior is untested; treat results as unvalidated. |
| `divert-to`, `divert` tag | — | **Verified in the lab** (gate `11-divert`): the packet is delivered to an `AF_DIVERT` socket (`PF_DIVERT=32`) served by `tools/pdivert`; with no listener it is dropped, exactly like BSD's `divs_noport`. |
| `route-to`, `dup-to` | — | **Verified in the lab** (gate `08-routeto.sh`): the forced gateway is used instead of the normal egress, and `dup-to` copies while keeping the flow. `fail-to`/`reply-to` load but have no gate yet. |
| `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**. |
| `scrub (min-ttl, max-mss, df, random-id)`, `set reassemble` | — | **Verified in the lab** (gate `09-scrub.sh`): `min-ttl` and `max-mss` measurably rewrite transit packets. || `divert-to`, `divert` tag | — | **Verified in the lab** (gate `11-divert`): the packet is delivered to an `AF_DIVERT` socket (`PF_DIVERT=32`) served by `tools/pdivert`; with no listener it is dropped, exactly like BSD's `divs_noport`. |
| `route-to`, `dup-to`, `reply-to` | — | **Verified in the lab** (gates `08-routeto.sh`, `13-replyto.sh`): the forced gateway is used instead of the normal egress, `dup-to` copies while keeping the flow, and `reply-to` pushes a flow's answers out through the gateway link. `fail-to` is not a keyword in OpenBSD 8.0 (removed upstream). |
| `queue` / `queue on` / `pfctl -s queue` | — | **Verified in the lab** (gate `15-queue.sh`): queues load, hfsc attaches, `pfctl -s queue -v` reports class statistics and a queued 2 MB flow is counted by the class. `bandwidth` is a *guarantee under contention*, not a cap - 8.0's pf.conf has no upperlimit keyword. |
| `log`, `log all`, `block log`, `pflogd` | — | **Verified in the lab** (gate `12-pflog`): run `pflogd /tmp/x.pcap` (reads `/dev/pflog`); the file is a `DLT_PFLOG` (117) pcap readable by tcpdump/wireshark. |
| `route-label …` | never (non-goal) | Rejected at the ioctl layer. |
| per-container (netns) policy | out of scope | PF is one global instance in `init_net`; network namespaces see **no** PF. |
+91
View File
@@ -939,3 +939,94 @@ 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.
+26
View File
@@ -111,6 +111,32 @@ 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)
Estes são desvios deliberados e marcados no código; importam ao raciocinar
+3 -3
View File
@@ -51,16 +51,16 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
| `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` | `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` | `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) |
## Roteamento / divert / filas / logging
| 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 { 10.0.3.2 }` | M4 | **`route-to` e `dup-to` VERIFICADOS em lab** (gate `08-routeto.sh`): o pacote sai pelo enlace do gateway em vez do egress normal (provado nas interfaces do próprio firewall) e o `dup-to` mantém o caminho original emitindo a cópia. `fail-to`/`reply-to` carregam; sem gate ainda. Desvio: o next-hop forçado é instalado com `rt_gw4`/`rt_gw_family` na rota consultada (o Linux só resolve o gateway quando a rota o declara). |
| 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 <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 |
| 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) |
+5 -5
View File
@@ -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 |
| 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 |
| M3 | NAT (nat/rdr/binat), scrub e2e, pflog | pendente |
| M4 | route-to/dup-to/fail-to, divert e2e | pendente |
| M5 | filas: back ends ifq (hfsc/fq_codel) + qdisc `pfq` | pendente |
| M6 | docs bilíngues completas, patchset arrumado, suíte inteira verde, tags de release | em andamento (este conjunto de docs) |
| M2 | núcleo do filtro: regras/estados/tabelas/âncoras, gates VM | **pronto** — gates 01–04 verdes |
| 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/reply-to, pools, divert e2e | **pronto** — gates 08/10/13 e divert gate 11 |
| M5 | filas: back ends ifq (hfsc/fq_codel) | **parcial** — back ends compilados no `pf.ko`; anexação/ritmo pendentes |
| 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
+5 -6
View File
@@ -20,12 +20,11 @@ userland forem usados.
Portanto, `patches/kernel/` não contém nenhum `000N-*.patch` hoje, e aplicar
o conjunto (vazio) é um no-op.
> Nota honesta: o install.md e o design mencionam `scripts/apply-patches.sh`,
> `scripts/unpatch.sh` e `patches/kernel/series`. Esses helpers **ainda não
> estão na árvore**; até entrarem, "aplicar o conjunto de patches" é, por
> definição, nada a fazer. Quando um patch real for necessário, o primeiro
> commit que o adicionar DEVE também adicionar os dois scripts e o arquivo
> `series` (ver as regras abaixo).
> Nota honesta: `scripts/apply-patches.sh`, `scripts/unpatch.sh` e
> `patches/kernel/series` **estão na árvore**. O `series` é vazio por projeto,
> então aplicá-lo é um no-op verificado (`PATCHES_APPLIED=0`). Quando um patch
> real for necessário, o commit que o adiciona também atualiza o `series` e
> as regras abaixo.
## O mecanismo (como o conjunto deve funcionar)
+3 -4
View File
@@ -64,10 +64,9 @@ matriz mudar.
| Sintaxe | Até | O que acontece hoje |
|---|---|---|
| `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. |
| `divert-to`, tag `divert` | — | **Verificado em lab** (gate `11-divert`): o pacote é entregue a um socket `AF_DIVERT` (`PF_DIVERT=32`) atendido pelo `tools/pdivert`; sem listener é descartado, exatamente como o `divs_noport` do BSD. |
| `route-to`, `dup-to` | — | **Verificado em lab** (gate `08-routeto.sh`): o gateway forçado é usado em vez do egress normal, e `dup-to` copia mantendo o fluxo. `fail-to`/`reply-to` carregam mas ainda sem gate. |
| `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**. |
| `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. |
| `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). |
| `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. |
| `log`, `log all`, `block log`, `pflogd` | — | **Verificado em lab** (gate `12-pflog`): rode `pflogd /tmp/x.pcap` (lê `/dev/pflog`); o arquivo é um pcap `DLT_PFLOG` (117) legível por tcpdump/wireshark. |
| `route-label …` | nunca (não-objetivo) | Rejeitado na camada de ioctl. |
| política por container (netns) | fora de escopo | O PF é uma instância global em `init_net`; network namespaces **não** enxergam o PF. |
+1 -1
View File
@@ -1 +1 @@
savedcmd_pf.mod := printf '%s\n' pfsrc/pfk_mod.o pfsrc/pfk_hooks.o compat/pfk_mbuf.o compat/pfk_kern.o compat/pfk_if.o compat/pfk_xmit.o compat/pfk_pcb.o compat/pfk_ip4.o compat/siphash.o compat/toeplitz.o compat/md5.o compat/sha2.o compat/radix.o compat/pfk_rtable.o compat/pfk_ip6.o compat/subr_tree.o pfsrc/pf_ruleset.o pfsrc/pf_table.o pfsrc/pf_norm.o pfsrc/pf_if.o pfsrc/pf_lb.o pfsrc/pf_osfp.o pfsrc/pf_syncookies.o pfsrc/pf_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.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
View File
@@ -20,7 +20,7 @@ pf-objs := pfsrc/pfk_mod.o pfsrc/pfk_hooks.o \
# ported OpenBSD 8.0 engine
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_ioctl.o pfsrc/pf.o pfsrc/divert_sock.o pfsrc/pfk_pflog.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
# semantics; the OpenBSD hfsc/fq_codel back ends land in milestone M5)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/md5.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/md5.o: $(deps_compat/md5.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_if.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_if.o: $(deps_compat/pfk_if.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ifq.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ifq.o: $(deps_compat/pfk_ifq.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip4.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ip4.o: $(deps_compat/pfk_ip4.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_ip6.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_ip6.o: $(deps_compat/pfk_ip6.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_kern.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_kern.o: $(deps_compat/pfk_kern.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_mbuf.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_mbuf.o: $(deps_compat/pfk_mbuf.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_pcb.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.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 \
compat/pfk_pcb.o: $(deps_compat/pfk_pcb.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_rtable.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/pfk_rtable.o: $(deps_compat/pfk_rtable.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/pfk_xmit.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.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)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/radix.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/radix.o: $(deps_compat/radix.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/sha2.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/sha2.o: $(deps_compat/sha2.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/siphash.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/siphash.o: $(deps_compat/siphash.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/subr_tree.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
compat/subr_tree.o: $(deps_compat/subr_tree.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_compat/toeplitz.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.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/linux/crypto.h \
+103 -27
View File
@@ -217,10 +217,14 @@ pfk_pfq_enqueue(void *arg, struct mbuf *m)
struct pfk_pfq *pfq = arg;
if (!pfq)
return NULL;
if (ifq_enqueue(&pfq->ifq, m) != 0)
return NULL;
return m;
return m; /* caller keeps ownership */
/*
* ifq_enqueue takes ownership of m (the scheduler may drop it); we then
* 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 *
@@ -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
* 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;
const struct pfq_ops * const pfq_hfsc_ops = &pfk_priq_pfq_ops;
const struct pfq_ops * const pfq_fqcodel_ops = &pfk_priq_pfq_ops;
/*
* M5: the real scheduler back ends (pfsrc/hfsc.c, pfsrc/fq_codel.c) define
* ifq_hfsc_ops/pfq_hfsc_ops/ifq_fqcodel_ops/pfq_fqcodel_ops themselves; the
* priq shim above stays as the default for interfaces without a queue rule.
*/
/* ---------------- framework ---------------- */
void
@@ -302,7 +307,36 @@ ifq_attach(struct ifqueue *ifq, const struct ifq_ops *ops, void *opsarg)
if (!opsarg && ops->ifqop_alloc)
opsarg = ops->ifqop_alloc(0, NULL);
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_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
@@ -312,19 +346,35 @@ ifq_destroy(struct ifqueue *ifq)
ifq->ifq_ops->ifqop_free(0, ifq->ifq_softc);
ifq->ifq_ops = 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
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;
}
if (ifq->ifq_maxlen && ifq->ifq_len >= ifq->ifq_maxlen) {
ifq->ifq_errors++;
ifq->ifq_qdrops++;
m_freem(m);
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 *
@@ -336,14 +386,16 @@ ifq_deq_begin(struct ifqueue *ifq)
if (!ifq->ifq_ops || !ifq->ifq_ops->ifqop_deq_begin)
return NULL;
m = ifq->ifq_ops->ifqop_deq_begin(ifq, &cookie);
ifq->ifq_dequeue = cookie;
return m;
}
void
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)
ifq->ifq_ops->ifqop_deq_commit(ifq, m, cookie);
}
@@ -365,12 +417,52 @@ ifq_dequeue(struct ifqueue *ifq)
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
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;
}
/*
* 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
ifq_purge(struct ifqueue *ifq)
{
@@ -423,19 +515,3 @@ ifq_hdatalen(struct ifqueue *ifq)
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");
}
+15 -2
View File
@@ -47,8 +47,21 @@ pfk_printk_compat(const char *fmt, ...)
void
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) ---------------- */
+11
View File
@@ -607,3 +607,14 @@ pfk_mbuf_exit(void)
{
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;
}
+37
View File
@@ -270,3 +270,40 @@ pfk_if_output(struct pfk_ifnet *ifp, struct mbuf *m, void *dst,
m_freem(mp);
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;
}
+1
View File
@@ -27,6 +27,7 @@ struct ifqueue {
struct mbuf_list *ifq_free; /* free list (unused in the port) */
unsigned int ifq_len;
unsigned int ifq_oactive;
void *ifq_dequeue; /* cookie between deq_begin/commit */
/* statistics */
u_int64_t ifq_packets;
+51
View File
@@ -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_ */
+122
View File
@@ -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_ */
+46
View File
@@ -1212,5 +1212,51 @@ struct pfk_pflog_stat {
};
#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 */
+73
View File
@@ -74,6 +74,79 @@ ml_dequeue(struct mbuf_list *ml)
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 */
struct pfk_ifnet *pfk_egress_if(void);
void pfk_egress_refresh(void);
+1 -1
View File
@@ -73,7 +73,7 @@ struct pkthdr {
u_int32_t ph_flowid;
u_int ph_ifidx;
u_int ph_rtableid;
struct pfk_timeval ph_timestamp;
int64_t ph_timestamp; /* OpenBSD 8.0: nanoseconds */
struct pfk_ifnet *ifp;
struct pfk_ifnet *rcvif;
u_short ph; /* tso segs: unused, kept as pad */
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/divert_sock.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
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
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf.o: $(deps_pfsrc/pf.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_if.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_if.o: $(deps_pfsrc/pf_if.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ioctl.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_ioctl.o: $(deps_pfsrc/pf_ioctl.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_lb.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_lb.o: $(deps_pfsrc/pf_lb.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_norm.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_norm.o: $(deps_pfsrc/pf_norm.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_osfp.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_osfp.o: $(deps_pfsrc/pf_osfp.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_ruleset.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_ruleset.o: $(deps_pfsrc/pf_ruleset.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_syncookies.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_syncookies.o: $(deps_pfsrc/pf_syncookies.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pf_table.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
pfsrc/pf_table.o: $(deps_pfsrc/pf_table.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_hooks.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_hooks.o: $(deps_pfsrc/pfk_hooks.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_mod.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_mod.o: $(deps_pfsrc/pfk_mod.o)
+2
View File
@@ -1828,6 +1828,8 @@ deps_pfsrc/pfk_pflog.o := \
include/compat/sys/rwlock.h \
include/compat/sys/mutex.h \
include/compat/sys/pclock.h \
include/net/hfsc.h \
include/net/fq_codel.h \
include/pfk_api.h \
pfsrc/pfk_pflog.o: $(deps_pfsrc/pfk_pflog.o)
+3 -1
View File
@@ -332,7 +332,9 @@ control_law(struct codel *cd, struct codel_params *cp, int64_t rts)
unsigned int idx;
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];
}
+12 -3
View File
@@ -335,9 +335,9 @@ hfsc_class_purge(struct hfsc_class *cl, struct mbuf_list *ml)
}
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);
}
@@ -481,7 +481,16 @@ hfsc_pf_qstats(struct pf_queuespec *q, void *ubuf, int *nbytes)
if (hif == NULL)
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);
return (EINVAL);
}
+5
View File
@@ -662,6 +662,11 @@ pf_create_queues(void)
}
qif->disc = qif->pfqops->pfq_alloc(ifp);
if (qif->disc == NULL) {
error = ENOBUFS;
free(qif, M_PF, sizeof(*qif));
goto error;
}
qif->next = list;
list = qif;
+16
View File
@@ -204,6 +204,22 @@ pfk_pkt_work(struct work_struct *w)
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
* held one extra skb reference — release it */
m_freem(m);
+13 -2
View File
@@ -115,11 +115,22 @@ 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;
n = min_t(size_t, want, skb->len);
if (skb_copy_bits(skb, 0, buf, n))
/*
* 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;
}
+54
View File
@@ -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
+68
View File
@@ -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
+52
View File
@@ -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
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# M5 gate: hfsc queue in the data path.
#
# What is verifiable with OpenBSD 8.0's pf: a queue configuration loads, the
# hfsc discipline attaches to the interface, `pfctl -s queue -v` reports the
# class statistics, a queued flow completes through the class, and that class's
# packet/byte counters advance - i.e. the scheduler really sits in the path.
#
# What is NOT a cap here: 8.0's pf.conf exposes only `bandwidth` (HFSC
# linkshare). There is no `minimum`/`maximum` keyword in this vintage, and
# linkshare is a guarantee under contention, not a limit on a lone flow - the
# same on OpenBSD. Timing is printed for information, not asserted.
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
SZ=2000000
IF_GW=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
[ -n "$IF_GW" ] || IF_GW=eth2
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/g15.conf <<C
queue rootq on $IF_GW bandwidth 10Mbit
queue leafq on $IF_GW 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
C
pfload /tmp/g15.conf || { bad "queue rules load"; finish; }
# 1) both classes are visible with statistics
Q=$(pf0 '/opt/pf/pfctl -s queue -v')
if echo "$Q" | grep -q 'rootq' && echo "$Q" | grep -q 'leafq'; then
ok "pfctl -s queue lists both classes"
else
bad "queue stats missing: $(echo "$Q" | head -2)"
fi
# 2) a queued flow completes
srv "pkill nc 2>/dev/null; head -c $SZ /dev/zero | tr '\\0' A > /tmp/pa.bin; sleep 1; (setsid nc -l -p 9095 < /tmp/pa.bin >/dev/null 2>&1 &); sleep 1" >/dev/null
T0=$(date +%s%N)
GOT=$(cli "nc -w60 10.0.2.20 9095 > /tmp/oa.bin; wc -c < /tmp/oa.bin" | tr -d '[:space:]')
T1=$(date +%s%N)
if [ "${GOT:-0}" -ge "$SZ" ]; then
ok "queued flow completed ($GOT bytes in $(( (T1-T0)/1000000 )) ms)"
else
bad "queued flow incomplete (got $GOT of $SZ)"
finish
fi
# 3) the scheduler actually counted the class
sleep 1
Q2=$(pf0 '/opt/pf/pfctl -s queue -v')
# line after the leafq header looks like: [ pkts: 37 bytes: 1932 dropped ...
LEAF_PKTS=$(printf '%s\n' "$Q2" | grep -A1 'leafq' | tail -1 |
sed -n 's/^[[:space:]]*\[[[:space:]]*pkts:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
if [ -n "$LEAF_PKTS" ] && [ "$LEAF_PKTS" -gt 0 ] 2>/dev/null; then
ok "hfsc class counted traffic (leafq pkts=$LEAF_PKTS)"
else
bad "hfsc class counters did not move: $(printf '%s\n' "$Q2" | grep -A1 leafq | tail -1)"
fi
# 4) the firewall is still healthy after queueing
pf0 'echo SSH_ALIVE' | grep -q SSH_ALIVE && ok "firewall responsive after queued transfer" \
|| bad "firewall unresponsive after queueing"
pf0 '/opt/pf/pfctl -d' >/dev/null
srv 'pkill nc 2>/dev/null; true' >/dev/null
finish