/* This file is part of Mailfromd. Copyright (C) 2005-2020 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 . */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "libmf.h" #include "dns.h" #define DEFAULT_QFLAGS \ (adns_qf_quoteok_cname|adns_qf_cname_loose|adns_qf_quoteok_query) static mu_debug_handle_t debug_handle; static adns_state state; static void dns_log_cb(adns_state ads, void *logfndata, const char *fmt, va_list al) { /* FIXME: Could have used just: mu_diag_vprintf(MU_DIAG_DEBUG, fmt, al); but it will emit \e directives in the middle of the string, which upsets the mailutils' logstream implementation. A possible work over would be to use logfndata to select between mu_diag_vprintf,mu_diag_cont_vprintf or appropriate mu_debug_log_ call. For the time being, a simplified approach is used: */ mu_stream_vprintf(mu_strerr, fmt, al); } void dnsbase_init(void) { if (!debug_handle) debug_handle = mu_debug_register_category("dns"); } void dnsbase_real_init(char *configtext) { int rc; int flags; mu_debug_level_t lev; flags = adns_if_nosigpipe; if (mu_debug_get_category_level(debug_handle, &lev) == 0 && (lev & MU_DEBUG_LEVEL_MASK(MU_DEBUG_TRACE9))) flags |= adns_if_debug; rc = adns_init_logfn(&state, flags, configtext, dns_log_cb, NULL); if (rc) { mu_diag_funcall(MU_DIAG_ERROR, "adns_init", NULL, rc); exit(1); } } void dnsbase_file_init(char *filename) { if (!filename) dnsbase_real_init(NULL); else { mu_stream_t str; mu_off_t sz; int rc; char *cfg; rc = mu_file_stream_create(&str, filename, MU_STREAM_READ); if (rc) { mu_diag_funcall(MU_DIAG_ERROR, "mu_file_stream_create", filename, rc); return; } rc = mu_stream_size(str, &sz); if (rc) { mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_size", filename, rc); mu_stream_destroy(&str); return; } if (sz > ((size_t)~0)) { mu_error(_("%s too big"), filename); mu_stream_destroy(&str); return; } cfg = mu_alloc(sz + 1); rc = mu_stream_read(str, cfg, sz, NULL); mu_stream_destroy(&str); if (rc) { mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_read", filename, rc); return; } cfg[sz] = 0; dnsbase_real_init(cfg); free(cfg); } } static adns_state get_state(void) { if (!state) dnsbase_real_init(NULL); return state; } static inline size_t dns_reply_elsize(struct dns_reply *reply) { switch (reply->type) { case dns_reply_ip: return sizeof(reply->data.ip[0]); case dns_reply_str: return sizeof(reply->data.str[0]); } abort(); } void dns_reply_init(struct dns_reply *reply, dns_reply_type type, size_t count) { reply->type = type; reply->count = count; reply->maxcount = count; if (count) reply->data.ptr = mu_calloc(count, dns_reply_elsize(reply)); else reply->data.ptr = NULL; } void dns_reply_ip_push(struct dns_reply *reply, void *item) { if (reply->count == reply->maxcount) reply->data.ip = mu_2nrealloc(reply->data.ip, &reply->maxcount, sizeof(reply->data.ip[0])); reply->data.ip[reply->count++] = *(GACOPYZ_UINT32_T*)item; } void dns_reply_str_push(struct dns_reply *reply, void *item) { if (reply->count == reply->maxcount) reply->data.str = mu_2nrealloc(reply->data.ip, &reply->maxcount, sizeof(reply->data.str[0])); reply->data.str[reply->count++] = item; } void dns_reply_push(struct dns_reply *reply, void *item) { switch (reply->type) { case dns_reply_ip: dns_reply_ip_push(reply, item); break; case dns_reply_str: dns_reply_str_push(reply, item); break; default: abort(); } } void dns_reply_free(struct dns_reply *reply) { int i; switch (reply->type) { case dns_reply_str: for (i = 0; i < reply->count; i++) free(reply->data.str[i]); free(reply->data.str); break; case dns_reply_ip: free(reply->data.ip); break; } } int dns_str_is_ipv4(const char *addr) { int dot_count; int digit_count; dot_count = 0; digit_count = 0; while (*addr != 0) { if (*addr == '.') { if (++dot_count > 4) return 0; digit_count = 0; } else if (!(isdigit(*addr) && ++digit_count <= 3)) { return 0; } addr++; } return dot_count == 3; } static int errno_to_dns_status(int e) { switch (e) { case 0: return dns_success; case EAGAIN: #ifdef EINPROGRESS case EINPROGRESS: #endif #ifdef ETIMEDOUT case ETIMEDOUT: #endif return dns_temp_failure; default: return dns_failure; } } /* Table of correspondence between ADNS status codes and dns status. Values are increased by 1 to be able to tell whether the entry is initialized or not. */ int adns_to_dns_tab[] = { #define STAT(s) ((s)+1) [adns_s_ok] = STAT(dns_success), [adns_s_nomemory] = STAT(dns_failure), [adns_s_unknownrrtype] = STAT(dns_failure), [adns_s_systemfail] = STAT(dns_failure), /* remotely induced errors), detected locally */ [adns_s_timeout] = STAT(dns_temp_failure), [adns_s_allservfail] = STAT(dns_temp_failure), [adns_s_norecurse] = STAT(dns_temp_failure), [adns_s_invalidresponse] = STAT(dns_failure), [adns_s_unknownformat] = STAT(dns_failure), /* remotely induced errors), reported by remote server to us */ [adns_s_rcodeservfail] = STAT(dns_not_found), [adns_s_rcodeformaterror] = STAT(dns_not_found), [adns_s_rcodenotimplemented] = STAT(dns_not_found), [adns_s_rcoderefused] = STAT(dns_not_found), [adns_s_rcodeunknown] = STAT(dns_not_found), /* remote configuration errors */ [adns_s_inconsistent] = STAT(dns_not_found), [adns_s_prohibitedcname] = STAT(dns_not_found), [adns_s_answerdomaininvalid] = STAT(dns_not_found), [adns_s_answerdomaintoolong] = STAT(dns_not_found), [adns_s_invaliddata] = STAT(dns_not_found), /* permanent problems with the query */ [adns_s_querydomainwrong] = STAT(dns_failure), [adns_s_querydomaininvalid] = STAT(dns_failure), [adns_s_querydomaintoolong] = STAT(dns_failure), /* permanent errors */ [adns_s_nxdomain] = STAT(dns_not_found), [adns_s_nodata] = STAT(dns_not_found), #undef STAT }; /* Convert ADNS status code E to DNS status. */ static int adns_to_dns_status(int e) { int r; /* If it is negative, fail right away */ if (e < 0) return dns_failure; /* If it is not in table, it still can be a valid, but unhandled value */ if (e >= MU_ARRAY_SIZE(adns_to_dns_tab)) return e < adns_s_max_permfail ? dns_not_found : dns_failure; /* Now, look up in the table */ if ((r = adns_to_dns_tab[e]) > 0) return r - 1; /* If not found in table, use adns_s_max_ constants to decide the error class. */ if (e < adns_s_max_localfail) return dns_failure; if (e < adns_s_max_remotefail) return dns_not_found; if (e < adns_s_max_tempfail) return dns_temp_failure; if (e < adns_s_max_misconfig) return dns_not_found; if (e < adns_s_max_misquery) return dns_not_found; return dns_not_found; } dns_status soa_check(const char *name, int ip, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; rc = adns_synchronous(get_state(), name, adns_r_soa_raw, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) { if (ip) { status = a_lookup(ans->rrs.soa->mname, reply); } else { dns_reply_init(reply, dns_reply_str, 1); reply->data.str[0] = mu_strdup (ans->rrs.soa->mname); } free(ans); } return status; } static dns_status dns_reply_resolve(struct dns_reply *reply) { size_t i; struct dns_reply res; dns_reply_init(&res, dns_reply_ip, 0); for (i = 0; i < reply->count; i++) { struct dns_reply r; dns_status stat = a_lookup(reply->data.str[i], &r); if (stat == dns_success) { size_t n; for (n = 0; n < r.count; n++) { dns_reply_push(&res, &r.data.ip[n]); } dns_reply_free(&r); } } dns_reply_free(reply); *reply = res; if (res.count == 0) return dns_not_found; return dns_success; } /* Return MX records for the given HOST. */ dns_status mx_lookup(const char *host, int resolve, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; int i; rc = adns_synchronous(get_state(), host, adns_r_mx, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status != dns_success) return status; dns_reply_init(reply, dns_reply_str, ans->nrrs); for (i = 0; i < ans->nrrs; i++) reply->data.str[i] = mu_strdup(ans->rrs.inthostaddr[i].ha.host); free(ans); if (resolve) status = dns_reply_resolve(reply); return status; } 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 **hbuf) { dns_status status = dns_failure; int rc; adns_answer *ans; char *name; adns_rrtype type; if (!domain || strcasecmp(domain, "in-addr.arpa") == 0) { IPBUF ipbuf; if (!dns_str_is_ipv4(ipstr)) return dns_failure; if (dns_reverse_ipstr(ipstr, ipbuf)) return dns_failure; mu_asprintf(&name, "%s.in-addr.arpa", ipbuf); type = adns_r_ptr_raw; } else { mu_asprintf(&name, "%s.%s", ipstr, domain); type = adns_r_a; } rc = adns_synchronous(get_state(), name, type, DEFAULT_QFLAGS, &ans); free(name); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) { if (ans->type == adns_r_ptr_raw) { *hbuf = mu_strdup(ans->rrs.str[0]); } else { *hbuf = mu_strdup(inet_ntoa(ans->rrs.inaddr[0])); } } free(ans); return status; } dns_status dns_resolve_hostname(const char *host, char **ipbuf) { dns_status status = dns_failure; int rc; adns_answer *ans; rc = adns_synchronous(get_state(), host, adns_r_a, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) *ipbuf = mu_strdup(inet_ntoa(ans->rrs.inaddr[0])); free(ans); return status; } dns_status a_lookup(const char *host, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; rc = adns_synchronous(get_state(), host, adns_r_a, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) { int i; dns_reply_init(reply, dns_reply_ip, ans->nrrs); for (i = 0; i < ans->nrrs; i++) reply->data.ip[i] = ans->rrs.inaddr[i].s_addr; } free(ans); return status; } dns_status ptr_lookup(struct in_addr ip, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; char *name; ip.s_addr = ntohl(ip.s_addr); mu_asprintf(&name, "%s.in-addr.arpa", inet_ntoa(ip)); rc = adns_synchronous(get_state(), name, adns_r_ptr_raw, DEFAULT_QFLAGS, &ans); free(name); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) { int i; dns_reply_init(reply, dns_reply_str, ans->nrrs); for (i = 0; i < ans->nrrs; i++) reply->data.str[i] = mu_strdup(ans->rrs.str[i]); } free(ans); return status; } dns_status txt_lookup(const char *name, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; rc = adns_synchronous(get_state(), name, adns_r_txt, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status == dns_success) { int i; dns_reply_init(reply, dns_reply_str, ans->nrrs); for (i = 0; i < ans->nrrs; i++) { size_t l = 0; int j; for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++) l += ans->rrs.manyistr[i][j].i; reply->data.str[i] = mu_alloc(l + 1); reply->data.str[i][0] = 0; l = 0; for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++) { memcpy(reply->data.str[i] + l, ans->rrs.manyistr[i][j].str, ans->rrs.manyistr[i][j].i); l += ans->rrs.manyistr[i][j].i; } reply->data.str[i][l] = 0; } } free(ans); return status; } #define VSPF1_STR "v=spf1" #define VSPF1_LEN (sizeof(VSPF1_STR)-1) dns_status spf_lookup(const char *domain, char **rec) { dns_status status; struct dns_reply reply; status = txt_lookup(domain, &reply); if (status == dns_success) { int i; status = dns_not_found; for (i = 0; i < reply.count; i++) { if (mu_c_strncasecmp(reply.data.str[i], VSPF1_STR, VSPF1_LEN) == 0 && (reply.data.str[i][VSPF1_LEN] == 0 || mu_isspace(reply.data.str[i][VSPF1_LEN]))) { *rec = mu_strdup(reply.data.str[i]); status = dns_success; break; } } dns_reply_free(&reply); } return status; } dns_status dkim_lookup(const char *domain, const char *sel, char ***retval) { dns_status status; struct dns_reply reply; char *dk; if (mu_asprintf(&dk, "%s._domainkey.%s", sel, domain)) mu_alloc_die(); status = txt_lookup(dk, &reply); free(dk); if (status == dns_success) { int i; char **rv = mu_calloc(reply.count + 1, sizeof(*rv)); for (i = 0; i < reply.count; i++) { rv[i] = mu_strdup(reply.data.str[i]); } *retval = rv; dns_reply_free(&reply); } return status; } /* rfc4408, chapter 5.5 */ dns_status ptr_validate(const char *ipstr, struct dns_reply *reply) { struct in_addr ip; size_t i; dns_status status; struct dns_reply ptr_reply; dns_status result = dns_not_found; if (!inet_aton(ipstr, &ip)) return dns_failure; status = ptr_lookup(ip, &ptr_reply); if (status != dns_success) return status; if (reply) dns_reply_init(reply, dns_reply_str, 0); for (i = 0; i < ptr_reply.count; i++) { struct dns_reply r; status = a_lookup(ptr_reply.data.str[i], &r); if (status == dns_success) { size_t k; for (k = 0; k < r.count; k++) { if (r.data.ip[k] == ip.s_addr) { result = dns_success; if (reply) dns_reply_push(reply, mu_strdup(ptr_reply.data.str[i])); break; } } dns_reply_free(&r); } } dns_reply_free(&ptr_reply); return result; } mf_status dns_to_mf_status(dns_status stat) { return (mf_status) stat; } dns_status mf_to_dns_status(mf_status stat) { return (dns_status) stat; } mf_status resolve_ipstr_domain(const char *ipstr, const char *domain, char **phbuf) { char *hbuf; dns_status dstat; mu_debug(debug_handle, MU_DEBUG_TRACE8, ("Getting canonical name for %s", ipstr)); dstat = dns_resolve_ipstr(ipstr, domain, &hbuf); switch (dstat) { case dns_success: mu_debug(debug_handle, MU_DEBUG_TRACE8, ("%s resolved to %s", ipstr, hbuf)); *phbuf = hbuf; break; default: mu_debug(debug_handle, MU_DEBUG_TRACE8, ("%s not resolved", ipstr)); } return dns_to_mf_status(dstat); } mf_status resolve_ipstr(const char *ipstr, char **phbuf) { return resolve_ipstr_domain(ipstr, NULL, phbuf); } mf_status resolve_hostname(const char *host, char **pipbuf) { char *ipbuf; dns_status dstat; mu_debug(debug_handle, MU_DEBUG_TRACE8, ("Getting IP address for %s", host)); dstat = dns_resolve_hostname(host, &ipbuf); switch (dstat) { case dns_success: mu_debug(debug_handle, MU_DEBUG_TRACE8, ("%s resolved to %s", host, ipbuf)); *pipbuf = ipbuf; break; default: mu_debug(debug_handle, MU_DEBUG_TRACE8, ("%s not resolved", host)); } return dns_to_mf_status(dstat); } /* Return NS records for the given DOMAIN. */ dns_status ns_lookup(const char *domain, int resolve, struct dns_reply *reply) { dns_status status = dns_failure; int rc; adns_answer *ans; int i; rc = adns_synchronous(get_state(), domain, adns_r_ns_raw, DEFAULT_QFLAGS, &ans); if (rc) return errno_to_dns_status(rc); status = adns_to_dns_status(ans->status); if (status != dns_success) return status; dns_reply_init(reply, dns_reply_str, ans->nrrs); for (i = 0; i < ans->nrrs; i++) reply->data.str[i] = mu_strdup(ans->rrs.str[i]); free(ans); if (resolve) status = dns_reply_resolve(reply); return status; }