(patch remade against latest rspier/qpsmtpd)

added remote_port, local_ip, local_port, and local_host to $qp->connection, as 
the p0f plugin relies on it.
added notes to TcpServer.pm and the p0f plugin noting the dependence, and the 
lack of support for models other than tcpserver.
---
 lib/Qpsmtpd/TcpServer.pm |   21 ++++++++++++--
 plugins/greylisting      |   68 ++++++++++++++++++++++++++++++++++++++-------
 plugins/ident/p0f        |    8 +++++
 3 files changed, 83 insertions(+), 14 deletions(-)

diff --git a/lib/Qpsmtpd/TcpServer.pm b/lib/Qpsmtpd/TcpServer.pm
index 3398c3e..0df93d2 100644
--- a/lib/Qpsmtpd/TcpServer.pm
+++ b/lib/Qpsmtpd/TcpServer.pm
@@ -30,7 +30,10 @@ my $first_0;
 sub start_connection {
     my $self = shift;
 
-    my ($remote_host, $remote_info, $remote_ip);
+    my (
+        $remote_host, $remote_info, $remote_ip, $remote_port,
+        $local_ip,    $local_port,  $local_host
+       );
 
     if ($ENV{TCPREMOTEIP}) {
        # started from tcpserver (or some other superserver which
@@ -38,6 +41,10 @@ sub start_connection {
        $remote_ip   = $ENV{TCPREMOTEIP};
        $remote_host = $ENV{TCPREMOTEHOST} || "[$remote_ip]";
        $remote_info = $ENV{TCPREMOTEINFO} ? 
"$env{tcpremoteinf...@$remote_host" : $remote_host;
+        $remote_port = $ENV{TCPREMOTEPORT};
+        $local_ip    = $ENV{TCPLOCALIP};
+        $local_port  = $ENV{TCPLOCALPORT};
+        $local_host  = $ENV{TCPLOCALHOST};
     } else {
        # Started from inetd or similar. 
        # get info on the remote host from the socket.
@@ -48,6 +55,10 @@ sub start_connection {
        $remote_ip     = inet_ntoa($iaddr);
        $remote_host    = gethostbyaddr($iaddr, AF_INET) || "[$remote_ip]";
        $remote_info    = $remote_host;
+### TODO 
+# set $remote_port, $local_ip, and $local_port. Those values are 
+# required for the p0f plugin to function.
+### /TODO
     }
     $self->log(LOGNOTICE, "Connection from $remote_info [$remote_ip]");
 
@@ -61,8 +72,12 @@ sub start_connection {
     $0 = "$first_0 [$remote_ip : $remote_host : $now]";
 
     $self->SUPER::connection->start(remote_info => $remote_info,
-                                   remote_ip   => $remote_ip,
-                                   remote_host => $remote_host,
+                                    remote_ip   => $remote_ip,
+                                    remote_host => $remote_host,
+                                    remote_port => $remote_port,
+                                    local_ip    => $local_ip,
+                                    local_port  => $local_port,
+                                    local_host  => $local_host,
                                    @_);
 }
 
diff --git a/plugins/greylisting b/plugins/greylisting
index 975563c..f5e7ba3 100644
--- a/plugins/greylisting
+++ b/plugins/greylisting
@@ -106,6 +106,23 @@ directories, if determined, supercede I<db_dir>.
 
 =back
 
+=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
+
 =head1 BUGS
 
 Database locking is implemented using flock, which may not work on 
@@ -116,6 +133,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) }
@@ -123,22 +142,23 @@ 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,
-  sender => 0,
-  recipient => 0,
-  black_timeout => 50 * 60,
-  grey_timeout =>  3 * 3600 + 20 * 60,
-  white_timeout => 36 * 24 * 3600,
-  mode => 'denysoft',
+    remote_ip => 1,
+    sender => 0,
+    recipient => 0,
+    black_timeout => 50 * 60,
+    grey_timeout =>  3 * 3600 + 20 * 60,
+    white_timeout => 36 * 24 * 3600,
+    mode => 'denysoft',
+    p0f  => undef,
 );
 
 sub register {
@@ -206,6 +226,9 @@ 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; 
   }
@@ -214,8 +237,10 @@ sub denysoft_greylist {
   my $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") {
-    last if $dbdir ||= $d && -d $d && $d;
+             "$QPHOME/var/db", "$QPHOME/config", '.' ) {
+      last if $dbdir && -d $dbdir;
+      next if ( ! $d || ! -d $d );
+      $dbdir = $d;
   }
   my $db = "$dbdir/$DB";
   $self->log(LOGINFO,"using $db as greylisting database");
@@ -292,5 +317,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/plugins/ident/p0f b/plugins/ident/p0f
index 720adca..98b56ec 100644
--- a/plugins/ident/p0f
+++ b/plugins/ident/p0f
@@ -18,6 +18,14 @@ things based on source OS.
 
 All code heavily based upon the p0fq.pl included with the p0f distribution.
 
+=head1 Environment requirements
+
+p0f requires four pieces of information to look up the p0f fingerprint:
+local_ip, local_port, remote_ip, and remote_port. TcpServer.pm has been
+has been updated to provide that information when running under djb's
+tcpserver. The async, forkserver, and prefork models will likely require
+some additional changes to make sure these fields are populated.
+
 =cut
 
 use IO::Socket;
-- 
1.7.0.6

Reply via email to