changeset 136:f4746d8a12a3

add smtp auth rate limits
author carl
date Tue, 26 Sep 2006 13:59:14 -0700
parents 8e813497582e
children a6190f7798f4
files ChangeLog NEWS configure.in dnsbl.conf hosts-ignore.conf src/context.cpp src/context.h src/dnsbl.cpp src/dnsbl.h xml/dnsbl.in
diffstat 10 files changed, 153 insertions(+), 25 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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
--- 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";
--- 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
+
--- 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");
--- 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<CONTEXTP>					  context_list;
 typedef map<char *, CONTEXTP, ltstr>	  context_map;
 typedef map<char *, int, ltstr> 		  ns_mapper;
+typedef map<char *, int, ltstr> 		  rcpt_rates;
 typedef map<char *, VERIFYP, ltstr> 	  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;
--- 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<int>	 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<CONFIG *> 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;
--- 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
--- 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.
             </para>
             <para>
+                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.
+            </para>
+            <para>
                 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 @@
         <refsect1 id='filtering.1'>
             <title>Filtering Procedure</title>
             <para>
-                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.
             </para>
             <orderedlist>
                 <listitem><para>
@@ -449,11 +456,6 @@
                 The following ideas are under consideration.
             </para>
             <para>
-                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.
-            </para>
-            <para>
                 Add a per-context option to reject mail if the number of digits in
                 the reverse dns client name exceeds some threshold.
             </para>
@@ -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)]]></literallayout>
@@ -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 {