#!/usr/bin/perl -w

# Creates an OID-Tree of pf stats using pfctl
# Built-in Nagios plugin
#
# requires SNMP::Extension::PassPersist from CPAN:
# cpan SNMP::Extension::PassPersist
#
# add to your snmpd.conf
# pass_persist .1.3.6.1.4.1.2021.55 /usr/bin/perl /usr/local/bin/pf-stats-snmp.pl
#
# in addition add "set loginterface $interface" to your pf.conf
#
# Helmut Schneider <jumper99@gmx.de>
# v2010042101

use strict;
use warnings;

use SNMP::Extension::PassPersist;
use Getopt::Long;
use Data::Dumper;

my $DEBUG = 0;
my $COUNTER64 = 0; # Set to 1 if your snmpd provides 64bit counters

# Nagios plugin
my $blockWarnThreshold = 1; # When to throw WARNING
my $blockCritThreshold = 5; # When to throw CRITICAL
my $statesWarnThreshold = 75; # % When to throw WARNING
my $statesCritThreshold = 90; # % When to throw CRITICAL

# Blocking tables to exclude from displaying
# See http://www.openbsd.org/faq/pf/filter.html#stateopts
my $critExclusions = [ "smtp_bruteforce", "ssh_bruteforce" ];

my $instances = 1;

my $options;
my $version;

sub usage()
{
        print <<"EOF";
usage: $0 \$options

  -C|community          : SNMP Community
  -v|snmpversion        : SNMP Version
  -Ob|oidBase           : Base OID tree
  -V|Version            : Print version
  -h|help               : This help
EOF
        exit;
}

### Check command line Options ###
Getopt::Long::Configure("no_ignore_case");
GetOptions("v|verbose"=>\$$options{verbose},
	"q|quiet"=>\$$options{quiet},
	"d|debug"=>\$$options{debug},
	"Ob|oidbase:s"=>\$$options{oidbase},
	"V|Version"=>\$$options{version},
	"h|help"=>\$$options{help},
) or die usage();
if ($$options{help}) {
	usage();
}
if ($$options{version}) {
	print "$version\n";
	exit 0;
}
if (!$$options{oidbase}) {
	print "-Ob|oidbase is required!\n" . Dumper($options) . "\n";
	usage();
}
if ($$options{debug}) {
	$$options{verbose} = 1;
	$DEBUG = 1;
}

my $PFSTATS = "/sbin/pfctl -s info";
my $PFTABLES = "/sbin/pfctl -s Tables";
my $PFRULES = "/sbin/pfctl -s rules";
my $PFLIMITS = "/sbin/pfctl -s memory";

# pass_persist handler setup
my $extsnmp_stats = SNMP::Extension::PassPersist->new(
	backend_collect => \&process_stats,
	idle_count      => 30,     # no more than 60 idle cycles
	refresh         => 60,     # refresh every 60 sec
);

sub process_stats {
	my $oidStatus = $$options{oidbase} . ".1"; # pf active?
	my $oidFlow = $$options{oidbase} . ".2"; # Flow counters
	my $oidStates = $$options{oidbase} . ".3"; # Stateful table operations
	my $oidBlocks = $$options{oidbase} . ".4"; # Currently blocked hosts
	my $oidFlowBytes = $oidFlow . ".1"; # Byte counters
	my $oidFlowPackets = $oidFlow . ".2"; # Packet counters
	my $oidStatesEntries = $oidStates . ".2"; # Stateful table operations
	my $oidStatesCounters = $oidStates . ".3"; # Stateful table counters
	my $oidStatesLimits = $oidStates . ".4"; # Stateful table limits

	my ($statesCounter, $statesLimit, $tables, $table, $entry);
	my ($statusIndex, $flowIndex, $flowBytesIndex, $flowPacketsIndex, $statesEntriesIndex, $statesCountersIndex, $statesLimitsIndex, $blocksIndex);
	$statusIndex = $flowIndex = $flowBytesIndex = $flowPacketsIndex = $statesEntriesIndex = $statesCountersIndex = $statesLimitsIndex = $blocksIndex = 1;

	### Get Stats
	my $oidHash;
	my ($interface, $prevLine);

	# Dummy entry, see http://rt.cpan.org/Public/Bug/Display.html?id=43579
	$$oidHash{$oidStatus . '.0'} = [ "integer" => 0 ];
	
	foreach my $line (`$PFSTATS`) {

		$line =~ s/  +/|/g;
		$line =~ s/^\|//g;
		chomp($line);
		print "\$line: $line\n" if ($DEBUG);

		### Nagios Plugin to check if pf is vital

		if ($line =~ /Status: Disabled/) {
			$$oidHash{$oidStatus . '.1.1.1'} = [ "integer" => 1 ];
			$$oidHash{$oidStatus . '.1.2.1'} = [ "string" => "CRITICAL: pf disabled!!" ];
			$$oidHash{$oidStatus . '.1.3.1'} = [ "counter" => 2 ];
			next;
		} elsif ($line =~ /Status: Enabled/) {
			my ($uptime) = split(/\|/, $line);
			$uptime =~ s/Status: Enabled for //g;
			chomp($uptime);
			unless (`$PFRULES`) {
				$$oidHash{$oidStatus . '.1.1.1'} = [ "integer" => 1 ];
				$$oidHash{$oidStatus . '.1.2.1'} = [ "string" => "WARNING: pf enbabled for $uptime but no rules loaded!" ];
				$$oidHash{$oidStatus . '.1.3.1'} = [ "counter" => 1 ];
			} else {
				my @rulesCount = `$PFRULES`;
				$$oidHash{$oidStatus . '.1.1.1'} = [ "integer" => 1 ];
				$$oidHash{$oidStatus . '.1.2.1'} = [ "string" => "OK: pf enabled for $uptime, " . @rulesCount . " rules loaded." ];
				$$oidHash{$oidStatus . '.1.3.1'} = [ "counter" => 0 ];
			}
			next;


		### Get the log interface

		} elsif ($line =~ m/Interface Stats for/) {
			my ($desc) = split(/\|/, $line);
			$desc =~ s/Interface Stats for //g;
			$interface = $desc;

			$$oidHash{$oidFlowBytes . "." . $flowBytesIndex . "." . $instances} = [ "integer" => 1 ];
			$$oidHash{$oidFlowBytes . "." . ($flowBytesIndex + 1) . "." . $instances} = [ "string" => $interface ];
			$$oidHash{$oidFlowBytes . "." . ($flowBytesIndex + 2) . "." . $instances} = [ "string" => "Byte Counters" ];
			$$oidHash{$oidFlowPackets . "." . $flowPacketsIndex . "." . $instances} = [ "integer" => 1 ];
			$$oidHash{$oidFlowPackets . "." . ($flowPacketsIndex + 1) . "." . $instances} = [ "string" => $interface ];
			$$oidHash{$oidFlowPackets . "." . ($flowPacketsIndex + 2) . "." . $instances} = [ "string" => "Packet Counters" ];
			$$oidHash{$oidStatesEntries . "." . $statesEntriesIndex . "." . $instances} = [ "integer" => 1 ];
			$$oidHash{$oidStatesEntries . "." . ($statesEntriesIndex + 1) . "." . $instances} = [ "string" => $interface ];
			$$oidHash{$oidStatesEntries . "." . ($statesEntriesIndex + 2) . "." . $instances} = [ "string" => "State Table Entries" ];
			$$oidHash{$oidStatesCounters . "." . $statesCountersIndex . "." . $instances} = [ "integer" => 1 ];
			$$oidHash{$oidStatesCounters . "." . ($statesCountersIndex + 1) . "." . $instances} = [ "string" => $interface ];
			$$oidHash{$oidStatesCounters . "." . ($statesCountersIndex + 2) . "." . $instances} = [ "string" => "State Counters" ];
			$$oidHash{$oidStatesLimits . "." . $statesLimitsIndex . "." . $instances} = [ "integer" => 1 ];
			$$oidHash{$oidStatesLimits . "." . ($statesLimitsIndex + 1) . "." . $instances} = [ "string" => $interface ];
			$$oidHash{$oidStatesLimits . "." . ($statesLimitsIndex + 2) . "." . $instances} = [ "string" => "pf State Table Limits" ];
			$flowBytesIndex = $flowPacketsIndex = $statesEntriesIndex = $statesCountersIndex = $statesLimitsIndex += 3;
			next;


		### Query Byte and Packet Counters

		} elsif ($line =~ m/(Bytes (In|Out))/) {
			my ($desc, $IPV4, $IPV6) = split(/\|/, $line);
			unless ($COUNTER64) {
				$IPV4 = $IPV4 % 4294967296;
				$IPV6 = $IPV6 % 4294967296;
			}
			$$oidHash{$oidFlowBytes . "." . $flowBytesIndex . "." . $instances} = [ "counter" => $IPV4 ];
			$$oidHash{$oidFlowBytes . "." . $flowBytesIndex . "." . ($instances + 1)} = [ "string" => "IPv4 $desc" ];
			$flowBytesIndex++;
			$$oidHash{$oidFlowBytes . "." . $flowBytesIndex . "." . $instances} = [ "counter" => $IPV6 ];
			$$oidHash{$oidFlowBytes . "." . $flowBytesIndex . "." . ($instances + 1)} = [ "string" => "IPv6 $desc" ];
			$flowBytesIndex++;
			next;
		} elsif ($line =~ m/Packets (In|Out)/) {
			$prevLine = $line;
			next;
		} elsif ($line =~ m/(Passed|Blocked)/) {
			my ($desc, $IPV4, $IPV6) = split(/\|/, $line);
			unless ($COUNTER64) {
				$IPV4 = $IPV4 % 4294967296;
				$IPV6 = $IPV6 % 4294967296;
			}
			$$oidHash{$oidFlowPackets . "." . $flowPacketsIndex . "." . $instances} = [ "counter" => $IPV4 ];
			$$oidHash{$oidFlowPackets . "." . $flowPacketsIndex . "." . ($instances + 1)} = [ "string" => "IPv4 $prevLine $desc" ];
			$flowPacketsIndex++;
			$$oidHash{$oidFlowPackets . "." . $flowPacketsIndex . "." . $instances} = [ "counter" => $IPV6 ];
			$$oidHash{$oidFlowPackets . "." . $flowPacketsIndex . "." . ($instances + 1)} = [ "string" => "IPv6 $prevLine $desc" ];
			$flowPacketsIndex++;
			next;


		### Query State Table Entries

		} elsif ($line =~ m/current entries|searches|inserts|removals/) {
			my ($desc, $TOTAL) = split(/\|/, $line);
			unless ($COUNTER64) {
				$TOTAL = $TOTAL % 4294967296;
			}
			$$oidHash{$oidStatesEntries . "." . $statesEntriesIndex . "." . $instances} = [ "counter" => $TOTAL ];
			$$oidHash{$oidStatesEntries . "." . $statesEntriesIndex . "." . ($instances + 1)} = [ "string" => "States $desc" ];
			if ($line =~ m/current entries/) {
				$statesCounter = $TOTAL;
			}
			$statesEntriesIndex++;
			next;


		### Query State Counters

		} elsif ($line =~ m/match|bad-offset|fragment|short|normalize|memory|bad-timestamp|congestion|ip-option|proto-cksum|state-(mismatch|insert|limit)|src-limit|synproxy/) {
			my ($desc, $TOTAL) = split(/\|/, $line);
			unless ($COUNTER64) {
				$TOTAL = $TOTAL % 4294967296;
			}
			$$oidHash{$oidStatesCounters . "." . $statesCountersIndex . "." . $instances} = [ "counter" => $TOTAL ];
			$$oidHash{$oidStatesCounters . "." . $statesCountersIndex . "." . ($instances + 1)} = [ "string" => "States Counter $desc" ];
			$statesCountersIndex++;
			next;
		}
	}


	### Query pf limits

	foreach my $line (`$PFLIMITS`) {
		$line =~ s/hard limit//g;
		$line =~ s/  +/|/g;
		$line =~ s/^\|//g;
		chomp($line);

		print "\$line: $line\n" if ($DEBUG);

		if ($line =~ m/states|src-nodes|frags|tables|table-entries/) {
			my ($desc, $LIMIT) = split(/\|/, $line);
			unless ($COUNTER64) {
				$LIMIT = $LIMIT % 4294967296;
			}
			$$oidHash{$oidStatesLimits . "." . $statesLimitsIndex . "." . $instances} = [ "counter" => $LIMIT ];
			$$oidHash{$oidStatesLimits . "." . $statesLimitsIndex . "." . ($instances + 1)} = [ "string" => "Limits $desc" ];
			if ($line =~ m/states/) {
				$statesLimit = $LIMIT;
			}
			$statesLimitsIndex++;
			next;
		}
	}

	$$oidHash{$oidStates . ".1.1." . $instances} = [ "integer" => 1 ];
	if (($statesCounter / $statesLimit) * 100 >= $statesCritThreshold) {
		$$oidHash{$oidStates . ".1.2." . $instances} = [ "string" => "CRITICAL - $statesCounter of $statesLimit used" ];
		$$oidHash{$oidStates . ".1.3." . $instances} = [ "integer" => 2 ];
	} elsif (($statesCounter / $statesLimit) * 100 >= $statesWarnThreshold) {
		$$oidHash{$oidStates . ".1.2." . $instances} = [ "string" => "WARNING - $statesCounter of $statesLimit used" ];
		$$oidHash{$oidStates . ".1.3." . $instances} = [ "integer" => 1 ];
	} else {
		$$oidHash{$oidStates . ".1.2." . $instances} = [ "string" => "OK - $statesCounter of $statesLimit used" ];
		$$oidHash{$oidStates . ".1.3." . $instances} = [ "integer" => 0 ];
	}


	### Get Blocks

	my $blockedTables = "";
	my ($blocks, $blocksExclusions) = (0, 0);

	foreach $table (`$PFTABLES`) {
		chomp $table;
		if ($table =~ m/bruteforce/) {
			$$tables{$table} = ();
			foreach my $entry (`pfctl -t $table -T show`) {
				push(@{$$tables{$table}}, $entry);
			}
		}
	}


	### Query number of currently blocked hosts

	while ($table = each %$tables) {
		my $entryCount = 0;
		if (defined $$tables{$table}) {
			$entryCount = @{$$tables{$table}};
			if (grep(/$table/, @$critExclusions)) {
				$blocksExclusions += $entryCount;
			} else {
				$blocks += $entryCount;
			}
			$blockedTables = $blockedTables . " " . $table . " (" . $entryCount . ")";
		}
		$$oidHash{$oidBlocks . ".2.1." . $blocksIndex} = [ "integer" => $blocksIndex ];
		$$oidHash{$oidBlocks . ".2.2." . $blocksIndex} = [ "string" => $table ];
		$$oidHash{$oidBlocks . ".2.3." . $blocksIndex} = [ "counter" => $entryCount ];
		$blocksIndex++;
	}


	### Nagios Plugin to report number of currently blocked hosts

	$$oidHash{$oidBlocks . ".1.1." . $instances} = [ "integer" => 1 ];
#	while ($table = each %$tables) {
	if ($blocks >= $blockCritThreshold) {
		$$oidHash{$oidBlocks . ".1.2." . $instances} = [ "string" => "CRITICAL - Blocked Hosts: " . ($blocks + $blocksExclusions) . ":" . $blockedTables ];
		$$oidHash{$oidBlocks . ".1.3." . $instances} = [ "integer" => 2 ];
	} elsif ($blocks >= $blockWarnThreshold) {
		$$oidHash{$oidBlocks . ".1.2." . $instances} = [ "string" => "WARNING - Blocked Hosts: " . ($blocks + $blocksExclusions) . ":" . $blockedTables ];
		$$oidHash{$oidBlocks . ".1.3." . $instances} = [ "integer" => 1 ];
	} else {
		if ($blocks + $blocksExclusions) {
			$$oidHash{$oidBlocks . ".1.2." . $instances} = [ "string" => "OK- Blocked Hosts: " . ($blocks + $blocksExclusions) . ":" . $blockedTables ];
		} else {
			$$oidHash{$oidBlocks . ".1.2." . $instances} = [ "string" => "OK - No hosts are currently blocked" ];
		}
		$$oidHash{$oidBlocks . ".1.3." . $instances} = [ "integer" => 0 ];
	}
		

	### Populate OID tree

	$extsnmp_stats->add_oid_tree($oidHash);
	print Dumper($oidHash) . "\n" if ($DEBUG);
}


$extsnmp_stats->run;
