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.
This commit is contained in:
Christopher Faulet 2021-03-15 12:03:44 +01:00
parent 6c1fd987f6
commit ae863c62e3
5 changed files with 136 additions and 9 deletions

View File

@ -11746,8 +11746,8 @@ tcp-request content <action> [{if | unless} <condition>]
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 <action> [{if | unless} <condition>]
- set-dst <expr>
- set-dst-port <expr>
- set-var(<var-name>) <expr>
- switch-mode http [ proto <name> ]
- unset-var(<var-name>)
- silent-drop
- send-spoe-group <engine-name> <group-name>
@ -11849,6 +11850,17 @@ tcp-request content <action> [{if | unless} <condition>]
<expr> 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
<var-name>.
@ -11897,12 +11909,21 @@ tcp-request content <action> [{if | unless} <condition>]
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

View File

@ -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) */

View File

@ -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 <why> (one of SF_ERR_*) */
void stream_shutdown(struct stream *stream, int why);

View File

@ -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) {

View File

@ -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 */ }
}};