2012-06-22 11:38:01 +02:00
|
|
|
#!/usr/bin/perl
|
|
|
|
package Karma;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) }
|
|
|
|
use AnyDBM_File;
|
|
|
|
use Data::Dumper;
|
|
|
|
use Fcntl qw(:DEFAULT :flock LOCK_EX LOCK_NB);
|
|
|
|
use Net::IP qw(:PROC);
|
|
|
|
use POSIX qw(strftime);
|
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
my $self = bless({args => {db_dir => 'config'},}, 'Karma');
|
2012-06-22 11:38:01 +02:00
|
|
|
my $command = $ARGV[0];
|
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
if (!$command) {
|
2012-06-22 11:38:01 +02:00
|
|
|
$self->usage();
|
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($command eq 'capture') {
|
|
|
|
$self->capture($ARGV[1]);
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($command eq 'release') {
|
|
|
|
$self->release($ARGV[1]);
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($command eq 'prune') {
|
|
|
|
$self->prune_db($ARGV[1] || 7);
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($command eq 'search' && is_ip($ARGV[1])) {
|
|
|
|
$self->show_ip($ARGV[1]);
|
2013-03-23 06:01:13 +01:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($command eq 'list' | $command eq 'search') {
|
2012-06-22 11:38:01 +02:00
|
|
|
$self->main();
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
sub usage {
|
|
|
|
print <<EO_HELP
|
|
|
|
karma_tool [ list search prune capture release ]
|
|
|
|
|
|
|
|
list takes no arguments.
|
|
|
|
|
2012-06-23 05:57:43 +02:00
|
|
|
search [ naughty nice both <ip> ]
|
2012-06-22 11:38:01 +02:00
|
|
|
and returns a list of matching IPs
|
|
|
|
|
|
|
|
capture [ IP ]
|
|
|
|
sends an IP to the penalty box
|
|
|
|
|
|
|
|
release [ IP ]
|
|
|
|
remove an IP from the penalty box
|
|
|
|
|
|
|
|
prune takes no arguments.
|
|
|
|
prunes database of entries older than 7 days
|
|
|
|
|
|
|
|
EO_HELP
|
2013-04-21 06:50:39 +02:00
|
|
|
;
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub capture {
|
|
|
|
my $self = shift;
|
|
|
|
my $ip = shift or return;
|
2013-04-21 06:50:39 +02:00
|
|
|
is_ip($ip) or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "not an IP: $ip\n";
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
my $db = $self->get_db_location();
|
2013-04-21 06:50:39 +02:00
|
|
|
my $lock = $self->get_db_lock($db) or return;
|
|
|
|
my $tied = $self->get_db_tie($db, $lock) or return;
|
|
|
|
my $key = $self->get_db_key($ip);
|
2012-06-22 11:38:01 +02:00
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/,
|
|
|
|
$tied->{$key};
|
2013-03-11 05:24:11 +01:00
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
$tied->{$key} = join(':', time, $naughty + 1, $nice, $connects);
|
|
|
|
return $self->cleanup_and_return($tied, $lock);
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub release {
|
|
|
|
my $self = shift;
|
|
|
|
my $ip = shift or return;
|
2013-04-21 06:50:39 +02:00
|
|
|
is_ip($ip) or do { warn "not an IP: $ip\n"; return; };
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
my $db = $self->get_db_location();
|
2013-04-21 06:50:39 +02:00
|
|
|
my $lock = $self->get_db_lock($db) or return;
|
|
|
|
my $tied = $self->get_db_tie($db, $lock) or return;
|
|
|
|
my $key = $self->get_db_key($ip);
|
2012-06-22 11:38:01 +02:00
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/,
|
|
|
|
$tied->{$key};
|
2013-03-11 05:24:11 +01:00
|
|
|
|
|
|
|
$tied->{$key} = join(':', 0, 0, $nice, $connects);
|
2013-04-21 06:50:39 +02:00
|
|
|
return $self->cleanup_and_return($tied, $lock);
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
2013-03-23 06:01:13 +01:00
|
|
|
sub show_ip {
|
|
|
|
my $self = shift;
|
2013-04-21 06:50:39 +02:00
|
|
|
my $ip = shift or return;
|
2013-03-23 06:01:13 +01:00
|
|
|
my $db = $self->get_db_location();
|
2013-04-21 06:50:39 +02:00
|
|
|
my $lock = $self->get_db_lock($db) or return;
|
|
|
|
my $tied = $self->get_db_tie($db, $lock) or return;
|
|
|
|
my $key = $self->get_db_key($ip);
|
|
|
|
|
|
|
|
my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/,
|
|
|
|
$tied->{$key};
|
|
|
|
$naughty ||= 0;
|
|
|
|
$nice ||= 0;
|
2013-03-23 06:01:13 +01:00
|
|
|
$connects ||= 0;
|
|
|
|
my $time_human = '';
|
2013-04-21 06:50:39 +02:00
|
|
|
if ($penalty_start_ts) {
|
2013-03-23 06:01:13 +01:00
|
|
|
$time_human = strftime "%a %b %e %H:%M", localtime $penalty_start_ts;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
|
|
|
my $hostname = `dig +short -x $ip` || '';
|
|
|
|
chomp $hostname;
|
|
|
|
print
|
|
|
|
" IP Address Penalty Naughty Nice Connects Hostname\n";
|
|
|
|
printf(" %-18s %24s %3s %3s %3s %-30s\n",
|
|
|
|
$ip, $time_human, $naughty, $nice, $connects, $hostname);
|
|
|
|
}
|
2013-03-23 06:01:13 +01:00
|
|
|
|
2012-06-22 11:38:01 +02:00
|
|
|
sub main {
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
my $db = $self->get_db_location();
|
2013-04-21 06:50:39 +02:00
|
|
|
my $lock = $self->get_db_lock($db) or return;
|
|
|
|
my $tied = $self->get_db_tie($db, $lock) or return;
|
2012-06-22 11:38:01 +02:00
|
|
|
my %totals;
|
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
print
|
|
|
|
" IP Address Penalty Naughty Nice Connects Hostname\n";
|
|
|
|
foreach my $r (sort keys %$tied) {
|
|
|
|
my $ip = ip_bintoip(ip_inttobin($r, 4), 4);
|
|
|
|
my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/,
|
|
|
|
$tied->{$r};
|
|
|
|
$naughty ||= '';
|
|
|
|
$nice ||= '';
|
2012-06-22 11:38:01 +02:00
|
|
|
$connects ||= '';
|
|
|
|
my $time_human = '';
|
2013-04-21 06:50:39 +02:00
|
|
|
if ($command eq 'search') {
|
2012-06-22 11:38:01 +02:00
|
|
|
my $search = $ARGV[1];
|
2013-04-21 06:50:39 +02:00
|
|
|
if ($search eq 'nice') {
|
|
|
|
next if !$nice;
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($search eq 'naughty') {
|
|
|
|
next if !$naughty;
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif ($search eq 'both') {
|
|
|
|
next if !$naughty || !$nice;
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
elsif (is_ip($ARGV[1]) && $search ne $ip) {
|
2012-06-22 11:38:01 +02:00
|
|
|
next;
|
|
|
|
}
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
|
|
|
if ($penalty_start_ts) {
|
|
|
|
$time_human = strftime "%a %b %e %H:%M",
|
|
|
|
localtime $penalty_start_ts;
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
my $hostname = '';
|
2013-04-21 06:50:39 +02:00
|
|
|
if ($naughty && $nice) {
|
|
|
|
|
2013-03-11 05:24:11 +01:00
|
|
|
#$hostname = `dig +short -x $ip`; chomp $hostname;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
|
|
|
printf(" %-18s %24s %3s %3s %3s %30s\n",
|
|
|
|
$ip, $time_human, $naughty, $nice, $connects, $hostname);
|
2012-06-22 11:38:01 +02:00
|
|
|
$totals{naughty} += $naughty if $naughty;
|
|
|
|
$totals{nice} += $nice if $nice;
|
|
|
|
$totals{connects} += $connects if $connects;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
print Dumper(\%totals);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub is_ip {
|
|
|
|
my $ip = shift || $ARGV[0];
|
2013-04-21 06:50:39 +02:00
|
|
|
new Net::IP($ip) or return;
|
2013-03-23 06:01:13 +01:00
|
|
|
return 1;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub cleanup_and_return {
|
2013-04-21 06:50:39 +02:00
|
|
|
my ($self, $tied, $lock) = @_;
|
2012-06-22 11:38:01 +02:00
|
|
|
untie $tied;
|
|
|
|
close $lock;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub get_db_key {
|
|
|
|
my $self = shift;
|
2013-04-21 06:50:39 +02:00
|
|
|
my $nip = Net::IP->new(shift) or return;
|
|
|
|
return $nip->intip; # convert IP to an int
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub get_db_tie {
|
2013-04-21 06:50:39 +02:00
|
|
|
my ($self, $db, $lock) = @_;
|
2012-06-22 11:38:01 +02:00
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
tie(my %db, 'AnyDBM_File', $db, O_CREAT | O_RDWR, 0600) or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "tie to database $db failed: $!";
|
|
|
|
close $lock;
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
return \%db;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub get_db_location {
|
|
|
|
my $self = shift;
|
|
|
|
|
|
|
|
# Setup database location
|
2013-04-21 06:50:39 +02:00
|
|
|
my @candidate_dirs = (
|
|
|
|
$self->{args}{db_dir},
|
|
|
|
"/var/lib/qpsmtpd/karma", "./var/db", "./config", '.'
|
|
|
|
);
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
my $dbdir;
|
2013-04-21 06:50:39 +02:00
|
|
|
for my $d (@candidate_dirs) {
|
|
|
|
next if !$d || !-d $d; # impossible
|
2012-06-22 11:38:01 +02:00
|
|
|
$dbdir = $d;
|
2013-04-21 06:50:39 +02:00
|
|
|
last; # first match wins
|
2012-06-22 11:38:01 +02:00
|
|
|
}
|
|
|
|
my $db = "$dbdir/karma.dbm";
|
|
|
|
print "using karma db at $db\n";
|
|
|
|
return $db;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub get_db_lock {
|
|
|
|
my ($self, $db) = @_;
|
|
|
|
|
|
|
|
return $self->get_db_lock_nfs($db) if $self->{_args}{nfslock};
|
|
|
|
|
|
|
|
# Check denysoft db
|
2013-04-21 06:50:39 +02:00
|
|
|
open(my $lock, ">$db.lock") or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "opening lockfile failed: $!";
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
flock($lock, LOCK_EX) or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "flock of lockfile failed: $!";
|
|
|
|
close $lock;
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
return $lock;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub get_db_lock_nfs {
|
|
|
|
my ($self, $db) = @_;
|
|
|
|
|
|
|
|
require File::NFSLock;
|
|
|
|
|
|
|
|
### set up a lock - lasts until object looses scope
|
|
|
|
my $nfslock = new File::NFSLock {
|
2013-04-21 06:50:39 +02:00
|
|
|
file => "$db.lock",
|
|
|
|
lock_type => LOCK_EX | LOCK_NB,
|
|
|
|
blocking_timeout => 10, # 10 sec
|
|
|
|
stale_lock_timeout => 30 * 60, # 30 min
|
|
|
|
}
|
|
|
|
or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "nfs lockfile failed: $!";
|
|
|
|
return;
|
2013-04-21 06:50:39 +02:00
|
|
|
};
|
2012-06-22 11:38:01 +02:00
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
open(my $lock, "+<$db.lock") or do {
|
2012-06-22 11:38:01 +02:00
|
|
|
warn "opening nfs lockfile failed: $!";
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
return $lock;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|
|
|
|
sub prune_db {
|
2013-04-21 06:50:39 +02:00
|
|
|
my $self = shift;
|
2012-06-22 11:38:01 +02:00
|
|
|
my $prune_days = shift;
|
|
|
|
|
2013-04-21 06:50:39 +02:00
|
|
|
my $db = $self->get_db_location();
|
|
|
|
my $lock = $self->get_db_lock($db) or return;
|
|
|
|
my $tied = $self->get_db_tie($db, $lock) or return;
|
2012-06-22 11:38:01 +02:00
|
|
|
my $count = keys %$tied;
|
|
|
|
|
|
|
|
my $pruned = 0;
|
2013-04-21 06:50:39 +02:00
|
|
|
foreach my $key (keys %$tied) {
|
2012-06-22 11:38:01 +02:00
|
|
|
my ($ts, $naughty, $nice, $connects) = split /:/, $tied->{$key};
|
2013-04-21 06:50:39 +02:00
|
|
|
my $days_old = (time - $ts) / 86400;
|
2012-06-22 11:38:01 +02:00
|
|
|
next if $days_old < $prune_days;
|
|
|
|
delete $tied->{$key};
|
|
|
|
$pruned++;
|
2013-04-21 06:50:39 +02:00
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
untie $tied;
|
|
|
|
close $lock;
|
|
|
|
warn "pruned $pruned of $count DB entries";
|
2013-04-21 06:50:39 +02:00
|
|
|
return $self->cleanup_and_return($tied, $lock);
|
|
|
|
}
|
2012-06-22 11:38:01 +02:00
|
|
|
|