/* $Id: spf.C,v 1.11 2006/04/03 18:21:47 dm Exp $ */ /* * * Copyright (C) 2004 David Mazieres (dm@uun.org) * * 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 2, 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * */ #include "asmtpd.h" spfhosts_map smap; #define SPF_VERS "v=spf1" static int spftrace (getenv ("SPF_TRACE") ? atoi (getenv ("SPF_TRACE")) : 0); static rxx spaceplus (" +"); static rxx domcidr ("^([\\w_\\-.]+)?(/(/)?(\\d{1,3}))?$"); static rxx ip4cidr ("^(\\d{1,3}(\\.\\d{1,3}){3})(/(\\d{1,3}))?$"); rxx & spfhosts_map::linerx () { static rxx spfrx ("^\\s*([^\\x00-\\x20\\x7f:]+)\\s*:\\s*(|\\S.*)$"); return spfrx; } bool spfhosts_map::lookup (str *spfrecp, str domain) { if (!load ()) return false; domain = mytolower (domain); if (str *sp = table[domain]) { *spfrecp = *sp; return true; } const char *p = domain; while ((p = strchr (p + 1, '.'))) { if (str *sp = table[domain]) { *spfrecp = *sp; return true; } } return false; } spf_t::spf_t (in_addr a, str f, ptr > lc, int rd) : addr (a), sender (f), fallback (NULL), tracedepth (0), recdepth (rd), loopcheck (lc ? lc : ptr > (New refcounted >)), ptr_cache_err (false), dnsrq (NULL), recurse (NULL) { } spf_t::~spf_t () { if (dnsrq) dnsreq_cancel (dnsrq); if (recurse) delete recurse; } void spf_t::getexp (cbv cb, ptr t, int err) { dnsrq = NULL; if (t) { strbuf sb; for (u_int i = 0; i < t->t_ntxt; i++) sb << t->t_txts[i]; explain = sb; } (*cb) (); } void spf_t::finish (spf_result stat) { if (stat != SPF_PASS && !explain && expdn && expdn.len ()) { if (!macro_subst (&expdn, expdn)) { getptr (wrap (this, &spf_t::finish, stat)); return; } str n = expdn; expdn = NULL; if (dnsreq_t *d = dns_txtbyname (n, wrap (this, &spf_t::getexp, wrap (this, &spf_t::finish, stat)))) dnsrq = d; return; } if (stat != SPF_PASS && explain && explain.len ()) { if (!macro_subst (&explain, explain, true)) { getptr (wrap (this, &spf_t::finish, stat)); return; } } if (spftrace > !!tracedepth) { warn ("SPF_TRACE: %*sRETURN ", tracedepth, "") << inet_ntoa (addr) << " / " << domain << " -> " << spf_print (stat) << "\n"; if (explain) warn ("SPF_TRACE: %*sEXPLANATION ", tracedepth, "") << explain << "\n"; } result = stat; cb_t c (cb); cb = NULL; domain = NULL; (*c) (this); if (cb) init (); else delete this; } bool spf_t::suffix_check (const char *targ, str suffix) { u_int n = strlen (targ); if (n < suffix.len ()) return false; if (n == suffix.len ()) return (!strcasecmp (targ, suffix)); const char *p = targ + n - suffix.len (); if (p[-1] != '.') return false; return !strcasecmp (p, suffix); } void spf_t::getptr (cbv cb) { if (dnsreq_t *d = dns_hostbyaddr (addr, wrap (this, &spf_t::getptr_2, cb))) dnsrq = d; } void spf_t::getptr_2 (cbv cb, ptr h, int err) { dnsrq = NULL; if (spftrace >= 4) printaddrs (sender, h, err); if (dns_tmperr (err)) { finish (SPF_ERROR); return; } ptr_cache = h; ptr_cache_err = err; (*cb) (); } void spf_t::init () { redirect = NULL; curmech = NULL; if (!domain) if (!(domain = extract_domain (sender))) domain = sender; if (!validate_domain (domain, true)) finish (SPF_UNKNOWN); else if (spfrec) parse_spf (); else if (!loopcheck->insert (mytolower (domain)) || recdepth > 20) { if (spftrace >= 2) warn ("SPF_TRACE: %*sLOOP ", tracedepth, "") << domain << "\n"; finish (SPF_UNKNOWN); } else if (dnsreq_t *d = dns_txtbyname (domain, wrap (this, &spf_t::gettxt))) dnsrq = d; } void spf_t::gettxt (ptr t, int err) { dnsrq = NULL; if (spftrace >= 4) printtxtlist (sender, t, err); if (err) { if (dns_tmperr (err)) finish (SPF_ERROR); else if (fallback && fallback->lookup (&spfrec, domain)) parse_spf (); else finish (SPF_NONE); return; } for (u_int i = 0; i < t->t_ntxt; i++) if (!strncmp (t->t_txts[i], SPF_VERS, sizeof (SPF_VERS) - 1) && (t->t_txts[i][sizeof (SPF_VERS) - 1] == ' ' || t->t_txts[i][sizeof (SPF_VERS) - 1] == '\0')) { if (spfrec) { finish (SPF_UNKNOWN); return; } else spfrec = t->t_txts[i] + sizeof (SPF_VERS) - 1; } if (spfrec) parse_spf (); else finish (SPF_NONE); } void spf_t::parse_spf () { vec dm; split (&dm, spaceplus, spfrec); if (spftrace > !!recdepth) { warn ("SPF_TRACE: %*sCHECK ", tracedepth, "") << inet_ntoa (addr) << " / " << domain << "\n"; if (spftrace >= 2) { warn ("SPF_TRACE: %*sRULES", tracedepth, ""); for (str *rp = dm.base (); rp < dm.lim (); rp++) warnx << " " << *rp; warnx << "\n"; } } for (str *dmp = dm.base (); dmp < dm.lim (); dmp++) { if (!dmp->len ()) continue; const char *eq = strchr (*dmp, '='); if (!strchr (*dmp, '=')) mechv.push_back (*dmp); else { const char *co = strchr (*dmp, ':'); if (co && co < eq) mechv.push_back (*dmp); else if (!strncasecmp (*dmp, "redirect=", 9)) redirect = dmp->cstr () + 9; else if (!strncasecmp (*dmp, "exp=", 4)) { expdn = dmp->cstr () + 4; explain = NULL; } } } mech_start (); } /* reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," unreserved = alphanum | mark mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" delims = "<" | ">" | "#" | "%" | <"> unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`" */ inline bool needsescape (u_char c) { if (isalnum (c)) return false; if (c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') return false; return true; } static str urlescape (str in) { strbuf sb; for (const char *p = in; *p; p++) if (needsescape (*p)) sb.fmt ("%%%02x", (u_char) *p); else sb.fmt ("%c", *p); return sb; } bool spf_t::macro_subst_inner (const strbuf &out, str in, bool exp) { if (!in.len ()) return true; str text; bool escape = isupper (in[0]); switch (tolower (in[0])) { case 'l': if (!(text = extract_local (sender))) text = "postmaster"; break; case 's': if (extract_local (sender)) text = sender; else text = strbuf ("postmaster@%s", sender.cstr ()); break; case 'o': if (!(text = extract_domain (sender))) text = sender; break; case 'd': text = domain; break; case 'i': text = inet_ntoa (addr); break; case 'p': if (!ptrok ()) return false; else if (ptr_cache) text = ptr_cache->h_name; else text = "unknown"; break; case 'v': text = "in-addr"; break; case 'h': text = helo ? helo : str ("unknown"); break; case 'r': text = opt->hostname; break; case 'c': if (exp) { text = inet_ntoa (addr); break; } /* cascade */ case 't': if (exp) { text = strbuf ("%lu", (u_long) time (NULL)); break; } /* cascade */ default: out << "%{" << in << "}"; return true; } static rxx td ("^.(\\d+)?(r)?([\\.\\-\\+,\\/_=]+)?"); if (!td.match (in)) { out << "%{" << in << "}"; return true; } str spn = td[3]; if (!spn) spn = "."; vec parts; const char *p = text; while (*p) { int n = strcspn (p, spn); parts.push_back (str (p, n)); p += n; if (*p) p++; } if (td[2]) { str *sp = parts.base (), *ep = parts.lim (); while (sp + 1 < ep) { str tmp = *--ep; *ep = *sp; *sp++ = tmp; } } if (str ns = td[1]) { u_int n = atoi (ns); if (n < parts.size ()) parts.popn_front (parts.size () - n ); } for (u_int i = 0; i < parts.size (); i++) if (i) out << "." << (escape ? urlescape (parts[i]) : parts[i]); else out << (escape ? urlescape (parts[i]) : parts[i]); return true; } bool spf_t::macro_subst (str *outp, str in, bool exp) { strbuf out; for (const char *p = in; *p; p++) { if (*p != '%') { out.fmt ("%c", *p); continue; } switch (*++p) { case '%': out.fmt ("%%"); break; case '_': out.fmt (" "); break; case '-': out.fmt ("%%20"); break; case '{': { const char *r = strchr (p, '}'); if (!r) { out << "%" << p; if (spftrace) warn << "malformed macro " << in << "\n"; *outp = out; return true; } if (!macro_subst_inner (out, str (p + 1, r - p - 1), exp)) return false; p = r; break; } case '\0': out.fmt ("%%"); if (spftrace >= 3) warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> "; *outp = out; if (spftrace >= 3) warnx << *outp << "\n"; return true; default: out.fmt ("%%%c", *p); break; } } if (spftrace >= 3) warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> "; *outp = out; if (spftrace >= 3) warnx << *outp << "\n"; return true; } bool spf_t::addr_check (int cidrlen, ref h) { if (spftrace >= 5) printaddrs ("addr_check", h); u_int32_t mask = ip4_mask (cidrlen); u_int32_t targ = addr.s_addr & mask; for (char **ap = h->h_addr_list; *ap; ap++) if (targ == (((in_addr *) *ap)->s_addr & mask)) return true; return false; } void spf_t::mech_start () { if (mechv.empty ()) { if (redirect) { curmech = (strbuf () << "redirect=" << redirect); if (!macro_subst (&redirect, redirect)) { getptr (wrap (this, &spf_t::mech_start)); return; } spfrec = NULL; recdepth++; if (spftrace >= 2) warn ("SPF_TRACE: %*sREDIRECT ", tracedepth, "") << domain << " -> " << redirect << "\n"; domain = redirect; init (); } else { curmech = NULL; finish (SPF_NEUTRAL); } return; } curmech = mechv.pop_front (); const char *mp = curmech; switch (*mp) { case '+': case '-': case '~': case '?': prefix = *mp++; break; default: prefix = '+'; break; } char *cp = strchr (mp, ':'); if (!cp) cp = strchr (mp, '/'); str arg; if (cp) arg = *cp == ':' ? cp + 1 : cp; str mech = mytolower (cp ? str (mp, cp - mp) : str (mp)); if (mech == "all") mech_end (SPF_PASS); else if (mech == "include") mech_include (arg); else if (mech == "a") mech_a (arg); else if (mech == "mx") mech_mx (arg); else if (mech == "ptr") mech_ptr (arg); else if (mech == "ip4") mech_ip4 (arg); else if (mech == "ip6") mech_ip6 (arg); else if (mech == "exists") mech_exists (arg); else finish (SPF_UNKNOWN); } void spf_t::mech_end (spf_result res) { if (spftrace >= 2) { warn ("SPF_TRACE: %*sTEST ", tracedepth, ""); if (curmech) warnx << curmech << " -> "; warnx << spf_print (res) << "\n"; } switch (res) { case SPF_PASS: switch (prefix) { case '+': finish (SPF_PASS); break; case '-': finish (SPF_FAIL); break; case '~': finish (SPF_SOFTFAIL); break; case '?': finish (SPF_NEUTRAL); break; } break; case SPF_FAIL: case SPF_SOFTFAIL: case SPF_NEUTRAL: mech_start (); break; case SPF_NONE: case SPF_ERROR: case SPF_UNKNOWN: finish (res); break; default: panic ("unknown SPF state %d\n", res); break; } } void spf_t::mech_include (str targ) { if (!targ) { mech_end (SPF_UNKNOWN); return; } if (!macro_subst (&targ, targ)) { getptr (wrap (this, &spf_t::mech_include, targ)); return; } if (spftrace >= 2) warn ("SPF_TRACE: %*sINCLUDE ", tracedepth, "") << targ << " ...\n"; recurse = New spf_t (addr, sender, loopcheck, recdepth + 1); recurse->cb = wrap (this, &spf_t::mech_include_2); recurse->domain = targ; recurse->helo = helo; recurse->ptr_cache = ptr_cache; recurse->tracedepth = tracedepth + 1; recurse->init (); } void spf_t::mech_include_2 (spf_t *) { spf_result res = recurse->result; bool error = (res == SPF_NONE || res == SPF_ERROR || res == SPF_UNKNOWN); if (error || res == SPF_PASS || spftrace) { strbuf sb; sb << curmech << " ("; if (recurse->curmech) sb << recurse->curmech << " -> "; sb << spf_print (res) << ")"; curmech = sb; } recurse = NULL; if (res == SPF_NONE) res = SPF_UNKNOWN; mech_end (res); } void spf_t::mech_a (str targ) { int cidrlen = 32; if (!targ) targ = domain; else { if (!macro_subst (&targ, targ)) { getptr (wrap (this, &spf_t::mech_a, targ)); return; } if (!domcidr.match (targ) || domcidr[3]) { mech_end (SPF_FAIL); return; } if (str cl = domcidr[4]) cidrlen = atoi (domcidr[4]); if (str d = domcidr[1]) targ = domcidr[1]; else targ = domain; } if (dnsreq_t *d = dns_hostbyname (targ, wrap (this, &spf_t::mech_a_2, cidrlen), false, false)) dnsrq = d; } void spf_t::mech_a_2 (int cidrlen, ptr h, int err) { dnsrq = NULL; if (err) mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL); else if (addr_check (cidrlen, h)) mech_end (SPF_PASS); else mech_end (SPF_FAIL); } void spf_t::mech_mx (str targ) { int cidrlen = 32; if (!targ) targ = domain; else { if (!macro_subst (&targ, targ)) { getptr (wrap (this, &spf_t::mech_a, targ)); return; } if (!domcidr.match (targ) || domcidr[3]) { mech_end (SPF_FAIL); return; } if (str cl = domcidr[4]) cidrlen = atoi (domcidr[4]); if (str d = domcidr[1]) targ = domcidr[1]; else targ = domain; } if (dnsreq_t *d = dns_mxbyname (targ, wrap (this, &spf_t::mech_mx_2, cidrlen))) dnsrq = d; } void spf_t::mech_mx_2 (int cidrlen, ptr mxl, int err) { dnsrq = NULL; if (spftrace >= 5) printmxlist ("mech_mx_2", mxl, err); if (err) mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL); else if (dnsreq_t *d = dns_hostbyname (mxl->m_mxes[0].name, wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, 1), false, false)) dnsrq = d; } void spf_t::mech_mx_3 (int cidrlen, ptr mxl, int n, ptr h, int err) { dnsrq = NULL; if (h && addr_check (cidrlen, h)) mech_end (SPF_PASS); else if (err && dns_tmperr (err)) mech_end (SPF_ERROR); else if (n >= mxl->m_nmx) mech_end (SPF_FAIL); else if (dnsreq_t *d = dns_hostbyname (mxl->m_mxes[n].name, wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, n+1), false, false)) dnsrq = d; } void spf_t::mech_ptr (str targ) { if (!ptrok ()) { getptr (wrap (this, &spf_t::mech_ptr, targ)); return; } if (!targ) targ = domain; macro_subst (&targ, targ); if (!ptr_cache) { mech_end (dns_tmperr (ptr_cache_err) ? SPF_ERROR : SPF_FAIL); return; } if (suffix_check (ptr_cache->h_name, targ)) { mech_end (SPF_PASS); return; } for (char **np = ptr_cache->h_aliases; *np; np++) if (suffix_check (targ, *np)) { mech_end (SPF_PASS); return; } mech_end (SPF_FAIL); } void spf_t::mech_ip4 (str targ) { in_addr a; if (!targ || !ip4cidr.match (targ) || !inet_aton (ip4cidr[1], &a)) { mech_end (SPF_UNKNOWN); return; } u_int32_t mask; if (str cl = ip4cidr[4]) mask = ip4_mask (atoi (cl)); else mask = 0xffffffff; if ((addr.s_addr & mask) == (a.s_addr & mask)) mech_end (SPF_PASS); else mech_end (SPF_FAIL); } void spf_t::mech_ip6 (str targ) { /* Never matches, no IPv6 support */ mech_end (SPF_FAIL); } void spf_t::mech_exists (str targ) { if (!targ) targ = domain; else if (!macro_subst (&targ, targ)) { getptr (wrap (this, &spf_t::mech_exists, targ)); return; } if (dnsreq_t *d = dns_hostbyname (targ, wrap (this, &spf_t::mech_exists_2, targ), false, false)) dnsrq = d; } void spf_t::mech_exists_2 (str targ, ptr h, int err) { dnsrq = NULL; if (spftrace >= 4) printaddrs (targ, h, err); if (h) mech_end (SPF_PASS); else if (dns_tmperr (err)) mech_end (SPF_ERROR); else mech_end (SPF_FAIL); } static void spf_check_3 (spfckcb_t cb, spf_result override, str omech, spf_t *spf) { spf_result res (spf->result); if (override != SPF_INVALID) { if (res == SPF_NEUTRAL || res == SPF_NONE || res == SPF_UNKNOWN) { res = override; (*cb) (res, spf->explain, omech); } else (*cb) (res, spf->explain, spf->curmech); return; } switch (res) { case SPF_FAIL: if (opt->spf_fail) { spf->spfrec = opt->spf_fail; spf->cb = wrap (spf_check_3, cb, res, spf->curmech); spf->domain = NULL; spf->explain = opt->spf_exp; return; } break; case SPF_NONE: if (opt->spf_none) { spf->spfrec = opt->spf_none; spf->cb = wrap (spf_check_3, cb, res, spf->curmech); spf->domain = NULL; spf->explain = opt->spf_exp; return; } break; default: break; } (*cb) (res, spf->explain, spf->curmech); } static void spf_check_2 (spfckcb_t cb, spf_t *spf) { spf_result res (spf->result); switch (res) { case SPF_PASS: case SPF_FAIL: case SPF_SOFTFAIL: case SPF_ERROR: (*cb) (res, spf->explain, NULL); return; default: break; } spf->spfrec = NULL; spf->explain = opt->spf_exp; spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL)); } void spf_check (in_addr a, str from, spfckcb_t cb, str helo, ptr ptrc) { spf_t *spf = New spf_t (a, from); spf->helo = helo; spf->ptr_cache = ptrc; spf->spfrec = opt->spf_local; spf->explain = opt->spf_exp; spf->fallback = &smap; if (opt->spf_local) spf->cb = wrap (spf_check_2, cb); else spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL)); spf->init (); } const char * spf_print (spf_result res) { switch (res) { case SPF_PASS: return "pass"; case SPF_FAIL: return "fail"; case SPF_SOFTFAIL: return "softfail"; case SPF_NEUTRAL: return "neutral"; case SPF_NONE: return "none"; case SPF_ERROR: return "error"; case SPF_UNKNOWN: return "unknown"; default: return "bad SPF result code"; } } const char * spf1_print (spf_result res) { switch (res) { case SPF_PASS: return "Pass"; case SPF_FAIL: return "Fail"; case SPF_SOFTFAIL: return "SoftFail"; case SPF_NEUTRAL: return "Neutral"; case SPF_NONE: return "None"; case SPF_ERROR: return "TempError"; case SPF_UNKNOWN: return "PermError"; default: return "bad SPF result code"; } }