#!/usr/bin/perl package Karma; use strict; use warnings; use Data::Dumper; use Net::IP qw(:PROC); use POSIX qw(strftime); use lib 'lib'; use Qpsmtpd::Base; use Qpsmtpd::DB; my $base = Qpsmtpd::Base->new(); my $self = bless({args => {db_dir => 'config'},}, 'Karma'); my $command = $ARGV[0]; if (!$command) { $self->usage(); } elsif ($command eq 'capture') { $self->capture($ARGV[1]); } elsif ($command eq 'release') { $self->release($ARGV[1]); } elsif ($command eq 'prune') { $self->prune_db($ARGV[1] || 7); } elsif ($command eq 'search' && is_ip($ARGV[1])) { $self->show_ip($ARGV[1]); } elsif ($command eq 'list' | $command eq 'search') { $self->main(); } exit(0); sub usage { print < ] 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 ; } sub db { my ( $self ) = @_; return $self->{db} if $self->{db}; $self->{db} = Qpsmtpd::DB->new( name => 'karma' ); $self->{db}->dir( $self->{args}{db_dir}, qw( /var/lib/qpsmtpd/karma ./var/db ./config . ) ); my $path = $self->{db}->path; print "using karma db at $path\n"; return $self->{db}; } sub capture { my $self = shift; my $ip = shift or return; is_ip($ip) or do { warn "not an IP: $ip\n"; return; }; $self->db->lock; my $key = $self->get_karma_key($ip); my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/, $self->db->get( $key ); $self->db->set( $key, join(':', time, $naughty + 1, $nice, $connects) ); $self->db->unlock; } sub release { my $self = shift; my $ip = shift or return; is_ip($ip) or do { warn "not an IP: $ip\n"; return; }; $self->db->lock; my $key = $self->get_karma_key($ip); my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/, $self->db->get( $key ); $self->db->set( $key, join(':', 0, 0, $nice, $connects) ); $self->db->unlock; } sub show_ip { my $self = shift; my $ip = shift or return; $self->db->lock; my $key = $self->get_karma_key($ip); my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/, $self->db->get( $key ); $self->db->unlock; $naughty ||= 0; $nice ||= 0; $connects ||= 0; my $time_human = ''; if ($penalty_start_ts) { $time_human = strftime "%a %b %e %H:%M", localtime $penalty_start_ts; } my $hostname = $base->resolve_ptr($ip); print " IP Address Penalty Naughty Nice Connects Hostname\n"; printf(" %-18s %24s %3s %3s %3s %-30s\n", $ip, $time_human, $naughty, $nice, $connects, $hostname); } sub main { my $self = shift; $self->db->lock; my %totals; print " IP Address Penalty Naughty Nice Connects Hostname\n"; foreach my $r ( $self->db->get_keys ) { my $ip = ip_bintoip(ip_inttobin($r, 4), 4); my ($penalty_start_ts, $naughty, $nice, $connects) = split /:/, $self->db->get( $r ); $naughty ||= ''; $nice ||= ''; $connects ||= ''; my $time_human = ''; if ($command eq 'search') { my $search = $ARGV[1]; if ($search eq 'nice') { next if !$nice; } elsif ($search eq 'naughty') { next if !$naughty; } elsif ($search eq 'both') { next if !$naughty || !$nice; } elsif (is_ip($ARGV[1]) && $search ne $ip) { next; } } if ($penalty_start_ts) { $time_human = strftime "%a %b %e %H:%M", localtime $penalty_start_ts; } my $hostname = ''; if ($naughty && $nice) { # this can be slow, waiting for each IP to resolve #$hostname = $base->resolve_ptr($ip); } printf(" %-18s %24s %3s %3s %3s %30s\n", $ip, $time_human, $naughty, $nice, $connects, $hostname); $totals{naughty} += $naughty if $naughty; $totals{nice} += $nice if $nice; $totals{connects} += $connects if $connects; } $self->db->unlock; print Dumper(\%totals); } sub is_ip { my $ip = shift || $ARGV[0]; new Net::IP($ip) or return; return 1; } sub get_karma_key { my $self = shift; my $nip = Net::IP->new(shift) or return; return $nip->intip; # convert IP to an int } sub prune_db { my $self = shift; my $prune_days = shift; $self->db->lock; my $count = $self->db->size; my $pruned = 0; foreach my $key ( $self->db->get_keys ) { my ($ts, $naughty, $nice, $connects) = split /:/, $self->db->get( $key ); my $days_old = (time - $ts) / 86400; next if $days_old < $prune_days; $self->db->delete( $key ); $pruned++; } $self->db->unlock; warn "pruned $pruned of $count DB entries"; }