From ae863c62e3c2d5b9aa19131ce674a75eb2ac81d3 Mon Sep 17 00:00:00 2001 From: Christopher Faulet Date: Mon, 15 Mar 2021 12:03:44 +0100 Subject: [PATCH] MEDIUM: Add tcp-request switch-mode action to perform HTTP upgrade It is now possible to perform HTTP upgrades on a TCP stream from the frontend side. To do so, a tcp-request content rule must be defined with the switch-mode action, specifying the mode (for now, only http is supported) and optionnaly the proto (h1 or h2). This way it could be possible to set HTTP directives on a TCP frontend which will only be evaluated if an upgrade is performed. This new way to perform HTTP upgrades should replace progressively the old way, consisting to route the request to an HTTP backend. And it should be also a good start to remove all HTTP processing from tcp-request content rules. This action is terminal, it stops the ruleset evaluation. It is only available on proxy with the frontend capability. The configuration manual has been updated accordingly. --- doc/configuration.txt | 27 ++++++++- include/haproxy/stream-t.h | 1 - include/haproxy/stream.h | 2 +- src/proxy.c | 2 +- src/stream.c | 113 ++++++++++++++++++++++++++++++++++++- 5 files changed, 136 insertions(+), 9 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index ac5f4a1cc3..a741920af0 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -11746,8 +11746,8 @@ tcp-request content [{if | unless} ] A request's contents can be analyzed at an early stage of request processing called "TCP content inspection". During this stage, ACL-based rules are evaluated every time the request contents are updated, until either an - "accept" or a "reject" rule matches, or the TCP request inspection delay - expires with no matching rule. + "accept", a "reject" or a "switch-mode" rule matches, or the TCP request + inspection delay expires with no matching rule. The first difference between these rules and "tcp-request connection" rules is that "tcp-request content" rules can make use of contents to take a @@ -11780,6 +11780,7 @@ tcp-request content [{if | unless} ] - set-dst - set-dst-port - set-var() + - switch-mode http [ proto ] - unset-var() - silent-drop - send-spoe-group @@ -11849,6 +11850,17 @@ tcp-request content [{if | unless} ] Is a standard HAProxy expression formed by a sample-fetch followed by some converters. + The "switch-mode" is used to perform a conntection upgrade. Only HTTP + upgrades are supported for now. The protocol may optionally be + specified. This action is only available for a proxy with the frontend + capability. The connection upgrade is immediately performed, following + "tcp-request content" rules are not evaluated. This upgrade method should be + preferred to the implicit one consisting to rely on the backend mode. When + used, it is possible to set HTTP directives in a frontend without any + warning. These directives will be conditionnaly evaluated if the HTTP upgrade + is performed. However, an HTTP backend must still be selected. It remains + unsupported to route an HTTP connection (upgraded or not) to a TCP server. + The "unset-var" is used to unset a variable. See above for details about . @@ -11897,12 +11909,21 @@ tcp-request content [{if | unless} ] Example: # Accept HTTP requests containing a Host header saying "example.com" - # and reject everything else. + # and reject everything else. (Only works for HTTP/1 connections) acl is_host_com hdr(Host) -i example.com tcp-request inspect-delay 30s tcp-request content accept if is_host_com tcp-request content reject + # Accept HTTP requests containing a Host header saying "example.com" + # and reject everything else. (works for HTTP/1 and HTTP/2 connections) + acl is_host_com hdr(Host) -i example.com + tcp-request inspect-delay 5s + tcp-request switch-mode http if HTTP + tcp-request reject # non-HTTP traffic is implicit here + ... + http-request reject unless is_host_com + Example: # reject SMTP connection if client speaks first tcp-request inspect-delay 30s diff --git a/include/haproxy/stream-t.h b/include/haproxy/stream-t.h index 7ddc9625fb..9499e94d77 100644 --- a/include/haproxy/stream-t.h +++ b/include/haproxy/stream-t.h @@ -52,7 +52,6 @@ #define SF_FORCE_PRST 0x00000010 /* force persistence here, even if server is down */ #define SF_MONITOR 0x00000020 /* this stream comes from a monitoring system */ #define SF_CURR_SESS 0x00000040 /* a connection is currently being counted on the server */ -/* unused: 0x00000080 */ #define SF_REDISP 0x00000100 /* set if this stream was redispatched from one server to another */ #define SF_IGNORE 0x00000200 /* The stream lead to a mux upgrade, and should be ignored */ #define SF_REDIRECTABLE 0x00000400 /* set if this stream is redirectable (GET or HEAD) */ diff --git a/include/haproxy/stream.h b/include/haproxy/stream.h index 6a49f14cc8..bb9f978c4c 100644 --- a/include/haproxy/stream.h +++ b/include/haproxy/stream.h @@ -60,7 +60,7 @@ extern struct data_cb sess_conn_cb; struct stream *stream_new(struct session *sess, enum obj_type *origin, struct buffer *input); int stream_create_from_cs(struct conn_stream *cs, struct buffer *input); int stream_upgrade_from_cs(struct conn_stream *cs, struct buffer *input); -int stream_set_http_mode(struct stream *s); +int stream_set_http_mode(struct stream *s, const struct mux_proto_list *mux_proto); /* kill a stream and set the termination flags to (one of SF_ERR_*) */ void stream_shutdown(struct stream *stream, int why); diff --git a/src/proxy.c b/src/proxy.c index fddb179042..fb60bf4a45 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -2160,7 +2160,7 @@ int stream_set_backend(struct stream *s, struct proxy *be) if (!IS_HTX_STRM(s) && be->mode == PR_MODE_HTTP) { /* If we chain a TCP frontend to an HTX backend, we must upgrade * the client mux */ - if (!stream_set_http_mode(s)) + if (!stream_set_http_mode(s, NULL)) return 0; } else if (IS_HTX_STRM(s) && be->mode != PR_MODE_HTTP) { diff --git a/src/stream.c b/src/stream.c index 124345f026..a0c68749a5 100644 --- a/src/stream.c +++ b/src/stream.c @@ -1481,9 +1481,9 @@ static int process_store_rules(struct stream *s, struct channel *rep, int an_bit /* Set the stream to HTTP mode, if necessary. The minimal request HTTP analysers * are set and the client mux is upgraded. It returns 1 if the stream processing * may continue or 0 if it should be stopped. It happens on error or if the - * upgrade required a new stream. + * upgrade required a new stream. The mux protocol may be specified. */ -int stream_set_http_mode(struct stream *s) +int stream_set_http_mode(struct stream *s, const struct mux_proto_list *mux_proto) { struct connection *conn; struct conn_stream *cs; @@ -1508,9 +1508,12 @@ int stream_set_http_mode(struct stream *s) if (s->si[0].wait_event.events) conn->mux->unsubscribe(cs, s->si[0].wait_event.events, &s->si[0].wait_event); + if (conn->mux->flags & MX_FL_NO_UPG) return 0; - if (conn_upgrade_mux_fe(conn, cs, &s->req.buf, ist(""), PROTO_MODE_HTTP) == -1) + if (conn_upgrade_mux_fe(conn, cs, &s->req.buf, + (mux_proto ? mux_proto->token : ist("")), + PROTO_MODE_HTTP) == -1) return 0; s->req.flags &= ~(CF_READ_PARTIAL|CF_AUTO_CONNECT); @@ -2843,6 +2846,109 @@ struct ist stream_generate_unique_id(struct stream *strm, struct list *format) /************************************************************************/ /* All supported ACL keywords must be declared here. */ /************************************************************************/ +static enum act_return tcp_action_switch_stream_mode(struct act_rule *rule, struct proxy *px, + struct session *sess, struct stream *s, int flags) +{ + enum pr_mode mode = (uintptr_t)rule->arg.act.p[0]; + const struct mux_proto_list *mux_proto = rule->arg.act.p[1]; + + if (!IS_HTX_STRM(s) && mode == PR_MODE_HTTP) { + if (!stream_set_http_mode(s, mux_proto)) { + channel_abort(&s->req); + channel_abort(&s->res); + return ACT_RET_ABRT; + } + } + return ACT_RET_STOP; +} + + +static int check_tcp_switch_stream_mode(struct act_rule *rule, struct proxy *px, char **err) +{ + const struct mux_proto_list *mux_ent; + const struct mux_proto_list *mux_proto = rule->arg.act.p[1]; + enum pr_mode pr_mode = (uintptr_t)rule->arg.act.p[0]; + enum proto_proxy_mode mode = (1 << (pr_mode == PR_MODE_HTTP)); + + if (mux_proto) { + mux_ent = conn_get_best_mux_entry(mux_proto->token, PROTO_SIDE_FE, mode); + if (!mux_ent || !isteq(mux_ent->token, mux_proto->token)) { + memprintf(err, "MUX protocol '%.*s' is not compatible with the selected mode", + (int)mux_proto->token.len, mux_proto->token.ptr); + return 0; + } + } + else { + mux_ent = conn_get_best_mux_entry(IST_NULL, PROTO_SIDE_FE, mode); + if (!mux_ent) { + memprintf(err, "Unable to find compatible MUX protocol with the selected mode"); + return 0; + } + } + + /* Update the mux */ + rule->arg.act.p[1] = (void *)mux_ent; + return 1; + +} + +static enum act_parse_ret stream_parse_switch_mode(const char **args, int *cur_arg, + struct proxy *px, struct act_rule *rule, + char **err) +{ + const struct mux_proto_list *mux_proto = NULL; + struct ist proto; + enum pr_mode mode; + + /* must have at least the mode */ + if (*(args[*cur_arg]) == 0) { + memprintf(err, "'%s %s' expects a mode as argument.", args[0], args[*cur_arg-1]); + return ACT_RET_PRS_ERR; + } + + if (!(px->cap & PR_CAP_FE)) { + memprintf(err, "'%s %s' not allowed because %s '%s' has no frontend capability", + args[0], args[*cur_arg-1], proxy_type_str(px), px->id); + return ACT_RET_PRS_ERR; + } + /* Check if the mode. For now "tcp" is disabled because downgrade is not + * supported and PT is the only TCP mux. + */ + if (strcmp(args[*cur_arg], "http") == 0) + mode = PR_MODE_HTTP; + else { + memprintf(err, "'%s %s' expects a valid mode (got '%s').", args[0], args[*cur_arg-1], args[*cur_arg]); + return ACT_RET_PRS_ERR; + } + + /* check the proto, if specified */ + if (*(args[*cur_arg+1]) && strcmp(args[*cur_arg+1], "proto") == 0) { + if (*(args[*cur_arg+2]) == 0) { + memprintf(err, "'%s %s': '%s' expects a protocol as argument.", + args[0], args[*cur_arg-1], args[*cur_arg+1]); + return ACT_RET_PRS_ERR; + } + + proto = ist2(args[*cur_arg+2], strlen(args[*cur_arg+2])); + mux_proto = get_mux_proto(proto); + if (!mux_proto) { + memprintf(err, "'%s %s': '%s' expects a valid MUX protocol, if specified (got '%s')", + args[0], args[*cur_arg-1], args[*cur_arg+1], args[*cur_arg+2]); + return ACT_RET_PRS_ERR; + } + *cur_arg += 2; + } + + (*cur_arg)++; + + /* Register processing function. */ + rule->action_ptr = tcp_action_switch_stream_mode; + rule->check_ptr = check_tcp_switch_stream_mode; + rule->action = ACT_CUSTOM; + rule->arg.act.p[0] = (void *)(uintptr_t)mode; + rule->arg.act.p[1] = (void *)mux_proto; + return ACT_RET_PRS_OK; +} /* 0=OK, <0=Alert, >0=Warning */ static enum act_parse_ret stream_parse_use_service(const char **args, int *cur_arg, @@ -3593,6 +3699,7 @@ INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws); /* main configuration keyword registration. */ static struct action_kw_list stream_tcp_keywords = { ILH, { + { "switch-mode", stream_parse_switch_mode }, { "use-service", stream_parse_use_service }, { /* END */ } }};