[MEDIUM] session-counters: add HTTP req/err tracking
This patch adds support for the following session counters :
- http_req_cnt : HTTP request count
- http_req_rate: HTTP request rate
- http_err_cnt : HTTP request error count
- http_err_rate: HTTP request error rate
The equivalent ACLs have been added to check the tracked counters
for the current session or the counters of the current source.
diff --git a/include/proto/session.h b/include/proto/session.h
index 133e6c9..a5dd881 100644
--- a/include/proto/session.h
+++ b/include/proto/session.h
@@ -98,6 +98,45 @@
s->term_trace |= code;
}
+/* Increase the number of cumulated HTTP requests in the tracked counters */
+static void inline session_inc_http_req_ctr(struct session *s)
+{
+ if (s->tracked_counters) {
+ void *ptr;
+
+ ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_REQ_CNT);
+ if (ptr)
+ stktable_data_cast(ptr, http_req_cnt)++;
+
+ ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_REQ_RATE);
+ if (ptr)
+ update_freq_ctr_period(&stktable_data_cast(ptr, http_req_rate),
+ s->tracked_table->data_arg[STKTABLE_DT_HTTP_REQ_RATE].u, 1);
+ }
+}
+
+/* Increase the number of cumulated failed HTTP requests in the tracked
+ * counters. Only 4xx requests should be counted here so that we can
+ * distinguish between errors caused by client behaviour and other ones.
+ * Note that even 404 are interesting because they're generally caused by
+ * vulnerability scans.
+ */
+static void inline session_inc_http_err_ctr(struct session *s)
+{
+ if (s->tracked_counters) {
+ void *ptr;
+
+ ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_ERR_CNT);
+ if (ptr)
+ stktable_data_cast(ptr, http_err_cnt)++;
+
+ ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_ERR_RATE);
+ if (ptr)
+ update_freq_ctr_period(&stktable_data_cast(ptr, http_err_rate),
+ s->tracked_table->data_arg[STKTABLE_DT_HTTP_ERR_RATE].u, 1);
+ }
+}
+
#endif /* _PROTO_SESSION_H */
/*
diff --git a/include/types/stick_table.h b/include/types/stick_table.h
index a1bfc1a..45eac02 100644
--- a/include/types/stick_table.h
+++ b/include/types/stick_table.h
@@ -49,6 +49,10 @@
STKTABLE_DT_CONN_CUR, /* concurrent number of connections */
STKTABLE_DT_SESS_CNT, /* cumulated number of sessions (accepted connections) */
STKTABLE_DT_SESS_RATE, /* accepted sessions rate */
+ STKTABLE_DT_HTTP_REQ_CNT, /* cumulated number of incoming HTTP requests */
+ STKTABLE_DT_HTTP_REQ_RATE,/* incoming HTTP request rate */
+ STKTABLE_DT_HTTP_ERR_CNT, /* cumulated number of HTTP requests errors (4xx) */
+ STKTABLE_DT_HTTP_ERR_RATE,/* HTTP request error rate */
STKTABLE_DT_BYTES_IN_CNT, /* cumulated bytes count from client to servers */
STKTABLE_DT_BYTES_IN_RATE,/* bytes rate from client to servers */
STKTABLE_DT_BYTES_OUT_CNT,/* cumulated bytes count from servers to client */
@@ -72,6 +76,10 @@
unsigned int conn_cur;
unsigned int sess_cnt;
struct freq_ctr_period sess_rate;
+ unsigned int http_req_cnt;
+ struct freq_ctr_period http_req_rate;
+ unsigned int http_err_cnt;
+ struct freq_ctr_period http_err_rate;
unsigned long long bytes_in_cnt;
struct freq_ctr_period bytes_in_rate;
unsigned long long bytes_out_cnt;
diff --git a/src/proto_http.c b/src/proto_http.c
index 57ab14b..7e28762 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -2439,6 +2439,10 @@
* we note the error in the session flags but don't set any state.
* Since the error will be noted there, it will not be counted by
* process_session() as a frontend error.
+ * Last, we may increase some tracked counters' http request errors on
+ * the cases that are deliberately the client's fault. For instance,
+ * a timeout or connection reset is not counted as an error. However
+ * a bad request is.
*/
if (unlikely(msg->msg_state < HTTP_MSG_BODY)) {
@@ -2446,6 +2450,8 @@
* First, let's catch bad requests.
*/
if (unlikely(msg->msg_state == HTTP_MSG_ERROR)) {
+ session_inc_http_req_ctr(s);
+ session_inc_http_err_ctr(s);
proxy_inc_fe_req_ctr(s->fe);
goto return_bad_req;
}
@@ -2459,6 +2465,8 @@
/* FIXME: check if URI is set and return Status
* 414 Request URI too long instead.
*/
+ session_inc_http_req_ctr(s);
+ session_inc_http_err_ctr(s);
proxy_inc_fe_req_ctr(s->fe);
goto return_bad_req;
}
@@ -2472,11 +2480,15 @@
goto failed_keep_alive;
/* we cannot return any message on error */
- if (msg->err_pos >= 0)
+ if (msg->err_pos >= 0) {
http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
+ session_inc_http_err_ctr(s);
+ }
+
msg->msg_state = HTTP_MSG_ERROR;
req->analysers = 0;
+ session_inc_http_req_ctr(s);
proxy_inc_fe_req_ctr(s->fe);
s->fe->counters.failed_req++;
if (s->listener->counters)
@@ -2496,13 +2508,16 @@
goto failed_keep_alive;
/* read timeout : give up with an error message. */
- if (msg->err_pos >= 0)
+ if (msg->err_pos >= 0) {
http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
+ session_inc_http_err_ctr(s);
+ }
txn->status = 408;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_408));
msg->msg_state = HTTP_MSG_ERROR;
req->analysers = 0;
+ session_inc_http_req_ctr(s);
proxy_inc_fe_req_ctr(s->fe);
s->fe->counters.failed_req++;
if (s->listener->counters)
@@ -2528,6 +2543,8 @@
msg->msg_state = HTTP_MSG_ERROR;
req->analysers = 0;
+ session_inc_http_err_ctr(s);
+ session_inc_http_req_ctr(s);
proxy_inc_fe_req_ctr(s->fe);
s->fe->counters.failed_req++;
if (s->listener->counters)
@@ -2588,6 +2605,7 @@
* left uninitialized (for instance in the absence of headers).
*/
+ session_inc_http_req_ctr(s);
proxy_inc_fe_req_ctr(s->fe); /* one more valid request for this FE */
if (txn->flags & TX_WAIT_NEXT_RQ) {
@@ -2867,6 +2885,7 @@
/* let's log the request time */
s->logs.tv_request = now;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+ session_inc_http_err_ctr(s);
goto return_prx_cond;
}
}
@@ -2898,6 +2917,7 @@
txn->status = 403;
s->logs.tv_request = now;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+ session_inc_http_err_ctr(s);
goto return_prx_cond;
}
@@ -2913,6 +2933,7 @@
/* let's log the request time */
s->logs.tv_request = now;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+ session_inc_http_err_ctr(s);
goto return_prx_cond;
}
@@ -2932,6 +2953,7 @@
req->analyse_exp = tick_add_ifset(now_ms, s->be->timeout.tarpit);
if (!req->analyse_exp)
req->analyse_exp = tick_add(now_ms, 0);
+ session_inc_http_err_ctr(s);
return 1;
}
}
@@ -3014,6 +3036,11 @@
chunk_initlen(&msg, trash, sizeof(trash), strlen(trash));
txn->status = 401;
stream_int_retnclose(req->prod, &msg);
+ /* on 401 we still count one error, because normal browsing
+ * won't significantly increase the counter but brute force
+ * attempts will.
+ */
+ session_inc_http_err_ctr(s);
goto return_prx_cond;
}
@@ -3578,8 +3605,10 @@
if (!ret)
goto missing_data;
- else if (ret < 0)
+ else if (ret < 0) {
+ session_inc_http_err_ctr(s);
goto return_bad_req;
+ }
}
/* Now we're in HTTP_MSG_DATA or HTTP_MSG_TRAILERS state.
@@ -3597,8 +3626,10 @@
missing_data:
/* we get here if we need to wait for more data */
- if (req->flags & BF_FULL)
+ if (req->flags & BF_FULL) {
+ session_inc_http_err_ctr(s);
goto return_bad_req;
+ }
if ((req->flags & BF_READ_TIMEOUT) || tick_is_expired(req->analyse_exp, now_ms)) {
txn->status = 408;
@@ -4178,8 +4209,10 @@
if (!ret)
goto missing_data;
- else if (ret < 0)
+ else if (ret < 0) {
+ session_inc_http_err_ctr(s);
goto return_bad_req;
+ }
/* otherwise we're in HTTP_MSG_DATA or HTTP_MSG_TRAILERS state */
}
else if (msg->msg_state == HTTP_MSG_DATA_CRLF) {
@@ -4194,8 +4227,10 @@
if (ret == 0)
goto missing_data;
- else if (ret < 0)
+ else if (ret < 0) {
+ session_inc_http_err_ctr(s);
goto return_bad_req;
+ }
/* we're in MSG_CHUNK_SIZE now */
}
else if (msg->msg_state == HTTP_MSG_TRAILERS) {
@@ -4203,8 +4238,10 @@
if (ret == 0)
goto missing_data;
- else if (ret < 0)
+ else if (ret < 0) {
+ session_inc_http_err_ctr(s);
goto return_bad_req;
+ }
/* we're in HTTP_MSG_DONE now */
}
else {
@@ -4512,6 +4549,14 @@
n = msg->sol[msg->sl.st.c] - '0';
if (n < 1 || n > 5)
n = 0;
+ /* when the client triggers a 4xx from the server, it's most often due
+ * to a missing object or permission. These events should be tracked
+ * because if they happen often, it may indicate a brute force or a
+ * vulnerability scan.
+ */
+ if (n == 4)
+ session_inc_http_err_ctr(s);
+
if (s->srv)
s->srv->counters.p.http.rsp[n]++;
diff --git a/src/session.c b/src/session.c
index aaa0622..1266935 100644
--- a/src/session.c
+++ b/src/session.c
@@ -2483,6 +2483,204 @@
return acl_fetch_sess_rate(&px->table, test, stktable_lookup_key(&px->table, key));
}
+/* set test->i to the cumulated number of sessions in the stksess entry <ts> */
+static int
+acl_fetch_http_req_cnt(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+ test->flags = ACL_TEST_F_VOL_TEST;
+ test->i = 0;
+ if (ts != NULL) {
+ void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_REQ_CNT);
+ if (!ptr)
+ return 0; /* parameter not stored */
+ test->i = stktable_data_cast(ptr, http_req_cnt);
+ }
+ return 1;
+}
+
+/* set test->i to the cumulated number of sessions from the session's tracked counters */
+static int
+acl_fetch_trk_http_req_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ if (!l4->tracked_counters)
+ return 0;
+
+ return acl_fetch_http_req_cnt(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the cumulated number of session from the session's source
+ * address in the table pointed to by expr.
+ */
+static int
+acl_fetch_src_http_req_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ struct stktable_key *key;
+
+ key = tcpv4_src_to_stktable_key(l4);
+ if (!key)
+ return 0; /* only TCPv4 is supported right now */
+
+ if (expr->arg_len)
+ px = find_stktable(expr->arg.str);
+
+ if (!px)
+ return 0; /* table not found */
+
+ return acl_fetch_http_req_cnt(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the session rate in the stksess entry <ts> over the configured period */
+static int
+acl_fetch_http_req_rate(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+ test->flags = ACL_TEST_F_VOL_TEST;
+ test->i = 0;
+ if (ts != NULL) {
+ void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_REQ_RATE);
+ if (!ptr)
+ return 0; /* parameter not stored */
+ test->i = read_freq_ctr_period(&stktable_data_cast(ptr, http_req_rate),
+ table->data_arg[STKTABLE_DT_HTTP_REQ_RATE].u);
+ }
+ return 1;
+}
+
+/* set test->i to the session rate from the session's tracked counters over
+ * the configured period.
+ */
+static int
+acl_fetch_trk_http_req_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ if (!l4->tracked_counters)
+ return 0;
+
+ return acl_fetch_http_req_rate(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the session rate from the session's source address in the
+ * table pointed to by expr, over the configured period.
+ */
+static int
+acl_fetch_src_http_req_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ struct stktable_key *key;
+
+ key = tcpv4_src_to_stktable_key(l4);
+ if (!key)
+ return 0; /* only TCPv4 is supported right now */
+
+ if (expr->arg_len)
+ px = find_stktable(expr->arg.str);
+
+ if (!px)
+ return 0; /* table not found */
+
+ return acl_fetch_http_req_rate(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the cumulated number of sessions in the stksess entry <ts> */
+static int
+acl_fetch_http_err_cnt(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+ test->flags = ACL_TEST_F_VOL_TEST;
+ test->i = 0;
+ if (ts != NULL) {
+ void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_ERR_CNT);
+ if (!ptr)
+ return 0; /* parameter not stored */
+ test->i = stktable_data_cast(ptr, http_err_cnt);
+ }
+ return 1;
+}
+
+/* set test->i to the cumulated number of sessions from the session's tracked counters */
+static int
+acl_fetch_trk_http_err_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ if (!l4->tracked_counters)
+ return 0;
+
+ return acl_fetch_http_err_cnt(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the cumulated number of session from the session's source
+ * address in the table pointed to by expr.
+ */
+static int
+acl_fetch_src_http_err_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ struct stktable_key *key;
+
+ key = tcpv4_src_to_stktable_key(l4);
+ if (!key)
+ return 0; /* only TCPv4 is supported right now */
+
+ if (expr->arg_len)
+ px = find_stktable(expr->arg.str);
+
+ if (!px)
+ return 0; /* table not found */
+
+ return acl_fetch_http_err_cnt(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the session rate in the stksess entry <ts> over the configured period */
+static int
+acl_fetch_http_err_rate(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+ test->flags = ACL_TEST_F_VOL_TEST;
+ test->i = 0;
+ if (ts != NULL) {
+ void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_ERR_RATE);
+ if (!ptr)
+ return 0; /* parameter not stored */
+ test->i = read_freq_ctr_period(&stktable_data_cast(ptr, http_err_rate),
+ table->data_arg[STKTABLE_DT_HTTP_ERR_RATE].u);
+ }
+ return 1;
+}
+
+/* set test->i to the session rate from the session's tracked counters over
+ * the configured period.
+ */
+static int
+acl_fetch_trk_http_err_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ if (!l4->tracked_counters)
+ return 0;
+
+ return acl_fetch_http_err_rate(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the session rate from the session's source address in the
+ * table pointed to by expr, over the configured period.
+ */
+static int
+acl_fetch_src_http_err_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ struct stktable_key *key;
+
+ key = tcpv4_src_to_stktable_key(l4);
+ if (!key)
+ return 0; /* only TCPv4 is supported right now */
+
+ if (expr->arg_len)
+ px = find_stktable(expr->arg.str);
+
+ if (!px)
+ return 0; /* table not found */
+
+ return acl_fetch_http_err_rate(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
/* set test->i to the number of kbytes received from clients matching the stksess entry <ts> */
static int
acl_fetch_kbytes_in(struct stktable *table, struct acl_test *test, struct stksess *ts)
@@ -2709,6 +2907,14 @@
{ "src_sess_cnt", acl_parse_int, acl_fetch_src_sess_cnt, acl_match_int, ACL_USE_TCP4_VOLATILE },
{ "trk_sess_rate", acl_parse_int, acl_fetch_trk_sess_rate, acl_match_int, ACL_USE_NOTHING },
{ "src_sess_rate", acl_parse_int, acl_fetch_src_sess_rate, acl_match_int, ACL_USE_TCP4_VOLATILE },
+ { "trk_http_req_cnt", acl_parse_int, acl_fetch_trk_http_req_cnt, acl_match_int, ACL_USE_NOTHING },
+ { "src_http_req_cnt", acl_parse_int, acl_fetch_src_http_req_cnt, acl_match_int, ACL_USE_TCP4_VOLATILE },
+ { "trk_http_req_rate", acl_parse_int, acl_fetch_trk_http_req_rate, acl_match_int, ACL_USE_NOTHING },
+ { "src_http_req_rate", acl_parse_int, acl_fetch_src_http_req_rate, acl_match_int, ACL_USE_TCP4_VOLATILE },
+ { "trk_http_err_cnt", acl_parse_int, acl_fetch_trk_http_err_cnt, acl_match_int, ACL_USE_NOTHING },
+ { "src_http_err_cnt", acl_parse_int, acl_fetch_src_http_err_cnt, acl_match_int, ACL_USE_TCP4_VOLATILE },
+ { "trk_http_err_rate", acl_parse_int, acl_fetch_trk_http_err_rate, acl_match_int, ACL_USE_NOTHING },
+ { "src_http_err_rate", acl_parse_int, acl_fetch_src_http_err_rate, acl_match_int, ACL_USE_TCP4_VOLATILE },
{ "trk_kbytes_in", acl_parse_int, acl_fetch_trk_kbytes_in, acl_match_int, ACL_USE_TCP4_VOLATILE },
{ "src_kbytes_in", acl_parse_int, acl_fetch_src_kbytes_in, acl_match_int, ACL_USE_TCP4_VOLATILE },
{ "trk_bytes_in_rate", acl_parse_int, acl_fetch_trk_bytes_in_rate, acl_match_int, ACL_USE_NOTHING },
diff --git a/src/stick_table.c b/src/stick_table.c
index 8b216ae..46d5e1e 100644
--- a/src/stick_table.c
+++ b/src/stick_table.c
@@ -552,6 +552,10 @@
[STKTABLE_DT_CONN_CUR] = { .name = "conn_cur", .data_length = stktable_data_size(conn_cur) },
[STKTABLE_DT_SESS_CNT] = { .name = "sess_cnt", .data_length = stktable_data_size(sess_cnt) },
[STKTABLE_DT_SESS_RATE] = { .name = "sess_rate", .data_length = stktable_data_size(sess_rate), .arg_type = ARG_T_DELAY },
+ [STKTABLE_DT_HTTP_REQ_CNT] = { .name = "http_req_cnt", .data_length = stktable_data_size(http_req_cnt) },
+ [STKTABLE_DT_HTTP_REQ_RATE] = { .name = "http_req_rate", .data_length = stktable_data_size(http_req_rate), .arg_type = ARG_T_DELAY },
+ [STKTABLE_DT_HTTP_ERR_CNT] = { .name = "http_err_cnt", .data_length = stktable_data_size(http_err_cnt) },
+ [STKTABLE_DT_HTTP_ERR_RATE] = { .name = "http_err_rate", .data_length = stktable_data_size(http_err_rate), .arg_type = ARG_T_DELAY },
[STKTABLE_DT_BYTES_IN_CNT] = { .name = "bytes_in_cnt", .data_length = stktable_data_size(bytes_in_cnt) },
[STKTABLE_DT_BYTES_IN_RATE] = { .name = "bytes_in_rate", .data_length = stktable_data_size(bytes_in_rate), .arg_type = ARG_T_DELAY },
[STKTABLE_DT_BYTES_OUT_CNT] = { .name = "bytes_out_cnt", .data_length = stktable_data_size(bytes_out_cnt) },