Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Naughty #24

Closed
wants to merge 16 commits into from

1 participant

Matt Simerson
Matt Simerson
Owner

naughty - dispose of naughty connections

Rather than immediately terminating naughty connections, plugins often mark
the connections and dispose of them later. Examples are B, B,
B, B and B.

Disconnecting later is inefficient because other plugins continue to do their
work, oblivious to the fact that the connection is destined for the bit bucket.

Naughty provides plugins with an efficient way to offer late disconnects. It
does this by allowing other plugins to detect that a connection is naughty.
For efficiency, other plugins should skip processing naughty connections.
Plugins like SpamAssassin and DSPAM can benefit from using naughty connections
to train their filters.

Instead of each plugin handling cleanup, B does it. Set I to
the hook you prefer to reject in and B will reject the naughty
connections, regardless of who identified them, exactly when you choose.

Matt Simerson msimerson closed this
Matt Simerson msimerson deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 4, 2012
  1. Matt Simerson
  2. Matt Simerson

    new plugin: naughty

    msimerson authored
  3. Matt Simerson
  4. Matt Simerson
  5. Matt Simerson
  6. Matt Simerson
  7. Matt Simerson
  8. Matt Simerson

    restore validate_password test

    msimerson authored
    not sure how/why that got removed, but it wasn't intentional
  9. Matt Simerson
  10. Matt Simerson

    added new t/config directory, with developer tests

    msimerson authored
    run when $ENV{QPSMTPD_DEVELOPER} is set
    
    plugins file is same as in config.sample, but with more stuff enabled
  11. Matt Simerson
Commits on Jun 7, 2012
  1. Matt Simerson

    badmailfrom: allow selective rejection

    msimerson authored
    default handling preserved.
    adds ability to disable rejection or handle via naughty
  2. Matt Simerson
  3. Matt Simerson

    added t/config/invalid_resolvable_fromhost

    msimerson authored
    require_resolvable_fromhost test requires it
  4. Matt Simerson
  5. Matt Simerson
This page is out of date. Refresh to see the latest.
2  config.sample/flat_auth_pw
View
@@ -1,2 +1,4 @@
+# used by plugins/auth/auth_flat_file
+# example entries
good@example.com:good_pass
bad@example.com:bad_pass
4 config.sample/plugins
View
@@ -45,6 +45,10 @@ check_spamhelo
# sender_permitted_from
# greylisting p0f genre,windows
+#auth/auth_checkpassword checkpw /usr/local/vpopmail/bin/vchkpw true /usr/bin/true
+#auth/auth_vpopmail
+#auth/auth_vpopmaild
+#auth/auth_vpopmail_sql
auth/auth_flat_file
auth/authdeny
1  config.sample/smtpauth-checkpassword
View
@@ -0,0 +1 @@
+/usr/local/vpopmail/bin/vchkpw /bin/true
4 lib/Qpsmtpd/Auth.pm
View
@@ -57,6 +57,10 @@ sub SASL {
( $msg ? " - $msg" : '');
$session->respond( 235, $msg );
$session->connection->relay_client(1);
+ if ( $session->connection->notes('naughty' ) ) {
+ $session->log( LOGINFO, "auth success cleared naughty" );
+ $session->connection->notes('naughty',0);
+ };
$session->log( LOGDEBUG, $msg ); # already logged by $session->respond
$session->{_auth_user} = $user;
1  plugins/auth/auth_checkpassword
View
@@ -124,6 +124,7 @@ sub auth_checkpassword {
my $binary = $self->connection->notes('auth_checkpassword_bin');
my $true = $self->connection->notes('auth_checkpassword_true');
+ chomp ($binary, $true);
my $sudo = get_sudo($binary);
4 plugins/auth/auth_vpopmail
View
@@ -45,7 +45,7 @@ use warnings;
use Qpsmtpd::Auth;
use Qpsmtpd::Constants;
-#use vpopmail; # we eval this in $test_vpopmail
+#use vpopmail; # we eval this in $test_vpopmail_module
sub register {
my ($self, $qp) = @_;
@@ -86,7 +86,7 @@ sub test_vpopmail_module {
my $self = shift;
# vpopmail will not allow vauth_getpw to succeed unless the requesting user is vpopmail or root.
# by default, qpsmtpd runs as the user 'qpsmtpd' and does not have permission.
- eval "use vpopmail";
+ eval 'use vpopmail';
if ( $@ ) {
$self->log(LOGERROR, "skip: is vpopmail perl module installed?");
return;
9 plugins/auth/auth_vpopmail_sql
View
@@ -69,11 +69,18 @@ use warnings;
use Qpsmtpd::Auth;
use Qpsmtpd::Constants;
-use DBI;
+#use DBI; # done in ->register
sub register {
my ( $self, $qp ) = @_;
+ eval 'use DBI';
+ if ( $@ ) {
+ warn "plugin disabled. is DBI installed?\n";
+ $self->log(LOGERROR, "skip: plugin disabled. is DBI installed?\n");
+ return;
+ };
+
$self->register_hook('auth-plain', 'auth_vmysql');
$self->register_hook('auth-login', 'auth_vmysql');
$self->register_hook('auth-cram-md5', 'auth_vmysql');
44 plugins/check_badmailfrom
View
@@ -17,6 +17,20 @@ listed in badmailfrom. A line in badmailfrom may be of the form
You may include an optional message after the sender address (leave a space),
to be used when rejecting the sender.
+=head1 CONFIGURATION
+
+=head2 reject
+
+ badmailfrom reject [ 0 | 1 | naughty ]
+
+I<0> will not reject any connections.
+
+I<1> will reject naughty senders.
+
+I<connect> is the most efficient setting. It's also the default.
+
+To reject at any other connection hook, use the I<naughty> setting and the
+B<naughty> plugin.
=head1 PATTERNS
@@ -42,14 +56,27 @@ stage, so store it until later.
=head1 AUTHORS
-initial author of badmailfrom - Jim Winstead
+2002 - Jim Winstead - initial author of badmailfrom
-pattern matching plugin - Johan Almqvist <johan-qpsmtpd@almqvist.net>
+2010 - Johan Almqvist <johan-qpsmtpd@almqvist.net> - pattern matching plugin
-merging of the two and plugin tests - Matt Simerson <matt@tnpi.net>
+2012 - Matt Simerson - merging of the two and plugin tests
=cut
+sub register {
+ my ($self,$qp) = shift, shift;
+ $self->{_args} = { @_ };
+
+ # preserve legacy "reject during rcpt" behavior
+ $self->{_args}{reject} = 1 if ! defined $self->{_args}{reject};
+
+ return if ! $self->{_args}{reject}; # reject 0, log only
+ return if $self->{_args}{reject} eq 'naughty'; # naughty will reject
+
+ $self->register_hook('rcpt', 'rcpt_handler');
+};
+
sub hook_mail {
my ($self, $transaction, $sender, %param) = @_;
@@ -69,8 +96,11 @@ sub hook_mail {
next unless $bad;
next unless $self->is_match( $from, $bad, $host );
$reason ||= "Your envelope sender is in my badmailfrom list";
- $transaction->notes('badmailfrom', $reason);
+ $self->connection->notes('naughty', $reason);
}
+ if ( ! $self->connection->notes('naughty') ) {
+ $self->log(LOGINFO, "pass");
+ };
return DECLINED;
}
@@ -96,11 +126,11 @@ sub is_match {
return 1;
};
-sub hook_rcpt {
+sub rcpt_handler {
my ($self, $transaction, $rcpt, %param) = @_;
- my $note = $transaction->notes('badmailfrom') or return (DECLINED);
+ my $note = $self->connection->notes('naughty') or return (DECLINED);
- $self->log(LOGINFO, $note);
+ $self->log(LOGINFO, "fail, $note");
return (DENY, $note);
}
161 plugins/naughty
View
@@ -0,0 +1,161 @@
+#!perl -w
+
+=head1 NAME
+
+naughty - dispose of naughty connections
+
+=head1 BACKGROUND
+
+Rather than immediately terminating naughty connections, plugins often mark
+the connections and dispose of them later. Examples are B<dnsbl>, B<karma>,
+B<greylisting>, B<resolvable_fromhost> and B<SPF>.
+
+This practice is based on RFC standards and the belief that malware will retry
+less if we disconnect after RCPT. This may have been true, and may still be,
+but my observations in 2012 suggest it makes no measurable difference whether
+I disconnect during connect or rcpt.
+
+Disconnecting later is inefficient because other plugins continue to do their
+work, oblivious to the fact that the connection is destined for the bit bucket.
+
+=head1 DESCRIPTION
+
+Naughty provides the following:
+
+=head2 efficiency
+
+Naughty provides plugins with an efficient way to offer late disconnects. It
+does this by allowing other plugins to detect that a connection is naughty.
+For efficiency, other plugins should skip processing naughty connections.
+Plugins like SpamAssassin and DSPAM can benefit from using naughty connections
+to train their filters.
+
+Since so many connections are from blacklisted IPs, naughty significantly
+reduces the processing time required for disposing of them. Over 80% of my
+connections are disposed of after after a few DNS queries (B<dnsbl> or one DB
+query (B<karma>) and 0.01s of compute time.
+
+=head2 naughty cleanup
+
+Instead of each plugin handling cleanup, B<naughty> does it. Set I<reject> to
+the hook you prefer to reject in and B<naughty> will reject the naughty
+connections, regardless of who identified them, exactly when you choose.
+
+=head2 simplicity
+
+Rather than having plugins split processing across hooks, they can run to
+completion when they have the information they need, issue a
+I<reject naughty> if warranted, and be done.
+
+This may help reduce the code divergence between the sync and async
+deployment models.
+
+=head2 authentication
+
+When a user authenticates, the naughty flag on their connection is cleared.
+This is to allow users to send email from IPs that fail connection tests such
+as B<dnsbl>. Keep in mind that if I<reject connect> is set, connections will
+not get the chance to authenticate.
+
+=head2 naughty
+
+<naughty> provides a a consistent way for plugins to mark connections as
+naughty. Set the connection note I<naughty> to the message you wish to send
+the naughty sender during rejection.
+
+ $self->connection->notes('naughty', $message);
+
+This happens for plugins automatically if they use the $self->get_reject()
+method and have set I<reject naughty> in the plugin configuration.
+
+=head1 CONFIGURATION
+
+=head2 reject
+
+ naughty reject [ connect | mail | rcpt | data | data_post ]
+
+The phase of the connection in which the naughty connection will be terminated.
+Keep in mind that if you choose rcpt and a plugin (like B<rcpt_ok>) runs first,
+and B<rcpt_ok> returns OK, then this plugin will not get called and the
+message will not get rejected.
+
+Solutions are to make sure B<naughty> is listed before rcpt_ok in config/plugins
+or set naughty to run in a phase after the one you wish to complete.
+In this case, use data instead of rcpt to disconnect after rcpt_ok. The latter
+is particularly useful if your rcpt plugins skip naughty testing. In that case,
+any recipient is accepted for naughty connections, which prevents spammers
+from detecting address validity.
+
+=head2 reject_type [ temp | perm | disconnect ]
+
+What type of rejection should be sent? See docs/config.pod
+
+=head2 loglevel
+
+Adjust the quantity of logging for this plugin. See docs/logging.pod
+
+=head1 EXAMPLES
+
+Here's how to use naughty and get_reject in your plugin:
+
+ sub register {
+ my ($self,$qp) = shift, shift;
+ $self->{_args} = { @_ };
+ $self->{_args}{reject} ||= 'naughty';
+ };
+
+ sub connect_handler {
+ my ($self, $transaction) = @_;
+ ... do a bunch of stuff ...
+ return DECLINED if is_okay();
+ return $self->get_reject( $message );
+ };
+
+=head1 AUTHOR
+
+ 2012 - Matt Simerson - msimerson@cpan.org
+
+=cut
+
+use strict;
+use warnings;
+
+use Qpsmtpd::Constants;
+
+sub register {
+ my ($self, $qp ) = shift, shift;
+ $self->log(LOGERROR, "Bad arguments") if @_ % 2;
+ $self->{_args} = { @_ };
+ $self->{_args}{reject} ||= 'rcpt';
+ $self->{_args}{reject_type} ||= 'disconnect';
+
+ my $reject = lc $self->{_args}{reject};
+ my %hooks = map { $_ => 1 }
+ qw/ connect mail rcpt data data_post hook_queue_post /;
+
+ if ( ! $hooks{$reject} ) {
+ $self->log( LOGERROR, "fail, invalid hook $reject" );
+ $self->register_hook( 'data_post', 'naughty');
+ return;
+ };
+
+ # just in case naughty doesn't disconnect, which can happen if a plugin
+ # with the same hook returned OK before naughty ran, or ....
+ if ( $reject ne 'data_post' && $reject ne 'hook_queue_post' ) {
+ $self->register_hook( 'data_post', 'naughty');
+ };
+
+ $self->log(LOGDEBUG, "registering hook $reject");
+ $self->register_hook( $reject, 'naughty');
+}
+
+sub naughty {
+ my $self = shift;
+ my $naughty = $self->connection->notes('naughty') or do {
+ $self->log(LOGINFO, "pass, clean");
+ return DECLINED;
+ };
+ $self->log(LOGINFO, "disconnecting");
+ return ( $self->get_reject_type(), $naughty );
+};
+
1  t/Test/Qpsmtpd.pm
View
@@ -69,6 +69,7 @@ sub input {
}
sub config_dir {
+ return './t/config' if $ENV{QPSMTPD_DEVELOPER};
'./config.sample';
}
4 t/Test/Qpsmtpd/Plugin.pm
View
@@ -5,8 +5,10 @@ package Test::Qpsmtpd::Plugin;
package Qpsmtpd::Plugin;
use strict;
-use Test::More;
+use warnings;
+
use Qpsmtpd::Constants;
+use Test::More;
sub register_tests {
# Virtual base method - implement in plugin
15 t/config.t
View
@@ -5,12 +5,17 @@ use strict;
use lib 't';
use_ok('Test::Qpsmtpd');
+my @mes;
+
BEGIN { # need this to happen before anything else
my $cwd = `pwd`;
chomp($cwd);
- open my $me_config, '>', "./config.sample/me";
- print $me_config "some.host.example.org";
- close $me_config;
+ @mes = qw{ ./config.sample/me ./t/config/me };
+ foreach my $f ( @mes ) {
+ open my $me_config, '>', $f;
+ print $me_config "some.host.example.org";
+ close $me_config;
+ };
}
ok(my ($smtpd, $conn) = Test::Qpsmtpd->new_conn(), "get new connection");
@@ -22,6 +27,8 @@ is($smtpd->config('me'), 'some.host.example.org', 'config("me")');
my $relayclients = join ",", sort $smtpd->config('relayclients');
is($relayclients, '127.0.0.1,192.168.', 'config("relayclients") are trimmed');
-unlink "./config.sample/me";
+foreach my $f ( @mes ) {
+ unlink $f if -f $f;
+};
4 t/config/badhelo
View
@@ -0,0 +1,4 @@
+# these domains never uses their domain when greeting us, so reject transactions
+aol.com
+yahoo.com
+
9 t/config/badrcptto
View
@@ -0,0 +1,9 @@
+######## entries used for testing ###
+bad@example.com
+@bad.example.com
+######## Example patterns #######
+# Format is pattern\s+Response
+# Don't forget to anchor the pattern if required
+! Sorry, bang paths not accepted here
+@.*@ Sorry, multiple at signs not accepted here
+% Sorry, percent hack not accepted here
1  t/config/dnsbl_zones
View
@@ -0,0 +1 @@
+zen.spamhaus.org
2  t/config/flat_auth_pw
View
@@ -0,0 +1,2 @@
+good@example.com:good_pass
+bad@example.com:bad_pass
6 t/config/invalid_resolvable_fromhost
View
@@ -0,0 +1,6 @@
+# include full network block including mask
+127.0.0.0/8
+0.0.0.0/8
+224.0.0.0/4
+169.254.0.0/16
+10.0.0.0/8
94 t/config/plugins
View
@@ -0,0 +1,94 @@
+#
+# Example configuration file for plugins
+#
+
+# enable this to get configuration via http; see perldoc
+# plugins/http_config for details.
+# http_config http://localhost/~smtpd/config/ http://www.example.com/smtp.pl?config=
+
+# hosts_allow does not work with the tcpserver deployment model!
+# perldoc plugins/hosts_allow for an alternative.
+#
+# The hosts_allow module must be loaded if you want the -m / --max-from-ip /
+# my $MAXCONNIP = 5; # max simultaneous connections from one IP
+# settings... without this it will NOT refuse more than $MAXCONNIP connections
+# from one IP!
+hosts_allow
+
+# information plugins
+ident/geoip
+#ident/p0f /tmp/.p0f_socket version 3
+connection_time
+
+# enable to accept MAIL FROM:/RCPT TO: addresses without surrounding <>
+dont_require_anglebrackets
+
+# enable to reject MAIL FROM:/RCPT TO: parameters if client helo was HELO
+# (strict RFC 821)... this is not used in EHLO ...
+parse_addr_withhelo
+
+quit_fortune
+# tls should load before count_unrecognized_commands
+#tls
+check_earlytalker
+count_unrecognized_commands 4
+check_relay
+
+require_resolvable_fromhost
+
+rhsbl
+dnsbl
+check_badmailfrom
+check_badrcptto
+check_spamhelo
+
+sender_permitted_from
+greylisting p0f genre,windows
+
+auth/auth_checkpassword checkpw /usr/local/vpopmail/bin/vchkpw true /usr/bin/true
+auth/auth_vpopmail
+auth/auth_vpopmaild
+auth/auth_vpopmail_sql
+auth/auth_flat_file
+auth/authdeny
+
+# this plugin needs to run after all other "rcpt" plugins
+rcpt_ok
+
+check_basicheaders days 5 reject_type temp
+domainkeys
+
+# content filters
+virus/klez_filter
+
+
+# You can run the spamassassin plugin with options. See perldoc
+# plugins/spamassassin for details.
+#
+spamassassin
+
+# rejects mails with a SA score higher than 20 and munges the subject
+# of the score is higher than 10.
+#
+# spamassassin reject_threshold 20 munge_subject_threshold 10
+
+# dspam must run after spamassassin for the learn_from_sa feature to work
+dspam learn_from_sa 7 reject 1
+
+# run the clamav virus checking plugin
+virus/clamav
+
+# You must enable a queue plugin - see the options in plugins/queue/ - for example:
+
+# queue to a maildir
+# queue/maildir /home/spamtrap/mail
+
+# queue the mail with qmail-queue
+queue/qmail-queue
+
+
+# If you need to run the same plugin multiple times, you can do
+# something like the following
+# check_relay
+# check_relay:0 somearg
+# check_relay:1 someotherarg
1  t/config/rcpthosts
View
@@ -0,0 +1 @@
+localhost
5 t/config/relayclients
View
@@ -0,0 +1,5 @@
+# Format is IP, or IP part with trailing dot
+# e.g. "127.0.0.1", or "192.168."
+127.0.0.1
+# leading/trailing whitespace is ignored
+ 192.168.
8 t/plugin_tests.t
View
@@ -7,3 +7,11 @@ my $qp = Test::Qpsmtpd->new();
$qp->run_plugin_tests();
+foreach my $file (
+ "./t/config/greylist.dbm",
+ "./t/config/greylist.dbm.lock"
+ ) {
+ next if ! -f $file;
+ unlink $file;
+};
+
2  t/plugin_tests/auth/auth_vpopmail
View
@@ -23,7 +23,7 @@ sub test_auth_vpopmail {
if ( ! $self->test_vpopmail_module ) {
warn "vpopmail plugin not configured\n";
- foreach ( 0..2) { ok( 1, "test_auth_vpopmail, skipped") };
+ foreach ( 0..2) { ok( 1, "skipped") };
return;
};
13 t/plugin_tests/auth/auth_vpopmail_sql
View
@@ -6,6 +6,11 @@ use warnings;
sub register_tests {
my $self = shift;
+ eval 'use DBI';
+ if ( $@ ) {
+ warn "skipping auth_vpopmail_sql tests, is DBI installed?\n";
+ return;
+ };
$self->register_test("auth_vpopmail_sql", 3);
}
@@ -15,7 +20,7 @@ sub auth_vpopmail_sql {
my $dbh = $self->get_db_handle() or do {
foreach ( 0..2 ) {
- ok( 1, "auth_vpopmail_sql, skipped (no DB)" );
+ ok( 1, "skipped (no DB)" );
};
return;
};
@@ -24,11 +29,11 @@ sub auth_vpopmail_sql {
my $vuser = $self->get_vpopmail_user( $dbh, 'postmaster@example.com' );
if ( ! $vuser || ! $vuser->{pw_passwd} ) {
foreach ( 0..1 ) {
- ok( 1, "auth_vpopmail_sql, no example.com domain" );
+ ok( 1, "no example.com domain" );
};
return;
};
- ok( ref $vuser, "auth_vpopmail_sql, found example.com domain" );
+ ok( ref $vuser, "found example.com domain" );
ok( $self->auth_vmysql(
$self->qp->transaction,
@@ -38,6 +43,6 @@ sub auth_vpopmail_sql {
$vuser->{pw_passwd},
$ticket,
),
- "auth_vpopmail_sql, postmaster"
+ "postmaster"
);
}
17 t/plugin_tests/check_badmailfrom
View
@@ -11,7 +11,7 @@ sub register_tests {
$self->register_test("test_badmailfrom_is_immune", 5);
$self->register_test("test_badmailfrom_match", 7);
$self->register_test("test_badmailfrom_hook_mail", 4);
- $self->register_test("test_badmailfrom_hook_rcpt", 2);
+ $self->register_test("test_badmailfrom_rcpt_handler", 2);
}
sub test_badmailfrom_is_immune {
@@ -50,29 +50,26 @@ sub test_badmailfrom_hook_mail {
$transaction->sender($address);
$self->{_badmailfrom_config} = ['matt@test.net','matt@test.com'];
- $transaction->notes('badmailfrom', '');
+ $self->connection->notes('badmailfrom', '');
my ($r) = $self->hook_mail( $transaction, $address );
ok( $r == 909, "badmailfrom hook_mail");
- ok( $transaction->notes('badmailfrom') eq 'Your envelope sender is in my badmailfrom list',
- "badmailfrom hook_mail: default reason");
+ cmp_ok( $self->connection->notes('naughty'), 'eq', 'Your envelope sender is in my badmailfrom list', "default reason");
$self->{_badmailfrom_config} = ['matt@test.net','matt@test.com Yer a spammin bastert'];
- $transaction->notes('badmailfrom', '');
+ $self->connection->notes('badmailfrom', '');
($r) = $self->hook_mail( $transaction, $address );
ok( $r == 909, "badmailfrom hook_mail");
- ok( $transaction->notes('badmailfrom') eq 'Yer a spammin bastert',
- "badmailfrom hook_mail: custom reason");
-
+ cmp_ok( $self->connection->notes('naughty'), 'eq', 'Yer a spammin bastert', "custom reason");
};
-sub test_badmailfrom_hook_rcpt {
+sub test_badmailfrom_rcpt_handler {
my $self = shift;
my $transaction = $self->qp->transaction;
$transaction->notes('badmailfrom', 'Yer a spammin bastart. Be gon wit yuh.' );
- my ($code,$note) = $self->hook_rcpt( $transaction );
+ my ($code,$note) = $self->rcpt_handler( $transaction );
ok( $code == 901, 'badmailfrom hook hit');
ok( $note, $note );
Something went wrong with that request. Please try again.