#!/bin/sh # ansipa_opnsense — CheckMK local check for an OPNsense firewall. # # Install: copy to /usr/local/lib/check_mk_agent/local/ansipa_opnsense, chmod 755 # (ansipa-opnsense-checkmk-install.sh does this for you). # # ── Why this is /bin/sh and not bash ──────────────────────────────────────── # OPNsense is FreeBSD. There is no bash in the base system, no GNU grep (so no # `grep -P`), and no `mapfile`. Everything here is POSIX sh + awk/sed, unlike # the sibling ansipa-pve/pbs-monitor.sh scripts which target Debian and may use # bashisms. Do not "modernise" this file with [[ ]] or grep -o -P; it will # silently stop producing output on the firewall. # # Emits (CheckMK local-check format: " "): # Ansipa_OPN_Gateways per-gateway up/down from the gateway status API # Ansipa_OPN_States pf state-table usage against the configured hard limit # Ansipa_OPN_Firmware pending firmware/package updates # Ansipa_OPN_CARP CARP interface health (skipped cleanly if unused) # # CPU / RAM / disk / interface counters are already covered by the stock # FreeBSD agent checks — deliberately not duplicated here. PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin export PATH # ── Gateways ──────────────────────────────────────────────────────────────── # `configctl interface gateways status` returns a JSON blob. There is no jq in # the OPNsense base system, so the objects are split onto separate lines and # read with sed. We count anything not explicitly Online/none as degraded. if command -v configctl >/dev/null 2>&1; then _gw=$(configctl interface gateways status 2>/dev/null) if [ -z "$_gw" ]; then echo "3 Ansipa_OPN_Gateways - could not query gateway status" else # One JSON object per line, then pull name + status_translated per line. _parsed=$(printf '%s' "$_gw" | sed 's/},[[:space:]]*{/}\ {/g') _total=$(echo "$_parsed" | grep -c '"name"' 2>/dev/null || echo 0) _downlist=$(echo "$_parsed" \ | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*"status_translated"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1=\2/p' \ | grep -v '=Online$' | tr '\n' ' ') _down=$(printf '%s' "$_downlist" | wc -w | tr -d ' ') if [ "${_total:-0}" -eq 0 ]; then echo "3 Ansipa_OPN_Gateways - no gateways found in status output" elif [ "${_down:-0}" -gt 0 ]; then echo "2 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=${_down};1;1 ${_down}/${_total} gateway(s) not online: ${_downlist}" else echo "0 Ansipa_OPN_Gateways gateways_total=${_total};; gateways_down=0;1;1 all ${_total} gateway(s) online" fi fi else echo "3 Ansipa_OPN_Gateways - configctl not found (not an OPNsense host?)" fi # ── pf state table ────────────────────────────────────────────────────────── # A firewall that hits its state limit drops new connections while looking # perfectly healthy on CPU/RAM, so this is the check that actually catches it. if command -v pfctl >/dev/null 2>&1; then _cur=$(pfctl -si 2>/dev/null | awk '/current entries/ {print $3; exit}') _lim=$(pfctl -sm 2>/dev/null | awk '/^states/ {print $4; exit}') if [ -z "$_cur" ] || [ -z "$_lim" ] || [ "${_lim:-0}" -eq 0 ] 2>/dev/null; then echo "3 Ansipa_OPN_States - could not read pf state table counters" else _pct=$(( _cur * 100 / _lim )) _perf="states=${_cur};$(( _lim * 80 / 100 ));$(( _lim * 90 / 100 ));0;${_lim} states_pct=${_pct}%;80;90" if [ "$_pct" -ge 90 ]; then echo "2 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%) — near the hard limit" elif [ "$_pct" -ge 80 ]; then echo "1 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)" else echo "0 Ansipa_OPN_States ${_perf} pf states ${_cur}/${_lim} (${_pct}%)" fi fi else echo "3 Ansipa_OPN_States - pfctl not found" fi # ── Firmware / package updates ────────────────────────────────────────────── # Read the cached firmware status rather than triggering a check: this runs # every agent poll, and hitting the mirrors that often is antisocial and slow. # `configctl firmware check` refreshes the cache; a cron job on the firewall # should do that once a day (the installer sets one up). _fw_status=/tmp/pkg_upgrade.progress if command -v opnsense-version >/dev/null 2>&1; then _pending="" if [ -r /var/cache/opnsense-update/changelog/firmware.json ]; then _pending=$(sed -n 's/.*"new_packages_count"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9]*\).*/\1/p' \ /var/cache/opnsense-update/changelog/firmware.json 2>/dev/null | head -1) fi if [ -z "$_pending" ] && command -v pkg >/dev/null 2>&1; then # Offline count from the local package DB — no network access. _pending=$(pkg version -vIL= 2>/dev/null | wc -l | tr -d ' ') fi _ver=$(opnsense-version 2>/dev/null | head -1) if [ -z "$_pending" ]; then echo "3 Ansipa_OPN_Firmware - could not determine pending update count (${_ver})" elif [ "$_pending" -gt 0 ] 2>/dev/null; then echo "1 Ansipa_OPN_Firmware pending_updates=${_pending};1;;0; ${_pending} update(s) pending — ${_ver}" else echo "0 Ansipa_OPN_Firmware pending_updates=0;1;;0; up to date — ${_ver}" fi else echo "3 Ansipa_OPN_Firmware - opnsense-version not found" fi unset _fw_status # ── CARP (high availability) ──────────────────────────────────────────────── # Only meaningful on an HA pair. On a standalone firewall there are no carp # interfaces and we stay silent rather than emitting a permanent UNKNOWN. if command -v ifconfig >/dev/null 2>&1; then _carp=$(ifconfig 2>/dev/null | grep -c ' carp:' 2>/dev/null || echo 0) if [ "${_carp:-0}" -gt 0 ]; then _demote=$(sysctl -n net.inet.carp.demotion 2>/dev/null || echo 0) _init=$(ifconfig 2>/dev/null | grep ' carp:' | grep -c 'INIT' || echo 0) if [ "${_init:-0}" -gt 0 ]; then echo "2 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; ${_init} CARP interface(s) in INIT state" elif [ "${_demote:-0}" -ne 0 ]; then echo "1 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=${_demote};; CARP demotion factor is ${_demote} (not 0)" else echo "0 Ansipa_OPN_CARP carp_ifaces=${_carp};; carp_demotion=0;; ${_carp} CARP interface(s) healthy" fi fi fi