[MINOR] tcp: add per-source connection rate limiting

This change makes use of the stick-tables to keep track of any source
address activity. Two ACLs make it possible to check the count of an
entry or update it and act accordingly. The typical usage will be to
reject a TCP request upon match of an excess value.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 8366636..aaeefd0 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -6265,9 +6265,36 @@
   certain resources such as statistics. Note that it is the TCP-level source
   address which is used, and not the address of a client behind a proxy.
 
+src_count <integer>
+src_count(backend) <integer>
+  Returns the number of occurrences of the source IPv4 address in the current
+  backend's stick-table or in the designated stick-table. If the address is not
+  found, zero is returned.
+
 src_port <integer>
   Applies to the client's TCP source port. This has a very limited usage.
 
+src_update_count <integer>
+src_update_count(backend) <integer>
+  Creates or updates the entry associated to the source IPv4 address in the
+  current backend's stick-table or in the designated stick-table. This table
+  must be configured to store the "conn_cum" data type, otherwise the match
+  will be ignored. The current count is incremented by one, and the expiration
+  timer refreshed. The updated count is returned, so this match can't return
+  zero. This is used to reject service abusers based on their source address.
+
+  Example :
+        # This frontend limits incoming SSH connections to 3 per 10 second for
+        # each source address, and rejects excess connections until a 10 second
+        # silence is observed. At most 20 addresses are tracked.
+        listen ssh
+            bind :22
+            mode tcp
+            maxconn 100
+            stick-table type ip size 20 expire 10s store conn_cum
+            tcp-request content reject if { src_update_count gt 3 }
+            server local 127.0.0.1:22
+
 srv_is_up(<server>)
 srv_is_up(<backend>/<server>)
   Returns true when the designated server is UP, and false when it is either
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index 1c93396..80c10fd 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -46,7 +46,9 @@
 #include <proto/protocols.h>
 #include <proto/proto_tcp.h>
 #include <proto/proxy.h>
+#include <proto/stick_table.h>
 #include <proto/stream_sock.h>
+#include <proto/task.h>
 
 #ifdef CONFIG_HAP_CTTPROXY
 #include <import/ip_tproxy.h>
@@ -1018,6 +1020,87 @@
 	return 1;
 }
 
+/* set test->i to the number of connections from the session's source address
+ * in the table pointed to by expr.
+ */
+static int
+acl_fetch_src_count(struct proxy *px, struct session *l4, void *l7, int dir,
+		    struct acl_expr *expr, struct acl_test *test)
+{
+	struct stksess *ts;
+
+	/* right now we only support IPv4 */
+	if (l4->cli_addr.ss_family != AF_INET)
+		return 0;
+
+	if (expr->arg_len) {
+		/* another table was designated, we must look for it */
+		for (px = proxy; px; px = px->next)
+			if (strcmp(px->id, expr->arg.str) == 0)
+				break;
+	}
+	if (!px)
+		return 0; /* table not found */
+
+	static_table_key.key = (void *)&((struct sockaddr_in *)&l4->frt_addr)->sin_addr;
+	test->flags = ACL_TEST_F_VOL_TEST;
+	test->i = 0;
+	if ((ts = stktable_lookup_key(&px->table, &static_table_key)) != NULL) {
+		void *ptr = stktable_data_ptr(&px->table, ts, STKTABLE_DT_CONN_CUM);
+		if (!ptr)
+			return 0; /* parameter not stored */
+		test->i = stktable_data_cast(ptr, conn_cum);
+	}
+
+	return 1;
+}
+
+/* set test->i to the number of connections from the session's source address
+ * in the table pointed to by expr, after updating it.
+ */
+static int
+acl_fetch_src_update_count(struct proxy *px, struct session *l4, void *l7, int dir,
+			   struct acl_expr *expr, struct acl_test *test)
+{
+	struct stksess *ts;
+	void *ptr;
+
+	/* right now we only support IPv4 */
+	if (l4->cli_addr.ss_family != AF_INET)
+		return 0;
+
+	if (expr->arg_len) {
+		/* another table was designated, we must look for it */
+		for (px = proxy; px; px = px->next)
+			if (strcmp(px->id, expr->arg.str) == 0)
+				break;
+	}
+	if (!px)
+		return 0;
+
+	static_table_key.key = (void *)&((struct sockaddr_in *)&l4->frt_addr)->sin_addr;
+	if ((ts = stktable_lookup_key(&px->table, &static_table_key)) == NULL) {
+		/* entry does not exist, initialize a new one */
+		ts = stksess_new(&px->table, &static_table_key);
+		if (!ts)
+			return 0;
+		stktable_store(&px->table, ts);
+	}
+	else if (px->table.expire) {
+		/* if entries can expire, let's update the entry and the table */
+		ts->expire = tick_add(now_ms, MS_TO_TICKS(px->table.expire));
+		px->table.exp_task->expire = px->table.exp_next = tick_first(ts->expire, px->table.exp_next);
+		task_queue(px->table.exp_task);
+	}
+
+	ptr = stktable_data_ptr(&px->table, ts, STKTABLE_DT_CONN_CUM);
+	if (!ptr)
+		return 0; /* parameter not stored in this table */
+
+	test->i = ++stktable_data_cast(ptr, conn_cum);
+	test->flags = ACL_TEST_F_VOL_TEST;
+	return 1;
+}
 
 static struct cfg_kw_list cfg_kws = {{ },{
 	{ CFG_LISTEN, "tcp-request", tcp_parse_tcp_req },
@@ -1030,6 +1113,8 @@
 	{ "src",        acl_parse_ip,    acl_fetch_src,      acl_match_ip,  ACL_USE_TCP4_PERMANENT|ACL_MAY_LOOKUP },
 	{ "dst",        acl_parse_ip,    acl_fetch_dst,      acl_match_ip,  ACL_USE_TCP4_PERMANENT|ACL_MAY_LOOKUP },
 	{ "dst_port",   acl_parse_int,   acl_fetch_dport,    acl_match_int, ACL_USE_TCP_PERMANENT  },
+	{ "src_count",  acl_parse_int,   acl_fetch_src_count,acl_match_int, ACL_USE_TCP4_PERMANENT },
+	{ "src_update_count",  acl_parse_int,   acl_fetch_src_update_count, acl_match_int, ACL_USE_TCP4_PERMANENT },
 	{ NULL, NULL, NULL, NULL },
 }};