added p0f config option added POD docs to explain usage modified $dbdir selection logic. The previous logic failed when QPHOME was not selected (as is the case when tests are being run). Added '.' as the dir of last resort for $dbdir selection (others $EMPTY/dir dumped greylisting database in / ) Added t/plugin_tests/greylisting, with greylist logic testing (tests are disabled by default, as greylisting is disabled in config.sample/plugins) --- plugins/greylisting | 57 +++++++++++++++++++++-- t/plugin_tests/greylisting | 109 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 t/plugin_tests/greylisting
diff --git a/plugins/greylisting b/plugins/greylisting index 2bdfbc1..9d28ce5 100644 --- a/plugins/greylisting +++ b/plugins/greylisting @@ -105,6 +105,23 @@ Flag to indicate whether to use per-recipient greylisting databases (default is to use a shared database). Per-recipient configuration directories, if determined, supercede I<db_dir>. +=item p0f + +Enable greylisting only when certain p0f criteria is met. The single +required argument is a comma delimited list of key/value pairs. The keys +are the following p0f TCP fingerprint elements: genre, detail, uptime, +link, and distance. + +To greylist emails from computers whose remote OS is windows, you'd use +this syntax: + + p0f genre,windows + +To greylist only windows computers on DSL links more than 3 network hops +away: + + p0f genre,windows,link,dsl,distance,3 + =back =head1 BUGS @@ -117,6 +134,8 @@ use something like File::NFSLock instead. Written by Gavin Carr <ga...@openfusion.com.au>. +Added p0f section <mattsimer...@cpan.org> (2010-05-03) + =cut BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) } @@ -124,13 +143,13 @@ use AnyDBM_File; use Fcntl qw(:DEFAULT :flock); use strict; -my $VERSION = '0.07'; +my $VERSION = '0.08'; my $DENYMSG = "This mail is temporarily denied"; my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!); my $DB = "denysoft_greylist.dbm"; my %PERMITTED_ARGS = map { $_ => 1 } qw(per_recipient remote_ip sender recipient - black_timeout grey_timeout white_timeout deny_late mode db_dir); + black_timeout grey_timeout white_timeout deny_late mode db_dir p0f ); my %DEFAULTS = ( remote_ip => 1, @@ -140,6 +159,7 @@ my %DEFAULTS = ( grey_timeout => 3 * 3600 + 20 * 60, white_timeout => 36 * 24 * 3600, mode => 'denysoft', + p0f => undef, ); sub register { @@ -224,17 +244,23 @@ sub denysoft_greylist { return DECLINED if $self->qp->connection->notes('whitelisthost'); return DECLINED if $transaction->notes('whitelistsender'); + # do not greylist if p0f matching is selected and message does not match + return DECLINED if $config->{'p0f'} && !$self->p0f_match( $config ); + if ($config->{db_dir} && $config->{db_dir} =~ m{^([-a-zA-Z0-9./_]+)$}) { $config->{db_dir} = $1; } # Setup database location - my $dbdir = $transaction->notes('per_rcpt_configdir') + my $dbdir; + $dbdir = $transaction->notes('per_rcpt_configdir') if $config->{per_recipient_db}; for my $d ($dbdir, $config->{db_dir}, "/var/lib/qpsmtpd/greylisting", - "$QPHOME/var/db", "$QPHOME/config") + "$QPHOME/var/db", "$QPHOME/config", '.' ) { - last if $dbdir ||= $d && -d $d && $d; + last if $dbdir && -d $dbdir; + next if ( ! $d || ! -d $d ); + $dbdir = $d; } my $db = "$dbdir/$DB"; $self->log(LOGINFO, "using $db as greylisting database"); @@ -316,5 +342,26 @@ sub denysoft_greylist { return $config->{mode} eq 'testonly' ? DECLINED : DENYSOFT, $DENYMSG; } +sub p0f_match { + my $self = shift; + my $config = shift; + + my $p0f = $self->connection->notes('p0f'); + return if !$p0f || !ref $p0f; # p0f fingerprint info not found + + my %valid_matches = map { $_ => 1 } qw( genre detail uptime link distance ); + my %requested_matches = split(/\,/, $config->{'p0f'} ); + + foreach my $key (keys %requested_matches) { + next if !defined $valid_matches{$key}; # discard invalid match keys + my $value = $requested_matches{$key}; + return 1 if $key eq 'distance' && $p0f->{$key} > $value; + return 1 if $key eq 'genre' && $p0f->{$key} =~ /$value/i; + return 1 if $key eq 'uptime' && $p0f->{$key} < $value; + return 1 if $key eq 'link' && $p0f->{$key} =~ /$value/i; + } + return; +} + # arch-tag: 6ef5919e-404b-4c87-bcfe-7e9f383f3901 diff --git a/t/plugin_tests/greylisting b/t/plugin_tests/greylisting new file mode 100644 index 0000000..5c8f068 --- /dev/null +++ b/t/plugin_tests/greylisting @@ -0,0 +1,109 @@ +use Qpsmtpd::Address; + +my $test_email = 'u...@example.com'; +my $address = Qpsmtpd::Address->new( "<$test_email>" ); + +my @greydbs = qw( denysoft_greylist.dbm denysoft_greylist.dbm.lock ); +foreach ( @greydbs ) { + unlink $_ if -f $_; +}; + +sub register_tests { + my $self = shift; + $self->register_test("test_greylist_p0f_genre_miss", 1); + $self->register_test("test_greylist_p0f_genre_hit", 1); + $self->register_test("test_greylist_p0f_distance_hit", 1); + $self->register_test("test_greylist_p0f_distance_miss", 1); + $self->register_test("test_greylist_p0f_link_hit", 1); + $self->register_test("test_greylist_p0f_link_miss", 1); + $self->register_test("test_greylist_p0f_uptime_hit", 1); + $self->register_test("test_greylist_p0f_uptime_miss", 1); +} + +sub test_greylist_p0f_genre_miss { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'genre,Linux'; + $self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r == 909, 'p0f genre miss'); +} + +sub test_greylist_p0f_genre_hit { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'genre,Windows'; + $self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r eq 'This mail is temporarily denied', 'p0f genre hit'); +} + +sub test_greylist_p0f_distance_hit { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'distance,8'; + $self->connection->notes('p0f'=> { distance=>9 } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r eq 'This mail is temporarily denied', 'p0f distance hit'); +} + +sub test_greylist_p0f_distance_miss { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'distance,8'; + $self->connection->notes('p0f'=> { distance=>7 } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r == 909, 'p0f distance miss'); +} + +sub test_greylist_p0f_link_hit { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'link,dsl'; + $self->connection->notes('p0f'=> { link=>'DSL' } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r eq 'This mail is temporarily denied', 'p0f link hit'); +} + +sub test_greylist_p0f_link_miss { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'link,dsl'; + $self->connection->notes('p0f'=> { link=>'Ethernet' } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r == 909, 'p0f link miss'); +} + +sub test_greylist_p0f_uptime_hit { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'uptime,100'; + $self->connection->notes('p0f'=> { uptime=> 99 } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r eq 'This mail is temporarily denied', 'p0f uptime hit'); +} + +sub test_greylist_p0f_uptime_miss { + my $self = shift; + + $self->{_greylist_config}{'p0f'} = 'uptime,100'; + $self->connection->notes('p0f'=> { uptime=>500 } ); + $self->qp->transaction->sender( $address ); + my $r = $self->rcpt_handler( $self->qp->transaction ); + + ok( $r == 909, 'p0f uptime miss'); +} + -- 1.7.0.6