changeset 414:d5a1ed33d3ae

spf code now handles mx,exists,ptr tags, multiple A records, %{i} macro
author Carl Byington <carl@five-ten-sg.com>
date Tue, 25 Apr 2017 14:48:19 -0700
parents 54809ee70bb8
children 16451edcb962
files ChangeLog NEWS configure.in dnsbl.spec.in src/context.cpp src/dnsbl.cpp xml/dnsbl.in
diffstat 7 files changed, 139 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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])
--- 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 <carl@five-ten-sg.com> - 6.57-1
+- spf code now handles mx,exists,ptr tags, multiple A records, %{i} macro
+
 * Wed Apr 19 2017 Carl Byington <carl@five-ten-sg.com> - 6.56-1
 - refactor spf code
 - allow wildcard *.example.com in dkim signing restrictions
--- 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;
--- 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
                 }
             }
         }
--- 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.
             </para>
             <para>
+                More complete SPF check.
+            </para>
+            <para>
                 Add config switch to require the HELO argument to resolve to an ip address.
             </para>
             <para>
@@ -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";