14937e7999fe1bc9e6931994ac7b06e124cb11c5
[exim.git] / src / src / miscmods / spf.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* SPF support.
6    Copyright (c) The Exim Maintainers 2015 - 2024
7    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
8    License: GPL
9    SPDX-License-Identifier: GPL-2.0-or-later
10 */
11
12 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
13
14 #include "../exim.h"
15 #ifdef SUPPORT_SPF
16
17 /* must be kept in numeric order */
18 static spf_result_id spf_result_id_list[] = {
19   /* name               value */
20   { US"invalid",        0},
21   { US"neutral",        1 },
22   { US"pass",           2 },
23   { US"fail",           3 },
24   { US"softfail",       4 },
25   { US"none",           5 },
26   { US"temperror",      6 }, /* RFC 4408 defined */
27   { US"permerror",      7 }  /* RFC 4408 defined */
28 };
29
30 SPF_server_t    *spf_server = NULL;
31 SPF_request_t   *spf_request = NULL;
32 SPF_response_t  *spf_response = NULL;
33 SPF_response_t  *spf_response_2mx = NULL;
34
35 SPF_dns_rr_t  * spf_nxdomain = NULL;
36
37 uschar * spf_guess              = US"v=spf1 a/24 mx/24 ptr ?all";
38 uschar * spf_header_comment     = NULL;
39 uschar * spf_received           = NULL;
40 uschar * spf_result             = NULL;
41 uschar * spf_smtp_comment       = NULL;
42 uschar * spf_smtp_comment_template
43                     /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
44                                 = US"Please%_see%_http://www.open-spf.org/Why";
45 BOOL    spf_result_guessed     = FALSE;
46
47
48
49
50 static gstring *
51 spf_lib_version_report(gstring * g)
52 {
53 int maj, min, patch;
54
55 SPF_get_lib_version(&maj, &min, &patch);
56 g = string_fmt_append(g, "Library version: spf2: Compile: %d.%d.%d\n",
57         SPF_LIB_VERSION_MAJOR, SPF_LIB_VERSION_MINOR, SPF_LIB_VERSION_PATCH);
58 g = string_fmt_append(g,    "                       Runtime: %d.%d.%d\n",
59          maj, min, patch);
60 return g;
61 }
62
63
64
65 static SPF_dns_rr_t *
66 SPF_dns_exim_lookup(SPF_dns_server_t *spf_dns_server,
67   const char *domain, ns_type rr_type, int should_cache)
68 {
69 dns_answer * dnsa = store_get_dns_answer();
70 dns_scan dnss;
71 SPF_dns_rr_t * spfrr;
72 unsigned found = 0;
73
74 SPF_dns_rr_t srr = {
75   .domain = CS domain,                  /* query information */
76   .domain_buf_len = 0,
77   .rr_type = rr_type,
78
79   .rr_buf_len = 0,                      /* answer information */
80   .rr_buf_num = 0, /* no free of s */
81   .utc_ttl = 0,
82
83   .hook = NULL,                         /* misc information */
84   .source = spf_dns_server
85 };
86
87 DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", domain);
88
89 /* Shortcircuit SPF RR lookups by returning NO_DATA.  They were obsoleted by
90 RFC 6686/7208 years ago. see bug #1294 */
91
92 if (rr_type == T_SPF)
93   {
94   HDEBUG(D_host_lookup) debug_printf("faking NO_DATA for SPF RR(99) lookup\n");
95   srr.herrno = NO_DATA;
96   SPF_dns_rr_dup(&spfrr, &srr);
97   store_free_dns_answer(dnsa);
98   return spfrr;
99   }
100
101 switch (dns_lookup(dnsa, US domain, rr_type, NULL))
102   {
103   case DNS_AGAIN:       srr.herrno = TRY_AGAIN;         break;
104   case DNS_NOMATCH:     srr.herrno = HOST_NOT_FOUND;    break;
105   case DNS_NODATA:      srr.herrno = NO_DATA;           break;
106   case DNS_FAIL:
107   default:              srr.herrno = NO_RECOVERY;       break;
108   case DNS_SUCCEED:
109     srr.herrno = NETDB_SUCCESS;
110     for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
111          rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
112       /* Need to alloc space for all records, so no early-out */
113       if (rr->type == rr_type) found++;
114     break;
115   }
116
117 if (found == 0)
118   {
119   SPF_dns_rr_dup(&spfrr, &srr);
120   store_free_dns_answer(dnsa);
121   return spfrr;
122   }
123
124 srr.rr = store_malloc(sizeof(SPF_dns_rr_data_t) * found);
125
126 found = 0;
127 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
128    rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
129   if (rr->type == rr_type)
130     {
131     const uschar * s = rr->data;
132
133     srr.ttl = rr->ttl;
134     switch(rr_type)
135       {
136       case T_MX:
137         if (rr->size < 2) continue;
138         s += 2; /* skip the MX precedence field */
139       case T_PTR:
140         {
141         uschar * buf = store_malloc(256);
142         (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
143           (DN_EXPAND_ARG4_TYPE)buf, 256);
144         s = buf;
145         break;
146         }
147
148       case T_TXT:
149         {
150         gstring * g = NULL;
151         uschar chunk_len;
152
153         if (rr->size < 1+6) continue;           /* min for version str */
154         if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
155           {
156           HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
157             (int) s[0], s+1);
158           continue;
159           }
160
161         /* require 1 byte for the chunk_len */
162         for (int off = 0; off < rr->size - 1; off += chunk_len)
163           {
164           if (  !(chunk_len = s[off++])
165              || rr->size < off + chunk_len      /* ignore bogus size chunks */
166              ) break;
167           g = string_catn(g, s+off, chunk_len);
168           }
169         if (!g)
170           continue;
171         gstring_release_unused(g);
172         s = string_copy_malloc(string_from_gstring(g));
173         DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
174         break;
175         }
176
177       case T_A:
178       case T_AAAA:
179       default:
180         {
181         uschar * buf = store_malloc(dnsa->answerlen + 1);
182         s = memcpy(buf, s, dnsa->answerlen + 1);
183         break;
184         }
185       }
186     srr.rr[found++] = (void *) s;
187     }
188
189 /* Did we filter out all TXT RRs? Return NO_DATA instead of SUCCESS with
190 empty ANSWER section. */
191
192 if (!(srr.num_rr = found))
193   srr.herrno = NO_DATA;
194
195 /* spfrr->rr must have been malloc()d for this */
196 SPF_dns_rr_dup(&spfrr, &srr);
197 store_free_dns_answer(dnsa);
198 return spfrr;
199 }
200
201
202
203 static SPF_dns_server_t *
204 SPF_dns_exim_new(int debug)
205 {
206 SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));
207
208 /* DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n"); */
209
210 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
211 spf_dns_server->destroy      = NULL;
212 spf_dns_server->lookup       = SPF_dns_exim_lookup;
213 spf_dns_server->get_spf      = NULL;
214 spf_dns_server->get_exp      = NULL;
215 spf_dns_server->add_cache    = NULL;
216 spf_dns_server->layer_below  = NULL;
217 spf_dns_server->name         = "exim";
218 spf_dns_server->debug        = debug;
219
220 /* XXX This might have to return NO_DATA sometimes. */
221
222 spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
223   "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
224 if (!spf_nxdomain)
225   {
226   store_free(spf_dns_server);
227   return NULL;
228   }
229
230 return spf_dns_server;
231 }
232
233
234
235
236 /* Construct the SPF library stack.
237    Return: Boolean success.
238 */
239
240 static BOOL
241 spf_init(void * dummy_ctx)
242 {
243 SPF_dns_server_t * dc;
244 int debug = 0;
245 const uschar *s;
246
247 DEBUG(D_receive) debug = 1;
248
249 /* We insert our own DNS access layer rather than letting the spf library
250 do it, so that our dns access path is used for debug tracing and for the
251 testsuite. */
252
253 if (!(dc = SPF_dns_exim_new(debug)))
254   {
255   DEBUG(D_receive) debug_printf("spf: SPF_dns_exim_new() failed\n");
256   return FALSE;
257   }
258 if (!(dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
259   {
260   DEBUG(D_receive) debug_printf("spf: SPF_dns_cache_new() failed\n");
261   return FALSE;
262   }
263 if (!(spf_server = SPF_server_new_dns(dc, debug)))
264   {
265   DEBUG(D_receive) debug_printf("spf: SPF_server_new() failed.\n");
266   return FALSE;
267   }
268
269 /* Override the outdated explanation URL.
270 See https://www.mail-archive.com/mailop@mailop.org/msg08019.html
271 Used to work as "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}",
272 but is broken now (May 18th, 2020) */
273
274 GET_OPTION("spf_smtp_comment_template");
275 if (!(s = expand_string(spf_smtp_comment_template)))
276   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "expansion of spf_smtp_comment_template failed");
277
278 SPF_server_set_explanation(spf_server, CCS s, &spf_response);
279 if (SPF_response_errcode(spf_response) != SPF_E_SUCCESS)
280   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", SPF_strerror(SPF_response_errcode(spf_response)));
281
282 return TRUE;
283 }
284
285
286 /* Set up a context that can be re-used for several
287    messages on the same SMTP connection (that come from the
288    same host with the same HELO string).
289
290 Return: OK/FAIL
291 */
292
293 static int
294 spf_conn_init(const uschar * spf_helo_domain, const uschar * spf_remote_addr)
295 {
296 DEBUG(D_receive)
297   debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);
298
299 if (!spf_server && !spf_init(NULL))
300   return FAIL;
301
302 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
303   {
304   DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n",
305     primary_hostname);
306   spf_server = NULL;
307   return FAIL;
308   }
309
310 spf_request = SPF_request_new(spf_server);
311
312 if (  SPF_request_set_ipv4_str(spf_request, CCS spf_remote_addr)
313    && SPF_request_set_ipv6_str(spf_request, CCS spf_remote_addr)
314    )
315   {
316   DEBUG(D_receive)
317     debug_printf("spf: SPF_request_set_ipv4_str() and "
318       "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
319   spf_server = NULL;
320   spf_request = NULL;
321   return FAIL;
322   }
323
324 if (SPF_request_set_helo_dom(spf_request, CCS spf_helo_domain))
325   {
326   DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n",
327     spf_helo_domain);
328   spf_server = NULL;
329   spf_request = NULL;
330   return FAIL;
331   }
332
333 return OK;
334 }
335
336 static void
337 spf_smtp_reset(void)
338 {
339 spf_header_comment = spf_received = spf_result = spf_smtp_comment = NULL;
340 spf_result_guessed = FALSE;
341 }
342
343
344 static void
345 spf_response_debug(SPF_response_t * spf_response)
346 {
347 if (SPF_response_messages(spf_response) == 0)
348   debug_printf(" (no errors)\n");
349 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
350   {
351   SPF_error_t * err = SPF_response_message(spf_response, i);
352   debug_printf( "%s_msg = (%d) %s\n",
353                   (SPF_error_errorp(err) ? "warn" : "err"),
354                   SPF_error_code(err),
355                   SPF_error_message(err));
356   }
357 }
358
359
360 /* spf_process adds the envelope sender address to the existing
361    context (if any), retrieves the result, sets up expansion
362    strings and evaluates the condition outcome.
363
364 Return: OK/FAIL  */
365
366 static int
367 spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
368   int action)
369 {
370 int sep = 0;
371 const uschar *list = *listptr;
372 uschar *spf_result_id;
373 int rc = SPF_RESULT_PERMERROR;
374
375 DEBUG(D_receive) debug_printf("spf_process\n");
376
377 if (!(spf_server && spf_request))
378   /* no global context, assume temp error and skip to evaluation */
379   rc = SPF_RESULT_PERMERROR;
380
381 else if (SPF_request_set_env_from(spf_request, CS spf_envelope_sender))
382   /* Invalid sender address. This should be a real rare occurrence */
383   rc = SPF_RESULT_PERMERROR;
384
385 else
386   {
387   /* get SPF result */
388   if (action == SPF_PROCESS_FALLBACK)
389     {
390     SPF_request_query_fallback(spf_request, &spf_response, CS spf_guess);
391     spf_result_guessed = TRUE;
392     }
393   else
394     SPF_request_query_mailfrom(spf_request, &spf_response);
395
396   /* set up expansion items */
397   spf_header_comment     = US SPF_response_get_header_comment(spf_response);
398   spf_received           = US SPF_response_get_received_spf(spf_response);
399   spf_result             = US SPF_strresult(SPF_response_result(spf_response));
400   spf_smtp_comment       = US SPF_response_get_smtp_comment(spf_response);
401
402   rc = SPF_response_result(spf_response);
403
404   DEBUG(D_acl) spf_response_debug(spf_response);
405   }
406
407 /* We got a result. Now see if we should return OK or FAIL for it */
408 DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
409
410 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
411   return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
412
413 while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
414   {
415   BOOL negate, result;
416
417   if ((negate = spf_result_id[0] == '!'))
418     spf_result_id++;
419
420   result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
421   if (negate != result) return OK;
422   }
423
424 /* no match */
425 return FAIL;
426 }
427
428
429
430 static gstring *
431 authres_spf(gstring * g)
432 {
433 uschar * s;
434 if (spf_result)
435   {
436   int start = 0;                /* Compiler quietening */
437   DEBUG(D_acl) start = gstring_length(g);
438
439   g = string_append(g, 2, US";\n\tspf=", spf_result);
440   if (spf_result_guessed)
441     g = string_cat(g, US" (best guess record for domain)");
442
443   s = expand_string(US"$sender_address_domain");
444   if (s && *s)
445     g = string_append(g, 2, US" smtp.mailfrom=", s);
446   else
447     {
448     s = sender_helo_name;
449     g = s && *s
450       ? string_append(g, 2, US" smtp.helo=", s)
451       : string_cat(g, US" smtp.mailfrom=<>");
452     }
453   DEBUG(D_acl) debug_printf("SPF:\tauthres '%.*s'\n",
454                   gstring_length(g) - start - 3, g->s + start + 3);
455   }
456 else
457   DEBUG(D_acl) debug_printf("SPF:\tno authres\n");
458 return g;
459 }
460
461
462 /* Ugly; used only by dmarc (peeking into our data!)
463 Exposure of values as $variables might be better? */
464
465 static SPF_response_t *
466 spf_get_response(void)
467 {
468 return spf_response;
469 }
470
471 /******************************************************************************/
472 /* Lookup support */
473
474 static void *
475 spf_lookup_open(const uschar * filename, uschar ** errmsg)
476 {
477 SPF_dns_server_t * dc;
478 SPF_server_t * spf_server = NULL;
479 int debug = 0;
480
481 DEBUG(D_lookup) debug = 1;
482
483 if ((dc = SPF_dns_exim_new(debug)))
484   if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
485     spf_server = SPF_server_new_dns(dc, debug);
486
487 if (!spf_server)
488   {
489   *errmsg = US"SPF_dns_exim_nnew() failed";
490   return NULL;
491   }
492 return (void *) spf_server;
493 }
494
495 static void
496 spf_lookup_close(void * handle)
497 {
498 SPF_server_t * spf_server = handle;
499 if (spf_server) SPF_server_free(spf_server);
500 }
501
502 static int
503 spf_lookup_find(void * handle, const uschar * filename,
504   const uschar * keystring, int key_len, uschar ** result, uschar ** errmsg,
505   uint * do_cache, const uschar * opts)
506 {
507 SPF_server_t *spf_server = handle;
508 SPF_request_t *spf_request;
509 SPF_response_t *spf_response = NULL;
510
511 if (!(spf_request = SPF_request_new(spf_server)))
512   {
513   *errmsg = US"SPF_request_new() failed";
514   return FAIL;
515   }
516
517 #if HAVE_IPV6
518 switch (string_is_ip_address(filename, NULL))
519 #else
520 switch (4)
521 #endif
522   {
523   case 4:
524     if (!SPF_request_set_ipv4_str(spf_request, CS filename))
525       break;
526     *errmsg = string_sprintf("invalid IPv4 address '%s'", filename);
527     return FAIL;
528 #if HAVE_IPV6
529
530   case 6:
531     if (!SPF_request_set_ipv6_str(spf_request, CS filename))
532       break;
533     *errmsg = string_sprintf("invalid IPv6 address '%s'", filename);
534     return FAIL;
535
536   default:
537     *errmsg = string_sprintf("invalid IP address '%s'", filename);
538     return FAIL;
539 #endif
540   }
541
542 if (SPF_request_set_env_from(spf_request, CS keystring))
543     {
544   *errmsg = string_sprintf("invalid envelope from address '%s'", keystring);
545   return FAIL;
546 }
547
548 SPF_request_query_mailfrom(spf_request, &spf_response);
549 *result = string_copy(US SPF_strresult(SPF_response_result(spf_response)));
550
551 DEBUG(D_lookup) spf_response_debug(spf_response);
552
553 SPF_response_free(spf_response);
554 SPF_request_free(spf_request);
555 return OK;
556 }
557
558
559 /******************************************************************************/
560 /* Module API */
561
562 static optionlist spf_options[] = {
563   { "spf_guess",                opt_stringptr,   {&spf_guess} },
564   { "spf_smtp_comment_template",opt_stringptr,   {&spf_smtp_comment_template} },
565 };
566
567 static void * spf_functions[] = {
568   [SPF_PROCESS] =       spf_process,
569   [SPF_GET_RESPONSE] =  spf_get_response,               /* ugly; for dmarc */
570   
571   [SPF_OPEN] =          spf_lookup_open,
572   [SPF_CLOSE] =         spf_lookup_close,
573   [SPF_FIND] =          spf_lookup_find,
574 };
575
576 static var_entry spf_variables[] = {
577   { "spf_guess",                vtype_stringptr,        &spf_guess },
578   { "spf_header_comment",       vtype_stringptr,        &spf_header_comment },
579   { "spf_received",             vtype_stringptr,        &spf_received },
580   { "spf_result",               vtype_stringptr,        &spf_result },
581   { "spf_result_guessed",       vtype_bool,             &spf_result_guessed },
582   { "spf_smtp_comment",         vtype_stringptr,        &spf_smtp_comment },
583 };
584
585 misc_module_info spf_module_info =
586 {
587   .name =               US"spf",
588 # if SUPPORT_SPF==2
589   .dyn_magic =          MISC_MODULE_MAGIC,
590 # endif
591   .init =               spf_init,
592   .lib_vers_report =    spf_lib_version_report,
593   .conn_init =          spf_conn_init,
594   .smtp_reset =         spf_smtp_reset,
595   .authres =            authres_spf,
596
597   .options =            spf_options,
598   .options_count =      nelem(spf_options),
599
600   .functions =          spf_functions,
601   .functions_count =    nelem(spf_functions),
602
603   .variables =          spf_variables,
604   .variables_count =    nelem(spf_variables),
605 };
606
607 #endif  /* almost all the file */