Add flush_reject, should've been a module, but, for now, it's just a separate program
This commit is contained in:
319
bin/flush_reject
Executable file
319
bin/flush_reject
Executable file
@ -0,0 +1,319 @@
|
||||
#!/usr/local/bin/perl
|
||||
|
||||
use DBI;
|
||||
use strict;
|
||||
use warnings;
|
||||
use Glib;
|
||||
use sigtrap qw/handler signal_handler normal-signals/;
|
||||
use My::Savepid qw(savepid loadpid);
|
||||
use Tie::Syslog;
|
||||
use LWP::UserAgent;
|
||||
|
||||
my $pid = $$;
|
||||
my $program = $0;
|
||||
my $daemonize = 1;
|
||||
my $verbose = 1;
|
||||
my $logfile = '/var/log/flush_reject.log';
|
||||
my $pidfile = '/var/run/flush_reject.pid';
|
||||
my $time_keep = '10 MINUTE'; # for how long do we want to block blacklisted host, sql time (SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, YEAR)
|
||||
my $default_ban = '6 HOUR'; # how long do we ban offenders? sql time
|
||||
my $max_block = 5; #max amount of blocks before we stop freeing them (aka permanent ban)
|
||||
my $max_time = '31 DAY'; #for how long do permanent bans last? sql time
|
||||
my $sql_user = '';
|
||||
my $sql_pwd = '';
|
||||
my $sql_host = '';
|
||||
my $table = '';
|
||||
my $block_table = '';
|
||||
my $opnsense = '';
|
||||
my $api_key = '';
|
||||
my $api_secret = '';
|
||||
my $api_alias = '';
|
||||
|
||||
my $logf;
|
||||
my $dp;
|
||||
my $dbh;
|
||||
my $loop;
|
||||
my $timer;
|
||||
my $x;
|
||||
if($daemonize) {
|
||||
if(my $tp = fork) {
|
||||
exit 0;
|
||||
} else {
|
||||
$pid = $$;
|
||||
&init;
|
||||
}
|
||||
} else {
|
||||
&init;
|
||||
}
|
||||
|
||||
sub init {
|
||||
&savepid($pid,$pidfile);
|
||||
open($logf, '>>', "$logfile") || die "Could not write to logfile";
|
||||
select $logf; $| = 1;
|
||||
if($daemonize) {
|
||||
$x = tie *STDERR, 'Tie::Syslog','local0.err',$program,'pid','unix';
|
||||
$x->ExtendedSTDERR();
|
||||
close STDIN;
|
||||
}
|
||||
&log(1,"$0($pid) started");
|
||||
$loop = Glib::MainLoop->new;
|
||||
$timer = Glib::Timeout -> add_seconds(300, \&update);
|
||||
$dbh = &connect;
|
||||
&update;
|
||||
$loop -> run;
|
||||
}
|
||||
|
||||
sub update {
|
||||
$dbh = &connect unless($dbh->ping);
|
||||
&update_old;
|
||||
my @work = &find_work;
|
||||
my $selrows = scalar(@work);
|
||||
&log(2,"Found $selrows fresh entries to work on") if($selrows);
|
||||
if($selrows) {
|
||||
foreach my $host(@work) {
|
||||
my @tolog;
|
||||
my $rows = &find_fresh($host);
|
||||
push(@tolog,"$rows nonsleeping < 6hours");
|
||||
if($rows > 1) {
|
||||
my $setsleeprows = &set_sleeping($host);
|
||||
push(@tolog,"$setsleeprows set as sleeping") if($setsleeprows);
|
||||
}
|
||||
my $trows = &find_sleeping($host);
|
||||
push(@tolog,"$trows sleeping entries") if($trows);
|
||||
my $age = &find_newest_entry($host);
|
||||
unless($trows) {
|
||||
if($rows <= 1) {
|
||||
if(&find_total($host) < $max_block) {
|
||||
push(@tolog, "deleting pf rule.");
|
||||
&clean_host($host,$age);
|
||||
} else {
|
||||
push(@tolog, "doing nothing, $host has too many blocks");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
push(@tolog, "$trows sleeping, noop");
|
||||
}
|
||||
if($age) {
|
||||
my $todel = 21600 - $age;
|
||||
my $page = 'last entry is '.&gettime($age).' old';
|
||||
my $ptodel = '';
|
||||
$ptodel = ', will be deleted in '.&gettime($todel) if($trows);
|
||||
push(@tolog,"$page$ptodel") if($age);
|
||||
}
|
||||
my $logline = "$host: ";
|
||||
$logline .= join(',', @tolog);
|
||||
&log(2,"$logline");
|
||||
}
|
||||
}
|
||||
system("/sbin/pfctl -t $block_table -T show > /etc/pf.$block_table");
|
||||
$dbh->disconnect;
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub opnsense_api_delete {
|
||||
my $ip = shift;
|
||||
my $ua = LWP::UserAgent->new;
|
||||
$ua->agent('flush_reject');
|
||||
$ua->timeout(1);
|
||||
my $req = HTTP::Request->new(POST => 'http://'.$opnsense.'/api/firewall/alias_util/delete/'.$api_alias);
|
||||
$req->authorization_basic($api_key, $api_secret);
|
||||
$req->content_type('application/json');
|
||||
$req->content('{"address":"'.$ip.'"}');
|
||||
my $res = $ua->request($req);
|
||||
unless ($res->is_success) {
|
||||
print $req->status_line."\n";
|
||||
print $req->as_string."\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub find_total {
|
||||
my $host = shift;
|
||||
my $sth = $dbh->prepare("
|
||||
SELECT
|
||||
count(ip)
|
||||
FROM
|
||||
$table
|
||||
WHERE
|
||||
ip = ? AND
|
||||
time > DATE_SUB(NOW(), INTERVAL $max_time)
|
||||
");
|
||||
$sth->execute($host);
|
||||
my $rows = $sth->fetchrow_arrayref->[0] || 0;
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub update_old {
|
||||
## Set entries older than 6 hours as not sleeping if they are set as so, this means they will be deleted unless they're permanently banned
|
||||
my $sth = $dbh->prepare("
|
||||
UPDATE $table
|
||||
SET sleep = 0
|
||||
WHERE deleted = 0 AND
|
||||
SLEEP = 1 AND
|
||||
time < DATE_SUB(NOW(), INTERVAL $default_ban)")
|
||||
|| die "Failed to update old records".DBI::errstr;
|
||||
$sth->execute();
|
||||
my $rows = $sth->rows || 0;
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub find_work {
|
||||
my @work;
|
||||
my %ret;
|
||||
my $sth = $dbh->prepare("
|
||||
SELECT ip
|
||||
FROM $table
|
||||
WHERE
|
||||
deleted = 0 AND
|
||||
time < DATE_SUB(NOW(), INTERVAL $time_keep)")
|
||||
|| die "Failed to find records to work on".DBI::errstr;
|
||||
$sth->execute;
|
||||
my $rows = $sth->rows;
|
||||
while(my $ref = $sth->fetchrow_hashref) {
|
||||
my $workip = $$ref{'ip'};
|
||||
$ret{$workip} = 1;
|
||||
}
|
||||
foreach my $key(keys %ret) {
|
||||
push(@work,$key);
|
||||
}
|
||||
return @work;
|
||||
}
|
||||
|
||||
sub set_sleeping {
|
||||
my $host = shift;
|
||||
my $sth = $dbh->prepare("
|
||||
UPDATE $table
|
||||
SET sleep = 1
|
||||
WHERE
|
||||
ip = ? AND
|
||||
deleted = 0
|
||||
AND sleep = 0")
|
||||
|| die "Failed to update record for $host with sleep=1 ".DBI::errstr;
|
||||
$sth->execute($host);
|
||||
my $rows = $sth->rows || 0;
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub find_fresh {
|
||||
my $host = shift;
|
||||
my $sth = $dbh->prepare("
|
||||
SELECT count(*)
|
||||
FROM $table
|
||||
WHERE
|
||||
ip = ? AND
|
||||
sleep = 0 AND
|
||||
time > DATE_SUB(NOW(), INTERVAL $default_ban)")
|
||||
|| die "Failed to find number of records for $host newer than 6 hours ".DBI::errstr;
|
||||
$sth->execute($host);
|
||||
my $rows = $sth->fetchrow_arrayref->[0];
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub find_sleeping {
|
||||
my $host = shift;
|
||||
my $sth = $dbh->prepare("
|
||||
SELECT count(*)
|
||||
FROM $table
|
||||
WHERE
|
||||
ip = ? AND
|
||||
sleep = 1")
|
||||
|| die "Failed to find number of records for sleeping host $host ".DBI::errstr;
|
||||
$sth->execute($host);
|
||||
my $trows = $sth->fetchrow_arrayref->[0] || 0;
|
||||
return $trows;
|
||||
}
|
||||
|
||||
sub find_newest_entry {
|
||||
my $host = shift;
|
||||
my $sth = $dbh->prepare("
|
||||
SELECT TIMESTAMPDIFF(SECOND, time, now()) AS age
|
||||
FROM $table
|
||||
WHERE ip = ?
|
||||
ORDER BY time DESC
|
||||
LIMIT 1")
|
||||
|| die "Failed to find age of last entry of $host ".DBI::errstr;
|
||||
$sth->execute($host);
|
||||
my $age = $sth->fetchrow_arrayref->[0] || 0;
|
||||
return $age;
|
||||
}
|
||||
|
||||
sub clean_host {
|
||||
my $ip = shift;
|
||||
my $age = shift || 0;
|
||||
my $sth = $dbh->prepare("
|
||||
UPDATE $table
|
||||
SET deleted = 1
|
||||
WHERE
|
||||
ip = ? AND
|
||||
deleted = 0")
|
||||
|| die "Failed to update record for $ip with deleted=1".DBI::errstr;
|
||||
$sth->execute($ip) || die "Failed to update record for $ip:".DBI::errstr;
|
||||
my $delrows = $sth->rows || 0;
|
||||
system "/sbin/pfctl -q -t $block_table -T delete $ip" if($delrows);
|
||||
opnsense_api_delete($ip);
|
||||
my $prettyage = &gettime($age);
|
||||
&log(2,"Updated $delrows rows for host $ip to mark them as deleted");
|
||||
&log(1,"Cleared $ip after $prettyage.");
|
||||
}
|
||||
|
||||
sub log {
|
||||
my $sev = shift;
|
||||
my $msg = shift;
|
||||
unless($msg) {
|
||||
$msg = $sev;
|
||||
$sev = 0;
|
||||
}
|
||||
print STDERR "$msg" unless($sev);
|
||||
if($verbose >= $sev) {
|
||||
my $now = localtime();
|
||||
print $logf "$now: $msg\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub reload_log {
|
||||
close($logf);
|
||||
open($logf, '>>', "$logfile") || die "Could not write to logfile";
|
||||
select $logf; $| = 1;
|
||||
}
|
||||
|
||||
sub signal_handler {
|
||||
my $signal = shift;
|
||||
&log(1,"Recieved $signal: ");
|
||||
if($signal eq 'HUP') {
|
||||
&log(1,'Reloaded log');
|
||||
&reload_log;
|
||||
} else {
|
||||
system("/sbin/pfctl -t $block_table -T show > /etc/pf.$block_table");
|
||||
&log(1,"$program($pid) Shutting down");
|
||||
$dbh->disconnect;
|
||||
undef $x;
|
||||
untie *STDERR;
|
||||
close($logf);
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
|
||||
sub connect {
|
||||
my $databasec;
|
||||
while(1) {
|
||||
last if($databasec = DBI->connect("DBI:mysql:database=syslog;host=$sql_host",$sql_user,$sql_pwd, { PrintError => 1, mysql_auto_reconnect=>1, AutoCommit => 1 }));
|
||||
&log(2,'Connection problems, reconnecting in 10 seconds');
|
||||
sleep(10);
|
||||
}
|
||||
return $databasec;
|
||||
}
|
||||
|
||||
sub gettime # gettime(seconds)
|
||||
{
|
||||
my $time = $_[0];
|
||||
if($time > 0) {
|
||||
my $days = int($time/24/60/60);
|
||||
my $hours = sprintf("%02d", (int($time/60/60)%24));
|
||||
my $minutes = sprintf("%02d", (int($time/60)%60));
|
||||
my $seconds = sprintf("%02d", (int($time)%60));
|
||||
$time = "$minutes:$seconds" if($minutes > 0);
|
||||
$time = "$hours hours $minutes:$seconds" if($hours > 0);
|
||||
$time = "$days days $hours:$minutes:$seconds" if($days > 0);
|
||||
return $time;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user