[MAJOR] implement tcp request content inspection
Some people need to inspect contents of TCP requests before
deciding to forward a connection or not. A future extension
of this demand might consist in selecting a server farm
depending on the protocol detected in the request.
For this reason, a new state CL_STINSPECT has been added on
the client side. It is immediately entered upon accept() if
the statement "tcp-request inspect-delay <xxx>" is found in
the frontend configuration. Haproxy will then wait up to
this amount of time trying to find a matching ACL, and will
either accept or reject the connection depending on the
"tcp-request content <action> {if|unless}" rules, where
<action> is either "accept" or "reject".
Note that it only waits that long if no definitive verdict
can be found earlier. That generally implies calling a fetch()
function which does not have enough information to decode
some contents, or a match() function which only finds the
beginning of what it's looking for.
It is only at the ACL level that partial data may be processed
as such, because we need to distinguish between MISS and FAIL
*before* applying the term negation.
Thus it is enough to add "| ACL_PARTIAL" to the last argument
when calling acl_exec_cond() to indicate that we expect
ACL_PAT_MISS to be returned if some data is missing (for
fetch() or match()). This is the only case we may return
this value. For this reason, the ACL check in process_cli()
has become a lot simpler.
A new ACL "req_len" of type "int" has been added. Right now
it is already possible to drop requests which talk too early
(eg: for SMTP) or which don't talk at all (eg: HTTP/SSL).
Also, the acl fetch() functions have been extended in order
to permit reporting of missing data in case of fetch failure,
using the ACL_TEST_F_MAY_CHANGE flag.
The default behaviour is unchanged, and if no rule matches,
the request is accepted.
As a side effect, all layer 7 fetching functions have been
cleaned up so that they now check for the validity of the
layer 7 pointer before dereferencing it.
diff --git a/src/acl.c b/src/acl.c
index ddbd4bc..036424e 100644
--- a/src/acl.c
+++ b/src/acl.c
@@ -818,11 +818,16 @@
}
/* Execute condition <cond> and return either ACL_PAT_FAIL, ACL_PAT_MISS or
- * ACL_PAT_PASS depending on the test results. This function only computes the
- * condition, it does not apply the polarity required by IF/UNLESS, it's up to
- * the caller to do this using something like this :
+ * ACL_PAT_PASS depending on the test results. ACL_PAT_MISS may only be
+ * returned if <dir> contains ACL_PARTIAL, indicating that incomplete data
+ * is being examined.
+ * This function only computes the condition, it does not apply the polarity
+ * required by IF/UNLESS, it's up to the caller to do this using something like
+ * this :
*
* res = acl_pass(res);
+ * if (res == ACL_PAT_MISS)
+ * return 0;
* if (cond->pol == ACL_COND_UNLESS)
* res = !res;
*/
@@ -866,8 +871,12 @@
/* we need to reset context and flags */
memset(&test, 0, sizeof(test));
fetch_next:
- if (!expr->kw->fetch(px, l4, l7, dir, expr, &test))
+ if (!expr->kw->fetch(px, l4, l7, dir, expr, &test)) {
+ /* maybe we could not fetch because of missing data */
+ if (test.flags & ACL_TEST_F_MAY_CHANGE && dir & ACL_PARTIAL)
+ acl_res |= ACL_PAT_MISS;
continue;
+ }
/* apply all tests to this value */
list_for_each_entry(pattern, &expr->patterns, list) {
@@ -898,6 +907,13 @@
if (test.flags & ACL_TEST_F_FETCH_MORE)
goto fetch_next;
+
+ /* sometimes we know the fetched data is subject to change
+ * later and give another chance for a new match (eg: request
+ * size, time, ...)
+ */
+ if (test.flags & ACL_TEST_F_MAY_CHANGE && dir & ACL_PARTIAL)
+ acl_res |= ACL_PAT_MISS;
}
/*
* Here we have the result of an ACL (cached or not).
diff --git a/src/cfgparse.c b/src/cfgparse.c
index b2af648..a492a93 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -614,6 +614,7 @@
LIST_INIT(&curproxy->redirect_rules);
LIST_INIT(&curproxy->mon_fail_cond);
LIST_INIT(&curproxy->switching_rules);
+ LIST_INIT(&curproxy->tcp_req.inspect_rules);
/* Timeouts are defined as -1, so we cannot use the zeroed area
* as a default value.
diff --git a/src/client.c b/src/client.c
index a40c2a4..e76a741 100644
--- a/src/client.c
+++ b/src/client.c
@@ -168,7 +168,10 @@
* backend must be assigned if set.
*/
if (p->mode == PR_MODE_HTTP) {
- s->cli_state = CL_STHEADERS;
+ if (s->fe->tcp_req.inspect_delay)
+ s->cli_state = CL_STINSPECT;
+ else
+ s->cli_state = CL_STHEADERS;
} else {
/* We must assign any default backend now since
* there will be no header processing.
@@ -178,7 +181,10 @@
s->be = p->defbe.be;
s->flags |= SN_BE_ASSIGNED;
}
- s->cli_state = CL_STDATA; /* no HTTP headers for non-HTTP proxies */
+ if (s->fe->tcp_req.inspect_delay)
+ s->cli_state = CL_STINSPECT;
+ else
+ s->cli_state = CL_STDATA; /* no HTTP headers for non-HTTP proxies */
}
s->srv_state = SV_STIDLE;
@@ -340,7 +346,7 @@
buffer_init(s->req);
s->req->rlim += BUFSIZE;
- if (s->cli_state == CL_STHEADERS) /* reserve some space for header rewriting */
+ if (p->mode == PR_MODE_HTTP) /* reserve some space for header rewriting */
s->req->rlim -= MAXREWRITE;
s->req->rto = s->fe->timeout.client;
@@ -390,6 +396,7 @@
s->rep->rex = TICK_ETERNITY;
s->rep->wex = TICK_ETERNITY;
s->txn.exp = TICK_ETERNITY;
+ s->inspect_exp = TICK_ETERNITY;
t->expire = TICK_ETERNITY;
if (s->fe->timeout.client) {
@@ -407,6 +414,10 @@
s->txn.exp = tick_add(now_ms, s->fe->timeout.httpreq);
t->expire = tick_first(t->expire, s->txn.exp);
}
+ else if (s->cli_state == CL_STINSPECT && s->fe->tcp_req.inspect_delay) {
+ s->inspect_exp = tick_add(now_ms, s->fe->tcp_req.inspect_delay);
+ t->expire = tick_first(t->expire, s->inspect_exp);
+ }
if (p->mode != PR_MODE_HEALTH)
task_wakeup(t);
diff --git a/src/proto_http.c b/src/proto_http.c
index 3a8ab9a..ac29c64 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -51,6 +51,7 @@
#include <proto/fd.h>
#include <proto/log.h>
#include <proto/hdr_idx.h>
+#include <proto/proto_tcp.h>
#include <proto/proto_http.h>
#include <proto/queue.h>
#include <proto/senddata.h>
@@ -680,6 +681,8 @@
t->expire = tick_first(t->expire, s->req->cex);
if (s->cli_state == CL_STHEADERS)
t->expire = tick_first(t->expire, s->txn.exp);
+ else if (s->cli_state == CL_STINSPECT)
+ t->expire = tick_first(t->expire, s->inspect_exp);
/* restore t to its place in the task list */
task_queue(t);
@@ -1550,7 +1553,104 @@
req->rex.tv_sec, req->rex.tv_usec,
rep->wex.tv_sec, rep->wex.tv_usec);
- if (c == CL_STHEADERS) {
+ if (c == CL_STINSPECT) {
+ struct tcp_rule *rule;
+ int partial;
+
+ /* We will abort if we encounter a read error. In theory,
+ * we should not abort if we get a close, it might be
+ * valid, also very unlikely. FIXME: we'll abort for now,
+ * this will be easier to change later.
+ */
+ if (unlikely(req->flags & (BF_READ_ERROR | BF_READ_NULL))) {
+ t->inspect_exp = TICK_ETERNITY;
+ buffer_shutr(req);
+ fd_delete(t->cli_fd);
+ t->cli_state = CL_STCLOSE;
+ t->fe->failed_req++;
+ if (!(t->flags & SN_ERR_MASK))
+ t->flags |= SN_ERR_CLICL;
+ if (!(t->flags & SN_FINST_MASK))
+ t->flags |= SN_FINST_R;
+ return 1;
+ }
+
+ /* Abort if client read timeout has expired */
+ else if (unlikely(tick_is_expired(req->rex, now_ms))) {
+ t->inspect_exp = TICK_ETERNITY;
+ buffer_shutr(req);
+ fd_delete(t->cli_fd);
+ t->cli_state = CL_STCLOSE;
+ t->fe->failed_req++;
+ if (!(t->flags & SN_ERR_MASK))
+ t->flags |= SN_ERR_CLITO;
+ if (!(t->flags & SN_FINST_MASK))
+ t->flags |= SN_FINST_R;
+ return 1;
+ }
+
+ /* We don't know whether we have enough data, so must proceed
+ * this way :
+ * - iterate through all rules in their declaration order
+ * - if one rule returns MISS, it means the inspect delay is
+ * not over yet, then return immediately, otherwise consider
+ * it as a non-match.
+ * - if one rule returns OK, then return OK
+ * - if one rule returns KO, then return KO
+ */
+
+ if (tick_is_expired(t->inspect_exp, now_ms))
+ partial = 0;
+ else
+ partial = ACL_PARTIAL;
+
+ list_for_each_entry(rule, &t->fe->tcp_req.inspect_rules, list) {
+ int ret = ACL_PAT_PASS;
+
+ if (rule->cond) {
+ ret = acl_exec_cond(rule->cond, t->fe, t, NULL, ACL_DIR_REQ | partial);
+ if (ret == ACL_PAT_MISS) {
+ req->rex = tick_add_ifset(now_ms, t->fe->timeout.client);
+ return 0;
+ }
+ ret = acl_pass(ret);
+ if (rule->cond->pol == ACL_COND_UNLESS)
+ ret = !ret;
+ }
+
+ if (ret) {
+ /* we have a matching rule. */
+ if (rule->action == TCP_ACT_REJECT) {
+ buffer_shutr(req);
+ fd_delete(t->cli_fd);
+ t->cli_state = CL_STCLOSE;
+ t->fe->failed_req++;
+ if (!(t->flags & SN_ERR_MASK))
+ t->flags |= SN_ERR_PRXCOND;
+ if (!(t->flags & SN_FINST_MASK))
+ t->flags |= SN_FINST_R;
+ t->inspect_exp = TICK_ETERNITY;
+ return 1;
+ }
+ /* otherwise accept */
+ break;
+ }
+ }
+
+ /* if we get there, it means we have no rule which matches, so
+ * we apply the default accept.
+ */
+ req->rex = tick_add_ifset(now_ms, t->fe->timeout.client);
+ if (t->fe->mode == PR_MODE_HTTP) {
+ t->cli_state = CL_STHEADERS;
+ t->txn.exp = tick_add_ifset(now_ms, t->fe->timeout.httpreq);
+ } else {
+ t->cli_state = CL_STDATA;
+ }
+ t->inspect_exp = TICK_ETERNITY;
+ return 1;
+ }
+ else if (c == CL_STHEADERS) {
/*
* Now parse the partial (or complete) lines.
* We will check the request syntax, and also join multi-line
@@ -2606,7 +2706,7 @@
* we need to defer server selection until more data arrives, if possible.
* This is rare, and only if balancing on parameter hash with values in the entity of a POST
*/
- if (c == CL_STHEADERS )
+ if (c == CL_STHEADERS || c == CL_STINSPECT)
return 0; /* stay in idle, waiting for data to reach the client side */
else if (c == CL_STCLOSE || c == CL_STSHUTW ||
(c == CL_STSHUTR &&
@@ -5202,6 +5302,9 @@
int meth;
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5259,6 +5362,9 @@
char *ptr;
int len;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5284,6 +5390,9 @@
char *ptr;
int len;
+ if (!txn)
+ return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5310,6 +5419,9 @@
char *ptr;
int len;
+ if (!txn)
+ return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5328,8 +5440,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5348,8 +5464,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5376,8 +5496,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5404,6 +5528,9 @@
struct hdr_idx *idx = &txn->hdr_idx;
struct hdr_ctx *ctx = (struct hdr_ctx *)test->ctx.a;
+ if (!txn)
+ return 0;
+
if (!(test->flags & ACL_TEST_F_FETCH_MORE))
/* search for header from the beginning */
ctx->idx = 0;
@@ -5427,8 +5554,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5442,6 +5573,9 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5460,6 +5594,9 @@
struct hdr_ctx ctx;
int cnt;
+ if (!txn)
+ return 0;
+
ctx.idx = 0;
cnt = 0;
while (http_find_header2(expr->arg.str, expr->arg_len, sol, idx, &ctx))
@@ -5476,8 +5613,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5491,6 +5632,9 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5509,6 +5653,9 @@
struct hdr_idx *idx = &txn->hdr_idx;
struct hdr_ctx *ctx = (struct hdr_ctx *)test->ctx.a;
+ if (!txn)
+ return 0;
+
if (!(test->flags & ACL_TEST_F_FETCH_MORE))
/* search for header from the beginning */
ctx->idx = 0;
@@ -5531,8 +5678,12 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
@@ -5546,6 +5697,9 @@
{
struct http_txn *txn = l7;
+ if (!txn)
+ return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_BODY)
return 0;
@@ -5562,8 +5716,12 @@
struct http_txn *txn = l7;
char *ptr, *end;
+ if (!txn)
+ return 0;
+
if (txn->req.msg_state != HTTP_MSG_BODY)
return 0;
+
if (txn->rsp.msg_state != HTTP_MSG_RPBEFORE)
/* ensure the indexes are not affected */
return 0;
diff --git a/src/proto_tcp.c b/src/proto_tcp.c
index d122a03..e2afb94 100644
--- a/src/proto_tcp.c
+++ b/src/proto_tcp.c
@@ -24,6 +24,7 @@
#include <sys/types.h>
#include <sys/un.h>
+#include <common/cfgparse.h>
#include <common/compat.h>
#include <common/config.h>
#include <common/debug.h>
@@ -47,6 +48,7 @@
#include <proto/fd.h>
#include <proto/protocols.h>
#include <proto/proto_tcp.h>
+#include <proto/proxy.h>
#include <proto/queue.h>
#include <proto/senddata.h>
#include <proto/session.h>
@@ -329,11 +331,139 @@
proto_tcpv6.nb_listeners++;
}
+/* This function should be called to parse a line starting with the "tcp-request"
+ * keyword.
+ */
+static int tcp_parse_tcp_req(char **args, int section_type, struct proxy *curpx,
+ struct proxy *defpx, char *err, int errlen)
+{
+ const char *ptr = NULL;
+ int val;
+ int retlen;
+
+ if (!*args[1]) {
+ snprintf(err, errlen, "missing argument for '%s' in %s '%s'",
+ args[0], proxy_type_str(proxy), curpx->id);
+ return -1;
+ }
+
+ if (!strcmp(args[1], "inspect-delay")) {
+ if (curpx == defpx) {
+ snprintf(err, errlen, "%s %s is not allowed in 'defaults' sections",
+ args[0], args[1]);
+ return -1;
+ }
+
+ if (!(curpx->cap & PR_CAP_FE)) {
+ snprintf(err, errlen, "%s %s will be ignored because %s '%s' has no %s capability",
+ args[0], args[1], proxy_type_str(proxy), curpx->id,
+ "frontend");
+ return 1;
+ }
+
+ if (!*args[2] || (ptr = parse_time_err(args[2], &val, TIME_UNIT_MS))) {
+ retlen = snprintf(err, errlen,
+ "'%s %s' expects a positive delay in milliseconds, in %s '%s'",
+ args[0], args[1], proxy_type_str(proxy), curpx->id);
+ if (ptr && retlen < errlen)
+ retlen += snprintf(err+retlen, errlen - retlen,
+ " (unexpected character '%c')", *ptr);
+ return -1;
+ }
+
+ if (curpx->tcp_req.inspect_delay) {
+ snprintf(err, errlen, "ignoring %s %s (was already defined) in %s '%s'",
+ args[0], args[1], proxy_type_str(proxy), curpx->id);
+ return 1;
+ }
+ curpx->tcp_req.inspect_delay = val;
+ return 0;
+ }
+
+ if (!strcmp(args[1], "content")) {
+ int action;
+ int pol = ACL_COND_NONE;
+ struct acl_cond *cond;
+ struct tcp_rule *rule;
+
+ if (curpx == defpx) {
+ snprintf(err, errlen, "%s %s is not allowed in 'defaults' sections",
+ args[0], args[1]);
+ return -1;
+ }
+
+ if (!strcmp(args[2], "accept"))
+ action = TCP_ACT_ACCEPT;
+ else if (!strcmp(args[2], "reject"))
+ action = TCP_ACT_REJECT;
+ else {
+ retlen = snprintf(err, errlen,
+ "'%s %s' expects 'accept' or 'reject', in %s '%s' (was '%s')",
+ args[0], args[1], proxy_type_str(curpx), curpx->id, args[2]);
+ return -1;
+ }
+
+ pol = ACL_COND_NONE;
+ cond = NULL;
+
+ if (!strcmp(args[3], "if"))
+ pol = ACL_COND_IF;
+ else if (!strcmp(args[3], "unless"))
+ pol = ACL_COND_UNLESS;
+
+ /* Note: we consider "if TRUE" when there is no condition */
+ if (pol != ACL_COND_NONE &&
+ (cond = parse_acl_cond((const char **)args+4, &curpx->acl, pol)) == NULL) {
+ retlen = snprintf(err, errlen,
+ "Error detected in %s '%s' while parsing '%s' condition",
+ proxy_type_str(curpx), curpx->id, args[3]);
+ return -1;
+ }
+
+ rule = (struct tcp_rule *)calloc(1, sizeof(*rule));
+ rule->cond = cond;
+ rule->action = action;
+ LIST_INIT(&rule->list);
+ LIST_ADDQ(&curpx->tcp_req.inspect_rules, &rule->list);
+ return 0;
+ }
+
+ snprintf(err, errlen, "unknown argument '%s' after '%s' in %s '%s'",
+ args[1], args[0], proxy_type_str(proxy), curpx->id);
+ return -1;
+}
+
+/* return the number of bytes in the request buffer */
+static int
+acl_fetch_req_len(struct proxy *px, struct session *l4, void *l7, int dir,
+ struct acl_expr *expr, struct acl_test *test)
+{
+ if (!l4 || !l4->req)
+ return 0;
+
+ test->i = l4->req->l;
+ test->flags = ACL_TEST_F_VOLATILE | ACL_TEST_F_MAY_CHANGE;
+ return 1;
+}
+
+
+static struct cfg_kw_list cfg_kws = {{ },{
+ { CFG_LISTEN, "tcp-request", tcp_parse_tcp_req },
+ { 0, NULL, NULL },
+}};
+
+static struct acl_kw_list acl_kws = {{ },{
+ { "req_len", acl_parse_int, acl_fetch_req_len, acl_match_int },
+ { NULL, NULL, NULL, NULL },
+}};
+
__attribute__((constructor))
static void __tcp_protocol_init(void)
{
protocol_register(&proto_tcpv4);
protocol_register(&proto_tcpv6);
+ cfg_register_keywords(&cfg_kws);
+ acl_register_keywords(&acl_kws);
}