docs: divert status corrected after AF_DIVERT restore (COMPILES untested, M4 e2e gate defined)
This commit is contained in:
1 parent
ac56534c3b
commit
0fe4d83b93
10 files changed
+185
-27
No files matched your search
@@ -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
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Executable
+141
@@ -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)
|
||||
Reference in new issue
Block a user