qpsmtpd/plugins/sender_permitted_from

271 lines
8.3 KiB
Plaintext
Raw Normal View History

#!perl -w
=head1 NAME
2012-06-25 08:55:02 +02:00
SPF - implement Sender Permitted From
=head1 SYNOPSIS
Prevents email sender address spoofing by checking the SPF policy of the purported senders domain.
Sets the transaction note spf_pass_host if the SPF result is pass.
=head1 DESCRIPTION
2012-06-25 08:55:02 +02:00
Sender Policy Framework (SPF) is an email validation system designed to prevent source address spoofing. SPF allows administrators to specify which hosts are allowed to send email from a given domain by creating a specific SPF record in the public DNS. Mail exchangers then use the DNS to verify that mail is being sent by a host sanctioned by a given domain administrators. -- http://en.wikipedia.org/wiki/Sender_Policy_Framework
The results of a SPF query are stored in a transaction note named 'spfquery';
=head1 CONFIGURATION
In config/plugins, add arguments to the sender_permitted_from line.
sender_permitted_from reject 3
=head2 reject
Set to a value between 1 and 6 to enable the following SPF behaviors:
1 annotate-only, add Received-SPF header, no rejections.
2 defer on DNS failures. Assure there's always a meaningful SPF header.
3 rejected if SPF record says 'fail'
4 stricter reject. Also rejects 'softfail'
5 reject 'neutral'
6 reject if no SPF records, or a syntax error
Most sites should start at level 3. It temporarily defers connections (4xx) that have soft SFP failures and only rejects (5xx) messages when the sending domains policy suggests it.
2012-06-25 08:55:02 +02:00
SPF levels above 4 are for crusaders who don't mind rejecting some valid mail when the sending server administrator hasn't dotted his i's and crossed his t's. May the deities bless their obsessive little hearts.
=head1 SEE ALSO
http://spf.pobox.com/
http://en.wikipedia.org/wiki/Sender_Policy_Framework
=head1 TODO
Check the scope of the SPF policy. If it's too broad (ie, the whole internet is valid), apply karma penalty
Examples of too broad: +all,
=head1 ACKNOWLDGEMENTS
The reject options are modeled after, and aim to match the functionality of those found in the SPF patch for qmail-smtpd.
=head1 AUTHOR
Matt Simerson - 2012 - increased policy options from 3 to 6
Matt Simerson - 2011 - rewrote using Mail::SPF
Matt Sergeant - 2003 - initial plugin
=cut
use strict;
use warnings;
#use Mail::SPF 2.000; # eval'ed in ->register
POD corrections, additional tests, plugin consistency on files in plugins dir: fixed a number of POD errors formatted some # comments into POD removed bare 1; (these are plugins, not perl modules) most instances of this were copy/pasted from a previous plugin that had it removed instances of # vim ts=N ... they weren't consistent, many didn't match .perltidyrc on modules that failed perl -c tests, added 'use Qpsmtpd::Constants;' Conflicts: plugins/async/check_earlytalker plugins/async/dns_whitelist_soft plugins/async/dnsbl plugins/async/queue/smtp-forward plugins/async/require_resolvable_fromhost plugins/async/rhsbl plugins/async/uribl plugins/auth/auth_checkpassword plugins/auth/auth_cvm_unix_local plugins/auth/auth_flat_file plugins/auth/auth_ldap_bind plugins/auth/auth_vpopmail plugins/auth/auth_vpopmail_sql plugins/auth/authdeny plugins/check_badmailfromto plugins/check_badrcptto_patterns plugins/check_bogus_bounce plugins/check_earlytalker plugins/check_norelay plugins/check_spamhelo plugins/connection_time plugins/dns_whitelist_soft plugins/dnsbl plugins/domainkeys plugins/greylisting plugins/hosts_allow plugins/http_config plugins/logging/adaptive plugins/logging/apache plugins/logging/connection_id plugins/logging/transaction_id plugins/logging/warn plugins/milter plugins/queue/exim-bsmtp plugins/queue/maildir plugins/queue/postfix-queue plugins/queue/smtp-forward plugins/quit_fortune plugins/random_error plugins/rcpt_map plugins/rcpt_regexp plugins/relay_only plugins/require_resolvable_fromhost plugins/rhsbl plugins/sender_permitted_from plugins/spamassassin plugins/tls plugins/tls_cert plugins/uribl plugins/virus/aveclient plugins/virus/bitdefender plugins/virus/clamav plugins/virus/clamdscan plugins/virus/hbedv plugins/virus/kavscanner plugins/virus/klez_filter plugins/virus/sophie plugins/virus/uvscan
2012-04-08 02:11:16 +02:00
use Qpsmtpd::Constants;
sub register {
my ($self, $qp, %args) = @_;
eval 'use Mail::SPF';
if ($@) {
warn "skip: plugin disabled, is Mail::SPF installed?\n";
$self->log(LOGERROR, "skip: plugin disabled, is Mail::SPF installed?");
return;
}
$self->{_args} = {%args};
if ($self->{_args}{spf_deny}) {
$self->{_args}{reject} = 3 if $self->{_args}{spf_deny} == 1;
$self->{_args}{reject} = 4 if $self->{_args}{spf_deny} == 2;
}
if (!$self->{_args}{reject} && $self->qp->config('spfbehavior')) {
$self->{_args}{reject} = $self->qp->config('spfbehavior');
}
$self->register_hook('mail', 'mail_handler');
$self->register_hook('data_post', 'data_post_handler');
}
sub mail_handler {
my ($self, $transaction, $sender, %param) = @_;
return (DECLINED) if $self->is_immune();
my $format = $sender->format;
if ($format eq '<>' || !$sender->host || !$sender->user) {
$self->log(LOGINFO, "skip, null sender");
2012-05-07 09:36:02 +02:00
return (DECLINED, "SPF - null sender");
}
if ($self->qp->connection->relay_client) {
$self->log(LOGINFO, "skip, relay_client");
return (DECLINED, "SPF - relaying permitted");
}
if (!$self->{_args}{reject}) {
$self->log(LOGINFO, "skip, reject disabled");
return (DECLINED);
}
my $client_ip = $self->qp->connection->remote_ip;
my $from = $sender->user . '@' . lc($sender->host);
my $helo = $self->qp->connection->hello_host;
my $scope = $from ? 'mfrom' : 'helo';
my %req_params = (
versions => [1, 2], # optional
scope => $scope,
ip_address => $client_ip,
);
if ($scope =~ /^mfrom|pra$/) {
$req_params{identity} = $from;
$req_params{helo_identity} = $helo if $helo;
}
elsif ($scope eq 'helo') {
$req_params{identity} = $helo;
$req_params{helo_identity} = $helo;
}
my $spf_server = Mail::SPF::Server->new();
my $request = Mail::SPF::Request->new(%req_params);
2012-06-25 08:55:02 +02:00
my $result = $spf_server->process($request) or do {
$self->log(LOGINFO, "fail, no result");
2012-06-25 08:55:02 +02:00
return DECLINED;
};
$transaction->notes('spfquery', $result);
my $code = $result->code;
my $why = $result->local_explanation;
my $reject = $self->{_args}{reject};
if (!$code) {
$self->log(LOGINFO, "fail, no response");
return (DENYSOFT, "SPF - no response") if $reject >= 2;
return (DECLINED, "SPF - no response");
}
if (!$reject) {
$self->log(LOGINFO, "fail, no reject policy ($code: $why)");
return (DECLINED, "SPF - $code: $why");
}
# SPF result codes: pass fail softfail neutral none error permerror temperror
2012-06-28 01:36:58 +02:00
return $self->handle_code_none($reject, $why) if $code eq 'none';
if ($code eq 'fail') {
$self->adjust_karma(-1);
2013-03-25 06:48:40 +01:00
return $self->handle_code_fail($reject, $why);
}
elsif ($code eq 'softfail') {
$self->adjust_karma(-1);
2013-03-25 06:48:40 +01:00
return $self->handle_code_softfail($reject, $why);
}
elsif ($code eq 'pass') {
$self->adjust_karma(1);
$transaction->notes('spf_pass_host', lc $sender->host);
$self->log(LOGINFO, "pass, $code: $why");
2012-06-25 08:55:02 +02:00
return (DECLINED);
}
elsif ($code eq 'neutral') {
$self->log(LOGINFO, "fail, $code, $why");
2012-06-25 08:55:02 +02:00
return (DENY, "SPF - $code: $why") if $reject >= 5;
}
elsif ($code eq 'error') {
$self->log(LOGINFO, "fail, $code, $why");
return (DENY, "SPF - $code: $why") if $reject >= 6;
2013-03-25 06:48:40 +01:00
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
}
elsif ($code eq 'permerror') {
$self->log(LOGINFO, "fail, $code, $why");
return (DENY, "SPF - $code: $why") if $reject >= 6;
2013-03-25 06:48:40 +01:00
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
}
elsif ($code eq 'temperror') {
$self->log(LOGINFO, "fail, $code, $why");
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
}
2012-06-25 08:55:02 +02:00
$self->log(LOGINFO, "SPF from $from was $code: $why");
return (DECLINED);
}
2012-06-28 01:36:58 +02:00
sub handle_code_none {
my ($self, $reject, $why) = @_;
2012-06-28 01:36:58 +02:00
if ($reject >= 6) {
$self->log(LOGINFO, "fail, none, $why");
2012-06-28 01:36:58 +02:00
return (DENY, "SPF - none: $why");
}
2012-06-28 01:36:58 +02:00
$self->log(LOGINFO, "pass, none, $why");
2012-06-28 01:36:58 +02:00
return DECLINED;
}
2012-06-28 01:36:58 +02:00
sub handle_code_fail {
my ($self, $reject, $why) = @_;
2012-06-28 01:36:58 +02:00
if ($reject >= 2) {
$self->log(LOGINFO, "fail, $why");
2012-06-28 01:36:58 +02:00
return (DENY, "SPF - forgery: $why") if $reject >= 3;
return (DENYSOFT, "SPF - fail: $why");
}
2012-06-28 01:36:58 +02:00
$self->log(LOGINFO, "pass, fail tolerated, $why");
2012-06-28 01:36:58 +02:00
return DECLINED;
}
2012-06-28 01:36:58 +02:00
sub handle_code_softfail {
my ($self, $reject, $why) = @_;
2012-06-28 01:36:58 +02:00
if ($reject >= 3) {
$self->log(LOGINFO, "fail, soft, $why");
return (DENY, "SPF - fail: $why") if $reject >= 4;
2012-06-28 01:36:58 +02:00
return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
}
2012-06-28 01:36:58 +02:00
$self->log(LOGINFO, "pass, softfail tolerated, $why");
2012-06-28 01:36:58 +02:00
return DECLINED;
}
2012-06-28 01:36:58 +02:00
sub data_post_handler {
my ($self, $transaction) = @_;
my $result = $transaction->notes('spfquery') or return DECLINED;
# if we skipped processing in mail_handler, we should skip here too
return (DECLINED) if $self->is_immune();
$self->log(LOGDEBUG, "result was $result->code");
if (!$transaction->header) {
$self->log(LOGERROR, "missing headers!");
return DECLINED;
}
$transaction->header->add('Received-SPF', $result->received_spf_header, 0);
# consider also adding SPF status to Authentication-Results header
return DECLINED;
}
sub is_special_recipient {
my ($self, $rcpt) = @_;
if (!$rcpt) {
$self->log(LOGINFO, "skip: missing recipient");
return 1;
}
if (!$rcpt->user) {
$self->log(LOGINFO, "skip: missing user");
return 1;
}
# special addresses don't get SPF-tested.
if ($rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i) {
$self->log(LOGINFO, "skip: special user (" . $rcpt->user . ")");
return 1;
}
return;
}