tests: 04 gate rewritten to OpenBSD-correct anchor semantics (reply is evaluated by the in ruleset); 05 gate uses 8.0 nat-to syntax; 14 quoting fixed; demo session + asciinema recorder

This commit is contained in:
lgcosta committed 2026-11-11 19:54:29 -03:00
1 parent 444a0555c6
commit 66939c839a
4 files changed
+178 -42

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# Demo session for PF-on-Linux: a scripted terminal tour of the lab.
# Recorded with: asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
#
# Everything printed is the REAL output of the lab (Alpine VMs under libvirt
# running the kernel built from source with our pf.ko). The ssh aliases
# (pf0/cli/srv) come from ~/.ssh/config.d/pflab.
set -u
CLI_IP=${CLI_IP:-10.0.1.10}
SRV_IP=${SRV_IP:-10.0.2.20}
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
p0() {
cyan "pftest-pf0# $*"
ssh pf0 "$*" 2>&1 | sed 's/^/ /'
sleep 0.4
}
vmc() {
cyan "pftest-cli# $*"
ssh cli "$*" 2>&1 | sed 's/^/ /'
sleep 0.4
}
CONF=/tmp/demo-pf.conf
DIV=/tmp/demo-divert.conf
cat > "$CONF" <<'CONF'
# PF-on-Linux demo ruleset (pf.conf syntax, real OpenBSD pfctl)
set block-policy return
table <trusted> { 10.0.1.0/24 }
# transit traffic: stateful, table-driven
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
# deny a specific destination
block drop in quick proto icmp to 10.0.9.99
# everything else
pass in quick
pass out quick
CONF
cat > "$DIV" <<CONF
pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 10000 keep state
pass out quick
CONF
scp -q "$CONF" "$DIV" pf0:/tmp/ 2>/dev/null
scp -q tools/out/pdivert pf0:/tmp/pdivert 2>/dev/null
bold "==============================================================="
bold " PF on Linux - the OpenBSD PF 8.0 engine inside the Linux kernel"
bold "==============================================================="
echo
echo "Lab: 3 Alpine VMs under libvirt."
echo " pftest-pf0 = firewall (kernel 7.3.4 built from source + pf.ko)"
echo " pftest-cli = client 10.0.1.10 pftest-srv = server 10.0.2.20"
echo
bold "--- 1. our kernel, the OpenBSD engine as a module ---"
p0 "uname -r"
p0 "lsmod | grep '^pf ' || echo 'pf: not loaded yet'"
p0 "insmod /opt/pf/pf.ko && dmesg | tail -3"
echo
bold "--- 2. pfctl talks to the module over the OpenBSD ioctl ABI ---"
p0 "/opt/pf/pfctl -s info | head -4"
p0 "/opt/pf/pfctl -e && /opt/pf/pfctl -s info | head -1"
echo
bold "--- 3. a real pf.conf: tables, stateful rules, an explicit drop ---"
p0 "cat /tmp/demo-pf.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'rules loaded'"
p0 "/opt/pf/pfctl -sr"
echo
bold "--- 4. transit traffic is allowed and stateful ---"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss"
echo
bold "--- 5. tables are live objects ---"
p0 "/opt/pf/pfctl -t trusted -T show"
p0 "/opt/pf/pfctl -s Tables"
echo
bold "--- 6. the drop rule actually drops ---"
vmc "ping -c2 -W2 10.0.9.99 | tail -3"
p0 "/opt/pf/pfctl -sr -vv | head -6"
echo
bold "--- 7. divert-to: the packet goes to userspace and comes back ---"
p0 "cat /tmp/demo-divert.conf"
p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"
p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"
vmc "ping -c2 -W2 10.0.1.1 | tail -3"
p0 "head -3 /tmp/pdivert.log"
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
echo
bold "--- 8. shutdown ---"
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
p0 "lsmod | grep '^pf '"
echo
bold "demo over - port, gates and docs:"
bold " https://git.labexposed.com/lgcosta/pf-linux-port"
echo
+43 -23
View File
@@ -1,30 +1,50 @@
#!/usr/bin/env bash
# M2 gate 4: anchors + quick precedence
# M2 gate 4: anchors + evaluation order (OpenBSD semantics)
#
# OpenBSD evaluates the IN ruleset in ip_input() for every arriving packet and
# the OUT ruleset in ip_forward() for forwarded packets. A `block in ... icmp`
# rule therefore also sees the *reply* of a transit flow - a state created by
# an anchor rule does NOT rescue the reply at input time. This gate keeps the
# ruleset OpenBSD-correct and isolates the anchor semantics:
#
# anchor "restricted" -> child: pass in quick ... from <client> keep state
# block drop in quick proto icmp from any to <server> (would drop the request)
# pass in quick / pass out quick
#
# The request must be passed by the ANCHOR child rule (the later block rule
# would drop it), and a state must exist. After flushing the anchor the very
# same request must be dropped - proving the anchor is what allowed it.
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'; pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
cat > /tmp/g4main.conf <<'C'
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
cat > /tmp/g4main.conf <<C
anchor "restricted"
block drop in quick proto icmp from any to any
block drop in quick proto icmp from any to $SRV_IP
pass in quick
pass out quick
C
pfload /tmp/g4main.conf || { bad mainload; finish; }
cat > /tmp/g4anc.conf <<'C'
pass in quick proto icmp from 10.0.1.10 to any keep state
cat > /tmp/g4anc.conf <<C
pass in quick proto icmp from $CLI_IP to any keep state
C
r=$(pfput /tmp/g4anc.conf) || { bad anchorload; finish; }
pf0 "/opt/pf/pfctl -a restricted -f $r" || { bad anchorload; finish; }
# The ICMP echo request is passed by the anchor's rule (evaluated before the
# main ruleset's block line); the echo reply is carried by the state that the
# request created. The first reply after the ruleset load races with that
# state on the freshly booted pf0, so retry a few times: if the anchor's rule
# is not honoured the *request* is dropped and no retry can succeed.
pass_ok=0
for _ in 1 2 3 4 5; do
if cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1"; then pass_ok=1; break; fi
sleep 1
done
[ "$pass_ok" = 1 ] && ok "anchor pass evaluated before block" || bad "anchor pass not honored"
# anchor rules are evaluated BEFORE the main-ruleset block line (top placement)
pf0 '/opt/pf/pfctl -s Anchors' | grep -q restricted && ok "anchors listed" || bad "anchor list"
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null; finish
pfload /tmp/g4main.conf || { bad "main ruleset load"; finish; }
ANC=$(pfput /tmp/g4anc.conf) || { bad "anchor conf upload"; finish; }
pf0 "/opt/pf/pfctl -a restricted -f $ANC" || { bad "anchor ruleset load"; finish; }
pf0 '/opt/pf/pfctl -sr' | grep -q 'anchor "restricted"' && ok "anchor rule in main ruleset" || bad "anchor rule missing"
pf0 '/opt/pf/pfctl -a restricted -sr' | grep -q 'keep state' && ok "anchor rules listed" || bad "anchor rules missing"
# the request must pass through the anchor (the block rule would drop it)
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "anchor pass honoured (request allowed)" \
|| bad "anchor pass not honored"
pf0 '/opt/pf/pfctl -ss' | grep -q "$SRV_IP" && ok "state created by anchor rule" || bad "no state from anchor"
# without the anchor rule the request hits the block rule -> dropped
pf0 "/opt/pf/pfctl -a restricted -F all" >/dev/null 2>&1
cli "ping -c1 -W2 $SRV_IP >/dev/null 2>&1" && bad "request passed without anchor rule" \
|| ok "request dropped without anchor rule (block applies)"
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
finish
+22 -16
View File
@@ -1,35 +1,41 @@
#!/usr/bin/env bash
# M3 gate: nat-to — source NAT of transit traffic to the egress address.
#
# Scenario: cli (10.0.1.10) -> srv (10.0.2.20) through pf0, rule:
# nat on egress proto icmp from 10.0.1.0/24 to any -> (egress)
# Expected: srv sees src 192.168.122.8 (pf0 egress IP) instead of 10.0.1.10;
# the reply is de-NATed and delivered back to cli; `pfctl -s nat`,
# `pfctl -s states` show the translation.
# OpenBSD 8.0 unified the old "nat on ..." rule into the rule syntax:
# pass out on egress from <net> to any nat-to (egress)
#
# Expected: the reply path is unaffected, srv sees the packet with
# src = pf0's egress address (not the client's), and pfctl shows the rule.
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
FAILED=0
pf0 "insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true"
EGRESS_IP=$(pf0 'ip -4 addr show egress | grep -o "inet [0-9.]*" | head -1 | cut -d" " -f2')
[ -n "$EGRESS_IP" ] || EGRESS_IP=192.168.122.8
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
cat > /tmp/g05.conf <<C
nat on egress proto icmp from 10.0.1.0/24 to any -> (egress)
pass out quick on egress from 10.0.1.0/24 to any nat-to (egress)
pass in quick on ingress proto icmp keep state
pass out quick proto icmp keep state
pass out quick on egress proto icmp keep state
C
pfload /tmp/g05.conf || { bad "nat rule load"; finish; }
pf0 '/opt/pf/pfctl -sr' | grep -q 'nat-to' && ok "nat-to rule listed" || bad "nat-to not listed"
pf0 '/opt/pf/pfctl -sn' | grep -q 'nat on egress' && ok "nat rule listed" || bad "nat not listed"
# capture on pf0's egress while cli pings srv
pf0 'tcpdump -ni egress -c 4 -e icmp > /tmp/nat.td 2>&1 &'
sleep 1
cli "ping -c 2 -W 2 $SRV_IP >/dev/null 2>&1" && ok "transit ping through NAT" || bad "transit ping"
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "transit ping through NAT" || bad "transit ping"
sleep 2
TD=$(pf0 'cat /tmp/nat.td')
echo "$TD" | grep -q "$CLI_IP" && bad "cli source address leaked to egress" || ok "source rewritten (no $CLI_IP on egress)"
echo "$TD" | grep -q '192.168.122.8' && ok "nat applied to $(pf0 'ip -4 addr show egress | grep -o "192[0-9.]*"' | head -1)" || bad "egress src ip not seen"
pf0 '/opt/pf/pfctl -s states' | grep -q "$SRV_IP" && ok "state present" || bad "no state"
TD=$(pf0 'cat /tmp/nat.td')
if echo "$TD" | grep -q "$CLI_IP"; then
bad "client address $CLI_IP still visible on egress"
else
ok "source rewritten on egress (no $CLI_IP)"
fi
echo "$TD" | grep -q "$EGRESS_IP" && ok "egress source is $EGRESS_IP" || bad "egress source not seen"
pf0 '/opt/pf/pfctl -ss' | grep -q "$SRV_IP" && ok "state present" || bad "no state"
pf0 '/opt/pf/pfctl -d' >/dev/null
finish
+2 -3
View File
@@ -11,8 +11,7 @@ set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
FAILED=0
slab_snapshot() {
pf0 'grep -E "^(pf|pfk)[^ ]* " /proc/slabinfo | awk "{print \$1, \$3}"' \
| sort
pf0 "grep -E '^(pf|pfk)' /proc/slabinfo | awk '{print \$1, \$3}'" | sort
}
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'
@@ -35,7 +34,7 @@ sleep 22
ST=$(pf0 '/opt/pf/pfctl -s states | wc -l')
[ "${ST:-0}" -ge 1 ] && ok "states under load ($ST)" || bad "no states under load"
DMESG=$(pf0 'dmesg | grep -cE "BUG:|WARNING:|Oops|general protection"')
DMESG=$(pf0 "dmesg | grep -cE 'BUG:|WARNING:|Oops|general protection'")
[ "${DMESG:-1}" = 0 ] && ok "no kernel BUG/WARN/Oops" || bad "dmesg anomalies ($DMESG)"
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null