#!/bin/sh # checks/pf.sh # pf health summary (enabled, state table usage, rule count), plus the # actual active filter and nat/rdr rules listed as rows below. The rule # rows use status "info" (a neutral gray square) rather than "ok" - # they're informational, not a health signal, and a green square there # would wrongly imply each individual rule was itself being health- # checked. set -eu DIR=$(dirname "$0") . "$DIR/../lib/common.sh" # abbreviate_rule RULE # Best-effort compression of pfctl's verbose rule syntax so rows fit a # narrow column without wrapping mid-word. Not a semantic parser - just # strips/replaces the tokens that are near-universal boilerplate on this # ruleset (quick, flags S/SA, proto/from-to-port verbosity) and swaps # in/out for arrows. Uses only the Arrows block (confirmed present in # the scientifica font); avoids Dingbats (✓/✗) since that block isn't # shipped. A rule shape this doesn't recognize just passes through # unshortened - still correct, just longer. abbreviate_rule() { printf '%s' "$1" | sed -E \ -e 's/ in / → /' \ -e 's/ out / ← /' \ -e 's/ quick//' \ -e 's/ on / /' \ -e 's/ flags [A-Za-z\/]+//' \ -e 's/ modulate state/ [state]/' \ -e 's/ keep state/ [state]/' \ -e 's/ proto (tcp|udp|icmp)/ \1/' \ -e 's/ from any to any port = ([a-zA-Z0-9]+)/ :\1/' \ -e 's/ from any to any/ /' \ -e 's/ from ([^ ]+) to ([^ ]+)/ \1 → \2/' \ -e 's/ inet / /' \ -e 's/ round-robin//' \ -e 's/ -> / → /' \ -e 's/ all$//' \ -e 's/ all / /' \ -e 's/ +/ /g' \ -e 's/^ +//' -e 's/ +$//' } INFO=$(pfctl -si 2>/dev/null) || INFO="" ENABLED=$(printf '%s\n' "$INFO" | awk -F'[ :]+' '/^Status:/{print $2; exit}') if [ "$ENABLED" = "Enabled" ]; then json_line "pf" "pf" "" "ok" else json_line "pf" "pf" "${ENABLED:-unknown}" "down" fi CURRENT=$(printf '%s\n' "$INFO" | awk '/current entries/{print $NF; exit}') LIMIT=$(pfctl -sm 2>/dev/null | awk '/^states/{print $NF; exit}') if [ -n "$CURRENT" ] && [ -n "$LIMIT" ] && [ "$LIMIT" -gt 0 ] 2>/dev/null; then PCT=$((CURRENT * 100 / LIMIT)) if [ "$PCT" -ge 90 ]; then SSTATUS=down elif [ "$PCT" -ge 75 ]; then SSTATUS=warn else SSTATUS=ok fi json_line "pf" "state table" "${CURRENT}/${LIMIT} (${PCT}%)" "$SSTATUS" fi RULE_COUNT=$(pfctl -sr 2>/dev/null | grep -c .) || RULE_COUNT=0 json_line "pf" "filter rules loaded" "$RULE_COUNT" "ok" # Group rules by interface (then in before out) rather than pfctl's raw # eval order, which is just how the ruleset happens to be authored and # reads as arbitrarily interleaved (e.g. an "out" rule sandwiched between # unrelated "in" rules for the same interface). This is a display-only # reorder - pf's actual evaluation order (and quick/first-match # semantics) is untouched, only pfctl -sr's raw output is re-sorted here. TAB=$(printf '\t') pfctl -sr 2>/dev/null | awk -v OFS="$TAB" ' { iface = "" n = split($0, w, " ") for (i = 1; i <= n; i++) { if (w[i] == "on" && i < n) { iface = w[i + 1]; break } } dir = "2" if ($0 ~ / in /) dir = "0" else if ($0 ~ / out /) dir = "1" print iface, dir, $0 }' | sort -t "$TAB" -k1,1 -k2,2 -k3 | cut -f3- | while IFS= read -r rule; do [ -n "$rule" ] || continue json_line "pf" " $(abbreviate_rule "$rule")" "" "info" done pfctl -sn 2>/dev/null | while IFS= read -r rule; do [ -n "$rule" ] || continue json_line "pf" " $(abbreviate_rule "$rule")" "" "info" done