/*
* 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;
}
syntax highlighted by Code2HTML, v. 0.9.1