Added a backwards-compatible interface to a fake DNS resolver for use by
[exim.git] / src / src / dns.c
index 9e66396..83daf50 100644 (file)
@@ -1,4 +1,4 @@
-/* $Cambridge: exim/src/src/dns.c,v 1.4 2005/01/04 10:00:42 ph10 Exp $ */
+/* $Cambridge: exim/src/src/dns.c,v 1.10 2005/09/13 15:40:07 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
@@ -23,6 +23,120 @@ static void dns_complete_a6(dns_address ***, dns_answer *, dns_record *,
 #endif
 
 
+/*************************************************
+*               Fake DNS resolver                *
+*************************************************/
+
+/* This function is called instead of res_search() when Exim is running in its
+test harness. It recognizes some special domain names, and uses them to force
+failure and retry responses (optionally with a delay). It also recognises the
+zones test.ex and 10.in-addr.arpa, and for those it calls an external utility
+that mock-up a nameserver, if it can find the utility. Otherwise, it passes its
+arguments on to res_search().
+
+Background: the original test suite required a real nameserver to carry the
+test.ex and 10.in-addr.arpa zones, whereas the new test suit has the fake
+server for portability. This code supports both.
+
+Arguments:
+  name        the domain name
+  type        the DNS record type
+  answerptr   where to put the answer
+  size        size of the answer area
+
+Returns:      length of returned data, or -1 on error (h_errno set)
+*/
+
+static int
+fakens_search(uschar *name, int type, uschar *answerptr, int size)
+{
+int len = Ustrlen(name);
+uschar *endname = name + len;
+
+if (len >= 14 && Ustrcmp(endname - 14, "test.again.dns") == 0)
+  {
+  int delay = Uatoi(name);  /* digits at the start of the name */
+  DEBUG(D_dns) debug_printf("Return from DNS lookup of %s (%s) faked for testing\n",
+    name, dns_text_type(type));
+  if (delay > 0)
+    {
+    DEBUG(D_dns) debug_printf("delaying %d seconds\n", delay);
+    sleep(delay);
+    }
+  h_errno = TRY_AGAIN;
+  return -1;
+  }
+
+if (len >= 13 && Ustrcmp(endname - 13, "test.fail.dns") == 0)
+  {
+  DEBUG(D_dns) debug_printf("Return from DNS lookup of %s (%s) faked for testing\n",
+    name, dns_text_type(type));
+  h_errno = NO_RECOVERY;
+  return -1;
+  }
+
+if (Ustrcmp(name, "test.ex") == 0 ||
+    (len > 8 && Ustrcmp(endname - 8, ".test.ex") == 0) ||
+    (len >= 16 && Ustrcmp(endname - 16, ".10.in-addr.arpa") == 0))
+  {
+  uschar utilname[256];
+  struct stat statbuf;
+
+  (void)string_format(utilname, sizeof(utilname), "%s/../bin/fakens",
+    spool_directory);
+
+  if (stat(CS utilname, &statbuf) >= 0)
+    {
+    pid_t pid;
+    int infd, outfd, rc;
+    uschar *argv[5];
+
+    DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) using fakens\n",
+      name, dns_text_type(type));
+
+    argv[0] = utilname;
+    argv[1] = spool_directory;
+    argv[2] = name;
+    argv[3] = dns_text_type(type);
+    argv[4] = NULL;
+
+    pid = child_open(argv, NULL, 0000, &infd, &outfd, FALSE);
+    if (pid < 0)
+      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to run fakens: %s",
+        strerror(errno));
+
+    len = 0;
+    rc = -1;
+    while (size > 0 && (rc = read(outfd, answerptr, size)) > 0)
+      {
+      len += rc;
+      answerptr += rc;
+      size -= rc;
+      }
+
+    if (rc < 0)
+      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "read from fakens failed: %s",
+        strerror(errno));
+
+    switch(child_close(pid, 0))
+      {
+      case 0: return len;
+      case 1: h_errno = HOST_NOT_FOUND; break;
+      case 2: h_errno = TRY_AGAIN; break;
+      default:
+      case 3: h_errno = NO_RECOVERY; break;
+      case 4: h_errno = NO_DATA; break;
+      }
+    return -1;
+    }
+  }
+
+/* Not test.ex or 10.in-addr.arpa, or fakens utility not found. */
+
+return res_search(CS name, C_IN, type, answerptr, size);
+}
+
+
 
 /*************************************************
 *        Initialize and configure resolver       *
@@ -153,9 +267,9 @@ else
 *************************************************/
 
 /* Call this with reset == RESET_ANSWERS to scan the answer block, reset ==
-RESET_ADDITIONAL to scan the additional records, and reset == RESET_NEXT to
-get the next record. The result is in static storage which must be copied if
-it is to be preserved.
+RESET_AUTHORITY to scan the authority records, reset == RESET_ADDITIONAL to
+scan the additional records, and reset == RESET_NEXT to get the next record.
+The result is in static storage which must be copied if it is to be preserved.
 
 Arguments:
   dnsa      pointer to dns answer block
@@ -192,12 +306,14 @@ if (reset != RESET_NEXT)
 
   dnss->rrcount = ntohs(h->ancount);
 
-  /* Skip over answers and NS records if wanting to look at the additional
+  /* Skip over answers if we want to look at the authority section. Also skip
+  the NS records (i.e. authority section) if wanting to look at the additional
   records. */
 
-  if (reset == RESET_ADDITIONAL)
+  if (reset == RESET_ADDITIONAL) dnss->rrcount += ntohs(h->nscount);
+
+  if (reset == RESET_AUTHORITY || reset == RESET_ADDITIONAL)
     {
-    dnss->rrcount += ntohs(h->nscount);
     while (dnss->rrcount-- > 0)
       {
       namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
@@ -207,11 +323,11 @@ if (reset != RESET_NEXT)
       GETSHORT(dnss->srr.size, dnss->aptr); /* size of data portion */
       dnss->aptr += dnss->srr.size;         /* skip over it */
       }
-    dnss->rrcount = ntohs(h->arcount);
+    dnss->rrcount = (reset == RESET_AUTHORITY)
+      ? ntohs(h->nscount) : ntohs(h->arcount);
     }
   }
 
-
 /* The variable dnss->aptr is now pointing at the next RR, and dnss->rrcount
 contains the number of RR records left. */
 
@@ -247,7 +363,8 @@ return &(dnss->srr);
 *            Turn DNS type into text             *
 *************************************************/
 
-/* Turn the coded record type into a string for printing.
+/* Turn the coded record type into a string for printing. All those that Exim
+uses should be included here.
 
 Argument:   record type
 Returns:    pointer to string
@@ -264,9 +381,10 @@ switch(t)
   case T_A6:    return US"A6";
   case T_TXT:   return US"TXT";
   case T_PTR:   return US"PTR";
+  case T_SOA:   return US"SOA";
   case T_SRV:   return US"SRV";
   case T_NS:    return US"NS";
-  case T_CNAME: return US"CNAME";  
+  case T_CNAME: return US"CNAME";
   default:      return US"?";
   }
 }
@@ -330,8 +448,8 @@ Returns:    DNS_SUCCEED   successful lookup
 int
 dns_basic_lookup(dns_answer *dnsa, uschar *name, int type)
 {
+int rc = -1;
 #ifndef STAND_ALONE
-int rc;
 uschar *save;
 #endif
 
@@ -357,35 +475,6 @@ if (previous != NULL)
   return previous->data.val;
   }
 
-/* If we are running in the test harness, recognize a couple of special
-names that always give error returns. This makes it straightforward to
-test the handling of DNS errors. */
-
-if (running_in_test_harness)
-  {
-  uschar *endname = name + Ustrlen(name);
-  if (Ustrcmp(endname - 14, "test.again.dns") == 0)
-    {
-    int delay = Uatoi(name);  /* digits at the start of the name */
-    DEBUG(D_dns) debug_printf("Real DNS lookup of %s (%s) bypassed for testing\n",
-      name, dns_text_type(type));
-    if (delay > 0)
-      {
-      DEBUG(D_dns) debug_printf("delaying %d seconds\n", delay);
-      sleep(delay);
-      }
-    DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n");
-    return dns_return(name, type, DNS_AGAIN);
-    }
-  if (Ustrcmp(endname - 13, "test.fail.dns") == 0)
-    {
-    DEBUG(D_dns) debug_printf("Real DNS lookup of %s (%s) bypassed for testing\n",
-      name, dns_text_type(type));
-    DEBUG(D_dns) debug_printf("returning DNS_FAIL\n");
-    return dns_return(name, type, DNS_FAIL);
-    }
-  }
-
 /* If configured, check the hygene of the name passed to lookup. Otherwise,
 although DNS lookups may give REFUSED at the lower level, some resolvers
 turn this into TRY_AGAIN, which is silly. Give a NOMATCH return, since such
@@ -434,10 +523,18 @@ if (check_dns_names_pattern[0] != 0 && type != T_PTR)
 #endif /* STAND_ALONE */
 
 /* Call the resolver; for an overlong response, res_search() will return the
-number of bytes the message would need, so we need to check for this case.
-The effect is to truncate overlong data. */
+number of bytes the message would need, so we need to check for this case. The
+effect is to truncate overlong data.
+
+If we are running in the test harness, instead of calling the normal resolver
+(res_search), we call fakens_search(), which recognizes certain special
+domains, and interfaces to a fake nameserver for certain special zones. */
+
+if (running_in_test_harness)
+  dnsa->answerlen = fakens_search(name, type, dnsa->answer, MAXPACKET);
+else
+  dnsa->answerlen = res_search(CS name, C_IN, type, dnsa->answer, MAXPACKET);
 
-dnsa->answerlen = res_search(CS name, C_IN, type, dnsa->answer, MAXPACKET);
 if (dnsa->answerlen > MAXPACKET) dnsa->answerlen = MAXPACKET;
 
 if (dnsa->answerlen < 0) switch (h_errno)
@@ -626,8 +723,8 @@ return DNS_FAIL;
 *    Do a DNS lookup and handle virtual types   *
 ************************************************/
 
-/* This function handles some invented "lookup types" that synthesize feature 
-not available in the basic types. The special types all have negative values. 
+/* This function handles some invented "lookup types" that synthesize feature
+not available in the basic types. The special types all have negative values.
 Positive type values are passed straight on to dns_lookup().
 
 Arguments:
@@ -645,7 +742,7 @@ Returns:                DNS_SUCCEED   successful lookup
 */
 
 int
-dns_special_lookup(dns_answer *dnsa, uschar *name, int type, 
+dns_special_lookup(dns_answer *dnsa, uschar *name, int type,
   uschar **fully_qualified_name)
 {
 if (type >= 0) return dns_lookup(dnsa, name, type, fully_qualified_name);
@@ -654,7 +751,7 @@ if (type >= 0) return dns_lookup(dnsa, name, type, fully_qualified_name);
 
 if (type == T_MXH) return dns_lookup(dnsa, name, T_MX, fully_qualified_name);
 
-/* Find nameservers for the domain or the nearest enclosing zone, excluding the 
+/* Find nameservers for the domain or the nearest enclosing zone, excluding the
 root servers. */
 
 if (type == T_ZNS)
@@ -665,10 +762,157 @@ if (type == T_ZNS)
     int rc = dns_lookup(dnsa, d, T_NS, fully_qualified_name);
     if (rc != DNS_NOMATCH && rc != DNS_NODATA) return rc;
     while (*d != 0 && *d != '.') d++;
-    if (*d++ == 0) break; 
+    if (*d++ == 0) break;
     }
-  return DNS_NOMATCH;     
-  } 
+  return DNS_NOMATCH;
+  }
+
+/* Try to look up the Client SMTP Authorization SRV record for the name. If
+there isn't one, search from the top downwards for a CSA record in a parent
+domain, which might be making assertions about subdomains. If we find a record
+we set fully_qualified_name to whichever lookup succeeded, so that the caller
+can tell whether to look at the explicit authorization field or the subdomain
+assertion field. */
+
+if (type == T_CSA)
+  {
+  uschar *srvname, *namesuff, *tld, *p;
+  int priority, weight, port;
+  int limit, rc, i;
+  BOOL ipv6;
+  dns_record *rr;
+  dns_scan dnss;
+
+  DEBUG(D_dns) debug_printf("CSA lookup of %s\n", name);
+
+  srvname = string_sprintf("_client._smtp.%s", name);
+  rc = dns_lookup(dnsa, srvname, T_SRV, NULL);
+  if (rc == DNS_SUCCEED || rc == DNS_AGAIN)
+    {
+    if (rc == DNS_SUCCEED) *fully_qualified_name = name;
+    return rc;
+    }
+
+  /* Search for CSA subdomain assertion SRV records from the top downwards,
+  starting with the 2nd level domain. This order maximizes cache-friendliness.
+  We skip the top level domains to avoid loading their nameservers and because
+  we know they'll never have CSA SRV records. */
+
+  namesuff = Ustrrchr(name, '.');
+  if (namesuff == NULL) return DNS_NOMATCH;
+  tld = namesuff + 1;
+  ipv6 = FALSE;
+  limit = dns_csa_search_limit;
+
+  /* Use more appropriate search parameters if we are in the reverse DNS. */
+
+  if (strcmpic(namesuff, US".arpa") == 0)
+    {
+    if (namesuff - 8 > name && strcmpic(namesuff - 8, US".in-addr.arpa") == 0)
+      {
+      namesuff -= 8;
+      tld = namesuff + 1;
+      limit = 3;
+      }
+    else if (namesuff - 4 > name && strcmpic(namesuff - 4, US".ip6.arpa") == 0)
+      {
+      namesuff -= 4;
+      tld = namesuff + 1;
+      ipv6 = TRUE;
+      limit = 3;
+      }
+    }
+
+  DEBUG(D_dns) debug_printf("CSA TLD %s\n", tld);
+
+  /* Do not perform the search if the top level or 2nd level domains do not
+  exist. This is quite common, and when it occurs all the search queries would
+  go to the root or TLD name servers, which is not friendly. So we check the
+  AUTHORITY section; if it contains the root's SOA record or the TLD's SOA then
+  the TLD or the 2LD (respectively) doesn't exist and we can skip the search.
+  If the TLD and the 2LD exist but the explicit CSA record lookup failed, then
+  the AUTHORITY SOA will be the 2LD's or a subdomain thereof. */
+
+  if (rc == DNS_NOMATCH)
+    {
+    /* This is really gross. The successful return value from res_search() is
+    the packet length, which is stored in dnsa->answerlen. If we get a
+    negative DNS reply then res_search() returns -1, which causes the bounds
+    checks for name decompression to fail when it is treated as a packet
+    length, which in turn causes the authority search to fail. The correct
+    packet length has been lost inside libresolv, so we have to guess a
+    replacement value. (The only way to fix this properly would be to
+    re-implement res_search() and res_query() so that they don't muddle their
+    success and packet length return values.) For added safety we only reset
+    the packet length if the packet header looks plausible. */
+
+    HEADER *h = (HEADER *)dnsa->answer;
+    if (h->qr == 1 && h->opcode == QUERY && h->tc == 0
+        && (h->rcode == NOERROR || h->rcode == NXDOMAIN)
+        && ntohs(h->qdcount) == 1 && ntohs(h->ancount) == 0
+        && ntohs(h->nscount) >= 1)
+      dnsa->answerlen = MAXPACKET;
+
+    for (rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY);
+         rr != NULL;
+         rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+      if (rr->type != T_SOA) continue;
+      else if (strcmpic(rr->name, US"") == 0 ||
+               strcmpic(rr->name, tld) == 0) return DNS_NOMATCH;
+      else break;
+    }
+
+  for (i = 0; i < limit; i++)
+    {
+    if (ipv6)
+      {
+      /* Scan through the IPv6 reverse DNS in chunks of 16 bits worth of IP
+      address, i.e. 4 hex chars and 4 dots, i.e. 8 chars. */
+      namesuff -= 8;
+      if (namesuff <= name) return DNS_NOMATCH;
+      }
+    else
+      /* Find the start of the preceding domain name label. */
+      do
+        if (--namesuff <= name) return DNS_NOMATCH;
+      while (*namesuff != '.');
+
+    DEBUG(D_dns) debug_printf("CSA parent search at %s\n", namesuff + 1);
+
+    srvname = string_sprintf("_client._smtp.%s", namesuff + 1);
+    rc = dns_lookup(dnsa, srvname, T_SRV, NULL);
+    if (rc == DNS_AGAIN) return rc;
+    if (rc != DNS_SUCCEED) continue;
+
+    /* Check that the SRV record we have found is worth returning. We don't
+    just return the first one we find, because some lower level SRV record
+    might make stricter assertions than its parent domain. */
+
+    for (rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
+         rr != NULL;
+         rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+      {
+      if (rr->type != T_SRV) continue;
+
+      /* Extract the numerical SRV fields (p is incremented) */
+      p = rr->data;
+      GETSHORT(priority, p);
+      GETSHORT(weight, p);
+      GETSHORT(port, p);
+
+      /* Check the CSA version number */
+      if (priority != 1) continue;
+
+      /* If it's making an interesting assertion, return this response. */
+      if (port & 1)
+        {
+        *fully_qualified_name = namesuff + 1;
+        return DNS_SUCCEED;
+        }
+      }
+    }
+  return DNS_NOMATCH;
+  }
 
 /* Control should never reach here */