# HG changeset patch # User Carl Byington # Date 1493156899 25200 # Node ID d5a1ed33d3aed1528fd88570fd99cc3474447eb8 # Parent 54809ee70bb89db6c21de88e308c54c693031626 spf code now handles mx,exists,ptr tags, multiple A records, %{i} macro diff -r 54809ee70bb8 -r d5a1ed33d3ae ChangeLog --- a/ChangeLog Wed Apr 19 09:36:16 2017 -0700 +++ b/ChangeLog Tue Apr 25 14:48:19 2017 -0700 @@ -1,3 +1,7 @@ +6.57 2017-04-25 + spf code now handles mx,exists,ptr tags, multiple A records, + %{i} macro + 6.56 2017-04-19 refactor spf code; allow wildcard *.example.com in dkim signing restrictions diff -r 54809ee70bb8 -r d5a1ed33d3ae NEWS --- a/NEWS Wed Apr 19 09:36:16 2017 -0700 +++ b/NEWS Tue Apr 25 14:48:19 2017 -0700 @@ -1,3 +1,4 @@ +6.57 2017-04-25 spf code now handles mx,exists,ptr tags, multiple A records, %{i} macro 6.56 2017-04-19 refactor spf code; allow wildcard *.example.com in dkim signing restrictions 6.55 2017-04-16 require 3 dots in bare ip addresses. 6.54 2017-03-30 document dmarc vs dnsbl dkim/spf; switch to . rather than " " for dkim impossible signer diff -r 54809ee70bb8 -r d5a1ed33d3ae configure.in --- a/configure.in Wed Apr 19 09:36:16 2017 -0700 +++ b/configure.in Tue Apr 25 14:48:19 2017 -0700 @@ -1,6 +1,6 @@ AC_PREREQ(2.59) -AC_INIT(dnsbl,6.56,carl@five-ten-sg.com) +AC_INIT(dnsbl,6.57,carl@five-ten-sg.com) AC_CONFIG_SRCDIR([config.h.in]) AC_CONFIG_HEADER([config.h]) AC_CONFIG_MACRO_DIR([m4]) diff -r 54809ee70bb8 -r d5a1ed33d3ae dnsbl.spec.in --- a/dnsbl.spec.in Wed Apr 19 09:36:16 2017 -0700 +++ b/dnsbl.spec.in Tue Apr 25 14:48:19 2017 -0700 @@ -155,6 +155,9 @@ %changelog +* Tue Apr 25 2017 Carl Byington - 6.57-1 +- spf code now handles mx,exists,ptr tags, multiple A records, %{i} macro + * Wed Apr 19 2017 Carl Byington - 6.56-1 - refactor spf code - allow wildcard *.example.com in dkim signing restrictions diff -r 54809ee70bb8 -r d5a1ed33d3ae src/context.cpp --- a/src/context.cpp Wed Apr 19 09:36:16 2017 -0700 +++ b/src/context.cpp Tue Apr 25 14:48:19 2017 -0700 @@ -1138,7 +1138,27 @@ dns_interface(*priv, from, ns_t_txt, false, NULL, buf, maxlen); if (*buf) { log(priv->queueid, "found txt record %s", buf); - char *p = strchr(buf, ' '); // must start with 'v=spf1 ' + // expand some macros here - a very restricted subset of all possible spf macros + // only expand the first one. + char *p = strstr(buf, "%{i}"); + if (p) { + char repl[maxlen]; + char adr[sizeof "255.255.255.255 "]; + adr[0] = '\0'; + inet_ntop(AF_INET, (const u_char *)&priv->ip, adr, sizeof(adr)); + size_t bn = strlen(buf); + size_t an = strlen(adr); + if ((bn - 4 + an) < maxlen) { + size_t n = p - buf; // leading part length + strncpy(repl, buf, n); // leading part + strcpy(repl+n, adr); // replacement + strcpy(repl+n+an, buf+n+4); // trailing part + strcpy(buf, repl); + } + log(priv->queueid, "have txt record %s", buf); + } + // + p = strchr(buf, ' '); // must start with 'v=spf1 ' if (!p) return false; // broken spf char *e = p + strlen(p); // point to trailing null while (true) { @@ -1146,7 +1166,7 @@ if (p >= e) break; char *b = strchr(p, ' '); if (b) *b = '\0'; - if ((*p != '-') && (*p != '~')) { + if ((*p != '-') && (*p != '~') && (*p != '?')) { if (*p == '+') p++; if (strncmp(p, "ip4:", 4) == 0) { p += 4; @@ -1161,27 +1181,72 @@ ipy &= low ^ 0xffffffff; if ((ipy <= ip) && (ip <= ipy + low)) { if (s) *s = '/'; - log(priv->queueid, "match %s", p); + log(priv->queueid, "match ip4:%s", p); return true; } } } } - else if (strncmp(p, "a:", 2) == 0) { - p += 2; - uint32_t ipy = ntohl(dns_interface(*priv, p, ns_t_a)); - if (ipy == ip) { - log(priv->queueid, "match %s", p); + else if (strncmp(p, "all", 3) == 0) { + // ignore it before looking for (a or a:) below + } + else if (strncmp(p, "exists:", 7) == 0) { + p += 7; + char buf[maxlen]; + uint32_t ipy = ntohl(dns_interface(*priv, p, ns_t_a, false, NULL, buf, maxlen)); + uint32_t *a = (uint32_t *)buf; + if (a[0]) { + log(priv->queueid, "match exists:%s", p); return true; } } - else if (strcmp(p, "a") == 0) { - uint32_t ipy = ntohl(dns_interface(*priv, from, ns_t_a)); + else if (strncmp(p, "mx", 2) == 0) { + const char *name = (p[2] == ':') ? p+2 : from; + char buf[maxlen]; + uint32_t c = ntohl(dns_interface(*priv, name, ns_t_mx, false, NULL, buf, maxlen)); + char *b = buf; + while (*b) { + log(priv->queueid, "found mx %s", b); + char abuf[maxlen]; + uint32_t ipy = ntohl(dns_interface(*priv, b, ns_t_a, false, NULL, buf, maxlen)); + uint32_t *a = (uint32_t *)buf; + size_t c = a[0]; + for (size_t i=1; i++; i<=c) { + ipy = ntohl(a[i]); if (ipy == ip) { - log(priv->queueid, "match %s", from); + log(priv->queueid, "match mx:%s", name); return true; } } + b += strlen(b) + 1; + } + } + else if (p[0] == 'a') { + const char *name = (p[1] == ':') ? p+2 : from; + char buf[maxlen]; + uint32_t ipy = ntohl(dns_interface(*priv, name, ns_t_a, false, NULL, buf, maxlen)); + uint32_t *a = (uint32_t *)buf; + size_t c = a[0]; + for (size_t i=1; i++; i<=c) { + ipy = ntohl(a[i]); + if (ipy == ip) { + log(priv->queueid, "match a:%s", name); + return true; + } + } + } + else if (priv->client_dns_name && (!priv->client_dns_forged) && (strncmp(p, "ptr", 3) == 0)) { + const char *name = (p[3] == ':') ? p+4 : from; + size_t n = strlen(name); + size_t d = strlen(priv->client_dns_name); + if (d >= n) { + if ((strncmp(priv->client_dns_name+d-n, name, n) == 0) && // trailing part matches + ((d == n) || (priv->client_dns_name[d-n-1] == '.'))) { // same length, or dot just before match + log(priv->queueid, "match ptr:%s", priv->client_dns_name); + return true; + } + } + } else if ((level < 5) && (strncmp(p, "redirect=", 9) == 0)) { p += 9; if (resolve_spf(p, ip, priv, level+1)) return true; diff -r 54809ee70bb8 -r d5a1ed33d3ae src/dnsbl.cpp --- a/src/dnsbl.cpp Wed Apr 19 09:36:16 2017 -0700 +++ b/src/dnsbl.cpp Tue Apr 25 14:48:19 2017 -0700 @@ -297,14 +297,16 @@ // Ask a dns question and get an A record answer in network byte order. // We don't try very hard, just using the default resolver retry settings. // If we cannot get an answer, we just accept the mail. -// If the qtype is ns_t_txt, the answer is placed in txt_answer which +// If the qtype is ns_t_txt, the answer is placed in my_answer which // must be non-null, and the return value can be ignored. -// A null string is returned in txt_answer in the case of errors. -// If the qtype is ns_t_a, the ip address is returned in network byte order. +// A null string is returned in my_answer in the case of errors. +// If the qtype is ns_t_a, one of the ip addresses is returned in network byte order, +// and if my_answer is non-null, all the ip addresses are returned in +// my_answer (in network byte order), prepended with the count. // IP address 0 is returned in case of errors. // -uint32_t dns_interface(mlfiPriv &priv, const char *question, int qtype, bool maybe_ip, ns_map *nameservers, char *txt_answer, size_t txt_size) { - if (txt_answer) txt_answer[0] = '\0'; // return null string if there are no txt answers +uint32_t dns_interface(mlfiPriv &priv, const char *question, int qtype, bool maybe_ip, ns_map *nameservers, char *my_answer, size_t my_size) { + if (my_answer) my_answer[0] = '\0'; // return null string if there are no txt answers // tell sendmail we are still working #if _FFR_SMFI_PROGRESS @@ -412,17 +414,45 @@ } int rrnum = 0; if (qtype == ns_t_a) { + uint32_t *ans = (uint32_t *)my_answer; + size_t c = 0; + size_t m = my_size/4; // 1 + max ipv4 addresses that will fit in the answer buffer while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) { if (ns_rr_type(rr) == qtype) { uint32_t address; memcpy(&address, ns_rr_rdata(rr), sizeof(address)); ret_address = address; + if (ans && (c < m-1)) { + c++; + ans[c] = address; } } } - if ((qtype == ns_t_txt) && (txt_answer) && (txt_size > 7)) { - txt_answer[0] = '\0'; // return null string if there are no txt answers - txt_size--; // allow room for terminating null; + if (ans && (c < m)) ans[0] = c; + } + if ((qtype == ns_t_mx) && (my_answer) && (my_size > 1)) { + uint32_t c = 0; + while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) { + if (ns_rr_type(rr) == qtype) { + char exchange[NS_MAXDNAME]; + size_t rdlen = ns_rr_rdlen(rr); + const unsigned char *rdata = ns_rr_rdata(rr); + const uint16_t pri = ns_get16(rdata); + int len = dn_expand(glom.answer, glom.answer+glom.length, rdata + 2, exchange, sizeof(exchange)); + if ((len > 0) && (my_size > len+2)) { + strcpy(my_answer, exchange); + my_answer += len + 1; + my_size -= len + 1; + c++; + } + } + } + *my_answer = '\0'; // final trailing null + ret_address = htonl(c); + } + if ((qtype == ns_t_txt) && (my_answer) && (my_size > 7)) { + my_answer[0] = '\0'; // return null string if there are no txt answers + my_size--; // allow room for terminating null; while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) { if (ns_rr_type(rr) == qtype) { size_t offset = 0; @@ -433,26 +463,26 @@ snprintf(text, sizeof(text), "found txt record rdlen = %d", rdlen); my_syslog(text); #endif - while ((offset < txt_size) && rdlen) { + while ((offset < my_size) && rdlen) { size_t slen = size_t(*(rdata++)); rdlen--; size_t m = min(slen, rdlen); - m = min(m, txt_size-offset); - memcpy(txt_answer+offset, rdata, m); + m = min(m, my_size-offset); + memcpy(my_answer+offset, rdata, m); offset += m; rdata += m; rdlen -= m; } - txt_answer[offset] = '\0'; // trailing null + my_answer[offset] = '\0'; // trailing null #ifdef RESOLVER_DEBUG - snprintf(text, sizeof(text), "found txt record %s", txt_answer); + snprintf(text, sizeof(text), "found txt record %s", my_answer); my_syslog(text); #endif - if (strncasecmp(txt_answer, "v=spf1 ", 7) == 0) break; + if (strncasecmp(my_answer, "v=spf1 ", 7) == 0) break; } } - if (strncasecmp(txt_answer, "v=spf1 ", 7) != 0) { - txt_answer[0] = '\0'; // return null string if there are no spf1 txt answers + if (strncasecmp(my_answer, "v=spf1 ", 7) != 0) { + my_answer[0] = '\0'; // return null string if there are no spf1 txt answers } } } diff -r 54809ee70bb8 -r d5a1ed33d3ae xml/dnsbl.in --- a/xml/dnsbl.in Wed Apr 19 09:36:16 2017 -0700 +++ b/xml/dnsbl.in Tue Apr 25 14:48:19 2017 -0700 @@ -719,6 +719,9 @@ The following ideas are under consideration. + More complete SPF check. + + Add config switch to require the HELO argument to resolve to an ip address. @@ -966,6 +969,9 @@ # some paychex mail is signed, some is unsigned but passes strong spf. paychex.com require_signed paychex.com; # + # whitelisting from mailchimp which needs wildcards + princetheater.org require_signed "mandrillapp.com,*.mcsignup.com,*.mcsv.net,*.rsgsv.net,*.mcdlv.net"; + # }; filter sbl-xbl.spamhaus.org "Mail containing %s rejected - sbl; see http://www.spamhaus.org/query/bl?ip=%s"; uribl multi.surbl.org "Mail containing %s rejected - surbl; see http://www.surbl.org/surbl-analysis?d=%s";