diff --git a/scripts/demo-session.sh b/scripts/demo-session.sh new file mode 100755 index 0000000..274d83d --- /dev/null +++ b/scripts/demo-session.sh @@ -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 { 10.0.1.0/24 } + +# transit traffic: stateful, table-driven +pass in quick on ingress proto icmp from 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" </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 child: pass in quick ... from keep state +# block drop in quick proto icmp from any to (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 < /tmp/g4anc.conf <<'C' -pass in quick proto icmp from 10.0.1.10 to any keep state +cat > /tmp/g4anc.conf </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 diff --git a/tests/vm/05-nat.sh b/tests/vm/05-nat.sh index 5c8d146..0e1e37d 100755 --- a/tests/vm/05-nat.sh +++ b/tests/vm/05-nat.sh @@ -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 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 < (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 diff --git a/tests/vm/14-soak.sh b/tests/vm/14-soak.sh index 8225328..69d3f11 100755 --- a/tests/vm/14-soak.sh +++ b/tests/vm/14-soak.sh @@ -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