Skip to content
This repository
Browse code

SPF plugin: refactored, tests, new config option

added POD description of spfquery note

changed spf_deny -> reject  (and offered 4 more options, see POD for reject)
	backwards compatible with old config settings
	replicates qmail-smtpd SPF patch behavior

improved logging (again)

uses a stringy eval 'use Mail::SPF' in the register sub. If missing, warn and log the error, and don't register any hooks. This is much nicer error than the current, "*** Remote host closed connection unexpectedly." broken mail server that results from enabling the SPF plugin without Mail::SPF installed.

background: I noticed I was deferring valid emails with the SPF plugin at 'spf_deny 1', and without changing the code, there wasn't a way to change how ~all records were handled. This provides that flexibility.
  • Loading branch information...
commit 51486d0b045fc20a45129a823c51cd713be4d09b 1 parent edacbf9
Matt Simerson authored May 09, 2012
2  Changes
... ...
@@ -1,6 +1,8 @@
1 1
 
2 2
 Next Version
3 3
 
  4
+  sender_permitted_from. see UPGRADING (Matt Simerson)
  5
+
4 6
   dspam plugin added (Matt Simerson)
5 7
 
6 8
   p0f version 3 supported and new default. see UPGRADING (Matt Simerson)
2  UPGRADING
@@ -3,6 +3,8 @@ When upgrading from:
3 3
 
4 4
 v 0.84 or below
5 5
 
  6
+SPF plugin: spf_deny setting deprecated. Use reject N setting instead, which provides administrators with more granular control over SPF. For backward compatibility, a spf_deny setting of 1 is mapped to 'reject 3' and a 'spf_deny 2' is mapped to 'reject 4'.
  7
+
6 8
 p0f plugin: now defaults to p0f v3
7 9
 
8 10
 Upgrade p0f to version 3 or add 'version 2' to your p0f line in config/plugins. perldoc plugins/ident/p0f for more details.
200  plugins/sender_permitted_from
@@ -12,20 +12,41 @@ Prevents email sender address spoofing by checking the SPF policy of the purport
12 12
 
13 13
 Sender Policy Framework (SPF) is an e-mail validation system designed to prevent spam by addressing source address spoofing. SPF allows administrators to specify which hosts are allowed to send e-mail from a given domain by creating a specific SPF record in the public DNS. Mail exchangers then use the DNS to check that mail from a given domain is being sent by a host sanctioned by that domain's administrators. -- http://en.wikipedia.org/wiki/Sender_Policy_Framework
14 14
 
  15
+The results of a SPF query are stored in a transaction note named 'spfquery';
  16
+
15 17
 =head1 CONFIGURATION
16 18
 
17 19
 In config/plugins, add arguments to the sender_permitted_from line.
18 20
 
19  
-  sender_permitted_from spf_deny 1
  21
+  sender_permitted_from reject 3
  22
+
  23
+=head2 reject
  24
+
  25
+Set to a value between 1 and 6 to enable the following SPF behaviors:
  26
+
  27
+ 1 annotate-only, add Received-SPF header, no rejections.
  28
+ 2 defer on DNS failures. Assure there's always a meaningful SPF header.
  29
+ 3 rejected if SPF record says 'fail'
  30
+ 4 stricter reject. Also rejects 'softfail'
  31
+ 5 reject 'neutral'
  32
+ 6 reject if no SPF records, or a syntax error
  33
+
  34
+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.
  35
+
  36
+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 theirobsessive little hearts.
  37
+
  38
+=head1 SEE ALSO
20 39
 
21  
-=head2 spf_deny
  40
+ http://spf.pobox.com/
  41
+ http://en.wikipedia.org/wiki/Sender_Policy_Framework
22 42
 
23  
-Setting spf_deny to 0 will prevent emails from being rejected, even if they fail SPF checks. sfp_deny 1 is the default, and a reasonable setting. It temporarily defers connections (4xx) that have soft SFP failures and only rejects (5xx) messages when the sending domains policy suggests it. Settings spf_deny to 2 is more aggressive and will cause soft failures to be rejected permanently.
  43
+=head1 ACKNOWLDGEMENTS
24 44
 
25  
-See also http://spf.pobox.com/
  45
+The reject options are modeled after, and aim to match the functionality of those found in the SPF patch for qmail-smtpd.
26 46
 
27 47
 =head1 AUTHOR
28 48
 
  49
+Matt Simerson - 2002 - increased policy options from 3 to 6
29 50
 Matt Simerson - 2011 - rewrote using Mail::SPF
30 51
 
31 52
 Matt Sergeant - 2003 - initial plugin
@@ -33,55 +54,57 @@ Matt Sergeant - 2003 - initial plugin
33 54
 =cut
34 55
 
35 56
 use strict;
36  
-use Mail::SPF 2.000;
  57
+use warnings;
  58
+
  59
+#use Mail::SPF 2.000;   # eval'ed in ->register
37 60
 use Qpsmtpd::Constants;
38 61
 
39 62
 sub register {
40  
-    my ($self, $qp, @args) = @_;
41  
-    %{$self->{_args}} = @args;
  63
+    my ($self, $qp, %args) = @_;
  64
+    eval "use Mail::SPF";
  65
+    if ( $@ ) {
  66
+        warn "skip: plugin disabled, could not find Mail::SPF\n";
  67
+        $self->log(LOGERROR, "skip: plugin disabled, is Mail::SPF installed?");
  68
+        return;
  69
+    };
  70
+    $self->{_args} = { %args };
  71
+    if ( $self->{_args}{spf_deny} ) {
  72
+        $self->{_args}{reject} = 3 if $self->{_args}{spf_deny} == 1;
  73
+        $self->{_args}{reject} = 4 if $self->{_args}{spf_deny} == 2;
  74
+    };
  75
+    if ( ! $self->{_args}{reject} && $self->qp->config('spfbehavior') ) {
  76
+        $self->{_args}{reject} = $self->qp->config('spfbehavior');
  77
+    };
42 78
 }
43 79
 
44 80
 sub hook_mail {
45 81
     my ($self, $transaction, $sender, %param) = @_;
46 82
 
47  
-    my $format    = $sender->format;
  83
+    if ( ! $self->{_args}{reject} ) {
  84
+        $self->log( LOGINFO, "skip: disabled in config" );
  85
+        return (DECLINED);
  86
+    };
  87
+
  88
+    my $format = $sender->format;
48 89
     if ( $format eq '<>' || ! $sender->host || ! $sender->user ) {
49  
-        $self->log( LOGDEBUG, "pass: null sender" );
  90
+        $self->log( LOGINFO, "skip: null sender" );
50 91
         return (DECLINED, "SPF - null sender");
51 92
     };
52 93
 
53  
-    my $client_ip = $self->qp->connection->remote_ip;
54  
-    my $from      = $sender->user . '@' . lc($sender->host);
55  
-    my $helo      = $self->qp->connection->hello_host;
56  
-
57  
-    # If we are receiving from a relay permitted host, then we are probably
58  
-    # not the delivery system, and so we shouldn't check
59  
-    if ( $self->qp->connection->relay_client() ) {
60  
-        $self->log( LOGDEBUG, "pass: relaying permitted (connection)" );
61  
-        return (DECLINED, "SPF - relaying permitted")
  94
+    if ( $self->is_relayclient() ) {
  95
+        return (DECLINED, "SPF - relaying permitted");
62 96
     };
63 97
 
64  
-    my @relay_clients      = $self->qp->config("relayclients");
65  
-    my $more_relay_clients = $self->qp->config("morerelayclients", "map");
66  
-    my %relay_clients      = map { $_ => 1 } @relay_clients;
67  
-    while ($client_ip) {
68  
-        if ( exists $relay_clients{$client_ip} ||
69  
-             exists $more_relay_clients->{$client_ip} ) {
70  
-            $self->log( LOGDEBUG, "pass: relaying permitted (config)" );
71  
-            return (DECLINED, "SPF - relaying permitted");
72  
-        };
73  
-        $client_ip =~ s/\d+\.?$//;    # strip off another 8 bits
74  
-    }
75  
-
76  
-    my $scope = $from ? 'mfrom' : 'helo';
77  
-    $client_ip = $self->qp->connection->remote_ip;
78  
-    my %req_params = (
79  
-        versions => [1, 2],           # optional
80  
-        scope => $scope,
81  
-        ip_address => $client_ip,
  98
+    my $client_ip  = $self->qp->connection->remote_ip;
  99
+    my $from       = $sender->user . '@' . lc($sender->host);
  100
+    my $helo       = $self->qp->connection->hello_host;
  101
+    my $scope      = $from ? 'mfrom' : 'helo';
  102
+    my %req_params = ( versions   => [1, 2],        # optional
  103
+                       scope      => $scope,
  104
+                       ip_address => $client_ip,
82 105
                      );
83 106
 
84  
-    if ($scope =~ /mfrom|pra/) {
  107
+    if ($scope =~ /^mfrom|pra$/) {
85 108
         $req_params{identity} = $from;
86 109
         $req_params{helo_identity} = $helo if $helo;
87 110
     }
@@ -95,44 +118,63 @@ sub hook_mail {
95 118
     my $result     = $spf_server->process($request);
96 119
 
97 120
     $transaction->notes('spfquery', $result);
98  
-    $transaction->notes('spfcode', $result->code);
99 121
 
100  
-    if ( $result->code eq 'pass' ) {    # this test passed
101  
-        $self->log( LOGINFO, "pass" );
  122
+    $self->log( LOGINFO, $result );
  123
+
  124
+    if ( $result->code eq 'pass' ) {
102 125
         return (OK);
103 126
     };
104 127
 
105  
-    $self->log( LOGINFO, "fail: " . $result );
106 128
     return (DECLINED, "SPF - $result->code");
107 129
 }
108 130
 
109 131
 sub hook_rcpt {
110 132
     my ($self, $transaction, $rcpt, %param) = @_;
111 133
 
112  
-    # special addresses don't get SPF-tested.
113  
-    return DECLINED
114  
-      if $rcpt
115  
-          and $rcpt->user
116  
-          and $rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i;
  134
+    return DECLINED if $self->is_special_recipient( $rcpt );
117 135
 
118 136
     my $result = $transaction->notes('spfquery') or return DECLINED;
119 137
     my $code   = $result->code;
120 138
     my $why    = $result->local_explanation;
121  
-    my $deny   = $self->{_args}{spf_deny};
  139
+    my $reject = $self->{_args}{reject};
  140
+
  141
+    if ( ! $code ) {
  142
+        return (DENYSOFT, "SPF - no response") if $reject >= 2;
  143
+        return (DECLINED, "SPF - no response");
  144
+    };
122 145
 
123  
-    return (DECLINED, "SPF - $code: $why")   if $code eq "pass";
124  
-    return (DECLINED, "SPF - $code, $why")   if !$deny;
125  
-    return (DENYSOFT, "SPF - $code: $why")   if $code eq "error";
126  
-    return (DENY,     "SPF - forgery: $why") if $code eq 'fail';
  146
+    return (DECLINED, "SPF - $code: $why") if ! $reject;
127 147
 
128  
-    if ($code eq "softfail") {
129  
-        return (DENY, "SPF probable forgery: $why") if $deny > 1;
130  
-        return (DENYSOFT, "SPF probable forgery: $why");
  148
+# SPF result codes: pass fail softfail neutral none error permerror temperror
  149
+    if    ( $code eq 'pass' ) { }
  150
+    elsif ( $code eq 'fail' ) {
  151
+        return (DENY, "SPF - forgery: $why") if $reject >= 3;
  152
+        return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
  153
+    }
  154
+    elsif ( $code eq 'softfail' ) {
  155
+        return (DENY, "SPF - forgery: $why") if $reject >= 4;
  156
+        return (DENYSOFT, "SPF - $code: $why") if $reject >= 3;
  157
+    }
  158
+    elsif ( $code eq 'neutral' ) {
  159
+        return (DENY, "SPF - forgery: $why") if $reject >= 5;
  160
+    }
  161
+    elsif ( $code eq 'none' ) {
  162
+        return (DENY, "SPF - forgery: $why") if $reject >= 6;
  163
+    }
  164
+    elsif ( $code eq 'error' ) {
  165
+        return (DENY, "SPF - $code: $why") if $reject >= 6;
  166
+        return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
  167
+    }
  168
+    elsif ( $code eq 'permerror' ) {
  169
+        return (DENY, "SPF - $code: $why") if $reject >= 6;
  170
+        return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
  171
+    }
  172
+    elsif ( $code eq 'temperror' ) {
  173
+        return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
131 174
     }
132 175
 
133 176
     $self->log(LOGDEBUG, "result for $rcpt->address was $code: $why");
134  
-
135  
-    return (DECLINED, "SPF - $code, $why");
  177
+    return (DECLINED, "SPF - $code: $why");
136 178
 }
137 179
 
138 180
 sub hook_data_post {
@@ -147,3 +189,49 @@ sub hook_data_post {
147 189
     return DECLINED;
148 190
 }
149 191
 
  192
+sub is_relayclient {
  193
+    my $self = shift;
  194
+
  195
+    # If we are receiving from a relay permitted host, then we are probably
  196
+    # not the delivery system, and so we shouldn't check
  197
+    if ( $self->qp->connection->relay_client() ) {
  198
+        $self->log( LOGINFO, "skip: relaying permitted (relay_client)" );
  199
+        return 1;
  200
+    };
  201
+
  202
+    my $client_ip          = $self->qp->connection->remote_ip;
  203
+    my @relay_clients      = $self->qp->config('relayclients');
  204
+    my $more_relay_clients = $self->qp->config('morerelayclients', 'map');
  205
+    my %relay_clients      = map { $_ => 1 } @relay_clients;
  206
+
  207
+    while ($client_ip) {
  208
+        if ( exists $relay_clients{$client_ip} ||
  209
+             exists $more_relay_clients->{$client_ip} ) {
  210
+            $self->log( LOGDEBUG, "skip: relaying permitted (config)" );
  211
+            return 1;
  212
+        };
  213
+        $client_ip =~ s/\d+\.?$// or last;   # strip off another 8 bits
  214
+    }
  215
+    return;
  216
+};
  217
+
  218
+sub is_special_recipient {
  219
+    my ($self, $rcpt) = @_;
  220
+
  221
+    if ( ! $rcpt ) {
  222
+        $self->log(LOGINFO, "skip: missing recipient");
  223
+        return 1;
  224
+    };
  225
+    if ( ! $rcpt->user ) {
  226
+        $self->log(LOGINFO, "skip: missing user");
  227
+        return 1;
  228
+    };
  229
+
  230
+    # special addresses don't get SPF-tested.
  231
+    if ( $rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i ) {
  232
+        $self->log(LOGINFO, "skip: special user (".$rcpt->user.")");
  233
+        return 1;
  234
+    };
  235
+
  236
+    return;
  237
+};
50  t/plugin_tests/sender_permitted_from
... ...
@@ -0,0 +1,50 @@
  1
+#!perl -w
  2
+
  3
+use strict;
  4
+use warnings;
  5
+
  6
+use Qpsmtpd::Constants;
  7
+
  8
+my $r;
  9
+
  10
+sub register_tests {
  11
+    my $self = shift;
  12
+
  13
+    eval 'use Mail::SPF';
  14
+    return if $@;
  15
+
  16
+    $self->register_test('test_is_relayclient', 3);
  17
+    $self->register_test('test_is_special_recipient', 5);
  18
+}
  19
+
  20
+sub test_is_relayclient {
  21
+    my $self = shift;
  22
+
  23
+    my $transaction = $self->qp->transaction;
  24
+    ok( ! $self->is_relayclient( $transaction ),
  25
+        "sender_permitted_from, is_relayclient -");
  26
+
  27
+    $self->qp->connection->relay_client(1);
  28
+    ok( $self->is_relayclient( $transaction ),
  29
+        "sender_permitted_from, is_relayclient +");
  30
+
  31
+    $self->qp->connection->relay_client(0);
  32
+    $self->qp->connection->remote_ip('192.168.7.5');
  33
+    my $client_ip = $self->qp->connection->remote_ip;
  34
+    ok( $client_ip, "sender_permitted_from, relayclients ($client_ip)");
  35
+};
  36
+
  37
+sub test_is_special_recipient {
  38
+    my $self = shift;
  39
+
  40
+    my $transaction = $self->qp->transaction;
  41
+    my $address     = Qpsmtpd::Address->new('user@example.com');
  42
+
  43
+    ok( ! $self->is_special_recipient( $address ), "is_special_recipient -");
  44
+
  45
+    foreach my $user ( qw/ postmaster abuse mailer-daemon root / ) {
  46
+        $address = Qpsmtpd::Address->new("$user\@example.com");
  47
+        ok( $self->is_special_recipient( $address ), "is_special_recipient ($user)");
  48
+    };
  49
+};
  50
+

0 notes on commit 51486d0

Please sign in to comment.
Something went wrong with that request. Please try again.