#!/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; } }