diff --git a/Makefile b/Makefile index a732d62..a88fd30 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # PF-on-Linux top-level make KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4 -.PHONY: module tools kernel test clean vm vm-destroy docs demo +.PHONY: module tools kernel test stress clean vm vm-destroy docs demo module: $(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules @@ -21,6 +21,12 @@ vm-destroy: test: ./scripts/vm-run-tests.sh +# Stress: throughput/state/rule/table churn against the lab, with a report in +# out/stress//report.md. Pass options through, e.g. +# make stress ARGS="--quick" +stress: + ./scripts/stress/run-stress.sh $(ARGS) + docs: @for f in docs/en/*.md; do echo "EN $$f"; done @for f in docs/pt-BR/*.md; do echo "PT $$f"; done diff --git a/scripts/stress/run-stress.sh b/scripts/stress/run-stress.sh new file mode 100755 index 0000000..2964079 --- /dev/null +++ b/scripts/stress/run-stress.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# run-stress.sh — drive the PF stress test (tests/vm/16-stress.sh) against the +# libvirt lab and turn the metrics into a report. +# +# ./scripts/stress/run-stress.sh # default profile +# ./scripts/stress/run-stress.sh --quick # smaller profile (smoke) +# ./scripts/stress/run-stress.sh --seconds 30 --connections 1000 +# +# Output: out/stress//report.md (+ the raw gate log). +# Exit status: 0 when every stress phase passed, 1 otherwise. +set -uo pipefail +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +DEB="${DEB:-root@192.168.1.242}" +PF0_IP=192.168.122.8 +TS="$(date +%Y%m%d-%H%M%S)" +OUTD="$REPO/out/stress/$TS" +mkdir -p "$OUTD" + +SECS=10 +PAR=4 +RATE=100M +CONNS=300 +RELOADS=200 +RULES=200 +TENTRIES=2000 +MAXOH=30 + +while [ $# -gt 0 ]; do + case "$1" in + --quick) SECS=5; CONNS=100; RELOADS=50; RULES=50; TENTRIES=500; shift ;; + --seconds) SECS=$2; shift 2 ;; + --parallel) PAR=$2; shift 2 ;; + --rate) RATE=$2; shift 2 ;; + --connections) CONNS=$2; shift 2 ;; + --reloads) RELOADS=$2; shift 2 ;; + --rules) RULES=$2; shift 2 ;; + --table-entries) TENTRIES=$2; shift 2 ;; + --max-overhead) MAXOH=$2; shift 2 ;; + -h|--help) sed -n '2,12p' "$0"; exit 0 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac +done + +SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 -i /root/.ssh/pflab_ed root@$PF0_IP" + +echo "pflab stress: profile seconds=$SECS parallel=$PAR rate/stream=$RATE connections=$CONNS reloads=$RELOADS rules=$RULES table_entries=$TENTRIES max_overhead=$MAXOH%" + +echo "pflab stress: resetting pftest-pf0" +ssh "$DEB" "virsh destroy pftest-pf0 >/dev/null 2>&1; virsh start pftest-pf0 >/dev/null" || { + echo "pflab stress FAIL: cannot restart pftest-pf0" >&2; exit 1; } +up=0 +for i in $(seq 1 20); do + sleep 6 + if ssh "$DEB" "$SSH true" 2>/dev/null; then up=1; break; fi +done +[ "$up" = 1 ] || { echo "pflab stress FAIL: pf0 never came back" >&2; exit 1; } + +echo "pflab stress: deploying pf.ko + pfctl to pf0:/opt/pf" +for i in 1 2 3; do + if scp -q -o ProxyJump=root@192.168.1.242 -o StrictHostKeyChecking=accept-new \ + -i "$HOME/.ssh/pflab_ed" "$REPO/module/pf.ko" "$REPO/tools/out/pfctl" \ + "$REPO/tools/out/pdivert" "$REPO/tools/out/pflogd" pf0:/tmp/ 2>/dev/null; then + break + fi + sleep 2 +done +ssh "$DEB" "$SSH 'mkdir -p /opt/pf; mv /tmp/pf.ko /tmp/pfctl /tmp/pdivert /tmp/pflogd /opt/pf/ 2>/dev/null; chmod +x /opt/pf/pfctl 2>/dev/null; true'" >/dev/null +ssh "$DEB" "$SSH 'test -s /opt/pf/pf.ko && test -x /opt/pf/pfctl'" >/dev/null 2>&1 || { + echo "pflab stress FAIL: pf.ko/pfctl missing on pf0" >&2; exit 1; } + +echo "pflab stress: running the stress gate" +# The guest's serial console is a pty: a panic or a hang is lost when the VM +# reboots. Capture it on the hypervisor (the redirect happens *there*) and +# copy it back when the run ends. +REMOTE_CONSOLE="/tmp/pf0-console-$TS.log" +ssh "$DEB" "PTY=\$(virsh ttyconsole pftest-pf0); setsid nohup timeout 1800 cat \$PTY >$REMOTE_CONSOLE 2>&1 /dev/null 2>&1 +sleep 2 +export STRESS_SECS="$SECS" STRESS_PARALLEL="$PAR" STRESS_RATE="$RATE" STRESS_CONNS="$CONNS" \ + STRESS_RELOADS="$RELOADS" STRESS_RULES="$RULES" \ + STRESS_TABLE_ENTRIES="$TENTRIES" STRESS_MAX_OVERHEAD_PCT="$MAXOH" +bash "$REPO/tests/vm/16-stress.sh" >"$OUTD/16-stress.sh.log" 2>&1 +RC=$? + +# --- report ---------------------------------------------------------------- +scp -q -o BatchMode=yes -o StrictHostKeyChecking=accept-new "$DEB:$REMOTE_CONSOLE" \ + "$OUTD/pf0-console.log" 2>/dev/null || true +{ + echo "# PF-on-Linux — stress report" + echo + echo "- date: $(date -Is)" + echo "- profile: seconds=$SECS parallel=$PAR rate/stream=$RATE connections=$CONNS reloads=$RELOADS rules=$RULES table_entries=$TENTRIES max_overhead=${MAXOH}%" + echo "- exit status: $RC" + echo "- guest serial console: \`pf0-console.log\` (kernel messages during the run)" + echo + echo "## Phases" + echo + echo '```' + grep '^STRESS ' "$OUTD/16-stress.sh.log" || echo "(no metrics captured)" + echo '```' + echo + echo "## Assertions" + echo + echo '```' + grep -E '^(OK|NOT OK)' "$OUTD/16-stress.sh.log" || echo "(no assertions)" + echo '```' +} >"$OUTD/report.md" + +echo +echo "pflab stress: report -> out/stress/$TS/report.md" +grep '^STRESS ' "$OUTD/16-stress.sh.log" || true +echo +if [ $RC -eq 0 ]; then + echo "STRESS RESULT: PASS" +else + echo "STRESS RESULT: FAIL (rc=$RC)" + sed -n '/^NOT OK/p' "$OUTD/16-stress.sh.log" | sed 's/^/ /' +fi +exit $RC diff --git a/tests/vm/16-stress.sh b/tests/vm/16-stress.sh new file mode 100755 index 0000000..5b57dd6 --- /dev/null +++ b/tests/vm/16-stress.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env bash +# M7 gate: stress — the PF engine under sustained load. +# +# Unlike the functional gates (01-15), this one does not ask "does the packet +# arrive?"; it asks "does the engine stay correct, bounded and leak-free while +# traffic, state, rules and tables churn?" Phases: +# +# 0 baseline throughput cli->srv at a bounded rate with the module OUT +# of the path +# 1 overhead same traffic with PF enabled + stateful rules +# 2 state churn 300 concurrent TCP connections; states must appear and +# must vanish again (pfctl -F s) -> no state-table leak +# 3 rule churn 200 reloads of a 200-rule ruleset, timed on the firewall +# 4 table churn 2000 pfr table entries added and flushed +# 5 integrity 2 MB through NAT *while* a concurrent stream runs; the +# payload must arrive byte-identical (sha256) +# 6 health no BUG/WARNING/Oops, counters move, firewall still routes +# +# Every phase prints `STRESS key=value ...` lines; scripts/stress/run-stress.sh +# turns them into a report. Thresholds are overridable from the environment +# (STRESS_SECS, STRESS_CONNS, STRESS_RELOADS, STRESS_TABLE_ENTRIES, ...). +set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh + +SECS=${STRESS_SECS:-10} +PAR=${STRESS_PARALLEL:-4} +RATE=${STRESS_RATE:-100M} +CONNS=${STRESS_CONNS:-300} +RELOADS=${STRESS_RELOADS:-200} +RULES=${STRESS_RULES:-200} +TENTRIES=${STRESS_TABLE_ENTRIES:-2000} +HOLD=${STRESS_HOLD:-6} +MAXOH=${STRESS_MAX_OVERHEAD_PCT:-30} + +# The traffic phases run at a BOUNDED rate ($RATE per stream). This is not +# cosmetic: PF's data path steals every skb onto one ordered workqueue +# (see docs/*/caveats.md §1), so the backlog — and with it the guest's +# memory — grows with the offered rate. Measured on the lab (1 GB guest): +# 400 Mbit/s is stable, ~1 Gbit/s OOM-panics the guest. The stress test +# therefore works in the regime the port survives and reports the ceiling +# separately, instead of killing the firewall under test. + +HELPER=$(pfput ./stress-helper.sh) || { bad "helper upload"; finish; } +pf0 "chmod +x $HELPER" >/dev/null + +EGRESS_DEV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2") +[ -n "$EGRESS_DEV" ] || EGRESS_DEV=eth2 +EGRESS_IP=$(pf0 "ip -4 addr show $EGRESS_DEV | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2") +[ -n "$EGRESS_IP" ] || EGRESS_IP=192.168.122.8 + +# --- helpers --------------------------------------------------------------- + +# iperf3 throughput cli->srv, in Mbit/s (one decimal is plenty) +iperf_mbit() { + local out line val unit + out=$(cli "iperf3 -c $SRV_IP -t $1 -P $PAR -b $RATE -i 0 2>/dev/null") + line=$(printf '%s\n' "$out" | grep '\[SUM\]' | tail -1) + [ -n "$line" ] || { echo 0; return; } + val=$(printf '%s\n' "$line" | sed -n 's/.*[^0-9.]\([0-9][0-9.]*\) \([KMG]\?bits\/sec\).*/\1/p') + unit=$(printf '%s\n' "$line" | sed -n 's/.* \([KMG]\?bits\/sec\).*/\1/p') + [ -n "$val" ] || { echo 0; return; } + awk -v v="$val" -v u="$unit" 'BEGIN{ + if (u ~ /^G/) printf "%.1f\n", v * 1000; + else if (u ~ /^M/) printf "%.1f\n", v; + else if (u ~ /^K/) printf "%.1f\n", v / 1000; + else printf "%.3f\n", v / 1000000; }' +} +# `pkill -x iperf3` matches the process *name* only. Never use `pkill -f` +# with a pattern that also appears in this very command line: it kills the +# invoking shell (see the note in 06-rdr.sh). +iperf_up() { srv 'pkill -x iperf3 2>/dev/null; iperf3 -s -D >/dev/null 2>&1; sleep 1; pgrep -x iperf3 >/dev/null || echo IPERF_SERVER_DOWN' >/dev/null; } + +# --- phase 0: baseline (module out of the path) ---------------------------- +pf0 'rmmod pf 2>/dev/null; true' >/dev/null +iperf_up +BASE=$(iperf_mbit "$SECS") +BASE=$(printf '%s' "$BASE" | cut -d. -f1) +if [ "${BASE:-0}" -gt 0 ]; then + ok "baseline throughput ${BASE} Mbit/s (module unloaded)" +else + bad "baseline throughput unavailable (iperf3 cli->srv failed)" + echo "STRESS phase=baseline baseline_mbit=0 seconds=$SECS parallel=$PAR rate_per_stream=$RATE" + finish +fi +echo "STRESS phase=baseline baseline_mbit=$BASE seconds=$SECS parallel=$PAR rate_per_stream=$RATE" + +# --- phase 1: same traffic with PF enabled --------------------------------- +pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true' >/dev/null +pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null + +cat > /tmp/g16.conf < index.html +for pid in $(ls /proc 2>/dev/null | grep -E '^[0-9]+$'); do + [ "$pid" = "$$" ] && continue + cmd=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null) || continue + case "$cmd" in + *stress-loop*|*http.server*) kill "$pid" 2>/dev/null ;; + esac +done +if [ -f /tmp/stress-httpd.pid ]; then kill $(cat /tmp/stress-httpd.pid) 2>/dev/null; rm -f /tmp/stress-httpd.pid; fi +setsid sh -c 'echo $$ > /tmp/stress-httpd.pid; exec python3 -m http.server 9097 --bind 0.0.0.0' >/dev/null 2>&1 /tmp/stress-setup.sh; sh /tmp/stress-setup.sh" >/dev/null +if srv 'netstat -lnt 2>/dev/null | grep -q ":9097 "' >/dev/null 2>&1; then + ok "churn listener up on srv:9097" +else + bad "churn listener NOT listening on srv:9097 (stale listener holding the port?)" + finish +fi +RULESET=$(pf0 '/opt/pf/pfctl -s rules | wc -l') +PFSTAT=$(pf0 '/opt/pf/pfctl -s info | head -1') +pf0 "/opt/pf/pfctl -F s" >/dev/null +S0=$(pf0 "$HELPER states") +# The connections must stay open while the states are counted: a peer that +# closes right away tears the state down before it can be observed. +# The iteration list is expanded HERE, on the build host: a `$(seq ...)` +# left in the command string would be expanded by the *intermediate* host +# (deb), whose newline-separated output breaks the `for` loop one hop later. +RANGE=$(seq -s' ' 1 "$CONNS") +cli "bash -c 'for i in $RANGE; do (exec 3<>/dev/tcp/$SRV_IP/9097; sleep $HOLD) 2>/dev/null & done; wait'" >/dev/null 2>&1 & +CLIPID=$! +sleep 4 +S1=$(pf0 "$HELPER states") +EST=$(srv 'netstat -tn 2>/dev/null | grep -c ":9097 "') +wait $CLIPID 2>/dev/null +if [ "${S1:-0}" -ge $((CONNS / 2)) ]; then + ok "state churn: $S1 states tracked for $CONNS connections" +else + bad "state churn: only $S1 states for $CONNS connections" +fi +S2=$(pf0 "$HELPER state-reset") +sleep 1 +S3=$(pf0 "$HELPER states") +if [ "${S3:-0}" -le "${S0:-0}" ]; then + ok "state table drained after reset ($S0 -> $S3)" +else + bad "state table did not drain ($S0 -> $S3)" +fi +echo "STRESS phase=state_churn connections=$CONNS states_before=$S0 states_created=$S1 states_after_reset=$S3 established_on_srv=${EST:-0} rules_loaded=$RULESET pf_status=$(printf '%s' "$PFSTAT" | tr -s ' ' | cut -d' ' -f2-)" + +# --- phase 3: rule churn --------------------------------------------------- +{ + echo "table { }" + echo "pass in quick proto tcp from $CLI_IP to $SRV_IP keep state" + echo "pass out quick keep state" + i=1 + while [ "$i" -le "$RULES" ]; do + # a distinct destination port per rule: rules with the same shape + # are folded by pfctl's optimizer into one automatic table, which + # would make the rule count meaningless + echo "block drop in quick proto tcp from 10.100.$((i % 250)).$((i % 250 + 1)) to any port $((1000 + i))" + i=$((i + 1)) + done +} > /tmp/g16-churn.conf +CH=$(pfput /tmp/g16-churn.conf) || { bad "churn ruleset upload"; finish; } +pf0 "/opt/pf/pfctl -f $CH" >/dev/null || { bad "churn ruleset load"; finish; } +R0=$(pf0 "$HELPER rules") +RL=$(pf0 "$HELPER reload-loop $RELOADS $CH") +eval "$RL" +if [ "${reload_fails:-1}" -eq 0 ]; then + ok "rule churn: $RELOADS reloads, no failure (avg ${reload_ms_avg:-?} ms, max ${reload_ms_max:-?} ms)" +else + bad "rule churn: ${reload_fails:-?} of $RELOADS reloads failed" +fi +R1=$(pf0 "$HELPER rules") +if [ "${R1:-0}" -eq "${R0:-0}" ] && [ "${R1:-0}" -eq "$((RULES + 2))" ]; then + ok "rule count stable across churn ($R1 rules)" +else + bad "rule count drifted: $R0 -> $R1 (expected $((RULES + 2)))" +fi +echo "STRESS phase=rule_churn reloads=$RELOADS rules_per_load=$RULES reload_fails=${reload_fails:-0} reload_ms_avg=${reload_ms_avg:-0} reload_ms_max=${reload_ms_max:-0} rules_final=$R1" + +# --- phase 4: table churn -------------------------------------------------- +TC=$(pf0 "$HELPER table-churn $TENTRIES stress") +eval "$TC" +if [ "${table_add_errors:-1}" -eq 0 ] && [ "${table_entries_after_add:-0}" -ge $((TENTRIES / 2)) ]; then + ok "table churn: ${table_entries_after_add} entries added" +else + bad "table churn: ${table_entries_after_add:-0} entries, ${table_add_errors:-?} errors" +fi +if [ "${table_entries_after_flush:-1}" -eq 0 ]; then + ok "table flushed back to 0 entries" +else + bad "table not empty after flush (${table_entries_after_flush})" +fi +echo "STRESS phase=table_churn entries_requested=$TENTRIES entries_added=${table_entries_after_add:-0} add_errors=${table_add_errors:-0} entries_after_flush=${table_entries_after_flush:-0}" + +# --- phase 5: integrity under load ----------------------------------------- +cat > /tmp/g16-nat.conf </dev/null; head -c 2000000 /dev/zero | tr "\0" A > /tmp/stress.bin; sleep 1; nohup setsid nc -l -p 9099 < /tmp/stress.bin >/dev/null 2>&1 & sleep 1' >/dev/null +SRC_SUM=$(srv 'sha256sum /tmp/stress.bin | cut -c1-64' | tr -d '[:space:]') +# saturating background stream while the payload crosses PF +cli "iperf3 -c $SRV_IP -t 15 -P $PAR -b $RATE >/dev/null 2>&1 &" >/dev/null 2>&1 +sleep 1 +GOT=$(cli 'nc -w20 10.0.2.20 9099 | sha256sum | cut -c1-64' | tr -d '[:space:]') +if [ -n "$SRC_SUM" ] && [ "$GOT" = "$SRC_SUM" ]; then + ok "2 MB through NAT intact under load (sha256 match)" + INTEG=yes +else + bad "payload corrupted under load (src=$SRC_SUM got=$GOT)" + INTEG=no +fi +echo "STRESS phase=integrity bytes=$SZ sha256_match=$INTEG" + +# --- phase 6: health ------------------------------------------------------- +ANOM=$(pf0 "$HELPER dmesg") +if [ -z "$ANOM" ]; then + ok "no kernel BUG/WARN/Oops after the stress run" +else + bad "dmesg anomalies:" + printf '%s\n' "$ANOM" | sed 's/^/ /' +fi +MATCHED=$(pf0 "$HELPER counter match") +ENTRIES=$(pf0 "$HELPER state-entries") +[ "${MATCHED:-0}" -gt 0 ] && ok "PF counters moved (match=${MATCHED:-0} state_entries=${ENTRIES:-0})" \ + || bad "PF counters did not move (match=${MATCHED:-0})" +cli "ping -c 3 -W 2 $SRV_IP >/dev/null 2>&1" && ok "firewall still routes traffic after stress" \ + || bad "firewall stopped routing traffic after stress" +echo "STRESS phase=health dmesg_anomalies=$(printf '%s' "$ANOM" | grep -c . ) match=${MATCHED:-0} state_entries=${ENTRIES:-0}" + +# --- cleanup --------------------------------------------------------------- +CLEAN=$(cat <<'EOS' | base64 | tr -d '\n' +if [ -f /tmp/stress-httpd.pid ]; then kill $(cat /tmp/stress-httpd.pid) 2>/dev/null; rm -f /tmp/stress-httpd.pid; fi +for pid in $(ls /proc 2>/dev/null | grep -E '^[0-9]+$'); do + [ "$pid" = "$$" ] && continue + cmd=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null) || continue + case "$cmd" in + *http.server*) kill "$pid" 2>/dev/null ;; + esac +done +pkill -x nc 2>/dev/null +pkill -x iperf3 2>/dev/null +true +EOS +) +srv "echo $CLEAN | base64 -d > /tmp/stress-clean.sh; sh /tmp/stress-clean.sh" >/dev/null +cli 'pkill -x iperf3 2>/dev/null; true' >/dev/null +pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null +finish diff --git a/tests/vm/stress-helper.sh b/tests/vm/stress-helper.sh new file mode 100755 index 0000000..e7fea57 --- /dev/null +++ b/tests/vm/stress-helper.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# Helper executed ON pf0 by 16-stress.sh (uploaded with pfput to avoid quoting +# hell through two ssh hops). POSIX sh: the guest is Alpine (busybox ash). +# +# $1 = "slab" | "dmesg" | "states" | "state-reset" | "rules" | "info" +# | "reload-loop " | "table-churn " +# +# Every subcommand prints machine-readable KEY=VALUE pairs on stdout so the +# gate can parse them without another ssh round trip. +PFCTL=/opt/pf/pfctl + +now_ms() { awk '{printf "%d", $1 * 1000}' /proc/uptime; } + +case "${1:-}" in +slab) + grep -E '^(pf|pfk)' /proc/slabinfo | tr -s ' ' | cut -d' ' -f1,3 | sort + ;; +dmesg) + # the CPU's Speculative Return Stack Overflow advisory is a kernel + # build/CPU feature notice, not a fault of this module + dmesg | grep -E 'BUG:|WARNING:|Oops|general protection|kernel BUG' \ + | grep -v 'Speculative Return Stack Overflow' || true + ;; +states) + # `pfctl -s states` prints one line per direction, so count the + # initiator lines ("->") to get the number of states + $PFCTL -s states 2>/dev/null | grep -c ' -> ' || true + ;; +state-reset) + # -F s clears the state table (-s s only *shows* it) + $PFCTL -F s >/dev/null 2>&1 || true + $PFCTL -s states 2>/dev/null | grep -c ' -> ' || true + ;; +rules) + $PFCTL -s rules 2>/dev/null | grep -c . || true + ;; +info) + $PFCTL -s info 2>/dev/null + ;; +counter) + # $2 = counter name as printed by `pfctl -s info` (match, short, + # state-mismatch, ...). Prints the value, or 0 when absent. + $PFCTL -s info 2>/dev/null | awk -v k="${2:-match}" '$1 == k { print $2; exit }' + ;; +state-entries) + # the number of entries the state table currently holds + $PFCTL -s info 2>/dev/null | awk '/current entries/ { print $NF; exit }' + ;; +reload-loop) + # $2 = iterations, $3 = ruleset file. Times each `pfctl -f` on the + # firewall itself (no ssh overhead in the measurement). + n="${2:-0}"; f="${3:-/tmp/g16-churn.conf}" + i=0; fails=0; min=999999999; max=0; sum=0 + while [ "$i" -lt "$n" ]; do + s=$(now_ms) + if ! $PFCTL -f "$f" >/dev/null 2>&1; then + fails=$((fails + 1)) + fi + e=$(now_ms) + d=$((e - s)); [ "$d" -lt 0 ] && d=0 + sum=$((sum + d)) + [ "$d" -lt "$min" ] && min=$d + [ "$d" -gt "$max" ] && max=$d + i=$((i + 1)) + done + [ "$min" = 999999999 ] && min=0 + avg=0; [ "$n" -gt 0 ] && avg=$((sum / n)) + echo "reload_iterations=$n reload_fails=$fails reload_ms_min=$min reload_ms_avg=$avg reload_ms_max=$max" + ;; +table-churn) + # $2 = number of addresses, $3 = table name. Adds N distinct addresses, + # counts the table, flushes it and counts again. + n="${2:-0}"; t="${3:-stress}" + $PFCTL -t "$t" -T flush >/dev/null 2>&1 || true + i=1; err=0 + while [ "$i" -le "$n" ]; do + # 10.0.. keeps every entry distinct without touching the + # lab's real segments (10.0.1.0/24 lan, 10.0.2.0/24 wan) + a=$(( (i - 1) / 250 + 100 )) + b=$(( (i - 1) % 250 + 1 )) + if ! $PFCTL -t "$t" -T add "10.$a.$b" >/dev/null 2>&1; then + err=$((err + 1)) + fi + i=$((i + 1)) + done + added=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true) + $PFCTL -t "$t" -T flush >/dev/null 2>&1 || true + left=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true) + if [ "${left:-0}" -ne 0 ]; then + # flush did not empty the table: fall back to deleting every entry + $PFCTL -t "$t" -T show 2>/dev/null | awk '{print $1}' | \ + while read -r a; do $PFCTL -t "$t" -T delete "$a" >/dev/null 2>&1 || true; done + left=$($PFCTL -t "$t" -T show 2>/dev/null | grep -c . || true) + fi + echo "table_entries_requested=$n table_add_errors=$err table_entries_after_add=$added table_entries_after_flush=$left" + ;; +*) + echo "unknown helper command: ${1:-}" >&2 + exit 2 + ;; +esac