(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 <[email protected]>.
+Added p0f section <[email protected]> (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