diff --git a/doc/configuration.txt b/doc/configuration.txt index ac5f4a1cc..a741920af 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 7ddc9625f..9499e94d7 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 6a49f14cc..bb9f978c4 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 fddb17904..fb60bf4a4 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 124345f02..a0c68749a 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 */ } }};