X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/059ec3d9952740285fb1ebf47961b8aca2eb1b4a..4362ff0d4b2a9feee34a595feee087d47faeb447:/src/src/dns.c diff --git a/src/src/dns.c b/src/src/dns.c index 237b734a6..a6c6d053b 100644 --- a/src/src/dns.c +++ b/src/src/dns.c @@ -1,10 +1,10 @@ -/* $Cambridge: exim/src/src/dns.c,v 1.1 2004/10/07 10:39:01 ph10 Exp $ */ +/* $Cambridge: exim/src/src/dns.c,v 1.16 2006/11/20 13:53:44 ph10 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* Copyright (c) University of Cambridge 1995 - 2006 */ /* See the file NOTICE for conditions of use and distribution. */ /* Functions for interfacing with the DNS. */ @@ -23,6 +23,133 @@ 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). Otherwise, it calls an +external utility that mocks-up a nameserver, if it can find the utility. +If not, it passes its arguments on to res_search(). The fake nameserver may +also return a code specifying that the name should be passed on. + +Background: the original test suite required a real nameserver to carry the +test zones, whereas the new test suit has the fake server for portability. This +code supports both. + +Arguments: + domain 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 *domain, int type, uschar *answerptr, int size) +{ +int len = Ustrlen(domain); +int asize = size; /* Locally modified */ +uschar *endname; +uschar name[256]; +uschar utilname[256]; +uschar *aptr = answerptr; /* Locally modified */ +struct stat statbuf; + +/* Remove terminating dot. */ + +if (domain[len - 1] == '.') len--; +Ustrncpy(name, domain, len); +name[len] = 0; +endname = name + len; + +/* This code, for forcing TRY_AGAIN and NO_RECOVERY, is here so that it works +for the old test suite that uses a real nameserver. When the old test suite is +eventually abandoned, this code could be moved into the fakens utility. */ + +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; + } + +/* Look for the fakens utility, and if it exists, call it. */ + +(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 (asize > 0 && (rc = read(outfd, aptr, asize)) > 0) + { + len += rc; + aptr += rc; /* Don't modify the actual arguments, because they */ + asize -= rc; /* may need to be passed on to res_search(). */ + } + + 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; return -1; + case 2: h_errno = TRY_AGAIN; return -1; + default: + case 3: h_errno = NO_RECOVERY; return -1; + case 4: h_errno = NO_DATA; return -1; + case 5: /* Pass on to res_search() */ + DEBUG(D_dns) debug_printf("fakens returned PASS_ON\n"); + } + } + +/* fakens utility not found, or it returned "pass on" */ + +DEBUG(D_dns) debug_printf("passing %s on to res_search()\n", domain); + +return res_search(CS domain, C_IN, type, answerptr, size); +} + + /************************************************* * Initialize and configure resolver * @@ -153,9 +280,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 +319,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 +336,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 +376,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 @@ -258,14 +388,17 @@ dns_text_type(int t) { switch(t) { - case T_A: return US"A"; - case T_MX: return US"MX"; - case T_AAAA: return US"AAAA"; - case T_A6: return US"A6"; - case T_TXT: return US"TXT"; - case T_PTR: return US"PTR"; - case T_SRV: return US"SRV"; - default: return US"?"; + case T_A: return US"A"; + case T_MX: return US"MX"; + case T_AAAA: return US"AAAA"; + 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"; + default: return US"?"; } } @@ -320,6 +453,7 @@ Arguments: Returns: DNS_SUCCEED successful lookup DNS_NOMATCH name not found (NXDOMAIN) or name contains illegal characters (if checking) + or name is an IP address (for IP address lookup) DNS_NODATA domain exists, but no data for this type (NODATA) DNS_AGAIN soft failure, try again later DNS_FAIL DNS failure @@ -328,8 +462,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 @@ -355,35 +489,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 @@ -432,10 +537,31 @@ 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. + +On some systems, res_search() will recognize "A-for-A" queries and return +the IP address instead of returning -1 with h_error=HOST_NOT_FOUND. Some +nameservers are also believed to do this. It is, of course, contrary to the +specification of the DNS, so we lock it out. */ + +if (( + #ifdef SUPPORT_A6 + type == T_A6 || + #endif + type == T_A || type == T_AAAA) && + string_is_ip_address(name, NULL) != 0) + return DNS_NOMATCH; + +/* 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) @@ -565,12 +691,10 @@ for (i = 0; i < 10; i++) else if (rr->type == T_CNAME) cname_rr = *rr; } - /* If a CNAME was found, take the fully qualified name from it; otherwise - from the first data record, if present. For testing, there is a magic name - that gets its casing adjusted, because my resolver doesn't seem to pass back - upper case letters in domain names. */ + /* For the first time round this loop, if a CNAME was found, take the fully + qualified name from it; otherwise from the first data record, if present. */ - if (fully_qualified_name != NULL) + if (i == 0 && fully_qualified_name != NULL) { if (cname_rr.data != NULL) { @@ -580,15 +704,9 @@ for (i = 0; i < 10; i++) } else if (type_rr.data != NULL) { - if (running_in_test_harness && - Ustrcmp(type_rr.name, "uppercase.test.ex") == 0) - *fully_qualified_name = US"UpperCase.test.ex"; - else - { - if (Ustrcmp(type_rr.name, *fully_qualified_name) != 0 && - type_rr.name[0] != '*') - *fully_qualified_name = string_copy_dnsdomain(type_rr.name); - } + if (Ustrcmp(type_rr.name, *fully_qualified_name) != 0 && + type_rr.name[0] != '*') + *fully_qualified_name = string_copy_dnsdomain(type_rr.name); } } @@ -606,6 +724,8 @@ for (i = 0; i < 10; i++) cname_rr.data, (DN_EXPAND_ARG4_TYPE)data, 256); if (datalen < 0) return DNS_FAIL; name = data; + + DEBUG(D_dns) debug_printf("CNAME found: change to %s\n", name); } /* Loop back to do another lookup */ /*Control reaches here after 10 times round the CNAME loop. Something isn't @@ -617,6 +737,211 @@ 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. +Positive type values are passed straight on to dns_lookup(). + +Arguments: + dnsa pointer to dns_answer structure + name domain name to look up + type DNS record type (T_A, T_MX, etc or a "special") + fully_qualified_name if not NULL, return the returned name here if its + contents are different (i.e. it must be preset) + +Returns: DNS_SUCCEED successful lookup + DNS_NOMATCH name not found + DNS_NODATA no data found + DNS_AGAIN soft failure, try again later + DNS_FAIL DNS failure +*/ + +int +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); + +/* The "mx hosts only" type doesn't require any special action here */ + +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 +root servers. */ + +if (type == T_ZNS) + { + uschar *d = name; + while (d != 0) + { + 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; + } + 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 */ + +return DNS_FAIL; +} + + + /* Support for A6 records has been commented out since they were demoted to experimental status at IETF 51. */