dnsbl: process DNS queries immediately
rather than deferring until RCPT. This greatly improves efficiency, since most connections will get marked naughty much sooner, having run fewer tests.
This commit is contained in:
parent
0fe884209e
commit
b245d30e9e
177
plugins/dnsbl
177
plugins/dnsbl
@ -13,9 +13,23 @@ a configurable set of RBL services.
|
|||||||
|
|
||||||
Add the following line to the config/plugins file:
|
Add the following line to the config/plugins file:
|
||||||
|
|
||||||
dnsbl [ reject_type disconnect ] [loglevel -1]
|
dnsbl
|
||||||
|
|
||||||
=head2 reject_type [ temp | perm ]
|
The following options are also availble:
|
||||||
|
|
||||||
|
=head2 reject [ 0 | 1 | naughty ]
|
||||||
|
|
||||||
|
dnsbl reject 0 <- do not reject
|
||||||
|
|
||||||
|
dnsbl reject 1 <- reject
|
||||||
|
|
||||||
|
dnsbl reject naughty <- See perldoc plugins/naughty
|
||||||
|
|
||||||
|
Also, when I<reject naughty> is set, DNS queries are processed during connect.
|
||||||
|
|
||||||
|
=head2 reject_type [ temp | perm | disconnect ]
|
||||||
|
|
||||||
|
Default: perm
|
||||||
|
|
||||||
To immediately drop the connection (since some blacklisted servers attempt
|
To immediately drop the connection (since some blacklisted servers attempt
|
||||||
multiple sends per session), set I<reject_type disconnect>. In most cases,
|
multiple sends per session), set I<reject_type disconnect>. In most cases,
|
||||||
@ -23,14 +37,12 @@ an IP address that is listed should not be given the opportunity to begin a
|
|||||||
new transaction, since even the most volatile blacklists will return the same
|
new transaction, since even the most volatile blacklists will return the same
|
||||||
answer for a short period of time (the minimum DNS cache period).
|
answer for a short period of time (the minimum DNS cache period).
|
||||||
|
|
||||||
Default: perm
|
|
||||||
|
|
||||||
=head2 loglevel
|
=head2 loglevel
|
||||||
|
|
||||||
Adjust the quantity of logging for this plugin. See docs/logging.pod
|
|
||||||
|
|
||||||
dnsbl [loglevel -1]
|
dnsbl [loglevel -1]
|
||||||
|
|
||||||
|
Adjust the quantity of logging for this plugin. See docs/logging.pod
|
||||||
|
|
||||||
=head1 CONFIG FILES
|
=head1 CONFIG FILES
|
||||||
|
|
||||||
This plugin uses the following configuration files. All are optional. Not
|
This plugin uses the following configuration files. All are optional. Not
|
||||||
@ -121,7 +133,7 @@ See: https://github.com/smtpd/qpsmtpd/commits/master/plugins/dnsbl
|
|||||||
=cut
|
=cut
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
my ($self, $qp) = shift, shift;
|
my ($self, $qp) = (shift, shift);
|
||||||
|
|
||||||
if ( @_ % 2 ) {
|
if ( @_ % 2 ) {
|
||||||
$self->{_args}{reject_type} = shift; # backwards compatibility
|
$self->{_args}{reject_type} = shift; # backwards compatibility
|
||||||
@ -129,53 +141,80 @@ sub register {
|
|||||||
else {
|
else {
|
||||||
$self->{_args} = { @_ };
|
$self->{_args} = { @_ };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# explicitly state legacy reject behavior
|
||||||
|
if ( ! defined $self->{_args}{reject_type} ) {
|
||||||
|
$self->{_args}{reject_type} = 'perm';
|
||||||
|
};
|
||||||
|
if ( ! defined $self->{_args}{reject} ) {
|
||||||
|
$self->{_args}{reject} = 1;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
sub hook_connect {
|
sub hook_connect {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
|
my $reject = $self->{_args}{reject};
|
||||||
|
|
||||||
|
# RBLSMTPD being non-empty means it contains the failure message to return
|
||||||
|
if ( defined $ENV{'RBLSMTPD'} && $ENV{'RBLSMTPD'} ne '' ) {
|
||||||
|
return $self->return_env_message() if $reject && $reject eq 'connect';
|
||||||
|
};
|
||||||
|
|
||||||
|
return DECLINED if $self->is_immune();
|
||||||
|
|
||||||
# perform RBLSMTPD checks to mimic Dan Bernstein's rblsmtpd
|
# perform RBLSMTPD checks to mimic Dan Bernstein's rblsmtpd
|
||||||
return DECLINED if $self->is_set_rblsmtpd();
|
return DECLINED if $self->is_set_rblsmtpd();
|
||||||
return DECLINED if $self->is_immune();
|
|
||||||
return DECLINED if $self->ip_whitelisted();
|
return DECLINED if $self->ip_whitelisted();
|
||||||
|
|
||||||
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
||||||
if ( ! %dnsbl_zones ) {
|
if ( ! %dnsbl_zones ) {
|
||||||
$self->log( LOGDEBUG, "skip: no list configured");
|
$self->log( LOGDEBUG, "skip, no zones");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
};
|
};
|
||||||
|
|
||||||
my $remote_ip = $self->qp->connection->remote_ip;
|
my $remote_ip = $self->qp->connection->remote_ip;
|
||||||
my $reversed_ip = join('.', reverse(split(/\./, $remote_ip)));
|
my $reversed_ip = join('.', reverse(split(/\./, $remote_ip)));
|
||||||
|
|
||||||
# we queue these lookups in the background and fetch the
|
$self->initiate_lookups( \%dnsbl_zones, $reversed_ip );
|
||||||
# results in the first rcpt handler
|
|
||||||
|
|
||||||
my $res = new Net::DNS::Resolver;
|
my $message = $self->process_sockets or do {
|
||||||
$res->tcp_timeout(30);
|
$self->log(LOGINFO, 'pass');
|
||||||
$res->udp_timeout(30);
|
return DECLINED;
|
||||||
|
};
|
||||||
|
|
||||||
my $sel = IO::Select->new();
|
return $self->get_reject( $message );
|
||||||
|
};
|
||||||
|
|
||||||
my $dom;
|
sub initiate_lookups {
|
||||||
for my $dnsbl (keys %dnsbl_zones) {
|
my ($self, $zones, $reversed_ip) = @_;
|
||||||
# fix to find A records, if the dnsbl_zones line has a second field 20/1/04 ++msp
|
|
||||||
$dom->{"$reversed_ip.$dnsbl"} = 1;
|
# we queue these lookups in the background and fetch the
|
||||||
if (defined($dnsbl_zones{$dnsbl})) {
|
# results in the first rcpt handler
|
||||||
$self->log(LOGDEBUG, "Checking $reversed_ip.$dnsbl for A record in the background");
|
|
||||||
$sel->add($res->bgsend("$reversed_ip.$dnsbl"));
|
my $res = new Net::DNS::Resolver;
|
||||||
|
$res->tcp_timeout(30);
|
||||||
|
$res->udp_timeout(30);
|
||||||
|
|
||||||
|
my $sel = IO::Select->new();
|
||||||
|
|
||||||
|
my $dom;
|
||||||
|
for my $dnsbl (keys %$zones) {
|
||||||
|
# fix to find A records, if the dnsbl_zones line has a second field 20/1/04 ++msp
|
||||||
|
$dom->{"$reversed_ip.$dnsbl"} = 1;
|
||||||
|
if (defined($zones->{$dnsbl})) {
|
||||||
|
$self->log(LOGDEBUG, "Checking $reversed_ip.$dnsbl for A record in the background");
|
||||||
|
$sel->add($res->bgsend("$reversed_ip.$dnsbl"));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$self->log(LOGDEBUG, "Checking $reversed_ip.$dnsbl for TXT record in the background");
|
||||||
|
$sel->add($res->bgsend("$reversed_ip.$dnsbl", "TXT"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
$self->log(LOGDEBUG, "Checking $reversed_ip.$dnsbl for TXT record in the background");
|
|
||||||
$sel->add($res->bgsend("$reversed_ip.$dnsbl", "TXT"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$self->connection->notes('dnsbl_sockets', $sel);
|
$self->connection->notes('dnsbl_sockets', $sel);
|
||||||
$self->connection->notes('dnsbl_domains', $dom);
|
$self->connection->notes('dnsbl_domains', $dom);
|
||||||
|
};
|
||||||
return DECLINED;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub is_set_rblsmtpd {
|
sub is_set_rblsmtpd {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
@ -199,26 +238,37 @@ sub is_set_rblsmtpd {
|
|||||||
sub ip_whitelisted {
|
sub ip_whitelisted {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
my $remote_ip = shift || $self->qp->connection->remote_ip;
|
my $remote_ip = $self->qp->connection->remote_ip;
|
||||||
|
|
||||||
return
|
return grep { s/\.?$/./;
|
||||||
grep { s/\.?$/./; $_ eq substr($remote_ip . '.', 0, length $_) }
|
$_ eq substr($remote_ip . '.', 0, length $_)
|
||||||
$self->qp->config('dnsbl_allow');
|
}
|
||||||
|
$self->qp->config('dnsbl_allow');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sub return_env_message {
|
||||||
|
my $self = shift;
|
||||||
|
my $result = $ENV{'RBLSMTPD'};
|
||||||
|
my $remote_ip = $self->qp->connection->remote_ip;
|
||||||
|
$result =~ s/%IP%/$remote_ip/g;
|
||||||
|
my $msg = $self->qp->config('dnsbl_rejectmsg');
|
||||||
|
$self->log(LOGINFO, "fail, $msg");
|
||||||
|
return ( $self->get_reject_type(), join(' ', $msg, $result));
|
||||||
|
}
|
||||||
|
|
||||||
sub process_sockets {
|
sub process_sockets {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
my $conn = $self->connection;
|
my $conn = $self->qp->connection;
|
||||||
|
|
||||||
return $conn->notes('dnsbl') if $conn->notes('dnsbl');
|
return $conn->notes('dnsbl') if $conn->notes('dnsbl');
|
||||||
|
|
||||||
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
|
||||||
|
|
||||||
my $sel = $conn->notes('dnsbl_sockets') or return '';
|
my $sel = $conn->notes('dnsbl_sockets') or return '';
|
||||||
my $dom = $conn->notes('dnsbl_domains');
|
my $dom = $conn->notes('dnsbl_domains');
|
||||||
my $remote_ip = $self->qp->connection->remote_ip;
|
my $remote_ip = $self->qp->connection->remote_ip;
|
||||||
|
|
||||||
|
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
||||||
|
|
||||||
my $result;
|
my $result;
|
||||||
my $res = new Net::DNS::Resolver;
|
my $res = new Net::DNS::Resolver;
|
||||||
$res->tcp_timeout(30);
|
$res->tcp_timeout(30);
|
||||||
@ -229,7 +279,7 @@ sub process_sockets {
|
|||||||
# don't wait more than 8 seconds here
|
# don't wait more than 8 seconds here
|
||||||
my @ready = $sel->can_read(8);
|
my @ready = $sel->can_read(8);
|
||||||
|
|
||||||
$self->log(LOGDEBUG, "DONE waiting for dnsbl dns, got ", scalar @ready, " answers ...");
|
$self->log(LOGDEBUG, "done waiting for dnsbl dns, got ", scalar @ready, " answers ...");
|
||||||
return '' unless @ready;
|
return '' unless @ready;
|
||||||
|
|
||||||
for my $socket (@ready) {
|
for my $socket (@ready) {
|
||||||
@ -294,33 +344,16 @@ sub process_sockets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub hook_rcpt {
|
sub hook_rcpt {
|
||||||
my ($self, $transaction, $rcpt, %param) = @_;
|
my ($self, $transaction, $rcpt, %param) = @_;
|
||||||
|
|
||||||
return DECLINED if $self->is_immune();
|
|
||||||
|
|
||||||
# RBLSMTPD being non-empty means it contains the failure message to return
|
|
||||||
if (defined $ENV{'RBLSMTPD'} && $ENV{'RBLSMTPD'} ne '') {
|
|
||||||
my $result = $ENV{'RBLSMTPD'};
|
|
||||||
my $remote_ip = $self->qp->connection->remote_ip;
|
|
||||||
$result =~ s/%IP%/$remote_ip/g;
|
|
||||||
my $msg = $self->qp->config('dnsbl_rejectmsg');
|
|
||||||
$self->log(LOGINFO, "fail: $msg");
|
|
||||||
return ( $self->get_reject_type(), join(' ', $msg, $result));
|
|
||||||
}
|
|
||||||
|
|
||||||
my $note = $self->process_sockets or return DECLINED;
|
|
||||||
if ( $self->ip_whitelisted() ) {
|
|
||||||
$self->log(LOGINFO, "skip: whitelisted");
|
|
||||||
return DECLINED;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( $rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i ) {
|
if ( $rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i ) {
|
||||||
$self->log(LOGWARN, "skip: don't blacklist special account: ".$rcpt->user);
|
$self->log(LOGWARN, "skip, don't blacklist special account: ".$rcpt->user);
|
||||||
return DECLINED;
|
|
||||||
|
# clear the naughty connection note here, if desired.
|
||||||
|
#$self->connection->notes('naughty', 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGINFO, 'fail');
|
return DECLINED;
|
||||||
return ( $self->get_reject_type(), $note);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub hook_disconnect {
|
sub hook_disconnect {
|
||||||
@ -331,13 +364,3 @@ sub hook_disconnect {
|
|||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub get_reject_type {
|
|
||||||
my $self = shift;
|
|
||||||
my $default = shift || DENY;
|
|
||||||
my $deny = $self->{_args}{reject_type} or return $default;
|
|
||||||
|
|
||||||
return $self->{_args}{reject_type} eq 'temp' ? DENYSOFT
|
|
||||||
: $self->{_args}{reject_type} eq 'disconnect' ? DENY_DISCONNECT
|
|
||||||
: $default;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@ -8,8 +8,7 @@ use Qpsmtpd::Constants;
|
|||||||
sub register_tests {
|
sub register_tests {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
$self->register_test('test_hook_connect', 2);
|
$self->register_test('test_hook_connect', 1);
|
||||||
$self->register_test('test_hook_rcpt', 2);
|
|
||||||
$self->register_test('test_ip_whitelisted', 3);
|
$self->register_test('test_ip_whitelisted', 3);
|
||||||
$self->register_test('test_is_set_rblsmtpd', 4);
|
$self->register_test('test_is_set_rblsmtpd', 4);
|
||||||
$self->register_test('test_hook_disconnect', 1);
|
$self->register_test('test_hook_disconnect', 1);
|
||||||
@ -54,21 +53,10 @@ sub test_hook_connect {
|
|||||||
$conn->relay_client(0); # other tests may leave it enabled
|
$conn->relay_client(0); # other tests may leave it enabled
|
||||||
$conn->remote_ip('127.0.0.2'); # standard dnsbl test value
|
$conn->remote_ip('127.0.0.2'); # standard dnsbl test value
|
||||||
|
|
||||||
cmp_ok( DECLINED, '==', $self->hook_connect($self->qp->transaction),
|
my ($rc, $mess) = $self->hook_connect($self->qp->transaction);
|
||||||
"connect +");
|
cmp_ok( $rc, '==', DENY, "connect +");
|
||||||
|
|
||||||
ok($self->connection->notes('dnsbl_sockets'), "sockets +");
|
|
||||||
ok($self->connection->notes('dnsbl_domains'), "domains +");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_hook_rcpt {
|
|
||||||
my $self = shift;
|
|
||||||
|
|
||||||
my $address = Qpsmtpd::Address->parse('<rcpt@example.com>');
|
|
||||||
my ($ret, $note) = $self->hook_rcpt($self->qp->transaction, $address);
|
|
||||||
is($ret, DENY, "Check we got a DENY ($note)");
|
|
||||||
#print("# dnsbl result: $note\n");
|
|
||||||
}
|
|
||||||
sub test_hook_disconnect {
|
sub test_hook_disconnect {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user