Merge pull request #174 from jaredj/test-greylisting

Get rid of grey_timeout and 'white' clients
This commit is contained in:
Matt Simerson 2014-12-28 19:59:12 -08:00
commit f8d66348f6
2 changed files with 64 additions and 25 deletions

View File

@ -33,9 +33,9 @@ How that works is best explained by example:
A new connection arrives from the host shvj1.jpmchase.com. The sender is A new connection arrives from the host shvj1.jpmchase.com. The sender is
chase@alerts.chase.com and the recipient is londonwhale@example.com. This is chase@alerts.chase.com and the recipient is londonwhale@example.com. This is
the first connection for that triplet so the connection is deferred for the first connection for that triplet so the connection is deferred for
I<black_timeout> minutes. After the timeout, but before the I<grey_timeout> I<black_timeout> minutes. After the timeout elapses, shvj1.jpmchase.com retries
elapses, shvj1.jpmchase.com retries and successfully delivers the mail. For and successfully delivers the mail. For the next I<white_timeout> days, emails
the next I<white_timeout> days, emails for that triplet are not delayed. for that triplet are not delayed.
The next day, shvj1.jpmchase.com tries to deliver a new email from The next day, shvj1.jpmchase.com tries to deliver a new email from
alerts@alerts.chase.com to jdimon@example.com. Since this triplet is new, it alerts@alerts.chase.com to jdimon@example.com. Since this triplet is new, it
@ -73,14 +73,6 @@ e.g. to allow per-recipient logging. Default: 0.
The initial period during which we issue DENYSOFTs for connections from an The initial period during which we issue DENYSOFTs for connections from an
unknown (or timed out) 'connection triplet'. Default: 50 minutes. unknown (or timed out) 'connection triplet'. Default: 50 minutes.
=head2 grey_timeout <timeout_seconds>
The subsequent 'grey' period, after the initial black blocking period,
when we will accept a delivery from a formerly-unknown connection
triplet. If a new connection is received during this time, we will
record a successful delivery against this IP address, which whitelists
it for future deliveries (see following). Default: 3 hours 20 minutes.
=head2 white_timeout <timeout_seconds> =head2 white_timeout <timeout_seconds>
The period after which a known connection triplet will be considered The period after which a known connection triplet will be considered
@ -180,8 +172,9 @@ my $VERSION = '0.12';
my $DENYMSG = "This mail is temporarily denied"; my $DENYMSG = "This mail is temporarily denied";
my %PERMITTED_ARGS = map { $_ => 1 } qw(per_recipient remote_ip sender my %PERMITTED_ARGS = map { $_ => 1 } qw(per_recipient remote_ip sender
recipient black_timeout grey_timeout white_timeout deny_late db_dir recipient black_timeout white_timeout deny_late db_dir
nfslock p0f reject loglevel geoip upgrade ); nfslock p0f reject loglevel geoip upgrade );
$PERMITTED_ARGS{grey_timeout} = 1; # Legacy argument now ignored
my %DEFAULTS = ( my %DEFAULTS = (
remote_ip => 1, remote_ip => 1,
@ -189,7 +182,6 @@ my %DEFAULTS = (
recipient => 0, recipient => 0,
reject => 1, reject => 1,
black_timeout => 50 * 60, # 50m black_timeout => 50 * 60, # 50m
grey_timeout => 3 * 3600 + 20 * 60, # 3h:20m
white_timeout => 36 * 3600 * 24, # 36 days white_timeout => 36 * 3600 * 24, # 36 days
nfslock => 0, nfslock => 0,
p0f => undef, p0f => undef,
@ -360,7 +352,7 @@ sub greylist {
my $value = $self->db->get($key); my $value = $self->db->get($key);
if ( ! $value ) { if ( ! $value ) {
# new IP or entry timed out - record new # new IP or entry timed out - record new
$self->db->set( $key, sprintf $fmt, time, 1, 0, 0 ); $self->db->set( $key, sprintf $fmt, $self->now, 1, 0, 0 );
$self->log(LOGWARN, "fail: initial DENYSOFT, unknown"); $self->log(LOGWARN, "fail: initial DENYSOFT, unknown");
return $self->cleanup_and_return(); return $self->cleanup_and_return();
} }
@ -371,8 +363,8 @@ sub greylist {
if ($white) { if ($white) {
# white IP - accept unless timed out # white IP - accept unless timed out
if (time - $ts < $config->{white_timeout}) { if ( $self->now - $ts < $config->{white_timeout} ) {
$self->db->set( $key, sprintf $fmt, time, $new, $black, ++$white ); $self->db->set( $key, sprintf $fmt, $self->now, $new, $black, ++$white );
$self->log(LOGINFO, "pass: white, $white deliveries"); $self->log(LOGINFO, "pass: white, $white deliveries");
return $self->cleanup_and_return(DECLINED); return $self->cleanup_and_return(DECLINED);
} }
@ -382,24 +374,20 @@ sub greylist {
} }
# Black IP - deny, but don't update timestamp # Black IP - deny, but don't update timestamp
if (time - $ts < $config->{black_timeout}) { if ( $self->now - $ts < $config->{black_timeout} ) {
$self->db->set( $key, sprintf $fmt, $ts, $new, ++$black, 0 ); $self->db->set( $key, sprintf $fmt, $ts, $new, ++$black, 0 );
$self->log(LOGWARN, $self->log(LOGWARN,
"fail: black DENYSOFT - $black deferred connections"); "fail: black DENYSOFT - $black deferred connections");
return $self->cleanup_and_return(); return $self->cleanup_and_return();
} }
# Grey IP - accept unless timed out
elsif (time - $ts < $config->{grey_timeout}) {
$self->db->set( $key, sprintf $fmt, time, $new, $black, 1 );
$self->log(LOGWARN, "pass: updated grey->white");
return $self->cleanup_and_return(DECLINED);
}
$self->log(LOGWARN, "pass: timed out (grey)"); $self->log(LOGWARN, "pass: timed out (grey)");
return $self->cleanup_and_return(DECLINED); return $self->cleanup_and_return(DECLINED);
} }
# This exists purely to be overridden for testing
sub now { time() }
sub cleanup_and_return { sub cleanup_and_return {
my ($self, $return_val) = @_; my ($self, $return_val) = @_;
@ -461,7 +449,7 @@ sub prune_db {
my $pruned = 0; my $pruned = 0;
foreach my $key ( $self->db->get_keys ) { foreach my $key ( $self->db->get_keys ) {
my ($ts, $new, $black, $white) = split /:/, $self->db->get($key); my ($ts, $new, $black, $white) = split /:/, $self->db->get($key);
my $age = time - $ts; my $age = $self->now - $ts;
next if $age < $self->{_args}{white_timeout}; next if $age < $self->{_args}{white_timeout};
$pruned++; $pruned++;
$self->db->delete($key); $self->db->delete($key);

View File

@ -26,6 +26,7 @@ sub register_tests {
$self->register_test("test_greylist_p0f_link"); $self->register_test("test_greylist_p0f_link");
$self->register_test("test_greylist_p0f_uptime"); $self->register_test("test_greylist_p0f_uptime");
$self->register_test('test_exclude_file_match'); $self->register_test('test_exclude_file_match');
$self->register_test('test_greylist');
} }
sub test_load_exclude_files { sub test_load_exclude_files {
@ -219,6 +220,56 @@ sub test_greylist_p0f_uptime {
ok( ! $self->p0f_match(), 'p0f uptime miss'); ok( ! $self->p0f_match(), 'p0f uptime miss');
} }
my $mocktime;
sub test_greylist {
my ( $self ) = @_;
$self->{_args} = {
remote_ip => 1,
sender => 0,
recipient => 0,
reject => 1,
black_timeout => 50 * 60, # 50m
grey_timeout => 3 * 3600 + 20 * 60, # 3h:20m
white_timeout => 36 * 3600 * 24, # 36 days
p0f => 0,
geoip => 0,
};
$self->connection->remote_host('example.com');
$self->connection->remote_ip('1.2.3.4');
my $sender = Qpsmtpd::Address->new( "<$test_email>" );
my $rcpt = Qpsmtpd::Address->new( "<$test_email>" );
my $start = time() - 40 * 3600 * 24; # 40 days ago
$mocktime = $start;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
'Initial connection attempt greylisted' );
$mocktime = $start + 60 * 49;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
'Greylisted 49 minutes later' );
$mocktime = $start + 60 * 51;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DECLINED',
'Allowed 51 minutes later' );
$mocktime = $start + 60 * 52 + 36 * 3600 * 24;
$self->prune_db;
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
'DENYSOFT: This mail is temporarily denied',
're-greylisted 36 days later' );
}
{
no warnings qw( redefine );
sub now { $mocktime || time() }
}
sub rc {
my ( $self, $r, $msg ) = @_;
return '' if ! defined $r;
return return_code($r) if ! defined $msg;
return return_code($r) . ": $msg";
}
sub _reset_transaction { sub _reset_transaction {
my $self = shift; my $self = shift;