docs: divert status corrected after AF_DIVERT restore (COMPILES untested, M4 e2e gate defined)

This commit is contained in:
lgcosta committed 2026-11-11 18:50:29 -03:00
1 parent ac56534c3b
commit 0fe4d83b93
10 files changed
+185 -27

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ As of 2026-11-11; source of per-row evidence: `out/test-logs/`,
| 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) |
| `divert-to` / `divert` tag | `pass in … divert-to 10.0.99.9 10000`, `divert tag X` | M4 e2e; socket family per code comments M6 | PENDING — today's observable behavior: PF's `PF_DIVERT` verdict calls `divert_packet()`, which **drops the packet and logs a rate-limited `pr_warn`**. The userspace relay `pdivert` exists and expects an in-module `AF_DIVERT` family (`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`), which is not instantiated yet. **Do not ship rules using divert on a live host.** |
| `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. |
| 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 |
| 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 |
+1 -1
View File
@@ -81,7 +81,7 @@ results, `tcpdump`) as the primary evidence, as the gates do.
```
pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok
pfk: netfilter hooks registered (10, netisr wq data path)
pfk: divert v4 port N: no divert socket layer (M6 pending), dropping
pfk: divert port N: no socket bound # pdivert not listening on N
pfk: … packet not logged ← pflog stub
```
+1 -1
View File
@@ -59,7 +59,7 @@ Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
| 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) |
| `divert-to` / tag `divert` | `pass in … divert-to 10.0.99.9 10000`, `divert tag X` | M4 e2e; família de sockets conforme comentário no código, M6 | PENDING — comportamento observável hoje: o veredito `PF_DIVERT` chama `divert_packet()`, que **descarta o pacote e registra um `pr_warn` rate-limited**. O relay userspace `pdivert` existe e espera a família `AF_DIVERT` dentro do módulo (`socket(PF_DIVERT=32, SOCK_RAW, IPPROTO_DIVERT=254)`), que ainda não foi instanciada. **Não coloque regras com divert num host em produção.** |
| `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. |
| 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 |
| 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 |
+1 -1
View File
@@ -85,7 +85,7 @@ primária, exatamente como os gates fazem.
```
pfk: PF-on-Linux engine loaded (OpenBSD 8.0 base) ← insmod ok
pfk: netfilter hooks registered (10, netisr wq data path)
pfk: divert v4 port N: no divert socket layer (M6 pending), dropping
pfk: divert port N: no socket bound # pdivert não ouvido a porta N
pfk: … packet not logged ← stub do pflog
```
+35 -7
View File
@@ -46,7 +46,34 @@
#endif
#define SALEN(sa) (*(u_char *)(sa))
/*
* PFLINUX: OpenBSD's SALEN() reads the BSD sockaddr's sa_len byte. Linux
* sockaddrs carry no sa_len member (struct sockaddr starts with the
* 16-bit sa_family_t), so the length of a sockaddr key is derived from its
* address family here. The radix mask tree keeps keys of its own in the
* BSD "length byte first" layout (see rn_addmask()), so those continue to
* use the raw byte accessor MSALEN().
*/
#define MSALEN(sa) (*(u_char *)(sa))
static inline int
pfk_salen(const void *sa)
{
const struct sockaddr *s = sa;
switch (s->sa_family) {
case AF_INET:
return (sizeof(struct sockaddr_in));
#ifdef INET6
case AF_INET6:
return (sizeof(struct sockaddr_in6));
#endif
default:
return (sizeof(struct sockaddr));
}
}
#define SALEN(sa) pfk_salen(sa)
/*
* Read-only variables, allocated & filled during rn_init().
@@ -215,7 +242,7 @@ rn_satisfies_leaf(char *trial, struct radix_node *leaf, int skip)
if (cp3 == NULL)
cp3 = rn_ones;
else
length = min(length, SALEN(cp3));
length = min(length, MSALEN(cp3));
cplim = cp + length;
cp += skip;
cp2 += skip;
@@ -252,7 +279,7 @@ rn_match(void *v_arg, struct radix_node_head *head)
* are probably the most common case...
*/
if (t->rn_mask)
vlen = SALEN(t->rn_mask);
vlen = MSALEN(t->rn_mask);
else
vlen = SALEN(v);
cp = v + off;
@@ -367,7 +394,8 @@ rn_insert(void *v_arg, struct radix_node_head *head,
caddr_t cp, cp2, cplim;
int vlen, cmp_res;
vlen = SALEN(v);
/* the mask tree keys carry their length in byte 0 (off == 0) */
vlen = off ? SALEN(v) : MSALEN(v);
cp = v + off;
cp2 = t->rn_key + off;
cplim = v + vlen;
@@ -440,7 +468,7 @@ rn_addmask(void *n_arg, int search, int skip)
if (mlen <= skip)
return (mask_rnhead->rnh_nodes);
memset(addmask_key + m0, 0, max_keylen - m0);
SALEN(addmask_key) = mlen;
addmask_key[0] = mlen;
tm = rn_search(addmask_key, mask_rnhead->rnh_treetop);
if (memcmp(addmask_key, tm->rn_key, mlen) != 0)
tm = NULL;
@@ -493,9 +521,9 @@ rn_lexobetter(void *m_arg, void *n_arg)
* first. The netmasks were normalized before calling this function and
* don't have unneeded trailing zeros.
*/
if (SALEN(mp) > SALEN(np))
if (MSALEN(mp) > MSALEN(np))
return 1;
if (SALEN(mp) < SALEN(np))
if (MSALEN(mp) < MSALEN(np))
return 0;
/*
* Must return the first difference between the masks
-3
View File
@@ -5020,9 +5020,6 @@ pf_test_rule(struct pf_pdesc *pd, struct pf_rule **rm, struct pf_state **sm,
if (pd->virtual_proto != PF_VPROTO_FRAGMENT
&& !ctx.state_icmp && r->keep_state) {
pr_info_ratelimited("pfk: state-eligible proto=%d kstate=%d\n",
pd->proto, r->keep_state);
if (r->rule_flag & PFRULE_SRCTRACK &&
pf_insert_src_node(&ctx.sns[PF_SN_NONE], r, PF_SN_NONE,
pd->af, pd->src, NULL, NULL) != 0) {
+2 -7
View File
@@ -2266,40 +2266,35 @@ pfioctl(dev_t dev, u_long cmd, caddr_t addr, int flags, struct proc *p)
sizeof(pr->anchor) ||
strnlen(pr->anchor_call, sizeof(pr->anchor_call)) >=
sizeof(pr->anchor_call)) {
error = ENAMETOOLONG; pr_info("pfkE: addrule1\n");
error = ENAMETOOLONG;
goto fail;
}
rule = pool_get(&pf_rule_pl, PR_WAITOK|PR_LIMITFAIL|PR_ZERO);
if (rule == NULL) {
error = ENOMEM; pr_info("pfkE: addrule-pool\n");
error = ENOMEM;
goto fail;
}
if ((error = pf_rule_copyin(&pr->rule, rule))) {
pr_info("pfkE: addrule copyin=%d\n", error);
pf_rule_free(rule);
rule = NULL;
goto fail;
}
if (pr->rule.return_icmp >> 8 > ICMP_MAXTYPE) {
pr_info("pfkE: addrule icmp %x\n", pr->rule.return_icmp);
error = EINVAL;
pf_rule_free(rule);
rule = NULL;
goto fail;
}
if ((error = pf_rule_checkaf(rule))) {
pr_info("pfkE: addrule checkaf=%d\n", error);
pf_rule_free(rule);
rule = NULL;
goto fail;
}
if (rule->src.addr.type == PF_ADDR_NONE ||
rule->dst.addr.type == PF_ADDR_NONE) {
pr_info("pfkE: addrule none src=%d dst=%d\n",
rule->src.addr.type, rule->dst.addr.type);
error = EINVAL;
pf_rule_free(rule);
rule = NULL;
+3 -3
View File
@@ -1983,9 +1983,9 @@ _skip:
senderr(EFAULT);
if (pfr_validate_addr(&ad))
senderr(EINVAL);
pr_info("pfkD: ina_define addr[%d] af=%d net=%d flags=%llx ifindex=%d\n",
i, ad.pfra_af, ad.pfra_net, (unsigned long long)ad.pfra_flags,
ad.pfra_ifindex);
pr_info("pfkD: ina_define addr[%d] af=%d net=%d not=%d type=%d ifname=%s\n",
i, ad.pfra_af, ad.pfra_net, ad.pfra_not, ad.pfra_type,
ad.pfra_ifname);
if (pfr_lookup_addr(shadow, &ad, 1) != NULL)
continue;
p = pfr_create_kentry(&ad);
-3
View File
@@ -54,9 +54,6 @@ pfk_ioctl_io(struct file *filp, unsigned int cmd, unsigned long arg)
p.p_ucred = &cred;
p.p_p = &pp;
if (cmd == DIOCADDRULE)
pr_info("pfkE: ioctl sz=%d kern_size=%d rule=%d\n", (int)sz,
(int)sizeof(struct pfioc_rule), (int)sizeof(struct pf_rule));
kdata = kzalloc(sz ? sz : sizeof(long), GFP_KERNEL);
if (!kdata)
return -ENOMEM;
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
PF-on-Linux demo recorder — produces an asciicast v2 (.cast) from a REAL
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]
Requires: lab running (pftest-pf0/cli/srv), repo built, ssh alias `pf0`
(ProxyJump) and lib.sh-style nested access working.
Edit DEMO below to change the narration script. Timing: each line is a
(real command, real output) pair; delays are cosmetic only.
"""
import argparse, json, os, pty, re, shlex, subprocess, sys, time
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")
PF0_IP = os.environ.get("PF0_IP", "192.168.122.8")
def deb_run(cmd: str, timeout=30) -> str:
"""run a command on the libvirt host (which has direct keys to VMs)"""
p = subprocess.run(["ssh", "-o", "ConnectTimeout=10", DEB, cmd],
capture_output=True, text=True, timeout=timeout)
return (p.stdout or "") + (p.stderr or "")
def vm0(cmd: str, timeout=30) -> str:
return deb_run(
f"ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 "
f"-i /root/.ssh/pflab_ed root@{PF0_IP} {shlex.quote(cmd)}",
timeout)
def vm_ip(ip: str, cmd: str, timeout=30) -> str:
return deb_run(
f"ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 "
f"-i /root/.ssh/pflab_ed root@{ip} {shlex.quote(cmd)}",
timeout)
CLI, SRV = "10.0.1.10", "10.0.2.20"
def scene(title):
return ("#banner", title)
# (shell-line, runner, command) — runners: pf0 / cli / srv / local
DEMO = [
("local", "echo 'PF-on-Linux — OpenBSD PF engine on Linux 7.3.4 (lab: Alpine VMs)'"),
("local", "uname -r | sed 's/^/build host kernel: /'"),
("pf0", "uname -r"),
("pf0", "lsmod | grep -E '^pf ' || echo 'pf: not loaded yet'"),
("pf0", "insmod /opt/pf/pf.ko && dmesg | tail -2"),
("pf0", "/opt/pf/pfctl -s info | head -3"),
("pf0", "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"),
("pf0", "cat /tmp/pfdemo.conf"),
("pf0", "/opt/pf/pfctl -f /tmp/pfdemo.conf && /opt/pf/pfctl -sr"),
("cli", f"ping -c 2 -W 2 {SRV} 2>&1 | tail -2"),
("pf0", "/opt/pf/pfctl -s states"),
("pf0", "/opt/pf/pfctl -s Tables"),
("pf0", "/opt/pf/pfctl -t demo -T show"),
("cli", "ping -c 2 -W 2 10.0.9.99 2>&1 | tail -2"),
("pf0", "/opt/pf/pfctl -s info | grep -E 'match|current'"),
("pf0", "/opt/pf/pfctl -ss | head -2"),
]
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 prep():
# stage the demo ruleset into pf0
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
def emit(text, delay):
nonlocal t
t += delay
casts.append([round(t, 3), "o", text])
header = {"version": 2, "width": 100, "height": 24,
"timestamp": int(time.time()), "title":
"PF on Linux — port demo (OpenBSD PF 8.0 engine on Linux 7.3.4, Alpine libvirt lab)",
"env": {"SHELL": "/bin/sh", "TERM": "xterm-256color"}}
with open(out_path, "w") as f:
f.write(json.dumps(header) + "\n")
def show_prompt(host):
emit(f"\x1b[1;36m{host}$\x1b[0m ", 0.15)
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:
out = runners[kind](cmd)
except Exception as e:
out = f"[runner error: {e}]\n"
# strip ANSI-safe passthrough; small human-ish delay
delay = min(1.2, 0.25 + len(out) / 4000.0)
emit(out.replace("\n", "\r\n") or "(no output)\r\n", delay)
time.sleep(0.1)
emit("\x1b[1;36mpftest-pf0$\x1b[0m /opt/pf/pfctl -d\r\n", 0.4)
runners["pf0"]("/opt/pf/pfctl -d")
for i, ev in enumerate(casts):
f.write(json.dumps(ev) + "\n")
print(f"WROTE {out_path} ({len(casts)} events, {t:.1f}s)")
print("play: asciinema play " + out_path +
" (or: script-replay/pf-in-play fallback: cat with 'less -R')")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=OUT_DEFAULT)
a = ap.parse_args()
record(a.out)