]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: Add tcp-request switch-mode action to perform HTTP upgrade
authorChristopher Faulet <cfaulet@haproxy.com>
Mon, 15 Mar 2021 11:03:44 +0000 (12:03 +0100)
committerChristopher Faulet <cfaulet@haproxy.com>
Thu, 1 Apr 2021 11:17:19 +0000 (13:17 +0200)
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
include/haproxy/stream-t.h
include/haproxy/stream.h
src/proxy.c
src/stream.c

index ac5f4a1cc39903b858e5294bfd1396250f917322..a741920af00399eb3d6a10573c01f0b53b63cf50 100644 (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
index 7ddc9625fbc19ac538d15b3252170ab22b38f053..9499e94d77feea0dad787eb3bd7b6b0375ca0148 100644 (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) */
index 6a49f14cc848440220dd8519f8e1b84497bc576b..bb9f978c4c901d97f1c80bd30fe427d8841b5f4f 100644 (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);
index fddb17904251f8bbde5bcf8d2bfee41c8f07f626..fb60bf4a4564eb2b0451dabbd6a3202763dc4b32 100644 (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) {
index 124345f0267cac00681d54c3d743791deb33d8d9..a0c68749a5fd27de9220e1ec48eeada09edc9517 100644 (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 */ }
 }};