# HG changeset patch # User Carl Byington # Date 1482023092 28800 # Node ID 9f8411f3919c527898c236ee4753044119d080a6 # Parent e172dc10fe246719ebd2f073dc26c8c5e67f1a01 add dkim white/black listing diff -r e172dc10fe24 -r 9f8411f3919c dnsbl.conf --- a/dnsbl.conf Sat Dec 17 13:47:28 2016 -0800 +++ b/dnsbl.conf Sat Dec 17 17:04:52 2016 -0800 @@ -53,6 +53,16 @@ require_rdns yes; content on { + dkim_signer { + sendgrid.me black; + weather.com white; + }; + + dkim_from { + yahoo.com require_signed yahoo.com; + gmail.com signed_white gmail.com; + girlscoutsla.org signed_white girlscoutsla.ccsend.com; + }; 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"; #uribl multi.uribl.com "Mail containing %s rejected - uribl; see http://l.uribl.com/?d=%s"; @@ -80,17 +90,6 @@ include "/etc/mail/local-host-names"; }; - dkim_signer { - sendgrid.me black; - weather.com white; - }; - - dkim_from { - yahoo.com require_signed yahoo.com; - gmail.com signed_white gmail.com; - girlscoutsla.org signed_white girlscoutsla.ccsend.com; - }; - context whitelist { content off {}; env_to { diff -r e172dc10fe24 -r 9f8411f3919c src/context.cpp --- a/src/context.cpp Sat Dec 17 13:47:28 2016 -0800 +++ b/src/context.cpp Sat Dec 17 17:04:52 2016 -0800 @@ -74,8 +74,8 @@ const char *token_signed_white; const char *token_signed_black; const char *token_require_signed; +const char *token_myhostname; -const char *token_myhostname; #ifndef HOST_NAME_MAX #define HOST_NAME_MAX 255 #endif @@ -1099,7 +1099,41 @@ } -bool CONTEXT::acceptable_content(recorder &memory, int score, int bulk, string& msg) { +bool CONTEXT::acceptable_content(recorder &memory, int score, int bulk, const char *signer, const char *from, string& msg) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "acceptable content from %s signer %s", (signer) ? signer : token_asterisk, (from) ? from : token_asterisk); + my_syslog(buf); + + const char *st = find_dkim_signer(signer); + if (st == token_white) return true; + if (st == token_black) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "Mail rejected - dkim signed by %s", signer); + msg = string(buf); + return false; + } + + DKIMP dk = find_dkim_from(from); + if (dk) { + st = dk->action; + // signed by a white listed signer + if ((st == token_signed_white) && (strcasecmp(signer,dk->signer) == 0)) return true; + // not signed by the required signer + if ((st == token_require_signed) && (strcasecmp(signer,dk->signer) != 0)) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "Mail rejected - not dkim signed by %s", dk->signer); + msg = string(buf); + return false; + } + // signed by a black listed signer + if ((st == token_signed_black) && (strcasecmp(signer,dk->signer) == 0)) { + char buf[maxlen]; + snprintf(buf, sizeof(buf), "Mail rejected - dkim signed by %s", dk->signer); + msg = string(buf); + return false; + } + } + if (spamassassin_limit && (score > spamassassin_limit)) { char buf[maxlen]; snprintf(buf, sizeof(buf), "Mail rejected - spam assassin score %d", score); @@ -1168,6 +1202,20 @@ if (content_filtering) { printf("%s content on { \n", indent); + printf("%s dkim_signer { \n", indent); + for (string_map::iterator i=dkim_signer_names.begin(); i!=dkim_signer_names.end(); i++) { + const char *n = (*i).first; + const char *a = (*i).second; + printf("%s %s %s; \n", indent, n, a); + } + printf("%s } \n", indent); + printf("%s dkim_from { \n", indent); + for (dkimp_map::iterator i=dkim_from_names.begin(); i!=dkim_from_names.end(); i++) { + const char *n = (*i).first; + DKIM &d = *(*i).second; + printf("%s %s %s %s; \n", indent, n, d.action, d.signer); + } + printf("%s } \n", indent); if (content_suffix) { printf("%s filter %s \"%s\"; \n", indent, content_suffix, content_message); } @@ -1231,23 +1279,6 @@ printf("%s content off {}; \n", indent); } - printf("%s dkim_signer { \n", indent); - for (string_map::iterator i=dkim_signer_names.begin(); i!=dkim_signer_names.end(); i++) { - const char *n = (*i).first; - const char *a = (*i).second; - printf("%s %s %s; \n", indent, n, a); - } - printf("%s } \n", indent); - - printf("%s dkim_from { \n", indent); - for (dkimp_map::iterator i=dkim_from_names.begin(); i!=dkim_from_names.end(); i++) { - const char *n = (*i).first; - DKIM &d = *(*i).second; - printf("%s %s %s %s; \n", indent, n, d.action, d.signer); - } - - printf("%s } \n", indent); - printf("%s env_to { \t// %s\n", indent, fullname); for (string_set::iterator i=env_to.begin(); i!=env_to.end(); i++) { printf("%s %s; \n", indent, *i); @@ -1463,6 +1494,62 @@ //////////////////////////////////////////////// // +bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me); +bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me) { + if (!tsa(tok, token_lbrace)) return false; + while (true) { + const char *have = tok.next(); + if (!have) break; + if (have == token_rbrace) break; + if (have == token_semi) { + // optional separators + } + else { + const char *signer = have; + const char *action = tok.next(); + if ((action == token_white) || (action == token_black)) { + me.add_dkim_signer(signer, action); + } + else { + tok.token_error("white/black", action); + } + } + } + return tsa(tok, token_semi); +} + + +//////////////////////////////////////////////// +// +bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me); +bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me) { + if (!tsa(tok, token_lbrace)) return false; + while (true) { + const char *have = tok.next(); + if (!have) break; + if (have == token_rbrace) break; + if (have == token_semi) { + // optional separators + } + else { + const char *from = have; + const char *action = tok.next(); + if ((action == token_signed_white) || (action == token_signed_black) || (action == token_require_signed)) { + const char *signer = tok.next(); + if (!signer) break; + else me.add_dkim_from(from, action, signer); + } + else { + tok.token_error("signed_white/signed_black/require_signed", action); + } + } + } + return tsa(tok, token_semi); +} + + +//////////////////////////////////////////////// +// bool parse_content(TOKEN &tok, CONFIG &dc, CONTEXT &me); bool parse_content(TOKEN &tok, CONFIG &dc, CONTEXT &me) { const char *setting = tok.next(); @@ -1628,6 +1715,12 @@ } if (!tsa(tok, token_semi)) return false; } + else if (have == token_dkim_signer) { + if (!parse_dkim_signer(tok, dc, me)) return false; + } + else if (have == token_dkim_from) { + if (!parse_dkim_from(tok, dc, me)) return false; + } else if (have == token_rbrace) { break; // done } @@ -1859,62 +1952,6 @@ //////////////////////////////////////////////// // -bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me); -bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me) { - if (!tsa(tok, token_lbrace)) return false; - while (true) { - const char *have = tok.next(); - if (!have) break; - if (have == token_rbrace) break; - if (have == token_semi) { - // optional separators - } - else { - const char *signer = have; - const char *action = tok.next(); - if ((action == token_white) || (action == token_black)) { - me.add_dkim_signer(signer, action); - } - else { - tok.token_error("white/black", action); - } - } - } - return tsa(tok, token_semi); -} - - -//////////////////////////////////////////////// -// -bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me); -bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me) { - if (!tsa(tok, token_lbrace)) return false; - while (true) { - const char *have = tok.next(); - if (!have) break; - if (have == token_rbrace) break; - if (have == token_semi) { - // optional separators - } - else { - const char *from = have; - const char *action = tok.next(); - if ((action == token_signed_white) || (action == token_signed_black) || (action == token_require_signed)) { - const char *signer = tok.next(); - if (!signer) break; - else me.add_dkim_from(from, action, signer); - } - else { - tok.token_error("signed_white/signed_black/require_signed", action); - } - } - } - return tsa(tok, token_semi); -} - - -//////////////////////////////////////////////// -// bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent); bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent) { const char *name = tok.next(); diff -r e172dc10fe24 -r 9f8411f3919c src/context.h --- a/src/context.h Sat Dec 17 13:47:28 2016 -0800 +++ b/src/context.h Sat Dec 17 17:04:52 2016 -0800 @@ -312,7 +312,7 @@ dnsblp_list& get_dnsbl_list(); dnswlp_list& get_dnswl_list(); - bool acceptable_content(recorder &memory, int score, int bulk, string& msg); + bool acceptable_content(recorder &memory, int score, int bulk, const char *signer, const char *from, string& msg); bool ignore_host(const char *host); void dump(bool isdefault, bool &spamass, int level = 0); @@ -389,10 +389,12 @@ extern const char *token_white; extern const char *token_white_regex; extern const char *token_yes; -extern const char *token_dkim; +extern const char *token_dkim_signer; +extern const char *token_dkim_from; extern const char *token_signed_white; extern const char *token_signed_black; extern const char *token_require_signed; +extern const char *token_myhostname; extern pthread_mutex_t verifier_mutex; // protect the verifier map extern pthread_mutex_t whitelister_mutex; // protect the diff -r e172dc10fe24 -r 9f8411f3919c src/dnsbl.cpp --- a/src/dnsbl.cpp Sat Dec 17 13:47:28 2016 -0800 +++ b/src/dnsbl.cpp Sat Dec 17 17:04:52 2016 -0800 @@ -98,6 +98,8 @@ const int maxlen = 1000; // used for snprintf buffers regex_t srs_pattern; // used to detect srs coding in mail addresses regex_t prvs_pattern; // used to detect prvs coding in mail addresses +regex_t dkim_pattern; // used to detect dkim signatures in authentication header generated by the upstream opendkim milter +regex_t from_pattern; // used to extract the senders mail domain from the body from: header pthread_mutex_t config_mutex; pthread_mutex_t syslog_mutex; @@ -522,6 +524,8 @@ mailaddr = NULL; fromaddr = NULL; header_count = 0; + dkim_ok = true; + dkim_signer = NULL; queueid = NULL; authenticated = NULL; client_name = NULL; @@ -570,6 +574,7 @@ } if (mailaddr) free((void*)mailaddr); if (fromaddr) free((void*)fromaddr); + if (dkim_signer) free((void*)dkim_signer); if (queueid) free((void*)queueid); if (authenticated) free((void*)authenticated); if (client_name) free((void*)client_name); @@ -587,6 +592,8 @@ mailaddr = NULL; fromaddr = NULL; header_count = 0; + dkim_ok = true; + dkim_signer = NULL; queueid = NULL; authenticated = NULL; client_name = NULL; @@ -1450,8 +1457,8 @@ { mlfiPriv &priv = *MLFIPRIV; priv.header_count++; + char msg[maxlen]; if ((priv.header_count < 4) || (strcasecmp(headerf, "from") == 0)) { - char msg[maxlen]; snprintf(msg, sizeof(msg), "header %s: %s", headerf, headerv); for (int i=0; i 2) && (strcasecmp(headerf, "from"))) { + const int nmatch = 2; + regmatch_t match[nmatch]; + if (0 == regexec(&from_pattern, msg, nmatch, match, 0)) { + int s1 = match[1].rm_so; // domain + int e1 = match[1].rm_eo; + if (s1 != -1) { + msg[e1] = '\0'; + priv.fromaddr = strdup(msg+s1); + } + } + } + } + // headers that avoid autowhitelisting if (((strcasecmp(headerf, "precedence") == 0) && (strcasecmp(headerv, "bulk") == 0)) || ((strcasecmp(headerf, "content-type") == 0) && (strncasecmp(headerv, "multipart/report", 16) == 0))) { @@ -1546,9 +1585,11 @@ for (context_map::iterator i=priv.env_to.begin(); i!=priv.env_to.end(); i++) { const char *rcpt = (*i).first; CONTEXT &con = *((*i).second); - if (!con.acceptable_content(*priv.memory, score, bulk, msg)) { + if (!con.acceptable_content(*priv.memory, score, bulk, priv.dkim_signer, priv.fromaddr, msg)) { // bad html tags or excessive hosts or // high spam assassin score or dcc bulk threshold exceedeed + // or signed by a dkim signer that we don't like + // or header from requires dkim signer that is missing smfi_delrcpt(ctx, (char*)rcpt); } else { @@ -1804,6 +1845,18 @@ exit(3); } + // setup dkim signature detection + if (regcomp(&dkim_pattern, " dkim=pass .* header.d=([^ ]+) ", REG_ICASE | REG_EXTENDED)) { + printf("cannot compile regex pattern to find dkim signatures\n"); + exit(3); + } + + // setup from domain extraction + if (regcomp(&from_pattern, "@([a-zA-Z0-9.-]+)", REG_ICASE | REG_EXTENDED)) { + printf("cannot compile regex pattern to find dkim signatures\n"); + exit(3); + } + // Process command line options while ((c = getopt(argc, argv, args)) != -1) { switch (c) { diff -r e172dc10fe24 -r 9f8411f3919c src/dnsbl.h --- a/src/dnsbl.h Sat Dec 17 13:47:28 2016 -0800 +++ b/src/dnsbl.h Sat Dec 17 17:04:52 2016 -0800 @@ -40,6 +40,8 @@ const char *mailaddr; // envelope from value const char *fromaddr; // header from value, set by mlfi_header() int header_count; // count of headers already seen + bool dkim_ok; // ok to proceed with dkim checking + const char *dkim_signer; // non null if message was validly signed const char *queueid; // sendmail queue id const char *authenticated; // client authenticated? if so, suppress all dnsbl checks, but check rate limits const char *client_name; // fully qualified host name of the smtp client xxx [ip.ad.dr.es] (may be forged) diff -r e172dc10fe24 -r 9f8411f3919c xml/dnsbl.in --- a/xml/dnsbl.in Sat Dec 17 13:47:28 2016 -0800 +++ b/xml/dnsbl.in Sat Dec 17 17:04:52 2016 -0800 @@ -706,7 +706,7 @@ CONTEXT = "context" NAME "{" {STATEMENT}+ "}" STATEMENT = ( DNSBL | DNSBLLIST | DNSWL | DNSWLLIST | CONTENT | ENV-TO | VERIFY | GENERIC | W_REGEX | AUTOWHITE | CONTEXT | ENV-FROM - | DKIM_SIGNER | DKIM_FROM | RATE-LIMIT | REQUIRERDNS) ";" + | RATE-LIMIT | REQUIRERDNS) ";" DNSBL = "dnsbl" NAME DNSPREFIX ERROR-MSG1 DNSBLLIST = "dnsbl_list" {NAME}* @@ -719,7 +719,8 @@ CONTENT = "content" ("on" | "off") "{" {CONTENT-ST}+ "}" CONTENT-ST = (FILTER | URIBL | IGNORE | TLD | HTML-TAGS | HTML-LIMIT | - HOST-LIMIT | SPAMASS | REQUIRE | DCCGREY | DCCBULK) ";" + HOST-LIMIT | SPAMASS | REQUIRE | DCCGREY | DCCBULK | DKIM_SIGNER | + DKIM_FROM) ";" FILTER = "filter" DNSPREFIX ERROR-MSG2 URIBL = "uribl" DNSPREFIX ERROR-MSG3 IGNORE = "ignore" "{" {HOSTNAME [";"]}+ "}"