MEDIUM: listener: add support for limiting the session rate in addition to the connection rate

It's sometimes useful to be able to limit the connection rate on a machine
running many haproxy instances (eg: per customer) but it removes the ability
for that machine to defend itself against a DoS. Thus, better also provide a
limit on the session rate, which does not include the connections rejected by
"tcp-request connection" rules. This permits to have much higher limits on
the connection rate without having to raise the session rate limit to insane
values.

The limit can be changed on the CLI using "set rate-limit sessions global",
or in the global section using "maxsessrate".
diff --git a/src/cfgparse.c b/src/cfgparse.c
index e11730e..cab9d6e 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -875,6 +875,19 @@
 		}
 		global.cps_lim = atol(args[1]);
 	}
+	else if (!strcmp(args[0], "maxsessrate")) {
+		if (global.sps_lim != 0) {
+			Alert("parsing [%s:%d] : '%s' already specified. Continuing.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT;
+			goto out;
+		}
+		if (*(args[1]) == 0) {
+			Alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+		global.sps_lim = atol(args[1]);
+	}
 	else if (!strcmp(args[0], "maxcomprate")) {
 		if (*(args[1]) == 0) {
 			Alert("parsing [%s:%d] : '%s' expects an integer argument in kb/s.\n", file, linenum, args[0]);
diff --git a/src/dumpstats.c b/src/dumpstats.c
index 227abc5..b6a4dc4 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -1146,6 +1146,7 @@
 			}
 
 			global.cps_max = 0;
+			global.sps_max = 0;
 			return 1;
 		}
 		else if (strcmp(args[1], "table") == 0) {
@@ -1424,6 +1425,43 @@
 					return 1;
 				}
 			}
+			else if (strcmp(args[2], "sessions") == 0) {
+				if (strcmp(args[3], "global") == 0) {
+					int v;
+
+					if (s->listener->bind_conf->level < ACCESS_LVL_ADMIN) {
+						appctx->ctx.cli.msg = stats_permission_denied_msg;
+						appctx->st0 = STAT_CLI_PRINT;
+						return 1;
+					}
+
+					if (!*args[4]) {
+						appctx->ctx.cli.msg = "Expects an integer value.\n";
+						appctx->st0 = STAT_CLI_PRINT;
+						return 1;
+					}
+
+					v = atoi(args[4]);
+					if (v < 0) {
+						appctx->ctx.cli.msg = "Value out of range.\n";
+						appctx->st0 = STAT_CLI_PRINT;
+						return 1;
+					}
+
+					global.sps_lim = v;
+
+					/* Dequeues all of the listeners waiting for a resource */
+					if (!LIST_ISEMPTY(&global_listener_queue))
+						dequeue_all_listeners(&global_listener_queue);
+
+					return 1;
+				}
+				else {
+					appctx->ctx.cli.msg = "'set rate-limit sessions' only supports 'global'.\n";
+					appctx->st0 = STAT_CLI_PRINT;
+					return 1;
+				}
+			}
 			else if (strcmp(args[2], "http-compression") == 0) {
 				if (strcmp(args[3], "global") == 0) {
 					int v;
@@ -1444,7 +1482,7 @@
 				}
 			}
 			else {
-				appctx->ctx.cli.msg = "'set rate-limit' supports 'connections' and 'http-compression'.\n";
+				appctx->ctx.cli.msg = "'set rate-limit' supports 'connections', 'sessions', and 'http-compression'.\n";
 				appctx->st0 = STAT_CLI_PRINT;
 				return 1;
 			}
@@ -2182,6 +2220,9 @@
 	             "ConnRate: %d\n"
 	             "ConnRateLimit: %d\n"
 	             "MaxConnRate: %d\n"
+	             "SessRate: %d\n"
+	             "SessRateLimit: %d\n"
+	             "MaxSessRate: %d\n"
 	             "CompressBpsIn: %u\n"
 	             "CompressBpsOut: %u\n"
 	             "CompressBpsRateLim: %u\n"
@@ -2209,6 +2250,7 @@
 #endif
 		     global.maxpipes, pipes_used, pipes_free,
 	             read_freq_ctr(&global.conn_per_sec), global.cps_lim, global.cps_max,
+	             read_freq_ctr(&global.sess_per_sec), global.sps_lim, global.sps_max,
 	             read_freq_ctr(&global.comp_bps_in), read_freq_ctr(&global.comp_bps_out),
 	             global.comp_rate_lim,
 #ifdef USE_ZLIB
diff --git a/src/listener.c b/src/listener.c
index 836ca70..95a1199 100644
--- a/src/listener.c
+++ b/src/listener.c
@@ -263,13 +263,31 @@
 		return;
 	}
 
-	if (global.cps_lim && !(l->options & LI_O_UNLIMITED)) {
+	if (!(l->options & LI_O_UNLIMITED) && global.sps_lim) {
+		int max = freq_ctr_remain(&global.sess_per_sec, global.sps_lim, 0);
+		int expire;
+
+		if (unlikely(!max)) {
+			/* frontend accept rate limit was reached */
+			limit_listener(l, &global_listener_queue);
+			expire = tick_add(now_ms, next_event_delay(&global.sess_per_sec, global.sps_lim, 0));
+			task_schedule(global_listener_queue_task, tick_first(expire, global_listener_queue_task->expire));
+			return;
+		}
+
+		if (max_accept > max)
+			max_accept = max;
+	}
+
+	if (!(l->options & LI_O_UNLIMITED) && global.cps_lim) {
 		int max = freq_ctr_remain(&global.conn_per_sec, global.cps_lim, 0);
+		int expire;
 
 		if (unlikely(!max)) {
 			/* frontend accept rate limit was reached */
 			limit_listener(l, &global_listener_queue);
-			task_schedule(global_listener_queue_task, tick_add(now_ms, next_event_delay(&global.conn_per_sec, global.cps_lim, 0)));
+			expire = tick_add(now_ms, next_event_delay(&global.conn_per_sec, global.cps_lim, 0));
+			task_schedule(global_listener_queue_task, tick_first(expire, global_listener_queue_task->expire));
 			return;
 		}
 
@@ -411,6 +429,13 @@
 			return;
 		}
 
+		/* increase the per-process number of cumulated connections */
+		if (!(l->options & LI_O_UNLIMITED)) {
+			update_freq_ctr(&global.sess_per_sec, 1);
+			if (global.sess_per_sec.curr_ctr > global.sps_max)
+				global.sps_max = global.sess_per_sec.curr_ctr;
+		}
+
 	} /* end of while (max_accept--) */
 
 	/* we've exhausted max_accept, so there is no need to poll again */