a7b6c6a8dd693439502e8f72fbcc20250273e612
[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: Boolean success
291 */
292
293 static BOOL
294 spf_conn_init(uschar * spf_helo_domain, 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)) return FALSE;
300
301 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
302   {
303   DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n",
304     primary_hostname);
305   spf_server = NULL;
306   return FALSE;
307   }
308
309 spf_request = SPF_request_new(spf_server);
310
311 if (  SPF_request_set_ipv4_str(spf_request, CS spf_remote_addr)
312    && SPF_request_set_ipv6_str(spf_request, CS spf_remote_addr)
313    )
314   {
315   DEBUG(D_receive)
316     debug_printf("spf: SPF_request_set_ipv4_str() and "
317       "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
318   spf_server = NULL;
319   spf_request = NULL;
320   return FALSE;
321   }
322
323 if (SPF_request_set_helo_dom(spf_request, CS spf_helo_domain))
324   {
325   DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n",
326     spf_helo_domain);
327   spf_server = NULL;
328   spf_request = NULL;
329   return FALSE;
330   }
331
332 return TRUE;
333 }
334
335 static void
336 spf_smtp_reset(void)
337 {
338 spf_header_comment = spf_received = spf_result = spf_smtp_comment = NULL;
339 spf_result_guessed = FALSE;
340 }
341
342
343 static void
344 spf_response_debug(SPF_response_t * spf_response)
345 {
346 if (SPF_response_messages(spf_response) == 0)
347   debug_printf(" (no errors)\n");
348 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
349   {
350   SPF_error_t * err = SPF_response_message(spf_response, i);
351   debug_printf( "%s_msg = (%d) %s\n",
352                   (SPF_error_errorp(err) ? "warn" : "err"),
353                   SPF_error_code(err),
354                   SPF_error_message(err));
355   }
356 }
357
358
359 /* spf_process adds the envelope sender address to the existing
360    context (if any), retrieves the result, sets up expansion
361    strings and evaluates the condition outcome.
362
363 Return: OK/FAIL  */
364
365 static int
366 spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
367   int action)
368 {
369 int sep = 0;
370 const uschar *list = *listptr;
371 uschar *spf_result_id;
372 int rc = SPF_RESULT_PERMERROR;
373
374 DEBUG(D_receive) debug_printf("spf_process\n");
375
376 if (!(spf_server && spf_request))
377   /* no global context, assume temp error and skip to evaluation */
378   rc = SPF_RESULT_PERMERROR;
379
380 else if (SPF_request_set_env_from(spf_request, CS spf_envelope_sender))
381   /* Invalid sender address. This should be a real rare occurrence */
382   rc = SPF_RESULT_PERMERROR;
383
384 else
385   {
386   /* get SPF result */
387   if (action == SPF_PROCESS_FALLBACK)
388     {
389     SPF_request_query_fallback(spf_request, &spf_response, CS spf_guess);
390     spf_result_guessed = TRUE;
391     }
392   else
393     SPF_request_query_mailfrom(spf_request, &spf_response);
394
395   /* set up expansion items */
396   spf_header_comment     = US SPF_response_get_header_comment(spf_response);
397   spf_received           = US SPF_response_get_received_spf(spf_response);
398   spf_result             = US SPF_strresult(SPF_response_result(spf_response));
399   spf_smtp_comment       = US SPF_response_get_smtp_comment(spf_response);
400
401   rc = SPF_response_result(spf_response);
402
403   DEBUG(D_acl) spf_response_debug(spf_response);
404   }
405
406 /* We got a result. Now see if we should return OK or FAIL for it */
407 DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
408
409 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
410   return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
411
412 while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
413   {
414   BOOL negate, result;
415
416   if ((negate = spf_result_id[0] == '!'))
417     spf_result_id++;
418
419   result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
420   if (negate != result) return OK;
421   }
422
423 /* no match */
424 return FAIL;
425 }
426
427
428
429 static gstring *
430 authres_spf(gstring * g)
431 {
432 uschar * s;
433 if (spf_result)
434   {
435   int start = 0;                /* Compiler quietening */
436   DEBUG(D_acl) start = gstring_length(g);
437
438   g = string_append(g, 2, US";\n\tspf=", spf_result);
439   if (spf_result_guessed)
440     g = string_cat(g, US" (best guess record for domain)");
441
442   s = expand_string(US"$sender_address_domain");
443   if (s && *s)
444     g = string_append(g, 2, US" smtp.mailfrom=", s);
445   else
446     {
447     s = sender_helo_name;
448     g = s && *s
449       ? string_append(g, 2, US" smtp.helo=", s)
450       : string_cat(g, US" smtp.mailfrom=<>");
451     }
452   DEBUG(D_acl) debug_printf("SPF:\tauthres '%.*s'\n",
453                   gstring_length(g) - start - 3, g->s + start + 3);
454   }
455 else
456   DEBUG(D_acl) debug_printf("SPF:\tno authres\n");
457 return g;
458 }
459
460
461 /* Ugly; used only by dmarc (peeking into our data!)
462 Exposure of values as $variables might be better? */
463
464 static SPF_response_t *
465 spf_get_response(void)
466 {
467 return spf_response;
468 }
469
470 /******************************************************************************/
471 /* Lookup support */
472
473 static void *
474 spf_lookup_open(const uschar * filename, uschar ** errmsg)
475 {
476 SPF_dns_server_t * dc;
477 SPF_server_t * spf_server = NULL;
478 int debug = 0;
479
480 DEBUG(D_lookup) debug = 1;
481
482 if ((dc = SPF_dns_exim_new(debug)))
483   if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
484     spf_server = SPF_server_new_dns(dc, debug);
485
486 if (!spf_server)
487   {
488   *errmsg = US"SPF_dns_exim_nnew() failed";
489   return NULL;
490   }
491 return (void *) spf_server;
492 }
493
494 static void
495 spf_lookup_close(void * handle)
496 {
497 SPF_server_t * spf_server = handle;
498 if (spf_server) SPF_server_free(spf_server);
499 }
500
501 static int
502 spf_lookup_find(void * handle, const uschar * filename,
503   const uschar * keystring, int key_len, uschar ** result, uschar ** errmsg,
504   uint * do_cache, const uschar * opts)
505 {
506 SPF_server_t *spf_server = handle;
507 SPF_request_t *spf_request;
508 SPF_response_t *spf_response = NULL;
509
510 if (!(spf_request = SPF_request_new(spf_server)))
511   {
512   *errmsg = US"SPF_request_new() failed";
513   return FAIL;
514   }
515
516 #if HAVE_IPV6
517 switch (string_is_ip_address(filename, NULL))
518 #else
519 switch (4)
520 #endif
521   {
522   case 4:
523     if (!SPF_request_set_ipv4_str(spf_request, CS filename))
524       break;
525     *errmsg = string_sprintf("invalid IPv4 address '%s'", filename);
526     return FAIL;
527 #if HAVE_IPV6
528
529   case 6:
530     if (!SPF_request_set_ipv6_str(spf_request, CS filename))
531       break;
532     *errmsg = string_sprintf("invalid IPv6 address '%s'", filename);
533     return FAIL;
534
535   default:
536     *errmsg = string_sprintf("invalid IP address '%s'", filename);
537     return FAIL;
538 #endif
539   }
540
541 if (SPF_request_set_env_from(spf_request, CS keystring))
542     {
543   *errmsg = string_sprintf("invalid envelope from address '%s'", keystring);
544   return FAIL;
545 }
546
547 SPF_request_query_mailfrom(spf_request, &spf_response);
548 *result = string_copy(US SPF_strresult(SPF_response_result(spf_response)));
549
550 DEBUG(D_lookup) spf_response_debug(spf_response);
551
552 SPF_response_free(spf_response);
553 SPF_request_free(spf_request);
554 return OK;
555 }
556
557
558 /******************************************************************************/
559 /* Module API */
560
561 static optionlist spf_options[] = {
562   { "spf_guess",                opt_stringptr,   {&spf_guess} },
563   { "spf_smtp_comment_template",opt_stringptr,   {&spf_smtp_comment_template} },
564 };
565
566 static void * spf_functions[] = {
567   spf_conn_init,
568   spf_process,
569   authres_spf,
570   spf_get_response,             /* ugly; for dmarc */
571   spf_smtp_reset,
572   
573   spf_lookup_open,
574   spf_lookup_close,
575   spf_lookup_find,
576 };
577
578 static var_entry spf_variables[] = {
579   { "spf_guess",                vtype_stringptr,        &spf_guess },
580   { "spf_header_comment",       vtype_stringptr,        &spf_header_comment },
581   { "spf_received",             vtype_stringptr,        &spf_received },
582   { "spf_result",               vtype_stringptr,        &spf_result },
583   { "spf_result_guessed",       vtype_bool,             &spf_result_guessed },
584   { "spf_smtp_comment",         vtype_stringptr,        &spf_smtp_comment },
585 };
586
587 misc_module_info spf_module_info =
588 {
589   .name =               US"spf",
590 # if SUPPORT_SPF==2
591   .dyn_magic =          MISC_MODULE_MAGIC,
592 # endif
593   .init =               spf_init,
594   .lib_vers_report =    spf_lib_version_report,
595
596   .options =            spf_options,
597   .options_count =      nelem(spf_options),
598
599   .functions =          spf_functions,
600   .functions_count =    nelem(spf_functions),
601
602   .variables =          spf_variables,
603   .variables_count =    nelem(spf_variables),
604 };
605
606 #endif  /* almost all the file */