#!/usr/pkg/bin/perl
# $Id: listenban.pl 291 2012-05-02 10:42:55Z bouyer $

use strict;
use IO::Socket::INET;
use Socket qw(SOL_SOCKET SO_KEEPALIVE);
use Proc::Daemon;
use Sys::Syslog qw(:standard :macros);
use Getopt::Std;

my $pid_file;
my $isdaemon = 0;

my %bancmds = (
	"ssh"		=> "echo 'block in log quick proto tcp from %IP% to any port = (22,23) group 10' |  ipf -f -",
	"mail"		=> "echo 'block in log quick proto tcp from %IP% to any port = (25,465) group 10' |  ipf -f -",
	"dovecot"	=> "echo 'block in log quick proto tcp from %IP% to any port = (110,995,143,220,993) group 10' |  ipf -f -",
	"http"	=> "echo 'block in log quick proto tcp from %IP% to any port = (80,443,8080) group 10' |  ipf -f -",
	"all"	=> "echo 'block in quick proto tcp from %IP% to any group 10' |  ipf -f -",
);
my %unbancmds = (
	"ssh"		=> "echo 'block in log quick proto tcp from %IP% to any port = (22,23) group 10' |  ipf -r -f -",
	"mail"		=> "echo 'block in log quick proto tcp from %IP% to any port = (25,465) group 10' |  ipf -r -f -",
	"dovecot"	=> "echo 'block in log quick proto tcp from %IP% to any port = (110,995,143,220,993) group 10' |  ipf -r -f -",
	"http"	=> "echo 'block in log quick proto tcp from %IP% to any port = (80,443,8080) group 10' |  ipf -r -f -",
	"all"	=> "echo 'block in quick proto tcp from %IP% to any group 10' |  ipf -r -f -",
);

my $cleancmd = "ipfstat -i | egrep ' group 10\$' | ipf -r -f -";

our($opt_f, $opt_P, $opt_h, $opt_p);
getopts("fP:h:p:") or usage();

if (!defined($opt_h) || !defined($opt_p)) {
	usage();
}

openlog('listenban', 'pid', 'local5');
# try to connect to server now
my $sock = IO::Socket::INET->new(
	PeerAddr => $opt_h,
	PeerPort => $opt_p,
	Proto => 'tcp') or mylog(LOG_ERR, "can't connect to $opt_h:$opt_p: $@, retrying in background");

#now daemonize if requested
closelog();
if ($opt_f ne "1") {
        my $daemon;
        if (defined($opt_P)) {
		if ($sock) {
			$daemon = Proc::Daemon->new(
			    pid_file => $opt_P,
			    dont_close_fh => [ $sock ]
			);
		} else {
			$daemon = Proc::Daemon->new(
			    pid_file => $opt_P,
			);
		}
        } else {
		if ($sock) {
			$daemon = Proc::Daemon->new(
			    dont_close_fh => [ $sock ]
			);
		} else {
			$daemon = Proc::Daemon->new(
			);
		}
        }
        my $pid = $daemon->init;
        if ($pid) {
                exit(0);
        }
        $isdaemon = 1;
}
openlog('listenban', 'pid', 'local5');
mylog(LOG_INFO, "listenban starting with pid " . $$);

#purge existing banned entries
mysystem($cleancmd);
while (1) {
	if (!$sock) {
		sleep(30);
		$sock = IO::Socket::INET->new(
			PeerAddr => $opt_h,
			PeerPort => $opt_p,
			Proto => 'tcp') or
			mylog(LOG_ERR, "can't connect to $opt_h:$opt_p: $@");
	} else {
		mylog(LOG_ERR, "$opt_h:$opt_p connected");
		setsockopt($sock, SOL_SOCKET, SO_KEEPALIVE, 1)
		    or mylog(LOG_ERR, "can't set keepalive: $!");
		#request all existing ban entries
		printf $sock "GETALL\n" or
			mylog(LOG_ERR, "can't send GETALL: $!");
		while (my $line = <$sock>) {
			chop $line;
			mylog(LOG_DEBUG, "got $line");
			handle_cmd($line);
		}
		mylog(LOG_ERR, "$opt_h:$opt_p disconnected");
		close($sock);
		$sock = 0;
		#purge existing banned entries
		mysystem($cleancmd);
	}
}

sub handle_cmd {
    my ($buf) = @_;
    if ($buf =~ /^BAN (\d+\.\d+\.\d+\.\d+) (\w+) (\d+)$/) {
	my $ip = $1;
	if (exists($bancmds{$2})) {
	    mylog(LOG_INFO, "BAN $ip $2");
	    my $cmd = $bancmds{$2};
	    $cmd =~ s/%IP%/$ip/g;
	    mysystem($cmd);
         }
    } elsif ($buf =~ /^UNBAN (\d+\.\d+\.\d+\.\d+) (\w+)$/) {
	my $ip = $1;
	if (exists($unbancmds{$2})) {
	    mylog(LOG_INFO, "UNBAN $ip $2");
	    my $cmd = $unbancmds{$2};
	    $cmd =~ s/%IP%/$ip/g;
	    mysystem($cmd);
         }
    } else {
	mylog(LOG_ERR, "bad message $buf from server");
    }
}

sub mysystem {
    my ($cmd) = @_;
    mylog(LOG_DEBUG, "running cmd $cmd");
    my $st = system($cmd);
    if ($st == -1) {
	mylog(LOG_ERR, "exec'ing $cmd failed to execute: $!");
    } elsif ($st & 127) {
	mylog(LOG_ERR, "$cmd died with signal " . ($st & 127) . (($st & 128) ? "(core dumped)" : "") );
    } elsif ($st != 0) {
	mylog(LOG_ERR, "$cmd died with status " . ($st ) );
    }
}

sub mylog {
  my ($level, $str) = @_;
  if ($isdaemon != "1") {
	  print STDERR "$str\n";
  }
  syslog $level, $str;
}

sub mydie {
  mylog(LOG_CRIT, @_);
  exit(1);
}

sub usage {
	print STDERR "usage: $ARGV[0] [-f] [-P<pid file>] -h<host> -p<port>\n";
	exit 1;
}
