From bdfeca60a665247a2f54fdee141d173458e55481 Mon Sep 17 00:00:00 2001 From: ltning Date: Thu, 22 May 2025 02:10:50 +0200 Subject: [PATCH] Initial commit --- README.md | 25 +++++++ pfctl_exporter.sh | 163 +++++++++++++++++++++++++++++++++++++++++++ pfctl_maintenance.sh | 41 +++++++++++ 3 files changed, 229 insertions(+) create mode 100644 README.md create mode 100644 pfctl_exporter.sh create mode 100644 pfctl_maintenance.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..a479c2e --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Script to produce Prometheus-style `.prom` files from `pfctl(8)` output. +By default places output in `/var/tmp/node_exporter`, for the benefit of `node_exporter`. + +Currently only collects data from running jails (not the host itself). + +## Assumptions +The script first checks for the existence of `/dev/pf`. + +Jails with a `/dev/pf` have paths either of the form + /x/y +or + /x/y/root + +Both cases are checked, and if `/sbin/pfctl` and `/libexec/ld-elf.so.1` are found, they are used as-is. + +If not, the former pattern is expected to be the parent of a wrapped jail, with no libraries or binaries in the regular paths. +The inner jail is expected to have a path prefix of /root relative to the parent; In such cases, the script sets `LD_LIBRARY_PATH` +to `/root/lib` and invokes `/root/libexec/ld-elf.so.1` to execute `/root/sbin/pfctl`. + +## Work to be done +- Be more flexible about jail path layouts +- Support collecting pf stats for the host +- Collect per-rule statistics +- Collect per-table statistics + diff --git a/pfctl_exporter.sh b/pfctl_exporter.sh new file mode 100644 index 0000000..391d25c --- /dev/null +++ b/pfctl_exporter.sh @@ -0,0 +1,163 @@ +#!/bin/sh +set -e +sleeptime='10' +tmpfile='/var/tmp/node_exporter/.pfctl_exporter.prom.tmp' +dstfile='/var/tmp/node_exporter/pfctl_exporter.prom' +counters_pat='/var/tmp/node_exporter/.counters.pat' +tracking_pat='/var/tmp/node_exporter/.tracking.pat' + +create_patterns() { + echo x - ${counters_pat} + sed -E 's/^ *X//' >${counters_pat} << ' COUNTERS' + X^match .* + X^bad_offset .* + X^fragment .* + X^short .* + X^normalize .* + X^memory .* + X^bad_timestamp .* + X^congestion .* + X^ip_option .* + X^proto_cksum .* + X^state_mismatch .* + X^state_insert .* + X^state_limit .* + X^src_limit .* + X^synproxy .* + X^map_failed .* + X^max_states_per_rule .* + X^max_src_states .* + X^max_src_nodes .* + X^max_src_conn .* + X^max_src_conn_rate .* + X^overload_table_insertion .* + X^overload_flush_states .* + X^synfloods_detected .* + COUNTERS + + echo x - ${tracking_pat} + sed -E 's/^ *X//' >${tracking_pat} << ' TRACKING' + X^current_entries .* + X^searches .* + X^inserts .* + X^removals .* + TRACKING +} + +startup() { + local jail="$1" + + if [ -z "${jail}" ] || ! jpath=$(jls -j "${jail}" path 2>/dev/null) ; then + echo "Jail not specified or not running" 1>&2 + return 1 + fi + + if ! [ -c "${jpath}/dev/pf" ] ; then + echo "No /dev/pf in ${jpath}!" 1>&2 + return 1 + fi + + if [ -s "${jpath}/sbin/pfctl" ] && + [ -s "{jpath}/libexec/ld-elf.so.1" ] ; then + J_LD_LIBRARY_PATH='/lib' + pfctl='/sbin/pfctl' + elif [ "${jpath%%/root}" = "$jpath" ] && + [ -s "${jpath}/root/sbin/pfctl" ] && + [ -s "{jpath}/root/libexec/ld-elf.so.1" ]; then + J_LD_LIBRARY_PATH='/root/lib' + pfctl='/root/libexec/ld-elf.so.1 /root/sbin/pfctl' + else + echo "pfctl binary and/or supporting libraries not found in ${jpath}." + return 1 + fi +} + +jail_collect_stats() { + local jail="$1" + LD_LIBRARY_PATH="${J_LD_LIBRARY_PATH}" jexec "${jail}" $pfctl -vvsi 2>/dev/null | \ + tr '-' '_' | \ + /usr/bin/sed -E -e 's/([^ ]) ([^ ])/\1_\2/g' -e 's/^ *//g' | \ + cut -f 1,2 -w | \ + tr '\t' ' ' +} + +jail_parse_stats() { + local jail="$1" + local stats="$2" + + # State tracking table + IFS="$(printf "\n\r")" + for l in $(echo "$stats" | grep -f ${tracking_pat} | head -"$(wc -l < ${tracking_pat} | tr -d ' ')") ; do + unset IFS + set -- $l + name=$1 ; value=$2 + echo "jail_pf_states_${name}{name=\"${jail}\"} ${value}" + done + + # Source tracking table + IFS="$(printf "\n\r")" + for l in $(echo "$stats" | grep -f ${tracking_pat} | tail -"$(wc -l < ${tracking_pat} | tr -d ' ')") ; do + unset IFS + set -- $l + name=$1 ; value=$2 + echo "jail_pf_sources_${name}{name=\"${jail}\"} ${value}" + done + + # Syncookies + IFS="$(printf "\n\r")" + tags="name=\"${jail}\"" + for l in $(echo "$stats" | grep -E 'highwater|lowwater|mode|active') ; do + unset IFS + set -- $l + name=$1 ; value=${2%%_%} + [ "$name" = 'highwater' ] && name='highwater_pct' + [ "$name" = 'lowwater' ] && name='lowwater_pct' + tags="${tags},${name}=\"${value}\"" + done + IFS="$(printf "\n\r")" + for l in $(echo "$stats" | grep -E 'syncookies|halfopen') ; do + unset IFS + set -- $l + name=${1##syncookies_} ; value=$2 + echo "jail_pf_syncookies_${name}{${tags}} ${value}" + done + + # Counters + IFS="$(printf "\n\r")" + for l in $(echo "$stats" | grep -f ${counters_pat}) ; do + unset IFS + set -- $l + name=$1 ; value=$2 + echo "jail_pf_counters{name=\"${jail}\",counter=\"${name}\"} ${value}" + done +} + +echo "$(date) - Starting up" +create_patterns +if [ ! -s ${tracking_pat} ] || [ ! -s ${counters_pat} ] ; then + echo "Pattern files not found in $(pwd)" + exit 1 +fi + +while sleep "$sleeptime" ; do + for j in $(jls name) ; do + echo "Running for ${j}.." 1>&2 + if startup "$j" ; then + if stats="$(jail_collect_stats "$j")" ; then + jail_parse_stats "$j" "$stats" >> "$tmpfile" + else + echo "$(date) - Failed to collect stats for ${j} - did jail disappear?" + fi + else + echo "Skipping ${j}.." 1>&2 + fi + done + + if [ "$?" -eq 0 ] ; then + mv "$tmpfile" "$dstfile" + else + rm -f "$dstfile" + fi + echo "Sleeping.." 1>&2 +done +rm -f ${counters_pat} ${tracking_pat} diff --git a/pfctl_maintenance.sh b/pfctl_maintenance.sh new file mode 100644 index 0000000..543093f --- /dev/null +++ b/pfctl_maintenance.sh @@ -0,0 +1,41 @@ +#!/bin/sh +set -e + +startup() { + local jail="$1" + + if [ -z "${jail}" ] || ! jpath=$(jls -j "${jail}" path 2>/dev/null) ; then + echo "Jail not specified or not running" 1>&2 + return 1 + fi + + if ! [ -c "${jpath}/dev/pf" ] ; then + echo "No /dev/pf in ${jpath}!" 1>&2 + return 1 + fi + + if [ "${jpath%%/root}" = "$jpath" ] ; then + J_LD_LIBRARY_PATH='/root/lib' + pfctl='/root/libexec/ld-elf.so.1 /root/sbin/pfctl' + else + J_LD_LIBRARY_PATH='/lib' + pfctl='/sbin/pfctl' + fi +} + +jail_expire_table() { + local jail="$1" + local table="$2" + local expiry="$3" + LD_LIBRARY_PATH="${J_LD_LIBRARY_PATH}" jexec "${jail}" $pfctl -T expire "$expiry" -t "$table" +} + +echo "$(date) - Starting up" +for j in $(jls name) ; do + echo "Running for ${j}.." 1>&2 + if startup "$j" ; then + jail_expire_table "$j" "badhosts" 300 + else + echo "Skipping ${j}.." 1>&2 + fi +done