diff --git a/plugins/greylisting b/plugins/greylisting index 5289601..763384f 100644 --- a/plugins/greylisting +++ b/plugins/greylisting @@ -360,7 +360,7 @@ sub greylist { my $value = $self->db->get($key); if ( ! $value ) { # 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"); return $self->cleanup_and_return(); } @@ -371,8 +371,8 @@ sub greylist { if ($white) { # white IP - accept unless timed out - if (time - $ts < $config->{white_timeout}) { - $self->db->set( $key, sprintf $fmt, time, $new, $black, ++$white ); + if ( $self->now - $ts < $config->{white_timeout} ) { + $self->db->set( $key, sprintf $fmt, $self->now, $new, $black, ++$white ); $self->log(LOGINFO, "pass: white, $white deliveries"); return $self->cleanup_and_return(DECLINED); } @@ -382,7 +382,7 @@ sub greylist { } # 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->log(LOGWARN, "fail: black DENYSOFT - $black deferred connections"); @@ -390,8 +390,8 @@ sub greylist { } # Grey IP - accept unless timed out - elsif (time - $ts < $config->{grey_timeout}) { - $self->db->set( $key, sprintf $fmt, time, $new, $black, 1 ); + elsif ( $self->now - $ts < $config->{grey_timeout} ) { + $self->db->set( $key, sprintf $fmt, $self->now, $new, $black, 1 ); $self->log(LOGWARN, "pass: updated grey->white"); return $self->cleanup_and_return(DECLINED); } @@ -400,6 +400,9 @@ sub greylist { return $self->cleanup_and_return(DECLINED); } +# This exists purely to be overridden for testing +sub now { time() } + sub cleanup_and_return { my ($self, $return_val) = @_; @@ -461,7 +464,7 @@ sub prune_db { my $pruned = 0; foreach my $key ( $self->db->get_keys ) { 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}; $pruned++; $self->db->delete($key); diff --git a/t/plugin_tests/greylisting b/t/plugin_tests/greylisting index 4576ecb..434de4d 100644 --- a/t/plugin_tests/greylisting +++ b/t/plugin_tests/greylisting @@ -26,6 +26,7 @@ sub register_tests { $self->register_test("test_greylist_p0f_link"); $self->register_test("test_greylist_p0f_uptime"); $self->register_test('test_exclude_file_match'); + $self->register_test('test_greylist'); } sub test_load_exclude_files { @@ -219,6 +220,68 @@ sub test_greylist_p0f_uptime { 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' ); + + $self->connection->remote_ip('4.3.2.1'); + $mocktime = $start; + is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ), + 'DENYSOFT: This mail is temporarily denied', + 'Initial connection attempt greylisted for new IP' ); + $mocktime = $start + 36 * 3600 * 24 - 60; + is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ), +# 'DENYSOFT: This mail is temporarily denied', +# 'New IP still greylisted when greylist_timeout window is missed' ); + 'DECLINED', + "New IP allowed for 36 days, just like 'white' clients (huh?)" ); +} + +{ + 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 { my $self = shift;