/* This file is part of mailfromd. Copyright (C) 2005, 2006, 2007 Sergey Poznyakoff This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #define MF_SOURCE_NAME MF_SOURCE_DNSBASE #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include "mailfromd.h" struct mx_buffer { unsigned pref; char *name; }; static int comp_pref(const void *a, const void *b) { const struct mx_buffer *ma = a, *mb = b; if (ma->pref > mb->pref) return 1; else if (ma->pref < mb->pref) return -1; return strcmp(ma->name, mb->name); } /* Obtain MX records for domain name HOST. Return them in mxbuf, sorted by preference in ascending order. Notice that we *have* to use old BIND-4-style method, since glibc folks in their incredible wiseness have not exported new interface functions from libresolve.so. The only way to access them would be to link with -lresolve statically. Let's hope that someday this stupidity will change. */ static dns_status _getmx(const char *host, char *answer, size_t answer_size, mxbuf_t mxbuf, size_t *pcount, unsigned long *pttl) { int i, n, nmx; struct mx_buffer mx_buffer[MAXMXCOUNT]; HEADER *hp; unsigned char *eom, *cp; unsigned short qdcount, ancount; struct __res_state stat; debug1(80,"Getting MX records for %s", host); memset((void *)&stat, 0, sizeof(struct __res_state)); res_ninit(&stat); n = res_nquery (&stat, host, C_IN, T_MX, answer, answer_size); res_nclose(&stat); if (n < 0) { debug2(10, "res_nquery failed (errno=%s, h_errno=%d)", mu_strerror(errno), h_errno); switch (h_errno) { case NO_DATA: case NO_RECOVERY: case HOST_NOT_FOUND: return dns_not_found; case TRY_AGAIN: case -1: return dns_temp_failure; default: mu_error(_("res_nquery(%s) failed with unexpected h_errno %d"), host, h_errno); return dns_failure; } } if (n > answer_size) n = answer_size; hp = (HEADER*)answer; cp = (unsigned char *) answer + HFIXEDSZ; eom = (unsigned char *) answer + n; /* Skip query part */ for (qdcount = ntohs((unsigned short)hp->qdcount); qdcount--; cp += n + QFIXEDSZ) { if ((n = dn_skipname(cp, eom)) < 0) return dns_failure; } ancount = ntohs((unsigned short)hp->ancount); /* Collect MX records */ for (i = nmx = 0; i < ancount; i++) { unsigned short pref, type; char tname[NS_MAXDNAME]; unsigned long ttl; if ((n = dn_expand((unsigned char *)answer, eom, cp, tname, sizeof tname)) < 0) break; cp += n; GETSHORT(type, cp); cp += INT16SZ; GETLONG(ttl, cp); if (pttl) UPDATE_TTL(*pttl, ttl); GETSHORT(n, cp); if (type != T_MX) { debug2(90,"unexpected answer type %d, size %d\n", type, n); cp += n; continue; } GETSHORT(pref, cp); if ((n = dn_expand((u_char *)answer, eom, cp, tname, sizeof tname)) < 0) break; cp += n; mx_buffer[nmx].pref = pref; mx_buffer[nmx].name = strdup(tname); debug2(20,"MX %u %s", mx_buffer[nmx].pref, mx_buffer[nmx].name); if (++nmx >= MAXMXCOUNT) break; } /* Sort according to preference value */ qsort(mx_buffer, nmx, sizeof mx_buffer[0], comp_pref); /* Prepare return value */ memset(mxbuf, 0, sizeof(mxbuf_t)); for (i = 0; i < nmx; i++) mxbuf[i] = mx_buffer[i].name; *pcount = nmx; return dns_success; } int dns_str_is_ipv4(const char *addr) { int dot_count; int digit_count; dot_count = 0; digit_count = 0; while (*addr != 0 && *addr != ' ') { if (*addr == '.') { if (++dot_count > 3) break; digit_count = 0; } else if (!(isdigit(*addr) && ++digit_count <= 3)) { return 0; } addr++; } return dot_count == 3; } MUTEX_DCL(dns_mutex) /* Return MX records for the given HOST. If no records were found, recurse to its parent domains until any record is found or recursion depth reaches MAXDEPTH */ dns_status dns_get_mx_records(const char *host, int maxdepth, mxbuf_t mxbuf, size_t *mxcount, unsigned long *ttl) { char *hbuf = NULL; dns_status status = dns_failure; MUTEX_LOCK(dns_mutex); if (dns_str_is_ipv4(host)) { status = resolve_ipstr(host, &hbuf); if (status != dns_success) host = NULL; } if (host) { unsigned char *answer = malloc(MAXPACKET); if (!answer) status = dns_failure; else { const char *p; int depth; for (p = host, depth = 0; p && depth < maxdepth; p++, depth++) { if (ttl) *ttl = ~(unsigned long)0; status = _getmx(p, answer, MAXPACKET, mxbuf, mxcount, ttl); if (status == dns_success || status == dns_temp_failure) break; p = strchr(p, '.'); if (!p) break; } free(answer); } } MUTEX_UNLOCK(dns_mutex); free(hbuf); return status; } void dns_freemx(mxbuf_t mxbuf) { int i; for (i = 0; i < MAXMXCOUNT && mxbuf[i]; i++) free(mxbuf[i]); } #define LOOKUP_FAILURE -1 #define LOOKUP_SUCCESS 0 #define LOOKUP_CNAME 1 struct loop_data { int qtype; /* Type of the query */ char *name; /* Key to look up */ size_t name_size; /* Length of the key */ char *domain; /* Domain name */ size_t domain_size; /* Length of the domain name */ char *answer; /* Answer buffer */ size_t answer_size; /* Size of answer buffer */ /* Return data: */ char *hbuf; /* Return buffer */ size_t hbsize; /* Size of return buffer */ size_t hbcount; /* Number of items returned */ time_t ttl; /* TTL value */ dns_status status; /* Status */ int atype; /* On input: desired answer type or T_ANY On output: Answer type */ /* Internal data */ size_t loopcnt; /* Number of CNAME loops allowed */ }; #define NSIZE MAX(MAXPACKET, MAXDNAME*2+2) #define SET_STATUS(lp,s) if ((lp)->status != dns_success) (lp)->status = s typedef struct { GACOPYZ_UINT32_T x; char a; } align; static int domain_name_cmp(const char *ptr, const char *name, const char *domain) { int c; while (*name && *ptr) if (c = *ptr++ - *name++) return c; if (*domain) { if (*ptr != '.') return *ptr - *domain; ptr++; while (*domain && *ptr) if (c = *ptr++ - *domain++) return c; if (*domain == '.' && domain[1] == 0) domain++; if (*ptr == '.' && ptr[1] == 0) ptr++; if (*domain || *ptr) return 1; } return 0; } static int cname_loop_body(struct loop_data *lp) { int i, n, rc; HEADER *hp; unsigned char *eom, *cp; unsigned short qdcount, ancount; char *p; size_t len; struct __res_state statb; memset((void *)&statb, 0, sizeof(struct __res_state)); res_ninit(&statb); n = res_nquerydomain (&statb, lp->name, lp->domain, C_IN, lp->qtype, lp->answer, lp->answer_size); res_nclose(&statb); if (n < 0) { debug2(10, "res_nquerydomain failed (errno=%s, h_errno=%d)", mu_strerror(errno), h_errno); switch (h_errno) { case NO_DATA: case NO_RECOVERY: case HOST_NOT_FOUND: SET_STATUS(lp, dns_not_found); return LOOKUP_FAILURE; case TRY_AGAIN: case -1: SET_STATUS(lp, dns_temp_failure); return LOOKUP_FAILURE; default: mu_error(_("res_nquerydomain(%s) failed with unexpected h_errno %d"), lp->name, h_errno); SET_STATUS(lp, dns_failure); return LOOKUP_FAILURE; } } if (n > lp->answer_size) n = lp->answer_size; hp = (HEADER*) lp->answer; cp = (unsigned char *) lp->answer + HFIXEDSZ; eom = (unsigned char *) lp->answer + n; /* Skip query part */ for (qdcount = ntohs((unsigned short)hp->qdcount); qdcount--; cp += n + QFIXEDSZ) { if ((n = dn_skipname(cp, eom)) < 0) { SET_STATUS(lp, dns_failure); return LOOKUP_FAILURE; } } SET_STATUS(lp, dns_not_found); for (ancount = ntohs((unsigned short) hp->ancount), i = 0; i < ancount && cp < eom; cp += n, i++) { unsigned short type; unsigned long ttl = ~(unsigned long)0; char nbuf[NSIZE]; char *bp = nbuf; size_t blen = sizeof nbuf; size_t l; n = dn_expand((unsigned char *) lp->answer, eom, cp, nbuf, sizeof nbuf); if (n < 0) break; cp += n; GETSHORT(type, cp); cp += INT16SZ; /* skip over class */ GETLONG(ttl, cp); UPDATE_TTL(lp->ttl, ttl); GETSHORT(n, cp); /* rdlength */ switch (type) { case T_A: if (lp->atype != T_ANY && lp->atype != type) continue; if (domain_name_cmp(bp, lp->name, lp->domain) != 0) continue; /* Skip host name */ l = strlen(bp) + 1; bp += l; blen -= l; blen -= sizeof(align) - ((u_long)bp % sizeof(align)); bp += sizeof(align) - ((u_long)bp % sizeof(align)); if (bp + n >= nbuf + blen) { debug1(1, "size (%d) too big", n); } else { if (n != sizeof(GACOPYZ_UINT32_T)) { debug1(1, "unsupported address size: %d", n); } else if (lp->hbcount * sizeof(GACOPYZ_UINT32_T) >= lp->hbsize) debug(1, "Too many addresses"); else { memmove((GACOPYZ_UINT32_T *) lp->hbuf + lp->hbcount, cp, n); lp->hbcount++; } } bp += n; blen -= n; lp->atype = T_A; SET_STATUS(lp, dns_success); break; case T_PTR: if (lp->atype != T_ANY && lp->atype != type) continue; if ((rc = dn_expand((unsigned char *)lp->answer, eom, cp, nbuf, sizeof(nbuf))) < 0) { SET_STATUS(lp, dns_failure); return LOOKUP_FAILURE; } l = strlen(nbuf); if (lp->hbcount + l >= lp->hbsize) break; memcpy(lp->hbuf + lp->hbcount, nbuf, l); lp->hbcount += l; lp->hbuf[lp->hbcount++] = 0; lp->atype = T_PTR; SET_STATUS(lp, dns_success); break; case T_TXT: if (lp->atype != type) continue; l = cp[0]; if (lp->hbcount + l >= lp->hbsize) break; memcpy(lp->hbuf + lp->hbcount, cp + 1, l); lp->hbcount += l; lp->hbuf[lp->hbcount++] = 0; lp->atype = T_TXT; SET_STATUS(lp, dns_success); break; case T_CNAME: if (--lp->loopcnt == 0) { mu_error(_("DNS failure: CNAME loop for %s"), lp->name); SET_STATUS(lp, dns_failure); return LOOKUP_FAILURE; } if ((rc = dn_expand((unsigned char *)lp->answer, eom, cp, nbuf, sizeof(nbuf))) < 0) { SET_STATUS(lp, dns_failure); return LOOKUP_FAILURE; } /* RFC 1034 section 3.6 specifies that CNAME should point at the canonical name -- but urges software to try again anyway. */ p = strchr (nbuf, '.'); if (!p) return LOOKUP_SUCCESS; len = p - nbuf; if (len + 1 >= lp->name_size) return LOOKUP_FAILURE; memcpy(lp->name, nbuf, len); lp->name[len] = 0; len = strlen (p + 1); if (len + 1 >= lp->domain_size) return LOOKUP_FAILURE; strcpy(lp->domain, p + 1); return LOOKUP_CNAME; } } return lp->status == dns_success ? LOOKUP_SUCCESS : LOOKUP_FAILURE; } static void cnameloop(struct loop_data *lp) { MUTEX_LOCK(dns_mutex); if (!lp->answer) { static unsigned char *answer; if (!answer) answer = xmalloc(MAXPACKET); lp->answer = answer; lp->answer_size = MAXPACKET; } while (cname_loop_body(lp) == LOOKUP_CNAME) ; MUTEX_UNLOCK(dns_mutex); } typedef char IPBUF[3*4+3+1]; int dns_reverse_ipstr(const char *ipstr, char *revipstr) { int i; const char *p; char *q; q = revipstr + strlen(ipstr); *q = 0; for (i = 0, p = ipstr; *p && i < 4; i++) { int len; for (len = 0; p[len] && p[len] != '.'; len++) ; q -= len; memcpy(q, p, len); if (q > revipstr) *--q = '.'; p += len; if (*p == '.') p++; } return *p || i != 4; } dns_status dns_resolve_ipstr(const char *ipstr, const char *domain, char *answer, size_t answer_size, char *hbuf, size_t hbsize, unsigned long *ttl) { char namebuf[NSIZE]; char domainbuf[NSIZE]; struct loop_data ld; ld.qtype = ld.atype = T_ANY; if (!domain) { if (dns_reverse_ipstr(ipstr, namebuf)) return dns_failure; ld.name = namebuf; ld.name_size = sizeof namebuf; } else { strncpy(namebuf, ipstr, sizeof(namebuf)-1); namebuf[sizeof(namebuf)-1] = 0; ld.name = namebuf; ld.name_size = strlen(ld.name); } ld.domain = domainbuf; strcpy(domainbuf, domain ? domain : "in-addr.arpa"); ld.domain_size = sizeof domainbuf; ld.answer = answer; ld.answer_size = answer_size; ld.hbuf = hbuf; ld.hbsize = hbsize; ld.hbcount = 0; ld.ttl = ~(unsigned long)0; ld.status = dns_failure; ld.loopcnt = MAXCNAMEDEPTH; cnameloop(&ld); if (ld.status == dns_success && ld.atype == T_A) { struct in_addr s; char *p; s.s_addr = *(GACOPYZ_UINT32_T*)hbuf; p = inet_ntoa(s); strncpy(hbuf, p, hbsize); } *ttl = ld.ttl; return ld.status; } dns_status dns_resolve_hostname(const char *host, char *answer, size_t answer_size, char *ipbuf, size_t ipbsize, unsigned long *ttl) { struct loop_data ld; char namebuf[NSIZE]; char domainbuf[NSIZE]; domainbuf[0] = 0; ld.qtype = ld.atype = T_A; strncpy(namebuf, host, sizeof namebuf - 1); ld.name = namebuf; ld.name_size = sizeof namebuf; ld.domain = domainbuf; ld.domain_size = sizeof domainbuf; ld.answer = answer; ld.answer_size = answer_size; ld.hbuf = ipbuf; ld.hbsize = ipbsize; ld.hbcount = 0; ld.ttl = ~(unsigned long)0; ld.status = dns_failure; ld.loopcnt = MAXCNAMEDEPTH; cnameloop(&ld); if (ld.status == dns_success && ld.atype == T_A) { struct in_addr s; char *p; s.s_addr = *(GACOPYZ_UINT32_T*)ipbuf; p = inet_ntoa(s); strncpy(ipbuf, p, ipbsize); } *ttl = ld.ttl; return ld.status; } dns_status a_lookup(const char *host, GACOPYZ_UINT32_T *ipbuf, size_t ipbsize, size_t *ipcount, unsigned long *ttl, char *answer, size_t answer_size) { struct loop_data ld; char namebuf[NSIZE]; char domainbuf[NSIZE]; domainbuf[0] = 0; ld.qtype = ld.atype = T_A; strncpy(namebuf, host, sizeof namebuf - 1); ld.name = namebuf; ld.name_size = sizeof namebuf; ld.domain = domainbuf; ld.domain_size = sizeof domainbuf; ld.answer = answer; ld.answer_size = answer_size; ld.hbuf = (char*) ipbuf; ld.hbsize = ipbsize * sizeof ipbuf[0]; ld.hbcount = 0; ld.ttl = ~(unsigned long)0; ld.status = dns_failure; ld.loopcnt = MAXCNAMEDEPTH; cnameloop(&ld); if (ld.status == dns_success) { if (ipcount) *ipcount = ld.hbcount; if (ttl) *ttl = ld.ttl; } return ld.status; } static void textbuf_to_argv(const char *hbuf, size_t hsize, char **argv, size_t argc) { size_t i; const char *p; for (i = 0, p = hbuf; i < argc - 1 && p < hbuf + hsize; p += strlen(p) + 1) argv[i++] = xstrdup(p); argv[i] = NULL; } dns_status ptr_lookup(struct in_addr ip, char **names, size_t maxnames, unsigned long *ttl, char *answer, size_t answer_size) { char namebuf[NSIZE]; char domainbuf[NSIZE]; char hbuf[NSIZE]; struct loop_data ld; char *p; ip.s_addr = ntohl(ip.s_addr); p = inet_ntoa(ip); strncpy(namebuf, p, sizeof namebuf); ld.qtype = ld.atype = T_PTR; ld.name = namebuf; ld.name_size = sizeof namebuf; ld.domain = domainbuf; strcpy(domainbuf, "in-addr.arpa"); ld.domain_size = sizeof domainbuf; ld.answer = answer; ld.answer_size = answer_size; ld.hbuf = hbuf; ld.hbsize = sizeof hbuf; ld.hbcount = 0; ld.ttl = ~(unsigned long)0; ld.status = dns_failure; ld.loopcnt = MAXCNAMEDEPTH; cnameloop(&ld); if (ld.status == dns_success) { textbuf_to_argv(ld.hbuf, ld.hbcount, names, maxnames); if (ttl) *ttl = ld.ttl; } return ld.status; } dns_status txt_lookup(const char *name, char **names, size_t maxnames, unsigned long *ttl, char *answer, size_t answer_size) { char domainbuf[NSIZE]; char hbuf[NSIZE]; struct loop_data ld; ld.qtype = T_TXT; ld.atype = T_TXT; ld.name = xstrdup(name); ld.name_size = strlen(ld.name); domainbuf[0] = 0; ld.domain = domainbuf; ld.domain_size = sizeof domainbuf; ld.answer = answer; ld.answer_size = answer_size; ld.hbuf = hbuf; ld.hbsize = sizeof hbuf; ld.hbcount = 0; ld.ttl = ~(unsigned long)0; ld.status = dns_failure; ld.loopcnt = MAXCNAMEDEPTH; cnameloop(&ld); free(ld.name); if (ld.status == dns_success) { textbuf_to_argv(ld.hbuf, ld.hbcount, names, maxnames); if (ttl) *ttl = ld.ttl; } return ld.status; } /* FIXME: This is a placeholder for a function that should look up for any SPF or TXT records for DOMAIN. If any SPF are found, TXT should be discarded. For the time being, it handles only TXT */ dns_status spf_lookup(const char *domain, char **txt, size_t maxtxt, unsigned long *ttl, char *answer, size_t answer_size) { return txt_lookup(domain, txt, maxtxt, ttl, answer, answer_size); } /* rfc4408, chapter 5.5 */ dns_status ptr_validate(const char *ipstr, char ***vnptr, size_t *vcount, unsigned long *pttl) { struct in_addr ip; char *names[11], **p; char *vnames[10]; size_t vi = 0; int status; unsigned long minttl = ~(unsigned long)0; unsigned long ttl; if (!inet_aton(ipstr, &ip)) return dns_failure; status = ptr_lookup(ip, names, NELEMS(names), &ttl, NULL, 0); if (status != dns_success) return status; UPDATE_TTL(minttl, ttl); for (p = names; *p; p++) { GACOPYZ_UINT32_T ipbuf[10]; size_t ipcount; status = a_lookup(*p, ipbuf, NELEMS(ipbuf), &ipcount, &ttl, NULL, 0); if (status == dns_success && vi < NELEMS(vnames[0])) { size_t i; UPDATE_TTL(minttl, ttl); for (i = 0; i < ipcount; i++) if (ipbuf[i] == ip.s_addr) { vnames[vi++] = *p; if (vnptr) *p = NULL; break; } } free(*p); } if (vi > 0) { if (pttl) *pttl = minttl; if (vnptr) { size_t i; *vnptr = xcalloc(vi+1, sizeof vnptr[0]); for (i = 0; i < vi; i++) (*vnptr)[i] = vnames[i]; (*vnptr)[i] = NULL; *vcount = vi; } } return vi > 0 ? dns_success : dns_not_found; }