changeset 381:879a470c6ac3

fetch spf txt records for required dkim signers
author Carl Byington <carl@five-ten-sg.com>
date Tue, 28 Feb 2017 17:02:07 -0800
parents 0495e767bfb7
children c378e9d03f37
files ChangeLog NEWS configure.in dnsbl.spec.in src/context.cpp src/context.h src/dnsbl.cpp
diffstat 7 files changed, 89 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Mon Feb 20 08:43:41 2017 -0800
+++ b/ChangeLog	Tue Feb 28 17:02:07 2017 -0800
@@ -1,7 +1,10 @@
-6.50 2017-02-18
+6.51 2017-02-28
+    fetch spf txt records for required dkim signers
+
+6.50 2017-02-22
     reject if dkim signer is listed on surbl
 
-6.49 2017-02-07
+6.49 2017-02-08
     RHEL7 systemd and /var/run on tmpfs.
 
 6.48 2016-12-17
--- a/NEWS	Mon Feb 20 08:43:41 2017 -0800
+++ b/NEWS	Tue Feb 28 17:02:07 2017 -0800
@@ -1,5 +1,6 @@
-6.50 2017-02-18 reject if dkim signer is listed on surbl
-6.49 2017-02-07 RHEL7 systemd and /var/run on tmpfs
+6.51 2017-02-28 fetch spf txt records for required dkim signers
+6.50 2017-02-22 reject if dkim signer is listed on surbl
+6.49 2017-02-08 RHEL7 systemd and /var/run on tmpfs
 6.48 2016-12-17 Add dkim white/black listing
 6.47 2016-09-21 Better smtp verify logging
 6.46 2016-09-19 Enable smtp verify logging
--- a/configure.in	Mon Feb 20 08:43:41 2017 -0800
+++ b/configure.in	Tue Feb 28 17:02:07 2017 -0800
@@ -1,6 +1,6 @@
 
 AC_PREREQ(2.59)
-AC_INIT(dnsbl,6.50,carl@five-ten-sg.com)
+AC_INIT(dnsbl,6.51,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	Mon Feb 20 08:43:41 2017 -0800
+++ b/dnsbl.spec.in	Tue Feb 28 17:02:07 2017 -0800
@@ -25,7 +25,7 @@
 %endif
 Requires:               sendmail >= 8.12.1
 Requires:               sendmail-cf
-Requires:               spamassassin
+Requires:               spamassassin, dcc
 %if "%{?dist}" == ".el7"
 Requires(pre):  systemd-sysv, shadow-utils
 Requires(post): systemd-units
@@ -143,7 +143,8 @@
 %docdir %{_datadir}/doc/%{name}-%{version}
 %{_datadir}/doc/%{name}-%{version}
 %config(noreplace) %attr(0750,%{name},root) %{_sysconfdir}/%{name}
-%config(noreplace) %attr(0644,root,root) /var/dcc/userdirs/dnsblnogrey/whiteclnt
+%dir %attr(0750,dcc,dcc) /var/dcc/userdirs/dnsblnogrey
+%config(noreplace) %attr(0644,dcc,dcc) /var/dcc/userdirs/dnsblnogrey/whiteclnt
 %if "%{?dist}" == ".el7"
 %{_prefix}/lib/tmpfiles.d/%{name}.conf
 %{_unitdir}/%{name}.service
@@ -154,8 +155,12 @@
 
 
 %changelog
-* Sat Feb 18 2017 Carl Byington <carl@five-ten-sg.com> - 6.50-1
+* Tue Feb 28 2017 Carl Byington <carl@five-ten-sg.com> - 6.51-1
+- fetch spf txt records for required dkim signers.
+
+* Wed Feb 22 2017 Carl Byington <carl@five-ten-sg.com> - 6.50-1
 - reject if dkim signer is listed on surbl
+- require dcc, this package owns /var/dcc/userdirs/dnsblnogrey
 
 * Wed Feb 08 2017 Carl Byington <carl@five-ten-sg.com> - 6.49-1
 - RHEL7 systemd and /var/run is on tmpfs
--- a/src/context.cpp	Mon Feb 20 08:43:41 2017 -0800
+++ b/src/context.cpp	Tue Feb 28 17:02:07 2017 -0800
@@ -1122,7 +1122,20 @@
 }
 
 
-const char *CONTEXT::acceptable_content(recorder &memory, int score, int bulk, const char *queueid, string_set &signers, const char *from, string& msg) {
+#ifdef NS_PACKETSZ
+bool CONTEXT::resolve_spf(const char *from, int32_t ip, mlfiPriv *priv)
+{
+    char buf[maxlen];
+    dns_interface(*priv, from, ns_t_txt, false, NULL, buf, maxlen);
+    if (*buf) {
+        log(priv->queueid, "found txt record %s", buf);
+    }
+    return false;
+}
+#endif
+
+
+const char *CONTEXT::acceptable_content(recorder &memory, int score, int bulk, const char *queueid, string_set &signers, const char *from, mlfiPriv *priv, string& msg) {
     DKIMP dk = find_dkim_from(from);
 
     for (string_set::iterator s=signers.begin(); s!=signers.end(); s++) {
@@ -1156,6 +1169,13 @@
             }
         }
         if (st == token_require_signed) {
+#ifdef NS_PACKETSZ
+            // not signed by the required signers, but maybe passes strong spf check
+            if (resolve_spf(from, priv->ip, priv) {
+                log(queueid, "spf pass for %s rather than required dkim signer", from);
+                return token_white;
+            }
+#endif
             char buf[maxlen];
             snprintf(buf, sizeof(buf), "Mail rejected - not dkim signed by %s", dk->signer);
             msg = string(buf);
--- a/src/context.h	Mon Feb 20 08:43:41 2017 -0800
+++ b/src/context.h	Tue Feb 28 17:02:07 2017 -0800
@@ -23,6 +23,7 @@
 class WHITELISTER;
 class DELAYWHITE;
 class recorder;
+class mlfiPriv;
 
 typedef map<const char *, const char *, ltstr>  string_map;
 typedef set<int>                                int_set;
@@ -314,7 +315,7 @@
 
     void        log(const char *queueid, const char *msg, const char *v);
     bool        in_signing_set(const char *s, const char *signers);
-    const char *acceptable_content(recorder &memory, int score, int bulk, const char *queueid, string_set &signers, const char *from, string& msg);
+    const char *acceptable_content(recorder &memory, int score, int bulk, const char *queueid, string_set &signers, const char *from, mlfiPriv *priv, string& msg);
     bool        ignore_host(const char *host);
 
     void        dump(bool isdefault, bool &spamass, int level = 0);
--- a/src/dnsbl.cpp	Mon Feb 20 08:43:41 2017 -0800
+++ b/src/dnsbl.cpp	Tue Feb 28 17:02:07 2017 -0800
@@ -302,13 +302,15 @@
 
 
 ////////////////////////////////////////////////
-//  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.
+//  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
+//  must be non-null, and the return value can be ignored.
+//  If the qtype is ns_t_a, the ip address is returned in network byte order.
 //
-//
-uint32_t dns_interface(mlfiPriv &priv, const char *question, bool maybe_ip, ns_map *nameservers);
-uint32_t dns_interface(mlfiPriv &priv, const char *question, bool maybe_ip, ns_map *nameservers) {
+uint32_t dns_interface(mlfiPriv &priv, const char *question, int qtype, bool maybe_ip = false, ns_map *nameservers = NULL, char *txt_answer = NULL, size_t txt_size = 0);
+uint32_t dns_interface(mlfiPriv &priv, const char *question, int qtype, bool maybe_ip, ns_map *nameservers, char *txt_answer, size_t txt_size) {
     // tell sendmail we are still working
     #if _FFR_SMFI_PROGRESS
         if (priv.eom) smfi_progress(priv.ctx);
@@ -318,13 +320,15 @@
     // milter thread is talking over its own socket to a separate resolver
     // process, which does the actual dns resolution.
     if (priv.err) return 0; // cannot ask more questions on this socket.
-    if (maybe_ip) {
+    if (maybe_ip && (qtype == ns_t_a)) {
         // might be a bare ip address, try this first to avoid dns lookups that may not be needed
         in_addr ip;
         if (inet_aton(question, &ip)) {
             return ip.s_addr;
         }
     }
+    int8_t qt = qtype;
+    priv.my_write((const char *)&qt, 1);// write the query type
     size_t n = strlen(question);
     if (question[n-1] == '.') {
         priv.my_write(question, n+1);   // write the question including the null terminator
@@ -405,14 +409,39 @@
                 }
             }
             int rrnum = 0;
+            if (qtype == ns_t_a) {
             while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) {
-                if (ns_rr_type(rr) == ns_t_a) {
+                    if (ns_rr_type(rr) == qtype) {
                     uint32_t address;
                     memcpy(&address, ns_rr_rdata(rr), sizeof(address));
                     ret_address = address;
                 }
             }
         }
+            if ((qtype == ns_t_txt) && (txt_answer) && (txt_size > 5)) {
+                txt_size--; // allow room for terminating null;
+                txt_answer[0] = '\0';   // return null string if there are no answers
+                while (ns_parserr(&handle, ns_s_an, rrnum++, &rr) == 0) {
+                    size_t offset = 0;
+                    if (ns_rr_type(rr) == qtype) {
+                        size_t rdlen = ns_rr_rdlen(rr);
+                        const unsigned char *rdata = ns_rr_rdata(rr);
+                        while ((offset < txt_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);
+                            offset += m;
+                            rdata  += m;
+                            rdlen  -= m;
+                        }
+                    }
+                    txt_answer[offset] = '\0';  // trailing null
+                    if (strcasecmp(txt_answer, "v=spf1 ") == 0) break;
+                }
+            }
+        }
     pthread_mutex_unlock(&resolve_mutex);
     #ifdef RESOLVER_DEBUG
         snprintf(text, sizeof(text), "dns_interface() found ip %d", ret_address);
@@ -436,7 +465,7 @@
 bool uriblookup(mlfiPriv &priv, string_set &hosts, const char *hostname, const char *&found) {
     char buf[maxlen];
     snprintf(buf, sizeof(buf), "%s.%s.", hostname, priv.uribl_suffix);
-    uint32_t ip = ntohl(dns_interface(priv, buf, false, NULL));
+    uint32_t ip = ntohl(dns_interface(priv, buf, ns_t_a));
     if (ip and (ip != 0x7f000000)) {
         if (debug_syslog > 2) {
             char tmp[maxlen];
@@ -721,7 +750,6 @@
 }
 
 const char *mlfiPriv::check_uribl_signers() {
-    const char *st;
     if (uribl_suffix) {
         for (string_set::iterator s=dkim_signers.begin(); s!=dkim_signers.end(); s++) {
             if (check_uribl(*this, hosts_uribl, *s, host_uribl)) return host_uribl;
@@ -853,16 +881,16 @@
 #ifdef NS_PACKETSZ
         #ifdef RESOLVER_DEBUG
             char text[1000];
-            snprintf(text, sizeof(text), "process_resolver_requests() has a question %s", question);
+            snprintf(text, sizeof(text), "process_resolver_requests() has a question %s qtype %d", question+1, int8_t(question[0]));
             my_syslog(text);
         #endif
-        int res_result = res_search(question, ns_c_in, ns_t_a, glom.answer, sizeof(glom.answer));
+        int res_result = res_search(question+1, ns_c_in, int8_t(question[0]), glom.answer, sizeof(glom.answer));
         if (res_result < 0) glom.length = 0;   // represent all errors as zero length answers
         else                glom.length = (size_t)res_result;
 #else
         glom.length = sizeof(glom.answer);
         glom.answer = 0;
-        struct hostent *host = gethostbyname(question);
+        struct hostent *host = gethostbyname(question+1);
         if (host && (host->h_addrtype == AF_INET)) {
             memcpy(&glom.answer, host->h_addr, sizeof(glom.answer));
         }
@@ -913,7 +941,7 @@
 #endif
     snprintf(question, sizeof(question), "%u.%u.%u.%u.%s.", src[3], src[2], src[1], src[0], suffix);
     // ask the question, if we get an A record it implies a blacklisted ip address
-    return dns_interface(priv, question, false, NULL);
+    return dns_interface(priv, question, ns_t_a);
 }
 
 
@@ -1028,7 +1056,7 @@
             }
         }
         count++;
-        ip = dns_interface(priv, host, true, &nameservers);
+        ip = dns_interface(priv, host, ns_t_a, true, &nameservers);
         if (debug_syslog > 2) {
             char buf[maxlen];
             if (ip) {
@@ -1066,7 +1094,7 @@
         if ((count > limit) && (limit > 0)) return false;   // too many name servers to check them all
         host = (*i).first;  // a transient reference that needs to be replaced before we return it
         ip   = (*i).second;
-        if (!ip) ip = dns_interface(priv, host, false, NULL);
+        if (!ip) ip = dns_interface(priv, host, ns_t_a);
         if (debug_syslog > 2) {
             char buf[maxlen];
             if (ip) {
@@ -1655,7 +1683,7 @@
         for (context_map::iterator i=priv.env_to.begin(); i!=priv.env_to.end(); i++) {
             const char *rcpt   = (*i).first;
             CONTEXT &con = *((*i).second);
-            const char *st = con.acceptable_content(*priv.memory, score, bulk, priv.queueid, priv.dkim_signers, priv.fromaddr, msg);
+            const char *st = con.acceptable_content(*priv.memory, score, bulk, priv.queueid, priv.dkim_signers, priv.fromaddr, &priv, msg);
             if (st == token_black) {
                 // bad html tags or excessive hosts or
                 // high spam assassin score or dcc bulk threshold exceedeed