/* * Copyright (c) 2002-2005 Sendmail, Inc. and its suppliers. * All rights reserved. * * By using this file, you agree to the terms and conditions set * forth in the LICENSE file which can be found at the top level of * the sendmail distribution. */ #include "sm/generic.h" SM_RCSID("@(#)$Id: dnsdecode.c,v 1.43 2007/11/05 05:47:53 ca Exp $") #include "sm/ctype.h" #include "sm/cstr.h" #include "sm/dns.h" #include "sm/dns-int.h" #include "nameok.h" #if MTA_LIBDNS_DEBUG #include "sm/io.h" #endif /* ** DNS reply structure; used for communication between the various ** decoding functions. */ typedef struct dns_reply_S dns_reply_T, *dns_reply_P; struct dns_reply_S { sm_str_P dnsrpl_answer; /* answer data (reference only) */ ushort dnsrpl_qry_cnt; /* # of queries */ ushort dnsrpl_ans_cnt; /* # of answers */ uint16_t dnsrpl_id; /* id of reply */ HEADER *dnsrpl_header; uchar *dnsrpl_cur; /* current place in answer */ uchar *dnsrpl_end; /* first char past reply */ }; /* ** DNS_RES_ADD -- add DNS result (may remove CNAME) ** ** Parameters: ** dns_res -- DNS result header (this points to a list of entries ** to which the current DNS result is added) ** rrname -- name of query ** rrlen -- len of rrname ** host -- name of host ** len -- len of host ** ttl -- TTL ** pref -- preference ** type -- DNS query type ** ipv4 -- IPv4 address ** ** Returns: ** usual sm_error code */ static sm_ret_T dns_rr_add(dns_res_P dns_res, uchar *rrname, uint rrlen, uchar *host, uint len, uint ttl, ushort pref, dns_type_T type, ipv4_T ipv4) { sm_ret_T ret; uint i; bool replaced; dns_rese_P dns_rese, dns_reseh; if (dns_res->dnsres_entries >= dns_res->dnsres_maxentries) return sm_error_temp(SM_EM_DNS, SM_E_FULL); /* ** Look for CNAMEs that can be eliminated: ** Q T_CNAME R0 ** R1 T_x A ** If R0 == R1: get rid of T_CNAME entry and keep just A ** Note: this means dnsrese_name does NOT match the original query! ** Should this be "merged"? ** ** What about larger chains? ** Q T_CNAME R0 ** R1 T_CNAME R2 ** R3 T_x A ** R0 == R1 && R2 == R3? */ replaced = false; dns_reseh = NULL; if (dns_res->dnsres_entries > 0 && T_CNAME == type && host != NULL) { /* ** Case 1: Maybe add CNAME. ** Does R exist as dnsrese_val? ** Check whether some other entry has the name for this */ for (dns_rese = DRESL_FIRST(dns_res), i = 0; dns_rese != DRESL_END(dns_res) && i < dns_res->dnsres_entries; dns_rese = DRESL_NEXT(dns_rese), i++) { if (dns_rese->dnsrese_type == dns_res->dnsres_qtype && #if 0 /* T_A? not yet... */ T_A == dns_rese->dnsrese_type && dns_rese->dnsrese_val.dnsresu_a == ipv4 #endif /* 0 */ strcasecmp((const char *) host, (const char *) sm_cstr_data(dns_rese->dnsrese_name)) == 0) { MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_rr_add: CNAME=%s, found=%C\n", host, dns_rese->dnsrese_name)); /* found duplicate */ return SM_SUCCESS; } } } if (dns_res->dnsres_entries > 0) { /* ** Case 2: Add new RR. ** Check whether there is a CNAME entry for this. ** If yes: remove it and add only this. ** Which entry should be removed?? ** See note above; it would be best to "collapse" ** Q T_CNAME R ** R T_x A ** to ** Q T_x A */ for (dns_rese = DRESL_FIRST(dns_res), i = 0; dns_rese != DRESL_END(dns_res) && i < dns_res->dnsres_entries; dns_rese = DRESL_NEXT(dns_rese), i++) { if (T_CNAME == dns_rese->dnsrese_type && strcasecmp((const char *) rrname, (const char *) sm_cstr_data(dns_rese->dnsrese_val.dnsresu_name) ) == 0) { /* ** Found CNAME: remove it. ** Optimization: reuse this entry instead of ** calling free and new. ** Note: the subsequent new may fail and ** then we "lost" a valid entry; we could ** mark this entry for deletion and do it ** after the new succeeded. */ MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_rr_add: host=%s, ipv4=%A, CNAME=%C\n" , host, (ipv4_T) htonl(ipv4) , dns_rese->dnsrese_name)); DRESL_REMOVE(dns_res, dns_rese); dns_res->dnsres_entries--; dns_reseh = dns_rese; replaced = true; break; } } } #if 0 /* ** Check whether this RR name matches the original query. ** Note: it doesn't need to match if it's an expanded CNAME. ** Need to pass in: query and qlen? ** dns_res->dnsres_query is NOT valid here! ** Question: is this really necessary? A rogue DNS server ** can give us any data it likes, so why care? */ if (!replaced && (rrlen != qlen || !sm_memcaseeq(query, rrname, rrlen))) { sm_io_fprintf(smioerr, "dns_rr_add: query=%C, got=%C, BOGUS=IGNORED\n", dns_res->dnsres_query, dns_rese->dnsrese_name); } #endif /* 0 */ ret = dns_rese_new(rrname, rrlen, type, ttl, pref, 0, host, len, ipv4, &dns_rese); if (sm_is_err(ret)) goto error; if (dns_reseh != NULL && replaced) { /* ** Replace the name of the query R1 with Q. ** Q T_CNAME R0 ** R1 T_x A ** -> Q T_CNAME A ** If Q == A: loop! */ SM_CSTR_FREE(dns_rese->dnsrese_name); dns_rese->dnsrese_name = SM_CSTR_DUP(dns_reseh->dnsrese_name); dns_rese_free(dns_reseh); dns_reseh = NULL; if (dns_rese->dnsrese_type == T_CNAME && SM_CSTR_CASEQ(dns_rese->dnsrese_name, dns_rese->dnsrese_val.dnsresu_name)) { MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_rr_add: T_CNAME=%C, query=%C, error=points_to_itself\n" , dns_rese->dnsrese_name , dns_rese->dnsrese_val.dnsresu_name)); /* just ignore this? */ /* break; */ /* treat it as temporary error? */ dns_rese_free(dns_rese); ret = DNSR_TEMP; goto error; } MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_rr_add: eliminated one CNAME entry, query=%C, ipv4=%A, host=%C\n" , dns_rese->dnsrese_name , type == T_A ? (ipv4_T) htonl(dns_rese->dnsrese_val.dnsresu_a) : 0 , T_A == type ? NULL : dns_rese->dnsrese_val.dnsresu_name )); } DRESL_INSERT_TAIL(dns_res, dns_rese); dns_res->dnsres_entries++; return SM_SUCCESS; error: if (dns_reseh != NULL && replaced) { dns_rese_free(dns_reseh); dns_reseh = NULL; } return ret; } #define DNS_RESE_IS_WORSE(dns_rese, pref, weight) \ ((dns_rese)->dnsrese_pref > pref || \ ((dns_rese)->dnsrese_pref == pref && (dns_rese)->dnsrese_weight > weight)) /* ** DNS_MXRR_INSERT -- insert DNS result for MX RR at the right place. ** ** Parameters: ** dns_res -- DNS result header ** mxhost -- name of host ** len -- len of mxhost ** ttl -- TTL ** pref -- preference ** type -- DNS query type ** ** Returns: ** usual sm_error code */ static sm_ret_T dns_mxrr_insert(dns_res_P dns_res, uchar *mxhost, uint len, uint ttl, ushort pref, dns_type_T type) { ushort weight; sm_ret_T ret; dns_rese_P dns_rese, dns_rese_next; weight = mxrand(mxhost); /* ** Duplicate? Is this sanctioned by the RFCs? */ for (dns_rese = DRESL_FIRST(dns_res); dns_rese != DRESL_END(dns_res); dns_rese = dns_rese_next) { dns_rese_next = DRESL_NEXT(dns_rese); if (strcasecmp((const char *) mxhost, (const char *) sm_cstr_data(dns_rese->dnsrese_val.dnsresu_name)) == 0) { if (DNS_RESE_IS_WORSE(dns_rese, pref, weight)) { DRESL_REMOVE(dns_res, dns_rese); dns_rese_free(dns_rese); dns_res->dnsres_entries--; break; } else /* Existing MX record is "better" */ return SM_SUCCESS; } } /* Insert the record */ for (dns_rese = DRESL_FIRST(dns_res); dns_rese != DRESL_END(dns_res); dns_rese = dns_rese_next) { dns_rese_next = DRESL_NEXT(dns_rese); if (DNS_RESE_IS_WORSE(dns_rese, pref, weight)) { /* Enough space for one more? */ if (dns_res->dnsres_entries < dns_res->dnsres_maxentries) dns_res->dnsres_entries++; else { /* remove last element */ dns_rese_next = DRESL_END(dns_rese); DRESL_REMOVE(dns_res, dns_rese_next); dns_rese_free(dns_rese_next); } ret = dns_rese_new(NULL, 0, type, ttl, pref, weight, mxhost, len, (ipv4_T) 0, &dns_rese_next); if (sm_is_err(ret)) { /* More cleanup?? */ dns_res->dnsres_entries--; return ret; } DRESL_INSERT_BEFORE(dns_res, dns_rese, dns_rese_next); return SM_SUCCESS; } } /* Not reached if record has been inserted */ if (dns_res->dnsres_entries >= dns_res->dnsres_maxentries) return sm_error_temp(SM_EM_DNS, SM_E_FULL); ret = dns_rese_new(NULL, 0, type, ttl, pref, weight, mxhost, len, (ipv4_T) 0, &dns_rese); if (sm_is_err(ret)) { /* Cleanup?? */ return ret; } DRESL_INSERT_TAIL(dns_res, dns_rese); dns_res->dnsres_entries++; return SM_SUCCESS; } /* ** DNS_RD_HDR -- extract DNS header ** ** Parameters: ** ans -- DNS answer ** dns_reply -- DNS reply structure (provided by caller) ** ** Returns: ** usual sm_error code ** ** Side Effects: ** dns_reply is filled. */ static sm_ret_T dns_rd_hdr(sm_str_P ans, dns_reply_P dns_reply) { HEADER *hp; SM_REQUIRE(dns_reply != NULL); dns_reply->dnsrpl_cur = (uchar *) sm_str_data(ans) + HFIXEDSZ; dns_reply->dnsrpl_end = (uchar *) sm_str_data(ans) + sm_str_getlen(ans); if (dns_reply->dnsrpl_cur > dns_reply->dnsrpl_end) return sm_error_perm(SM_EM_DNS, EINVAL); hp = (HEADER *) sm_str_data(ans); dns_reply->dnsrpl_header = hp; dns_reply->dnsrpl_ans_cnt = ntohs((ushort) hp->ancount); dns_reply->dnsrpl_qry_cnt = ntohs((ushort) hp->qdcount); dns_reply->dnsrpl_id = hp->id; /* Watch out... this is NOT a copy! */ dns_reply->dnsrpl_answer = ans; return SM_SUCCESS; } /* ** DNS_RD_QUERY -- extract DNS query from DNS reply ** ** Parameters: ** dns_reply -- DNS reply structure (provided by caller) ** query -- query (output!, can be NULL) ** qlen -- length of query ** ptype -- (pointer to) DNS result type (output) ** ** Returns: ** usual sm_error code ** ** Side Effects: ** dns_reply is filled. */ static sm_ret_T dns_rd_query(dns_reply_P dns_reply, uchar *query, int qlen, dns_type_T *ptype) { int n; ushort qry_cnt; dns_type_T type; sm_str_P ans; SM_REQUIRE(dns_reply != NULL); ans = dns_reply->dnsrpl_answer; SM_REQUIRE(ans != NULL); /* SM_IS_STR()? sm/str-int.h ? */ type = 0; for (qry_cnt = dns_reply->dnsrpl_qry_cnt; qry_cnt > 0; qry_cnt--) { if (dns_reply->dnsrpl_cur >= dns_reply->dnsrpl_end) goto error; /* copy it first */ if (query != NULL) { n = dn_expand(sm_str_data(ans), dns_reply->dnsrpl_end, dns_reply->dnsrpl_cur, (RES_UNC_T) query, qlen); } n = dn_skipname(dns_reply->dnsrpl_cur, dns_reply->dnsrpl_end); if (n < 0) goto error; dns_reply->dnsrpl_cur += n; if (dns_reply->dnsrpl_cur >= dns_reply->dnsrpl_end) goto error; /* extract the type */ GETSHORT(type, dns_reply->dnsrpl_cur); /* skip the class (it's always C_IN) */ dns_reply->dnsrpl_cur += INT16SZ; } if (ptype != NULL) *ptype = type; return SM_SUCCESS; error: return sm_error_perm(SM_EM_DNS, EINVAL); } /* ** DNS_DECODE -- decode DNS resource records ** ** Parameters: ** ans -- answer from DNS server ** query -- query (output!, can be NULL) ** qlen -- length of query ** ptype -- (pointer to) DNS result type (output) ** dns_res -- DNS result struct (output) ** ** Returns: ** >=0: the number of resource records found. ** <0 if there is an internal failure. */ /* Check whether current pointer exceeds valid range */ #define DNS_CUR_CHK do { \ if (dns_reply.dnsrpl_cur >= dns_reply.dnsrpl_end) \ goto error; \ } while (0) /* Advance current pointer and check */ #define DNS_CUR_ADV_CHK(n) do { \ dns_reply.dnsrpl_cur += (n); \ DNS_CUR_CHK; \ } while (0) /* Advance current pointer, no check */ #define DNS_CUR_ADV(n) dns_reply.dnsrpl_cur += (n) /* Careful! This uses "continue", so it can't be in do{}while */ #define DNS_GET_NAME(bufp, buf, len, nullen) \ (len) = strlen((const char *) (bufp)); \ if ((len) >= sizeof(buf) - 2) /* paranoia */ \ goto error; \ \ /* Can this happen? */ \ if (0 == (len)) { \ nullen; \ } \ if ((bufp)[(len) - 1] != '.') { \ (bufp)[(len)] = '.'; \ (len)++; \ } \ (bufp)[(len)] = '\0' sm_ret_T dns_decode(sm_str_P ans, uchar *query, int qlen, dns_type_T *ptype, dns_res_P dns_res) { int n, ancount, buflen; uint l, r, ttl; ushort pref; dns_type_T type; ipv4_T ipv4; sm_ret_T ret; uchar *bp; uchar hostbuf[MAXPACKET], namebuf[MAXPACKET]; dns_reply_T dns_reply; /* SM_REQUIRE(sizeof(hostbuf) == sizeof(namebuf)); */ ttl = 0; /* extract header */ ret = dns_rd_hdr(ans, &dns_reply); if (sm_is_err(ret)) goto error; /* extract query */ ret = dns_rd_query(&dns_reply, query, qlen, ptype); if (sm_is_err(ret)) goto error; /* original query */ dns_res->dnsres_qtype = (ptype != NULL) ? *ptype : 0; dns_res->dnsres_id = dns_reply.dnsrpl_id; /* ** Extract return code (we need query first to associate it with a ** request) */ ret = SM_SUCCESS; switch (dns_reply.dnsrpl_header->rcode) { case NOERROR: break; /* treat all of these as permanent errors */ case REFUSED: case NOTIMP: case FORMERR: ret = DNSR_PERM; break; case NXDOMAIN: /* host doesn't exist in DNS; might be in /etc/hosts */ ret = DNSR_NOTFOUND; break; case SERVFAIL: ret = DNSR_TEMP; break; default: /* complain about unknown error code? */ ret = DNSR_TEMP; break; } if (sm_is_err(ret)) goto error; /* ** Extract data, store it in an appropriate place. */ buflen = sizeof(hostbuf) - 1; SM_ASSERT(buflen > 0); ancount = dns_reply.dnsrpl_ans_cnt; type = 0; /* See RFC 1035 for layout of RRs. */ while (--ancount >= 0 && dns_reply.dnsrpl_cur < dns_reply.dnsrpl_end && dns_res->dnsres_entries < dns_res->dnsres_maxentries) { DNS_CUR_CHK; bp = namebuf; n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end, dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen); if (n < 0) goto error; DNS_GET_NAME(bp, namebuf, r, continue); DNS_CUR_ADV_CHK(n); GETSHORT(type, dns_reply.dnsrpl_cur); /* skip over class */ DNS_CUR_ADV_CHK(INT16SZ); GETLONG(ttl, dns_reply.dnsrpl_cur); DNS_CUR_CHK; GETSHORT(n, dns_reply.dnsrpl_cur); /* rdlength */ DNS_CUR_CHK; bp = hostbuf; switch (type) { case T_MX: GETSHORT(pref, dns_reply.dnsrpl_cur); DNS_CUR_CHK; n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end, dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen); if (n < 0) goto error; DNS_CUR_ADV(n); /* MX 0 . results in len = 0 */ DNS_GET_NAME(bp, hostbuf, l, ret = DNSR_MXINVALID; goto error;); MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_MX=%s\n", bp)); /* can we make these flags configurable? */ if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS /*|DNS_DOT */)) { ret = DNSR_MXINVALID; goto error; } ret = dns_mxrr_insert(dns_res, bp, l, ttl, pref, type); if (sm_is_err(ret)) goto error; break; case T_A: sm_memcpy(&ipv4, dns_reply.dnsrpl_cur, INT32SZ); MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_A=%X\n", ipv4)); DNS_CUR_ADV(INT32SZ); ret = dns_rr_add(dns_res, namebuf, r, (uchar *) NULL, 0, ttl, (ushort) 0, type, ipv4); if (sm_is_err(ret)) goto error; break; case T_PTR: /* Check for correctness! */ n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end, dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen); if (n < 0) goto error; DNS_CUR_ADV(n); DNS_GET_NAME(bp, hostbuf, l, continue); MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_PTR=%s\n", bp)); #if 0 if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS)) { ret = DNSR_PTRINVALID; goto error; } #endif /* 0 */ ret = dns_rr_add(dns_res, namebuf, r, bp, l, ttl, (ushort) 0, type, 0); if (sm_is_err(ret)) goto error; break; case T_CNAME: /* Check for correctness! */ n = dn_expand(sm_str_data(ans), dns_reply.dnsrpl_end, dns_reply.dnsrpl_cur, (RES_UNC_T) bp, buflen); if (n < 0) goto error; DNS_CUR_ADV(n); DNS_GET_NAME(bp, hostbuf, l, continue); MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: name=%s, T_CNAME=%s\n" , namebuf, bp)); #if 0 /* problem: 176.233.31.209.in-addr.arpa. 2H IN CNAME 176.160/27.233.31.209.in-addr.arpa. 176.160/27.233.31.209.in-addr.arpa. 3h14m45s IN PTR knecht.sendmail.org. moreover, caller does not deal with this error (yet) */ if (!validdnsdomain(bp, DNS__OK|DNS_HYPHENS)) { ret = DNSR_CNINVALID; goto error; } #endif /* 0 */ if (r == l && sm_memcaseeq(namebuf, bp, r)) { MTA_LIBDNS_DBG_DPRINTF((smioerr, "dns_decode: T_CNAME=%s, error=points_to_itself\n", bp)); /* just ignore this? */ /* break; */ /* treat it as temporary error? */ ret = DNSR_TEMP; goto error; } ret = dns_rr_add(dns_res, namebuf, r, bp, l, ttl, (ushort) 0, type, 0); if (sm_is_err(ret)) goto error; break; default: DNS_CUR_ADV(n); break; } } if (dns_reply.dnsrpl_ans_cnt == 0) dns_res->dnsres_ret = DNSR_NOTFOUND; else dns_res->dnsres_ret = DNSR_OK; return dns_res->dnsres_entries; error: /* Remove results */ dns_resl_free(dns_res); dns_res->dnsres_ret = ret; return ret; }