MEDIUM: tcp: add registration and processing of TCP L5 rules

This commit introduces "tcp-request session" rules. These are very
much like "tcp-request connection" rules except that they're processed
after the handshake, so it is possible to consider SSL information and
addresses rewritten by the proxy protocol header in actions. This is
particularly useful to track proxied sources as this was not possible
before, given that tcp-request content rules are processed after each
HTTP request. Similarly it is possible to assign the proxied source
address or the client's cert to a variable.
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 17f9d19..cc2f507 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -358,6 +358,19 @@
 	return alertif_too_many_args_idx(maxarg, 0, file, linenum, args, err_code);
 }
 
+/* Report a warning if a rule is placed after a 'tcp-request session' rule.
+ * Return 1 if the warning has been emitted, otherwise 0.
+ */
+int warnif_rule_after_tcp_sess(struct proxy *proxy, const char *file, int line, const char *arg)
+{
+	if (!LIST_ISEMPTY(&proxy->tcp_req.l5_rules)) {
+		Warning("parsing [%s:%d] : a '%s' rule placed after a 'tcp-request session' rule will still be processed before.\n",
+			file, line, arg);
+		return 1;
+	}
+	return 0;
+}
+
 /* Report a warning if a rule is placed after a 'tcp-request content' rule.
  * Return 1 if the warning has been emitted, otherwise 0.
  */
@@ -465,6 +478,19 @@
 /* report a warning if a "tcp request connection" rule is dangerously placed */
 int warnif_misplaced_tcp_conn(struct proxy *proxy, const char *file, int line, const char *arg)
 {
+	return	warnif_rule_after_tcp_sess(proxy, file, line, arg) ||
+		warnif_rule_after_tcp_cont(proxy, file, line, arg) ||
+		warnif_rule_after_block(proxy, file, line, arg) ||
+		warnif_rule_after_http_req(proxy, file, line, arg) ||
+		warnif_rule_after_reqxxx(proxy, file, line, arg) ||
+		warnif_rule_after_reqadd(proxy, file, line, arg) ||
+		warnif_rule_after_redirect(proxy, file, line, arg) ||
+		warnif_rule_after_use_backend(proxy, file, line, arg) ||
+		warnif_rule_after_use_server(proxy, file, line, arg);
+}
+
+int warnif_misplaced_tcp_sess(struct proxy *proxy, const char *file, int line, const char *arg)
+{
 	return	warnif_rule_after_tcp_cont(proxy, file, line, arg) ||
 		warnif_rule_after_block(proxy, file, line, arg) ||
 		warnif_rule_after_http_req(proxy, file, line, arg) ||
@@ -7810,6 +7836,45 @@
 			}
 		}
 
+		/* find the target table for 'tcp-request' layer 5 rules */
+		list_for_each_entry(trule, &curproxy->tcp_req.l5_rules, list) {
+			struct proxy *target;
+
+			if (trule->action < ACT_ACTION_TRK_SC0 || trule->action > ACT_ACTION_TRK_SCMAX)
+				continue;
+
+			if (trule->arg.trk_ctr.table.n)
+				target = proxy_tbl_by_name(trule->arg.trk_ctr.table.n);
+			else
+				target = curproxy;
+
+			if (!target) {
+				Alert("Proxy '%s': unable to find table '%s' referenced by track-sc%d.\n",
+				      curproxy->id, trule->arg.trk_ctr.table.n,
+				      tcp_trk_idx(trule->action));
+				cfgerr++;
+			}
+			else if (target->table.size == 0) {
+				Alert("Proxy '%s': table '%s' used but not configured.\n",
+				      curproxy->id, trule->arg.trk_ctr.table.n ? trule->arg.trk_ctr.table.n : curproxy->id);
+				cfgerr++;
+			}
+			else if (!stktable_compatible_sample(trule->arg.trk_ctr.expr,  target->table.type)) {
+				Alert("Proxy '%s': stick-table '%s' uses a type incompatible with the 'track-sc%d' rule.\n",
+				      curproxy->id, trule->arg.trk_ctr.table.n ? trule->arg.trk_ctr.table.n : curproxy->id,
+				      tcp_trk_idx(trule->action));
+				cfgerr++;
+			}
+			else {
+				free(trule->arg.trk_ctr.table.n);
+				trule->arg.trk_ctr.table.t = &target->table;
+				/* Note: if we decide to enhance the track-sc syntax, we may be able
+				 * to pass a list of counters to track and allocate them right here using
+				 * stktable_alloc_data_type().
+				 */
+			}
+		}
+
 		/* find the target table for 'tcp-request' layer 6 rules */
 		list_for_each_entry(trule, &curproxy->tcp_req.inspect_rules, list) {
 			struct proxy *target;
@@ -8830,6 +8895,9 @@
 			if (!LIST_ISEMPTY(&curproxy->tcp_req.l4_rules))
 				listener->options |= LI_O_TCP_L4_RULES;
 
+			if (!LIST_ISEMPTY(&curproxy->tcp_req.l5_rules))
+				listener->options |= LI_O_TCP_L5_RULES;
+
 			if (curproxy->mon_mask.s_addr)
 				listener->options |= LI_O_CHK_MONNET;
 
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index 0223b8e..8b3d546 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -70,6 +70,7 @@
 
 /* List head of all known action keywords for "tcp-request connection" */
 struct list tcp_req_conn_keywords = LIST_HEAD_INIT(tcp_req_conn_keywords);
+struct list tcp_req_sess_keywords = LIST_HEAD_INIT(tcp_req_sess_keywords);
 struct list tcp_req_cont_keywords = LIST_HEAD_INIT(tcp_req_cont_keywords);
 struct list tcp_res_cont_keywords = LIST_HEAD_INIT(tcp_res_cont_keywords);
 
@@ -127,6 +128,11 @@
 	LIST_ADDQ(&tcp_req_conn_keywords, &kw_list->list);
 }
 
+void tcp_req_sess_keywords_register(struct action_kw_list *kw_list)
+{
+	LIST_ADDQ(&tcp_req_sess_keywords, &kw_list->list);
+}
+
 void tcp_req_cont_keywords_register(struct action_kw_list *kw_list)
 {
 	LIST_ADDQ(&tcp_req_cont_keywords, &kw_list->list);
@@ -138,13 +144,18 @@
 }
 
 /*
- * Return the struct http_req_action_kw associated to a keyword.
+ * Return the struct tcp_req_action_kw associated to a keyword.
  */
 static struct action_kw *tcp_req_conn_action(const char *kw)
 {
 	return action_lookup(&tcp_req_conn_keywords, kw);
 }
 
+static struct action_kw *tcp_req_sess_action(const char *kw)
+{
+	return action_lookup(&tcp_req_sess_keywords, kw);
+}
+
 static struct action_kw *tcp_req_cont_action(const char *kw)
 {
 	return action_lookup(&tcp_req_cont_keywords, kw);
@@ -1437,6 +1448,85 @@
 	return result;
 }
 
+/* This function performs the TCP layer5 analysis on the current request. It
+ * returns 0 if a reject rule matches, otherwise 1 if either an accept rule
+ * matches or if no more rule matches. It can only use rules which don't need
+ * any data. This only works on session-based client-facing stream interfaces.
+ * An example of valid use case is to track a stick-counter on the source
+ * address extracted from the proxy protocol.
+ */
+int tcp_exec_l5_rules(struct session *sess)
+{
+	struct act_rule *rule;
+	struct stksess *ts;
+	struct stktable *t = NULL;
+	int result = 1;
+	enum acl_test_res ret;
+
+	list_for_each_entry(rule, &sess->fe->tcp_req.l5_rules, list) {
+		ret = ACL_TEST_PASS;
+
+		if (rule->cond) {
+			ret = acl_exec_cond(rule->cond, sess->fe, sess, NULL, SMP_OPT_DIR_REQ|SMP_OPT_FINAL);
+			ret = acl_pass(ret);
+			if (rule->cond->pol == ACL_COND_UNLESS)
+				ret = !ret;
+		}
+
+		if (ret) {
+			/* we have a matching rule. */
+			if (rule->action == ACT_ACTION_ALLOW) {
+				break;
+			}
+			else if (rule->action == ACT_ACTION_DENY) {
+				sess->fe->fe_counters.denied_sess++;
+				if (sess->listener->counters)
+					sess->listener->counters->denied_sess++;
+
+				result = 0;
+				break;
+			}
+			else if (rule->action >= ACT_ACTION_TRK_SC0 && rule->action <= ACT_ACTION_TRK_SCMAX) {
+				/* Note: only the first valid tracking parameter of each
+				 * applies.
+				 */
+				struct stktable_key *key;
+
+				if (stkctr_entry(&sess->stkctr[tcp_trk_idx(rule->action)]))
+					continue;
+
+				t = rule->arg.trk_ctr.table.t;
+				key = stktable_fetch_key(t, sess->fe, sess, NULL, SMP_OPT_DIR_REQ|SMP_OPT_FINAL, rule->arg.trk_ctr.expr, NULL);
+
+				if (key && (ts = stktable_get_entry(t, key)))
+					stream_track_stkctr(&sess->stkctr[tcp_trk_idx(rule->action)], t, ts);
+			}
+			else {
+				/* Custom keywords. */
+				if (!rule->action_ptr)
+					break;
+				switch (rule->action_ptr(rule, sess->fe, sess, NULL, ACT_FLAG_FINAL | ACT_FLAG_FIRST)) {
+				case ACT_RET_YIELD:
+					/* yield is not allowed at this point. If this return code is
+					 * used it is a bug, so I prefer to abort the process.
+					 */
+					send_log(sess->fe, LOG_WARNING,
+					         "Internal error: yield not allowed with tcp-request session actions.");
+				case ACT_RET_STOP:
+					break;
+				case ACT_RET_CONT:
+					continue;
+				case ACT_RET_ERR:
+					result = 0;
+					break;
+				}
+				break; /* ACT_RET_STOP */
+			}
+		}
+	}
+	return result;
+}
+
 /*
  * Execute the "set-src" action. May be called from {tcp,http}request.
  * It only changes the address and tries to preserve the original port. If the
@@ -1885,6 +1975,11 @@
 			kw = tcp_req_conn_action(args[arg]);
 			rule->kw = kw;
 			rule->from = ACT_F_TCP_REQ_CON;
+		} else if (where & SMP_VAL_FE_SES_ACC) {
+			/* L5 */
+			kw = tcp_req_sess_action(args[arg]);
+			rule->kw = kw;
+			rule->from = ACT_F_TCP_REQ_SES;
 		} else {
 			/* L6 */
 			kw = tcp_req_cont_action(args[arg]);
@@ -1898,6 +1993,8 @@
 		} else {
 			if (where & SMP_VAL_FE_CON_ACC)
 				action_build_list(&tcp_req_conn_keywords, &trash);
+			else if (where & SMP_VAL_FE_SES_ACC)
+				action_build_list(&tcp_req_sess_keywords, &trash);
 			else
 				action_build_list(&tcp_req_cont_keywords, &trash);
 			memprintf(err,
@@ -2174,6 +2271,50 @@
 		warnif_misplaced_tcp_conn(curpx, file, line, args[0]);
 		LIST_ADDQ(&curpx->tcp_req.l4_rules, &rule->list);
 	}
+	else if (strcmp(args[1], "session") == 0) {
+		arg++;
+
+		if (!(curpx->cap & PR_CAP_FE)) {
+			memprintf(err, "%s %s is not allowed because %s %s is not a frontend",
+			          args[0], args[1], proxy_type_str(curpx), curpx->id);
+			goto error;
+		}
+
+		where |= SMP_VAL_FE_SES_ACC;
+
+		if (tcp_parse_request_rule(args, arg, section_type, curpx, defpx, rule, err, where, file, line) < 0)
+			goto error;
+
+		acl = rule->cond ? acl_cond_conflicts(rule->cond, where) : NULL;
+		if (acl) {
+			if (acl->name && *acl->name)
+				memprintf(err,
+					  "acl '%s' will never match in '%s %s' because it only involves keywords that are incompatible with '%s'",
+					  acl->name, args[0], args[1], sample_ckp_names(where));
+			else
+				memprintf(err,
+					  "anonymous acl will never match in '%s %s' because it uses keyword '%s' which is incompatible with '%s'",
+					  args[0], args[1],
+					  LIST_ELEM(acl->expr.n, struct acl_expr *, list)->kw,
+					  sample_ckp_names(where));
+			warn++;
+		}
+		else if (rule->cond && acl_cond_kw_conflicts(rule->cond, where, &acl, &kw)) {
+			if (acl->name && *acl->name)
+				memprintf(err,
+					  "acl '%s' involves keyword '%s' which is incompatible with '%s'",
+					  acl->name, kw, sample_ckp_names(where));
+			else
+				memprintf(err,
+					  "anonymous acl involves keyword '%s' which is incompatible with '%s'",
+					  kw, sample_ckp_names(where));
+			warn++;
+		}
+
+		/* the following function directly emits the warning */
+		warnif_misplaced_tcp_sess(curpx, file, line, args[0]);
+		LIST_ADDQ(&curpx->tcp_req.l5_rules, &rule->list);
+	}
 	else {
 		if (curpx == defpx)
 			memprintf(err,
@@ -2844,6 +2985,15 @@
 	{ /* END */ }
 }};
 
+static struct action_kw_list tcp_req_sess_actions = {ILH, {
+	{ "silent-drop",  tcp_parse_silent_drop },
+	{ "set-src",      tcp_parse_set_src_dst },
+	{ "set-src-port", tcp_parse_set_src_dst },
+	{ "set-dst"     , tcp_parse_set_src_dst },
+	{ "set-dst-port", tcp_parse_set_src_dst },
+	{ /* END */ }
+}};
+
 static struct action_kw_list tcp_req_cont_actions = {ILH, {
 	{ "silent-drop", tcp_parse_silent_drop },
 	{ /* END */ }
@@ -2880,6 +3030,7 @@
 	bind_register_keywords(&bind_kws);
 	srv_register_keywords(&srv_kws);
 	tcp_req_conn_keywords_register(&tcp_req_conn_actions);
+	tcp_req_sess_keywords_register(&tcp_req_sess_actions);
 	tcp_req_cont_keywords_register(&tcp_req_cont_actions);
 	tcp_res_cont_keywords_register(&tcp_res_cont_actions);
 	http_req_keywords_register(&http_req_actions);
diff --git a/src/proxy.c b/src/proxy.c
index b90773f..1955d82 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -737,6 +737,7 @@
 	LIST_INIT(&p->tcp_req.inspect_rules);
 	LIST_INIT(&p->tcp_rep.inspect_rules);
 	LIST_INIT(&p->tcp_req.l4_rules);
+	LIST_INIT(&p->tcp_req.l5_rules);
 	LIST_INIT(&p->req_add);
 	LIST_INIT(&p->rsp_add);
 	LIST_INIT(&p->listener_queue);
diff --git a/src/session.c b/src/session.c
index d160a05..cdf57e3 100644
--- a/src/session.c
+++ b/src/session.c
@@ -267,6 +267,10 @@
 	if (sess->fe->to_log & LW_XPRT)
 		cli_conn->flags |= CO_FL_XPRT_TRACKED;
 
+	/* we may have some tcp-request-session rules */
+	if ((l->options & LI_O_TCP_L5_RULES) && !tcp_exec_l5_rules(sess))
+		goto out_free_sess;
+
 	session_count_new(sess);
 	strm = stream_new(sess, t, &cli_conn->obj_type);
 	if (!strm)
@@ -435,6 +439,10 @@
 	if (sess->fe->to_log & LW_XPRT)
 		conn->flags |= CO_FL_XPRT_TRACKED;
 
+	/* we may have some tcp-request-session rules */
+	if ((sess->listener->options & LI_O_TCP_L5_RULES) && !tcp_exec_l5_rules(sess))
+		goto fail;
+
 	session_count_new(sess);
 	task->process = sess->listener->handler;
 	strm = stream_new(sess, task, &conn->obj_type);
diff --git a/src/stick_table.c b/src/stick_table.c
index b269bc1..7a2fcc2 100644
--- a/src/stick_table.c
+++ b/src/stick_table.c
@@ -1519,6 +1519,12 @@
 	{ /* END */ }
 }};
 
+static struct action_kw_list tcp_sess_kws = { { }, {
+	{ "sc-inc-gpc0", parse_inc_gpc0, 1 },
+	{ "sc-set-gpt0", parse_set_gpt0, 1 },
+	{ /* END */ }
+}};
+
 static struct action_kw_list tcp_req_kws = { { }, {
 	{ "sc-inc-gpc0", parse_inc_gpc0, 1 },
 	{ "sc-set-gpt0", parse_set_gpt0, 1 },
@@ -1572,6 +1578,7 @@
 {
 	/* register som action keywords. */
 	tcp_req_conn_keywords_register(&tcp_conn_kws);
+	tcp_req_sess_keywords_register(&tcp_sess_kws);
 	tcp_req_cont_keywords_register(&tcp_req_kws);
 	tcp_res_cont_keywords_register(&tcp_res_kws);
 	http_req_keywords_register(&http_req_kws);
diff --git a/src/vars.c b/src/vars.c
index a3dd85c..6f9b16a 100644
--- a/src/vars.c
+++ b/src/vars.c
@@ -359,7 +359,7 @@
 	return 1;
 }
 
-/* Returns 0 if fails, else returns 1. */
+/* Returns 0 if fails, else returns 1. Note that stream may be null for SCOPE_SESS. */
 static inline int sample_store_stream(const char *name, enum vars_scope scope, struct sample *smp)
 {
 	struct vars *vars;
@@ -504,6 +504,7 @@
 	int dir;
 
 	switch (rule->from) {
+	case ACT_F_TCP_REQ_SES: dir = SMP_OPT_DIR_REQ; break;
 	case ACT_F_TCP_REQ_CNT: dir = SMP_OPT_DIR_REQ; break;
 	case ACT_F_TCP_RES_CNT: dir = SMP_OPT_DIR_RES; break;
 	case ACT_F_HTTP_REQ:    dir = SMP_OPT_DIR_REQ; break;
@@ -587,6 +588,7 @@
 		return ACT_RET_PRS_ERR;
 
 	switch (rule->from) {
+	case ACT_F_TCP_REQ_SES: flags = SMP_VAL_FE_SES_ACC; break;
 	case ACT_F_TCP_REQ_CNT: flags = SMP_VAL_FE_REQ_CNT; break;
 	case ACT_F_TCP_RES_CNT: flags = SMP_VAL_BE_RES_CNT; break;
 	case ACT_F_HTTP_REQ:    flags = SMP_VAL_FE_HRQ_HDR; break;
@@ -663,7 +665,12 @@
 	{ /* END */ },
 }};
 
+static struct action_kw_list tcp_req_sess_kws = { { }, {
+	{ "set-var", parse_store, 1 },
+	{ /* END */ }
+}};
+
-static struct action_kw_list tcp_req_kws = { { }, {
+static struct action_kw_list tcp_req_cont_kws = { { }, {
 	{ "set-var", parse_store, 1 },
 	{ /* END */ }
 }};
@@ -698,7 +705,8 @@
 
 	sample_register_fetches(&sample_fetch_keywords);
 	sample_register_convs(&sample_conv_kws);
-	tcp_req_cont_keywords_register(&tcp_req_kws);
+	tcp_req_sess_keywords_register(&tcp_req_sess_kws);
+	tcp_req_cont_keywords_register(&tcp_req_cont_kws);
 	tcp_res_cont_keywords_register(&tcp_res_kws);
 	http_req_keywords_register(&http_req_kws);
 	http_res_keywords_register(&http_res_kws);