Add flush_reject, should've been a module, but, for now, it's just a separate program

This commit is contained in:
2024-10-29 20:41:30 +01:00
parent ac6d10a5da
commit 4c3a001d46

319
bin/flush_reject Executable file
View 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;
}
}