SPDX: license tags (mostly by guesswork)
[exim.git] / src / src / dnsbl.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
6 /* Copyright (c) University of Cambridge 1995 - 2018 */
7 /* See the file NOTICE for conditions of use and distribution. */
8 /* SPDX-License-Identifier: GPL-2.0-only */
9
10 /* Functions concerned with dnsbls */
11
12
13 #include "exim.h"
14
15 /* Structure for caching DNSBL lookups */
16
17 typedef struct dnsbl_cache_block {
18   time_t expiry;
19   dns_address *rhs;
20   uschar *text;
21   int rc;
22   BOOL text_set;
23 } dnsbl_cache_block;
24
25
26 /* Anchor for DNSBL cache */
27
28 static tree_node *dnsbl_cache = NULL;
29
30
31 /* Bits for match_type in one_check_dnsbl() */
32
33 #define MT_NOT 1
34 #define MT_ALL 2
35
36
37 /*************************************************
38 *          Perform a single dnsbl lookup         *
39 *************************************************/
40
41 /* This function is called from verify_check_dnsbl() below. It is also called
42 recursively from within itself when domain and domain_txt are different
43 pointers, in order to get the TXT record from the alternate domain.
44
45 Arguments:
46   domain         the outer dnsbl domain
47   domain_txt     alternate domain to lookup TXT record on success; when the
48                    same domain is to be used, domain_txt == domain (that is,
49                    the pointers must be identical, not just the text)
50   keydomain      the current keydomain (for debug message)
51   prepend        subdomain to lookup (like keydomain, but
52                    reversed if IP address)
53   iplist         the list of matching IP addresses, or NULL for "any"
54   bitmask        true if bitmask matching is wanted
55   match_type     condition for 'succeed' result
56                    0 => Any RR in iplist     (=)
57                    1 => No RR in iplist      (!=)
58                    2 => All RRs in iplist    (==)
59                    3 => Some RRs not in iplist (!==)
60                    the two bits are defined as MT_NOT and MT_ALL
61   defer_return   what to return for a defer
62
63 Returns:         OK if lookup succeeded
64                  FAIL if not
65 */
66
67 static int
68 one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain,
69   uschar *prepend, uschar *iplist, BOOL bitmask, int match_type,
70   int defer_return)
71 {
72 dns_answer * dnsa = store_get_dns_answer();
73 dns_scan dnss;
74 tree_node *t;
75 dnsbl_cache_block *cb;
76 int old_pool = store_pool;
77 uschar * query;
78 int qlen, yield;
79
80 /* Construct the specific query domainname */
81
82 query = string_sprintf("%s.%s", prepend, domain);
83 if ((qlen = Ustrlen(query)) >= 256)
84   {
85   log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long "
86     "(ignored): %s...", query);
87   yield = FAIL;
88   goto out;
89   }
90
91 /* Look for this query in the cache. */
92
93 if (  (t = tree_search(dnsbl_cache, query))
94    && (cb = t->data.ptr)->expiry > time(NULL)
95    )
96
97 /* Previous lookup was cached */
98
99   {
100   HDEBUG(D_dnsbl) debug_printf("dnslists: using result of previous lookup\n");
101   }
102
103 /* If not cached from a previous lookup, we must do a DNS lookup, and
104 cache the result in permanent memory. */
105
106 else
107   {
108   uint ttl = 3600;      /* max TTL for positive cache entries */
109
110   store_pool = POOL_PERM;
111
112   if (t)
113     {
114     HDEBUG(D_dnsbl) debug_printf("cached data found but past valid time; ");
115     }
116
117   else
118     {   /* Set up a tree entry to cache the lookup */
119     t = store_get(sizeof(tree_node) + qlen + 1 + 1, query);
120     Ustrcpy(t->name, query);
121     t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block), GET_UNTAINTED);
122     (void)tree_insertnode(&dnsbl_cache, t);
123     }
124
125   /* Do the DNS lookup . */
126
127   HDEBUG(D_dnsbl) debug_printf("new DNS lookup for %s\n", query);
128   cb->rc = dns_basic_lookup(dnsa, query, T_A);
129   cb->text_set = FALSE;
130   cb->text = NULL;
131   cb->rhs = NULL;
132
133   /* If the lookup succeeded, cache the RHS address. The code allows for
134   more than one address - this was for complete generality and the possible
135   use of A6 records. However, A6 records are no longer supported. Leave the code
136   here, just in case.
137
138   Quite apart from one A6 RR generating multiple addresses, there are DNS
139   lists that return more than one A record, so we must handle multiple
140   addresses generated in that way as well.
141
142   Mark the cache entry with the "now" plus the minimum of the address TTLs,
143   or the RFC 2308 negative-cache value from the SOA if none were found. */
144
145   switch (cb->rc)
146     {
147     case DNS_SUCCEED:
148       {
149       dns_address ** addrp = &cb->rhs;
150       dns_address * da;
151       for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
152            rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
153         if (rr->type == T_A && (da = dns_address_from_rr(dnsa, rr)))
154           {
155           *addrp = da;
156           while (da->next) da = da->next;
157           addrp = &da->next;
158           if (ttl > rr->ttl) ttl = rr->ttl;
159           }
160
161       if (cb->rhs)
162         {
163         cb->expiry = time(NULL) + ttl;
164         break;
165         }
166
167       /* If we didn't find any A records, change the return code. This can
168       happen when there is a CNAME record but there are no A records for what
169       it points to. */
170
171       cb->rc = DNS_NODATA;
172       }
173       /*FALLTHROUGH*/
174
175     case DNS_NOMATCH:
176     case DNS_NODATA:
177       {
178       /* Although there already is a neg-cache layer maintained by
179       dns_basic_lookup(), we have a dnslist cache entry allocated and
180       tree-inserted. So we may as well use it. */
181
182       time_t soa_negttl = dns_expire_from_soa(dnsa, T_A);
183       cb->expiry = soa_negttl ? soa_negttl : time(NULL) + ttl;
184       break;
185       }
186
187     default:
188       cb->expiry = time(NULL) + ttl;
189       break;
190     }
191
192   store_pool = old_pool;
193   HDEBUG(D_dnsbl) debug_printf("dnslists: wrote cache entry, ttl=%d\n",
194     (int)(cb->expiry - time(NULL)));
195   }
196
197 /* We now have the result of the DNS lookup, either newly done, or cached
198 from a previous call. If the lookup succeeded, check against the address
199 list if there is one. This may be a positive equality list (introduced by
200 "="), a negative equality list (introduced by "!="), a positive bitmask
201 list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/
202
203 if (cb->rc == DNS_SUCCEED)
204   {
205   dns_address * da = NULL;
206   uschar *addlist = cb->rhs->address;
207
208   /* For A and AAAA records, there may be multiple addresses from multiple
209   records. For A6 records (currently not expected to be used) there may be
210   multiple addresses from a single record. */
211
212   for (da = cb->rhs->next; da; da = da->next)
213     addlist = string_sprintf("%s, %s", addlist, da->address);
214
215   HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n",
216     query, addlist);
217
218   /* Address list check; this can be either for equality, or via a bitmask.
219   In the latter case, all the bits must match. */
220
221   if (iplist)
222     {
223     for (da = cb->rhs; da; da = da->next)
224       {
225       int ipsep = ',';
226       const uschar *ptr = iplist;
227       uschar *res;
228
229       /* Handle exact matching */
230
231       if (!bitmask)
232         {
233         while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0)))
234           if (Ustrcmp(CS da->address, res) == 0)
235             break;
236         }
237
238       /* Handle bitmask matching */
239
240       else
241         {
242         int address[4];
243         int mask = 0;
244
245         /* At present, all known DNS blocking lists use A records, with
246         IPv4 addresses on the RHS encoding the information they return. I
247         wonder if this will linger on as the last vestige of IPv4 when IPv6
248         is ubiquitous? Anyway, for now we use paranoia code to completely
249         ignore IPv6 addresses. The default mask is 0, which always matches.
250         We change this only for IPv4 addresses in the list. */
251
252         if (host_aton(da->address, address) == 1)
253           if ((address[0] & 0xff000000) != 0x7f000000)    /* 127.0.0.0/8 */
254             log_write(0, LOG_MAIN,
255               "DNS list lookup for %s at %s returned %s;"
256               " not in 127.0/8 and discarded",
257               keydomain, domain, da->address);
258
259           else
260             mask = address[0];
261
262         /* Scan the returned addresses, skipping any that are IPv6 */
263
264         while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0)))
265           if (host_aton(res, address) == 1)
266             if ((address[0] & mask) == address[0])
267               break;
268         }
269
270       /* If either
271
272          (a) An IP address in an any ('=') list matched, or
273          (b) No IP address in an all ('==') list matched
274
275       then we're done searching. */
276
277       if (((match_type & MT_ALL) != 0) == (res == NULL)) break;
278       }
279
280     /* If da == NULL, either
281
282        (a) No IP address in an any ('=') list matched, or
283        (b) An IP address in an all ('==') list didn't match
284
285     so behave as if the DNSBL lookup had not succeeded, i.e. the host is not on
286     the list. */
287
288     if ((match_type == MT_NOT || match_type == MT_ALL) != (da == NULL))
289       {
290       HDEBUG(D_dnsbl)
291         {
292         uschar *res = NULL;
293         switch(match_type)
294           {
295           case 0:
296             res = US"was no match"; break;
297           case MT_NOT:
298             res = US"was an exclude match"; break;
299           case MT_ALL:
300             res = US"was an IP address that did not match"; break;
301           case MT_NOT|MT_ALL:
302             res = US"were no IP addresses that did not match"; break;
303           }
304         debug_printf("=> but we are not accepting this block class because\n");
305         debug_printf("=> there %s for %s%c%s\n",
306           res,
307           match_type & MT_ALL ? "=" : "",
308           bitmask ? '&' : '=', iplist);
309         }
310       yield = FAIL;
311       goto out;
312       }
313     }
314
315   /* No address list check; discard any illegal returns and give up if
316   none remain. */
317
318   else
319     {
320     BOOL ok = FALSE;
321     for (da = cb->rhs; da; da = da->next)
322       {
323       int address[4];
324
325       if (  host_aton(da->address, address) == 1                /* ipv4 */
326          && (address[0] & 0xff000000) == 0x7f000000     /* 127.0.0.0/8 */
327          )
328         ok = TRUE;
329       else
330         log_write(0, LOG_MAIN,
331             "DNS list lookup for %s at %s returned %s;"
332             " not in 127.0/8 and discarded",
333             keydomain, domain, da->address);
334       }
335     if (!ok)
336       {
337       yield = FAIL;
338       goto out;
339       }
340     }
341
342   /* Either there was no IP list, or the record matched, implying that the
343   domain is on the list. We now want to find a corresponding TXT record. If an
344   alternate domain is specified for the TXT record, call this function
345   recursively to look that up; this has the side effect of re-checking that
346   there is indeed an A record at the alternate domain. */
347
348   if (domain_txt != domain)
349     {
350     yield = one_check_dnsbl(domain_txt, domain_txt, keydomain, prepend, NULL,
351       FALSE, match_type, defer_return);
352     goto out;
353     }
354
355   /* If there is no alternate domain, look up a TXT record in the main domain
356   if it has not previously been cached. */
357
358   if (!cb->text_set)
359     {
360     cb->text_set = TRUE;
361     if (dns_basic_lookup(dnsa, query, T_TXT) == DNS_SUCCEED)
362       for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
363            rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
364         if (rr->type == T_TXT)
365           {
366           int len = (rr->data)[0];
367           if (len > 511) len = 127;
368           store_pool = POOL_PERM;
369           cb->text = string_copyn_taint(CUS (rr->data+1), len, GET_TAINTED);
370           store_pool = old_pool;
371           break;
372           }
373     }
374
375   dnslist_value = addlist;
376   dnslist_text = cb->text;
377   yield = OK;
378   goto out;
379   }
380
381 /* There was a problem with the DNS lookup */
382
383 if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA)
384   {
385   log_write(L_dnslist_defer, LOG_MAIN,
386     "DNS list lookup defer (probably timeout) for %s: %s", query,
387     defer_return == OK ?   US"assumed in list" :
388     defer_return == FAIL ? US"assumed not in list" :
389                             US"returned DEFER");
390   yield = defer_return;
391   goto out;
392   }
393
394 /* No entry was found in the DNS; continue for next domain */
395
396 HDEBUG(D_dnsbl)
397   {
398   debug_printf("DNS lookup for %s failed\n", query);
399   debug_printf("=> that means %s is not listed at %s\n",
400      keydomain, domain);
401   }
402
403 yield = FAIL;
404
405 out:
406
407 store_free_dns_answer(dnsa);
408 return yield;
409 }
410
411
412
413
414 /*************************************************
415 *        Check host against DNS black lists      *
416 *************************************************/
417
418 /* This function runs checks against a list of DNS black lists, until one
419 matches. Each item on the list can be of the form
420
421   domain=ip-address/key
422
423 The domain is the right-most domain that is used for the query, for example,
424 blackholes.mail-abuse.org. If the IP address is present, there is a match only
425 if the DNS lookup returns a matching IP address. Several addresses may be
426 given, comma-separated, for example: x.y.z=127.0.0.1,127.0.0.2.
427
428 If no key is given, what is looked up in the domain is the inverted IP address
429 of the current client host. If a key is given, it is used to construct the
430 domain for the lookup. For example:
431
432   dsn.rfc-ignorant.org/$sender_address_domain
433
434 After finding a match in the DNS, the domain is placed in $dnslist_domain, and
435 then we check for a TXT record for an error message, and if found, save its
436 value in $dnslist_text. We also cache everything in a tree, to optimize
437 multiple lookups.
438
439 The TXT record is normally looked up in the same domain as the A record, but
440 when many lists are combined in a single DNS domain, this will not be a very
441 specific message. It is possible to specify a different domain for looking up
442 TXT records; this is given before the main domain, comma-separated. For
443 example:
444
445   dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \
446              socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3
447
448 The caching ensures that only one lookup in dnsbl.sorbs.net is done.
449
450 Note: an address for testing RBL is 192.203.178.39
451 Note: an address for testing DUL is 192.203.178.4
452 Note: a domain for testing RFCI is example.tld.dsn.rfc-ignorant.org
453
454 Arguments:
455   where        the acl type
456   listptr      the domain/address/data list
457   log_msgptr   log message on error
458
459 Returns:    OK      successful lookup (i.e. the address is on the list), or
460                       lookup deferred after +include_unknown
461             FAIL    name not found, or no data found for the given type, or
462                       lookup deferred after +exclude_unknown (default)
463             DEFER   lookup failure, if +defer_unknown was set
464 */
465
466 int
467 verify_check_dnsbl(int where, const uschar ** listptr, uschar ** log_msgptr)
468 {
469 int sep = 0;
470 int defer_return = FAIL;
471 const uschar *list = *listptr;
472 uschar *domain;
473 uschar revadd[128];        /* Long enough for IPv6 address */
474
475 /* Indicate that the inverted IP address is not yet set up */
476
477 revadd[0] = 0;
478
479 /* In case this is the first time the DNS resolver is being used. */
480
481 dns_init(FALSE, FALSE, FALSE);  /*XXX dnssec? */
482
483 /* Loop through all the domains supplied, until something matches */
484
485 while ((domain = string_nextinlist(&list, &sep, NULL, 0)))
486   {
487   int rc;
488   BOOL bitmask = FALSE;
489   int match_type = 0;
490   uschar *domain_txt;
491   uschar *comma;
492   uschar *iplist;
493   uschar *key;
494
495   HDEBUG(D_dnsbl) debug_printf("dnslists check: %s\n", domain);
496
497   /* Deal with special values that change the behaviour on defer */
498
499   if (domain[0] == '+')
500     {
501     if      (strcmpic(domain, US"+include_unknown") == 0) defer_return = OK;
502     else if (strcmpic(domain, US"+exclude_unknown") == 0) defer_return = FAIL;
503     else if (strcmpic(domain, US"+defer_unknown") == 0)   defer_return = DEFER;
504     else
505       log_write(0, LOG_MAIN|LOG_PANIC, "unknown item in dnslist (ignored): %s",
506         domain);
507     continue;
508     }
509
510   /* See if there's explicit data to be looked up */
511
512   if ((key = Ustrchr(domain, '/'))) *key++ = 0;
513
514   /* See if there's a list of addresses supplied after the domain name. This is
515   introduced by an = or a & character; if preceded by = we require all matches
516   and if preceded by ! we invert the result. */
517
518   if (!(iplist = Ustrchr(domain, '=')))
519     {
520     bitmask = TRUE;
521     iplist = Ustrchr(domain, '&');
522     }
523
524   if (iplist)                                  /* Found either = or & */
525     {
526     if (iplist > domain && iplist[-1] == '!')  /* Handle preceding ! */
527       {
528       match_type |= MT_NOT;
529       iplist[-1] = 0;
530       }
531
532     *iplist++ = 0;                             /* Terminate domain, move on */
533
534     /* If we found = (bitmask == FALSE), check for == or =& */
535
536     if (!bitmask && (*iplist == '=' || *iplist == '&'))
537       {
538       bitmask = *iplist++ == '&';
539       match_type |= MT_ALL;
540       }
541     }
542
543
544   /* If there is a comma in the domain, it indicates that a second domain for
545   looking up TXT records is provided, before the main domain. Otherwise we must
546   set domain_txt == domain. */
547
548   domain_txt = domain;
549   if ((comma = Ustrchr(domain, ',')))
550     {
551     *comma++ = 0;
552     domain = comma;
553     }
554
555   /* Check that what we have left is a sensible domain name. There is no reason
556   why these domains should in fact use the same syntax as hosts and email
557   domains, but in practice they seem to. However, there is little point in
558   actually causing an error here, because that would no doubt hold up incoming
559   mail. Instead, I'll just log it. */
560
561   for (uschar * s = domain; *s; s++)
562     if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
563       {
564       log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
565         "strange characters - is this right?", domain);
566       break;
567       }
568
569   /* Check the alternate domain if present */
570
571   if (domain_txt != domain) for (uschar * s = domain_txt; *s; s++)
572     if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
573       {
574       log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
575         "strange characters - is this right?", domain_txt);
576       break;
577       }
578
579   /* If there is no key string, construct the query by adding the domain name
580   onto the inverted host address, and perform a single DNS lookup. */
581
582   if (!key)
583     {
584     if (where == ACL_WHERE_NOTSMTP_START || where == ACL_WHERE_NOTSMTP)
585       {
586       *log_msgptr = string_sprintf
587         ("cannot test auto-keyed dnslists condition in %s ACL",
588           acl_wherenames[where]);
589       return ERROR;
590       }
591     if (!sender_host_address) return FAIL;    /* can never match */
592     if (revadd[0] == 0) invert_address(revadd, sender_host_address);
593     rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd,
594       iplist, bitmask, match_type, defer_return);
595     if (rc == OK)
596       {
597       dnslist_domain = string_copy(domain_txt);
598       dnslist_matched = string_copy(sender_host_address);
599       HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n",
600         sender_host_address, dnslist_domain);
601       }
602     if (rc != FAIL) return rc;     /* OK or DEFER */
603     }
604
605   /* If there is a key string, it can be a list of domains or IP addresses to
606   be concatenated with the main domain. */
607
608   else
609     {
610     int keysep = 0;
611     BOOL defer = FALSE;
612     uschar *keydomain;
613     uschar keyrevadd[128];
614
615     while ((keydomain = string_nextinlist(CUSS &key, &keysep, NULL, 0)))
616       {
617       uschar *prepend = keydomain;
618
619       if (string_is_ip_address(keydomain, NULL) != 0)
620         {
621         invert_address(keyrevadd, keydomain);
622         prepend = keyrevadd;
623         }
624
625       rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist,
626         bitmask, match_type, defer_return);
627       if (rc == OK)
628         {
629         dnslist_domain = string_copy(domain_txt);
630         dnslist_matched = string_copy(keydomain);
631         HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n",
632           keydomain, dnslist_domain);
633         return OK;
634         }
635
636       /* If the lookup deferred, remember this fact. We keep trying the rest
637       of the list to see if we get a useful result, and if we don't, we return
638       DEFER at the end. */
639
640       if (rc == DEFER) defer = TRUE;
641       }    /* continue with next keystring domain/address */
642
643     if (defer) return DEFER;
644     }
645   }        /* continue with next dnsdb outer domain */
646
647 return FAIL;
648 }
649
650 /* vi: aw ai sw=2
651 */
652 /* End of dnsbl.c.c */