# HG changeset patch # User carl # Date 1159304354 25200 # Node ID f4746d8a12a3ba15728aa2f11490320a4d2f86ce # Parent 8e813497582ec82bf85a7452d2e16b329218c03c add smtp auth rate limits diff -r 8e813497582e -r f4746d8a12a3 ChangeLog --- a/ChangeLog Wed Aug 02 21:33:34 2006 -0700 +++ b/ChangeLog Tue Sep 26 13:59:14 2006 -0700 @@ -1,5 +1,9 @@ $Id$ +5.21 2006-09-26 + Add SMTP AUTH recipient rate limits, to help throttle infected + client machines and accounts with weak cracked passwords. + 5.20 2006-08-02 Fully qualify all dns lookups. Fix my_read() bug. Try to convert names that might be ip addresses via inet_aton before doing dns diff -r 8e813497582e -r f4746d8a12a3 NEWS --- a/NEWS Wed Aug 02 21:33:34 2006 -0700 +++ b/NEWS Tue Sep 26 13:59:14 2006 -0700 @@ -1,5 +1,6 @@ $Id$ +5.21 2006-09-26 Add SMTP AUTH recipient rate limits 5.20 2006-08-02 fully qualify all dns lookups; fix my_read() bug 5.19 2006-08-01 uribl dnsl lookups fully qualified; allow two component host names; rpm properly creates user 5.18 2006-04-27 sendmail no longer guarantees <> wrapper on envelopes, don't ask uribls about rfc1918 space either diff -r 8e813497582e -r f4746d8a12a3 configure.in --- a/configure.in Wed Aug 02 21:33:34 2006 -0700 +++ b/configure.in Tue Sep 26 13:59:14 2006 -0700 @@ -1,7 +1,7 @@ AC_INIT(configure.in) AM_CONFIG_HEADER(config.h) -AM_INIT_AUTOMAKE(dnsbl,5.20) +AM_INIT_AUTOMAKE(dnsbl,5.21) AC_PATH_PROGS(BASH, bash) AC_LANG_CPLUSPLUS diff -r 8e813497582e -r f4746d8a12a3 dnsbl.conf --- a/dnsbl.conf Wed Aug 02 21:33:34 2006 -0700 +++ b/dnsbl.conf Tue Sep 26 13:59:14 2006 -0700 @@ -1,5 +1,6 @@ context main-default { // outbound dnsbl filtering to catch our own customers that end up on the sbl + dnsbl localp partial.blackholes.five-ten-sg.com "Mail from %s rejected - local; see http://www.five-ten-sg.com/blackhole.php?%s"; dnsbl local blackholes.five-ten-sg.com "Mail from %s rejected - local; see http://www.five-ten-sg.com/blackhole.php?%s"; dnsbl sbl sbl-xbl.spamhaus.org "Mail from %s rejected - sbl; see http://www.spamhaus.org/query/bl?ip=%s"; dnsbl dul dul.dnsbl.sorbs.net "Mail from %s rejected - dul; see http://www.sorbs.net/lookup.shtml?%s"; @@ -25,9 +26,15 @@ env_from unknown { "<>" black; }; + + // per recipient rates - only available in the default (first top level) context + rate_limit { + " " 30; // default specified by user name composed of a single blank + }; }; context main { + dnsbl localp partial.blackholes.five-ten-sg.com "Mail from %s rejected - local; see http://www.five-ten-sg.com/blackhole.php?%s"; dnsbl local blackholes.five-ten-sg.com "Mail from %s rejected - local; see http://www.five-ten-sg.com/blackhole.php?%s"; dnsbl sbl sbl-xbl.spamhaus.org "Mail from %s rejected - sbl; see http://www.spamhaus.org/query/bl?ip=%s"; dnsbl xbl xbl.spamhaus.org "Mail from %s rejected - xbl; see http://www.spamhaus.org/query/bl?ip=%s"; diff -r 8e813497582e -r f4746d8a12a3 hosts-ignore.conf --- a/hosts-ignore.conf Wed Aug 02 21:33:34 2006 -0700 +++ b/hosts-ignore.conf Tue Sep 26 13:59:14 2006 -0700 @@ -2,3 +2,9 @@ messenger.msn.click-url.com # hotmail using a spammer search.msn.click-url.com # hotmail using a spammer toolbar.msn.click-url.com # hotmail using a spammer + +# ignore some common hostnames in mail +www.microsoft.com +microsoft.com +www.w3.org + diff -r 8e813497582e -r f4746d8a12a3 src/context.cpp --- a/src/context.cpp Wed Aug 02 21:33:34 2006 -0700 +++ b/src/context.cpp Tue Sep 26 13:59:14 2006 -0700 @@ -45,6 +45,7 @@ char *token_ok2; char *token_ok; char *token_on; +char *token_rate; char *token_rbrace; char *token_semi; char *token_soft; @@ -553,6 +554,14 @@ } +int CONTEXT::find_rate(char *user) { + if (rcpt_per_hour.empty()) return INT_MAX; + rcpt_rates::iterator i = rcpt_per_hour.find(user); + if (i == rcpt_per_hour.end()) i = rcpt_per_hour.find(" "); + return (i == rcpt_per_hour.end()) ? INT_MAX : (*i).second; +} + + char *CONTEXT::find_from(char *from) { char *rc = token_inherit; string_map::iterator i = env_from.find(from); @@ -795,6 +804,16 @@ } printf("%s }; \n", indent); + if (!rcpt_per_hour.empty()) { + printf("%s rate_limit { \n", indent); + for (rcpt_rates::iterator j=rcpt_per_hour.begin(); j!=rcpt_per_hour.end(); j++) { + char *u = (*j).first; + int l = (*j).second; + printf("%s \"%s\" \t%d; \n", indent, u, l); + } + printf("%s }; \n", indent); + } + printf("%s }; \n", indent); } @@ -1171,6 +1190,28 @@ //////////////////////////////////////////////// // +bool parse_rate(TOKEN &tok, CONFIG &dc, CONTEXT &me); +bool parse_rate(TOKEN &tok, CONFIG &dc, CONTEXT &me) { + if (!tsa(tok, token_lbrace)) return false; + while (true) { + char *have = tok.next(); + if (!have) break; + if (have == token_rbrace) break; + if (have == token_semi) { + // optional separators + } + else { + char *limit = tok.next(); + int lim = (limit) ? atoi(limit) : 0; + me.add_rate(have, lim); + } + } + return tsa(tok, token_semi); +} + + +//////////////////////////////////////////////// +// bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent); bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent) { char *name = tok.next(); @@ -1199,6 +1240,9 @@ else if (have == token_envfrom) { if (!parse_envfrom(tok, dc, *con)) return false; } + else if ((have == token_rate) && (!parent) && (!dc.default_context)) { + if (!parse_rate(tok, dc, *con)) return false; + } else if (have == token_context) { if (!parse_context(tok, dc, con)) return false; } @@ -1300,6 +1344,7 @@ token_ok = register_string("ok"); token_ok2 = register_string("ok2"); token_on = register_string("on"); + token_rate = register_string("rate_limit"); token_rbrace = register_string("}"); token_semi = register_string(";"); token_soft = register_string("soft"); diff -r 8e813497582e -r f4746d8a12a3 src/context.h --- a/src/context.h Wed Aug 02 21:33:34 2006 -0700 +++ b/src/context.h Tue Sep 26 13:59:14 2006 -0700 @@ -28,6 +28,7 @@ typedef list context_list; typedef map context_map; typedef map ns_mapper; +typedef map rcpt_rates; typedef map verify_map; class SMTP { @@ -111,6 +112,8 @@ char * tag_limit_message; // error message for excessive bad html tags dnsblp_map dnsbl_names; // name to dnsbl mapping for lists that are available in this context and children dnsblp_list dnsbl_list; // list of dnsbls to be used in this context + rcpt_rates rcpt_per_hour; // per user limits on number of recipients per hour + public: CONTEXT(CONTEXTP parent_, char *name_); @@ -126,6 +129,9 @@ char* get_verify() {return verify_host;}; VERIFYP find_verify(char *to); + void add_rate(char *user, int limit) {rcpt_per_hour[user] = limit;}; + int find_rate(char *user); + void add_to(char *to) {env_to.insert(to);}; void add_from(char *from, char *status) {env_from[from] = status;}; void add_from_context(char *from, CONTEXTP con) {env_from_context[from] = con;}; @@ -194,6 +200,10 @@ void dump(); }; +struct RATELIMIT { + +}; + extern char *token_black; extern char *token_cctld; extern char *token_content; @@ -219,6 +229,7 @@ extern char *token_ok2; extern char *token_ok; extern char *token_on; +extern char *token_rate; extern char *token_rbrace; extern char *token_semi; extern char *token_soft; diff -r 8e813497582e -r f4746d8a12a3 src/dnsbl.cpp --- a/src/dnsbl.cpp Wed Aug 02 21:33:34 2006 -0700 +++ b/src/dnsbl.cpp Tue Sep 26 13:59:14 2006 -0700 @@ -86,6 +86,7 @@ pthread_mutex_t syslog_mutex; pthread_mutex_t resolve_mutex; pthread_mutex_t fd_pool_mutex; +pthread_mutex_t rate_mutex; std::set fd_pool; int NULL_SOCKET = -1; @@ -95,6 +96,7 @@ time_t last_error_time; int resolver_sock_count = 0; // protected with fd_pool_mutex int resolver_pool_size = 0; // protected with fd_pool_mutex +rcpt_rates rcpt_counts; // protected with rate_mutex struct ns_map { @@ -141,6 +143,19 @@ //////////////////////////////////////////////// +// helper to manipulate recipient counts +// +int incr_rcpt_count(char *user); +int incr_rcpt_count(char *user) { + pthread_mutex_lock(&rate_mutex); + rcpt_rates::iterator i = rcpt_counts.find(user); + int c = (i == rcpt_counts.end()) ? 0 : (*i).second; + rcpt_counts[user] = c++; + pthread_mutex_unlock(&rate_mutex); + return c; +} + +//////////////////////////////////////////////// // helper to discard the strings held by a context_map // void discard(context_map &cm); @@ -224,7 +239,7 @@ ip = 0; mailaddr = NULL; queueid = NULL; - authenticated = false; + authenticated = NULL; have_whites = false; only_whites = true; memory = NULL; @@ -247,13 +262,14 @@ void mlfiPriv::reset(bool final) { if (mailaddr) free(mailaddr); if (queueid) free(queueid); + if (authenticated) free(authenticated); discard(env_to); if (memory) delete memory; if (scanner) delete scanner; if (!final) { mailaddr = NULL; queueid = NULL; - authenticated = false; + authenticated = NULL; have_whites = false; only_whites = true; memory = NULL; @@ -912,7 +928,8 @@ { mlfiPriv &priv = *MLFIPRIV; priv.mailaddr = to_lower_string(from[0]); - priv.authenticated = (smfi_getsymval(ctx, "{auth_authen}") != NULL); + priv.authenticated = smfi_getsymval(ctx, "{auth_authen}"); + if (priv.authenticated) priv.authenticated = strdup(priv.authenticated); return SMFIS_CONTINUE; } @@ -936,8 +953,22 @@ char *fromvalue = con.find_from(priv.mailaddr); status st; if (priv.authenticated) { + int c = incr_rcpt_count(priv.authenticated); + int l = dc.default_context->find_rate(priv.authenticated); + if (c > l) { + smfi_setreply(ctx, "550", "5.7.1", "recipient rate limit exceeded"); + return SMFIS_REJECT; + } + else { + if (debug_syslog > 1) { + char buf[maxlen]; + char msg[maxlen]; + snprintf(msg, sizeof(msg), "authenticated id %s (%d recipients, %d limit)", priv.authenticated, c, l); + my_syslog(&priv, msg); + } st = white; } + } else if (fromvalue == token_black) { st = black; } @@ -1127,14 +1158,24 @@ // thread to watch the old config files for changes // and reload when needed. we also cleanup old // configs whose reference count has gone to zero. +// we also clear the SMTP AUTH recipient counts hourly // void* config_loader(void *arg); void* config_loader(void *arg) { + int loop = 0; typedef set configp_set; configp_set old_configs; while (loader_run) { sleep(180); // look for modifications every 3 minutes if (!loader_run) break; + loop++; + if (loop == 20) { + // three minutes thru each loop, 20 loops per hour + // clear the recipient counts + pthread_mutex_lock(&rate_mutex); + rcpt_counts.clear(); + pthread_mutex_unlock(&rate_mutex); + } CONFIG &dc = *config; time_t then = dc.load_time; struct stat st; diff -r 8e813497582e -r f4746d8a12a3 src/dnsbl.h --- a/src/dnsbl.h Wed Aug 02 21:33:34 2006 -0700 +++ b/src/dnsbl.h Tue Sep 26 13:59:14 2006 -0700 @@ -22,7 +22,7 @@ // message specific data char *mailaddr; // envelope from value char *queueid; // sendmail queue id - bool authenticated; // client authenticated? if so, suppress all dnsbl checks + char *authenticated; // client authenticated? if so, suppress all dnsbl checks, but check rate limits bool have_whites; // have at least one whitelisted recipient? need to accept content and remove all non-whitelisted recipients if it fails bool only_whites; // every recipient is whitelisted? context_map env_to; // map each non-whitelisted recipient to their filtering context diff -r 8e813497582e -r f4746d8a12a3 xml/dnsbl.in --- a/xml/dnsbl.in Wed Aug 02 21:33:34 2006 -0700 +++ b/xml/dnsbl.in Tue Sep 26 13:59:14 2006 -0700 @@ -171,6 +171,11 @@ specified limit. + This milter can also impose hourly rate limits on the number of + recipients accepted from SMTP AUTH connections, that would otherwise be + allowed to relay thru this mail server with no spam filtering. + + The DNSBL milter reads a text configuration file (dnsbl.conf) on startup, and whenever the config file (or any of the referenced include files) is changed. The entire configuration file is case insensitive. @@ -247,10 +252,12 @@ Filtering Procedure - If the client has authenticated with sendmail, the mail is accepted, the - filtering contexts are not used, the dns lists are not checked, and the - body content is not scanned. Otherwise, we follow these steps for each - recipient. + If the client has authenticated with sendmail, the rate limits are + checked. If the authenticated user has not exceeded the hourly rate + limits, then the mail is accepted, the filtering contexts are not used, + the dns lists are not checked, and the body content is not scanned. If + the client has not authenticated with sendmail, we follow these steps + for each recipient. @@ -449,11 +456,6 @@ The following ideas are under consideration. - Add mail volume limits based on smtp auth accounts, to prevent - customers from sending too much mail. This should catch customers - that get infected with malware that knows about smtp auth. - - Add a per-context option to reject mail if the number of digits in the reverse dns client name exceeds some threshold. @@ -523,7 +525,7 @@ CONFIG = {CONTEXT ";"}+ CONTEXT = "context" NAME "{" {STATEMENT}+ "}" STATEMENT = (DNSBL | DNSBLLIST | CONTENT | ENV-TO | VERIFY | - CONTEXT | ENV-FROM) ";" + CONTEXT | ENV-FROM | RATE-LIMIT) ";" DNSBL = "dnsbl" NAME DNSPREFIX ERROR-MSG1 @@ -560,6 +562,10 @@ ENV_FROM = "env_from" [DEFAULT] "{" {(FROM-ADDR | DCC-FROM)}+ "}" FROM-ADDR = ADDRESS VALUE [";"] DCC-FROM = "dcc_from" "{" DCCINCLUDEFILE "}" ";" + +RATE-LIMIT = "rate_limit" "{" (RATE)+ "}" +RATE = USER LIMIT [";"] + DEFAULT = ("white" | "black" | "unknown" | "inherit" | "") ADDRESS = (USER@ | DOMAIN | USER@DOMAIN) VALUE = ("white" | "black" | "unknown" | CHILD-CONTEXT-NAME)]]> @@ -595,6 +601,13 @@ env_from unknown { "<>" black; }; + + // per recipient rates - only available in the default (first top level) context + rate_limit { + " " 30; // default specified by user name composed of a single blank + fred 100; // override default limits + joe 10; + }; }; context sample {