source: npl/mailserver/netqmail/qmail-spp-spf-20091020.c @ c5c522c

gcc484ntopperl-5.22
Last change on this file since c5c522c was c5c522c, checked in by Edwin Eefting <edwin@datux.nl>, 8 years ago

initial commit, transferred from cleaned syn3 svn tree

  • Property mode set to 100644
File size: 14.6 KB
Line 
1/*
2 * Copyright (C) 2003-2005 Pawel Foremski <pjf@asn.pl>
3 *   - Original imported from dirqmail.
4 *
5 * Copyright (C) 2008 Chris Caputo <ccaputo@alt.net>
6 *   - Oct 2008: Adapted to work with libspf2-1.2.8.
7 *               Added support for IPv6 via TCP6REMOTEIP.
8 *               Altered configuration methodology to use envars.
9 *   - Nov 2008: Added SPP_SPF_DONT_ALLOW_RANDOM_IP_PASS.
10 *
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU General Public License
13 * as published by the Free Software Foundation; either
14 * version 2 of the License, or (at your option) any later
15 * version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software Foundation,
24 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 *
26 */
27
28/*
29   This is an implementation of SPF as a qmail-spp module.  It requires
30   libspf2.  For more information, consult:
31
32     http://www.openspf.org/
33     http://qmail-spp.sourceforge.net/
34     http://www.libspf2.org/
35
36   If an SPF record is not found or doesn't process, a fallback SPF record
37   of "v=spf1 mx -all" can be used to test if the client is listed in the MX
38   records of the envelope domain.
39   
40   Compile plugin using something like:
41
42     gcc -Wall -o qmail-spp-spf qmail-spp-spf.c -lspf2 -I/usr/include/spf2
43
44   Put this in the qmail plugins directory (ex. "/var/qmail/plugins") and add
45   to smtpplugins file (ex. "/var/qmail/control/smtpplugins") after [mail]
46   section:
47
48     [mail]
49     plugins/qmail-spp-spf
50
51   If the "RELAYCLIENT" environment variable (envar) is set, this module
52   exits without doing anything, since the client has permission to relay.
53
54   IPv6 is supported if TCPREMOTEIP contains an IPv6 address or if
55   TCP6REMOTEIP envar is set.
56
57   Set these envars as desired to instruct the module how to handle each SPF
58   result.  Only envars defined will be used.
59
60     SPP_SPF_NO_RESULT          - Used if both SPF and MX checks can't be done.
61
62     SPP_SPF_RESULT_NEUTRAL    \
63     SPP_SPF_RESULT_PASS       |
64     SPP_SPF_RESULT_FAIL       |- Refer to http://www.openspf.org/ for
65     SPP_SPF_RESULT_SOFTFAIL   |  definitions.
66     SPP_SPF_RESULT_NONE       |
67     SPP_SPF_RESULT_TEMPERROR  |
68     SPP_SPF_RESULT_PERMERROR  /
69
70     SPP_SPF_MX_RESULT_PASS    \  If any set, MX check of sender is done when
71     SPP_SPF_MX_RESULT_FAIL    |- SPF record doesn't exist or SPF check result
72     SPP_SPF_MX_RESULT_UNKNOWN /  is None, PermError, TempError or invalid.
73
74   Possible settings of the above envars are taken from
75   http://qmail-spp.sourceforge.net/doc/ :
76
77     Command       Description
78     -----------------------------------------------------------------------
79     A             accept mail - turn off qmail-spp in this session
80     N             next - accept current SMTP command (do not execute
81                   remaining plugins for this command)
82     O             ok - like N, but omits qmail checks in MAIL and RCPT
83     Emsg          error - do not accept this SMTP command and immediately
84                   send msg to the client
85     LMmsg         later, mail - like E, but shows error after MAIL command
86     LRmsg         later, rcpt - like E, but shows error after RCPT command
87     LDmsg         later, data - like E, but shows error after DATA command
88     Rmsg          reject mail - send msg to the client and drop connection
89     D             drop connection immediately, without printing anything
90     Svar=value    set environmental variable var to value
91     Uvar          unset var variable
92     Hcontent      header - add header content (eg. X-Spam-Flag: YES)
93     Cfoo@bar.com  change last address provided by the client to foo@bar.com
94                   (MAIL FROM or RCPT TO address)
95     Pmsg          print - send msg to the client
96
97   Separate commands are separated by a comma or a carriage return.  Be
98   careful not to include a comma for any other reason.
99
100   Except for the SPP_SPF_NO_RESULT and SPP_SPF_MX_RESULT_xx envars, if any
101   envars include the special string "spf_smtp_msg" then "spf_smtp_msg" will
102   be replaced by the output of libspf2's SPF_response_get_smtp_comment()
103   function.  For example:
104
105     SPP_SPF_RESULT_FAIL="E550 spf_smtp_msg"
106
107   If the actual SPF query is able to be done, this module also sets the
108   environmental variable SPP_SPF_RESULT to one of the following (via the
109   qmail-spp 'S' command):
110
111     pass
112     fail
113     softfail
114     neutral
115     none
116     permerror
117     temperror
118
119   In addition, a "Received-SPF:" header is added to the message via the
120   qmail-spp 'H' command when the SPF query is able to be done.
121
122   It is okay to not set a particular SPP_SPF_xxx envar.  If that particular
123   case is hit the module will only return the "SSPP_SPF_RESULT=<result>" and
124   "HReceived-SPF:" commands if the SPF query is done.
125
126   If the SPP_SPF_DONT_ALLOW_RANDOM_IP_PASS envar is set, then when an SPF pass
127   result is obtained, two random IP addresses will also be tried to see if the
128   SPF definition is passing everything as if "+all" is declared.  If the two
129   random IP addresses also receive a pass from the SPF library, then the
130   original pass is ignored.
131
132   Example:
133
134      In /etc/tcprules.d/tcp.qmail-smtp change ":allow" line to be as follows:
135
136         :allow,SPP_SPF_RESULT_PASS="HX-Spam-Flag: No,A",SPP_SPF_RESULT_FAIL="E550 spf_smtp_msg",SPP_SPF_NO_RESULT="SSPF_MODULE_FAILED=1"
137
138      or
139
140         :allow,SPP_SPF_RESULT_PASS="A",SPP_SPF_MX_RESULT_PASS="A"
141
142      (Be sure to rebuild tcp.qmail-smtp.cdb after modification, such as with
143      "make" or "tcprules" commands.)
144*/
145
146#include <stdio.h>
147#include <stdlib.h>
148#include <string.h>
149#include <netinet/in.h>
150#include <time.h>
151#include <arpa/inet.h>
152#include <unistd.h>
153#include "spf.h"
154
155#define LOG "qmail-spp-spf: "
156#define LOGR "qmail-spp-spf:%s: "
157#define SPF_SMTP_MSG "spf_smtp_msg"
158#define SPF_SMTP_MSG_LEN 12
159
160
161void envcmd(char *envstr,
162            SPF_response_t *spf_response)
163{
164  char *p;
165
166  /* Always issue these when there is a response, regardless of whether envar
167     was passed in.  Also, these need to issue first since an envar can include
168     a qmail-spp processing termination command. */
169  if (NULL != spf_response)
170    {
171      printf("SSPP_SPF_RESULT=%s\n",
172             SPF_strresult(SPF_response_result(spf_response)));
173      printf("H%s\n", SPF_response_get_received_spf(spf_response));
174    }
175
176  if (NULL == envstr || 0 == *envstr)
177    {
178      return;
179    }
180
181  p = envstr;
182  while (*p)
183    {
184      if (NULL != spf_response &&
185          !strncmp(p, SPF_SMTP_MSG, SPF_SMTP_MSG_LEN))
186        {
187          printf("%s", SPF_response_get_smtp_comment(spf_response));
188          p += SPF_SMTP_MSG_LEN;
189        }
190      else
191        {
192          printf("%c", ',' == *p ? '\n' : *p);
193          p++;
194        }
195    }
196
197  /* don't print extra carriage return if envstr already ends with one */
198  if (p > envstr       &&
199      ','  != *(p - 1) &&
200      '\n' != *(p - 1))
201    printf("\n");
202}
203
204
205/* Returns 1 on success, 0 on failure. */
206int set_spf_ip(SPF_request_t *spf_request,
207               int fIPv4,
208               char *remote)
209{
210  if (fIPv4)
211    {
212      if (SPF_request_set_ipv4_str(spf_request, remote))
213        {
214          fprintf(stderr,
215                  LOGR "SPF_request_set_ipv4_str('%s') failed.\n",
216                  remote,
217                  remote);
218          return 0;
219        }
220    }
221  else
222    {
223      if (SPF_request_set_ipv6_str(spf_request, remote))
224        {
225          fprintf(stderr,
226                  LOGR "SPF_request_set_ipv6_str('%s') failed.\n",
227                  remote,
228                  remote);
229          return 0;
230        }
231    }
232
233  return 1;
234}
235
236
237/* Returns 1 if a random IP address also results in an SPF pass.
238   0 otherwise (or on error).
239*/
240int random_ip_passes(SPF_request_t *spf_request,
241                     int fIPv4,
242                     char *remote)
243{
244  int ret = 0;
245  struct in_addr addr4;
246  struct in6_addr addr6;
247  char szIP[INET6_ADDRSTRLEN];
248  SPF_response_t *spf_response = NULL;
249  SPF_errcode_t spf_err;
250
251  if (fIPv4)
252    {
253      addr4.s_addr = random();
254
255      if (SPF_request_set_ipv4(spf_request, addr4))
256        {
257          fprintf(stderr,
258                  LOGR "SPF_request_set_ipv4('%s') failed.\n",
259                  remote,
260                  inet_ntop(AF_INET, &addr4, szIP, sizeof(szIP)));
261          goto done;
262        }
263    }
264  else
265    {
266      addr6.s6_addr32[0] = random();
267      addr6.s6_addr32[1] = random();
268      addr6.s6_addr32[2] = random();
269      addr6.s6_addr32[3] = random();
270
271      if (SPF_request_set_ipv6(spf_request, addr6))
272        {
273          fprintf(stderr,
274                  LOGR "SPF_request_set_ipv6('%s') failed.\n",
275                  remote,
276                  inet_ntop(AF_INET6, &addr6, szIP, sizeof(szIP)));
277          goto done;
278        }
279    }
280
281  if (SPF_E_SUCCESS ==
282      (spf_err = SPF_request_query_mailfrom(spf_request, &spf_response)))
283    {
284      if (SPF_RESULT_PASS == SPF_response_result(spf_response))
285        ret = 1;
286    }
287  else
288    {
289      fprintf(stderr,
290              LOGR "SPF_request_query_mailfrom (random remote='%s'): "
291              "surprisingly failed since first test succeeded: %s\n",
292              remote,
293              inet_ntop(fIPv4 ? AF_INET : AF_INET6,
294                        fIPv4 ? (const void *)&addr4 : &addr6,
295                        szIP,
296                        sizeof(szIP)),
297              SPF_strerror(spf_err));
298    }
299
300 done:
301  /* restore original IP settings.  no need to check result since this worked
302     already */
303  set_spf_ip(spf_request, fIPv4, remote);
304
305  if (NULL != spf_response) SPF_response_free(spf_response);
306  return ret;
307}
308
309
310int main()
311{
312  char *remote   = NULL;
313  char *helo     = NULL;
314  char *sender   = NULL;
315  char *mxpass   = NULL;
316  char *mxfail   = NULL;
317  char *mxunk    = NULL;
318  SPF_server_t   *spf_server     = NULL;
319  SPF_request_t  *spf_request    = NULL;
320  SPF_response_t *spf_response   = NULL;
321  SPF_response_t *spf_responsemx = NULL;
322  SPF_errcode_t spf_err;
323  int fQuerySuccessful = 0;
324  int fConsiderMXCheck = 0;
325  int fIPv4 = 0;
326
327  /**
328   * env variables
329   **/
330  if (getenv("RELAYCLIENT")) /* known user, don't do anything */
331    return 0;
332
333  remote = getenv("TCPREMOTEIP");
334  if (!remote)
335    remote = getenv("TCP6REMOTEIP");
336  if (!remote) /* should never happen */
337    {
338      fprintf(stderr, LOG "ERROR: can't read TCPREMOTEIP or TCP6REMOTEIP\n");
339      goto done;
340    }
341  fIPv4 = (NULL == strchr(remote, ':'));
342
343  sender = getenv("SMTPMAILFROM");
344  helo   = getenv("SMTPHELOHOST");
345  if (!sender && !helo) /* should never happen */
346    {
347      fprintf(stderr,
348              LOGR "can't read SMTPMAILFROM or SMTPHELOHOST\n",
349              remote);
350      goto done;
351    }
352
353  /**
354   * SPF
355   **/
356  if (NULL == (spf_server = SPF_server_new(SPF_DNS_RESOLV, 0)))
357    {
358      fprintf(stderr, LOGR "SPF_server_new failed.\n", remote);
359      goto done;
360    }
361  if (NULL == (spf_request = SPF_request_new(spf_server)))
362    {
363      fprintf(stderr, LOGR "SPF_request_new failed.\n", remote);
364      goto done;
365    }
366  if (!set_spf_ip(spf_request, fIPv4, remote))
367    goto done;
368  if (helo && SPF_request_set_helo_dom(spf_request, helo))
369    {
370      fprintf(stderr,
371              LOGR "SPF_request_set_helo_dom('%s') failed.\n",
372              remote,
373              helo);
374      goto done;
375    }
376  if (sender && SPF_request_set_env_from(spf_request, sender))
377    {
378      fprintf(stderr,
379              LOGR "SPF_request_set_env_from('%s') failed.\n",
380              remote,
381              sender);
382      goto done;
383    }
384  if (SPF_E_SUCCESS ==
385      (spf_err = SPF_request_query_mailfrom(spf_request, &spf_response)))
386    {
387      fQuerySuccessful = 1;
388      fprintf(stderr,
389              LOGR "%s\n",
390              remote,
391              SPF_response_get_received_spf(spf_response));
392      switch (SPF_response_result(spf_response))
393        {
394        case SPF_RESULT_NEUTRAL:
395          envcmd(getenv("SPP_SPF_RESULT_NEUTRAL"), spf_response);
396          break;
397        case SPF_RESULT_PASS:
398          if (getenv("SPP_SPF_DONT_ALLOW_RANDOM_IP_PASS"))
399            {
400              /* not intended to be cryptographically secure */
401              srandom(time(NULL) * getpid());
402
403              /* test twice.  if both pass, something is odd. */
404              if (random_ip_passes(spf_request, fIPv4, remote) &&
405                  random_ip_passes(spf_request, fIPv4, remote))
406                {
407                  fConsiderMXCheck = 1;
408                  fprintf(stderr,
409                          LOGR "Two random IP addresses also passed SPF "
410                          "check, so ignoring this result.  Seems like SPF "
411                          "record may contain \"+all\"!\n",
412                          remote);
413                  break;
414                }
415            }
416          envcmd(getenv("SPP_SPF_RESULT_PASS"), spf_response);
417          break;
418        case SPF_RESULT_FAIL:
419          envcmd(getenv("SPP_SPF_RESULT_FAIL"), spf_response);
420          break;
421        case SPF_RESULT_SOFTFAIL:
422          envcmd(getenv("SPP_SPF_RESULT_SOFTFAIL"), spf_response);
423          break;
424        case SPF_RESULT_NONE:
425          envcmd(getenv("SPP_SPF_RESULT_NONE"), spf_response);
426          fConsiderMXCheck = 1;
427          break;
428        case SPF_RESULT_TEMPERROR:
429          envcmd(getenv("SPP_SPF_RESULT_TEMPERROR"), spf_response);
430          fConsiderMXCheck = 1;
431          break;
432        case SPF_RESULT_PERMERROR:
433          envcmd(getenv("SPP_SPF_RESULT_PERMERROR"), spf_response);
434          fConsiderMXCheck = 1;
435          break;
436        case SPF_RESULT_INVALID:
437        default:
438          fConsiderMXCheck = 1;
439          fprintf(stderr,
440                  LOGR "SPF_request_query_mailfrom: invalid or unknown "
441                  "result.\n",
442                  remote);
443          break;
444        }
445    }
446  else
447    {
448      fConsiderMXCheck = 1;
449      fprintf(stderr,
450              LOGR "SPF_request_query_mailfrom (helo='%s', mailfrom='%s'): "
451              "failed: %s\n",
452              remote,
453              helo ? helo : "",
454              sender ? sender : "",
455              SPF_strerror(spf_err));
456    }
457
458  /**
459   * Fallback MX check
460   **/
461  if (fConsiderMXCheck)
462    {
463#define SPF_MX_STR "v=spf1 mx -all"
464      mxpass = getenv("SPP_SPF_MX_RESULT_PASS");
465      mxfail = getenv("SPP_SPF_MX_RESULT_FAIL");
466      mxunk  = getenv("SPP_SPF_MX_RESULT_UNKNOWN");
467      if (mxpass || mxfail || mxunk)
468        {
469          if (SPF_E_SUCCESS ==
470              (spf_err = SPF_request_query_fallback(spf_request,
471                                                    &spf_responsemx,
472                                                    SPF_MX_STR)))
473            {
474              fQuerySuccessful = 1;
475              fprintf(stderr,
476                      LOGR "Fallback MX check \"" SPF_MX_STR "\" "
477                      "(helo='%s', mailfrom='%s'): %s\n",
478                      remote,
479                      helo ? helo : "",
480                      sender ? sender : "",
481                      SPF_strresult(SPF_response_result(spf_responsemx)));
482              switch (SPF_response_result(spf_responsemx))
483                {
484                case SPF_RESULT_PASS:
485                  envcmd(mxpass, NULL);
486                  break;
487                case SPF_RESULT_FAIL:
488                  envcmd(mxfail, NULL);
489                  break;
490                default:
491                  envcmd(mxunk, NULL);
492                  break;
493                }
494            }
495          else
496            {
497              fprintf(stderr,
498                      LOGR "SPF_request_query_fallback \"" SPF_MX_STR "\" "
499                      "(helo='%s', mailfrom='%s') failed: %s\n",
500                      remote,
501                      helo ? helo : "",
502                      sender ? sender : "",
503                      SPF_strerror(spf_err));
504            }
505        }
506    }
507
508 done:
509  if (NULL != spf_responsemx) SPF_response_free(spf_responsemx);
510  if (NULL != spf_response) SPF_response_free(spf_response);
511  if (NULL != spf_request) SPF_request_free(spf_request);
512  if (NULL != spf_server) SPF_server_free(spf_server);
513
514  if (0 == fQuerySuccessful)
515    {
516      envcmd(getenv("SPP_SPF_NO_RESULT"), NULL);
517    }
518     
519  return 0;
520}
Note: See TracBrowser for help on using the repository browser.