]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: proxy: force traffic on unpublished/disabled backends
authorAmaury Denoyelle <adenoyelle@haproxy.com>
Wed, 7 Jan 2026 13:15:14 +0000 (14:15 +0100)
committerAmaury Denoyelle <adenoyelle@haproxy.com>
Thu, 15 Jan 2026 08:08:19 +0000 (09:08 +0100)
A recent patch has introduced a new state for proxies : unpublished
backends. Such backends won't be eligilible for traffic, thus
use_backend/default_backend rules which target them won't match and
content switching rules processing will continue.

This patch defines a new frontend keywords 'force-be-switch'. This
keyword allows to ignore unpublished or disabled state. Thus,
use_backend/default_backend will match even if the target backend is
unpublished or disabled. This is useful to be able to test a backend
instance before exposing it outside.

This new keyword is converted into a persist rule of new type
PERSIST_TYPE_BE_SWITCH, stored in persist_rules list proxy member. This
is the only persist rule applicable to frontend side. Prior to this
commit, pure frontend proxies persist_rules list were always empty.

This new features requires adjustment in process_switching_rules(). Now,
when a use_backend/default_backend rule matches with an non eligible
backend, frontend persist_rules are inspected to detect if a
force-be-switch is present so that the backend may be selected.

doc/configuration.txt
include/haproxy/http_ana-t.h
reg-tests/stream/test_content_switching.vtc
src/proxy.c
src/stream.c

index 9a5363f7684477e8bde4729ff98d18667618b338..e5dd53c1b0945a7f40fb40e853d0897959bd21f6 100644 (file)
@@ -5820,6 +5820,7 @@ errorloc302                               X          X         X         X
 errorloc303                               X          X         X         X
 error-log-format                          X          X         X         -
 force-persist                             -          -         X         X
+force-be-switch                           -          X         X         -
 filter                                    -          X         X         X
 fullconn                                  X          -         X         X
 guid                                      -          X         X         X
@@ -7149,7 +7150,11 @@ disabled
   is possible to disable many instances at once by adding the "disabled"
   keyword in a "defaults" section.
 
-  See also : "enabled"
+  By default, a disabled backend cannot be selected for content-switching.
+  However, a portion of the traffic can ignore this when "force-be-switch" is
+  used.
+
+  See also : "enabled", "force-be-switch"
 
 
 dispatch <address>:<port>   (deprecated)
@@ -7559,6 +7564,19 @@ force-persist { if | unless } <condition>
              and section 7 about ACL usage.
 
 
+force-be-switch { if | unless } <condition>
+  Allow content switching to select a backend instance even if it is disabled
+  or unpublished. This rule can be used by admins to test traffic to services
+  prior to expose them to the outside world.
+
+  May be used in the following contexts: tcp, http
+
+  May be used in sections:    defaults | frontend | listen | backend
+                                 no    |   yes    |   yes  |   no
+
+  See also : "disabled"
+
+
 filter <name> [param*]
   Add the filter <name> in the filter list attached to the proxy.
 
index b2937e86cb8464030cf6d96cd1460b84cbe15730..aa496e394fc92e220c1dfe6769846ce71160c23b 100644 (file)
@@ -184,6 +184,7 @@ enum {
        PERSIST_TYPE_NONE = 0,          /* no persistence */
        PERSIST_TYPE_FORCE,             /* force-persist */
        PERSIST_TYPE_IGNORE,            /* ignore-persist */
+       PERSIST_TYPE_BE_SWITCH,         /* force-be-switch */
 };
 
 /* final results for http-request rules */
index dd5087edcf34a6921a6de9c2701d3586c0d53648..ef518690afeef4e7b04bfcfd9609b05a82a43bb0 100644 (file)
@@ -33,12 +33,14 @@ haproxy h1 -conf {
 
        frontend fe
                bind "fd@${fe1S}"
+
                use_backend %[req.hdr("x-target")] if { req.hdr("x-dyn") "1" }
                use_backend be          if { req.hdr("x-target") "be" }
 
        frontend fe_default
                bind "fd@${fe2S}"
 
+               force-be-switch if { req.hdr("x-force") "1" }
                use_backend %[req.hdr("x-target")] if { req.hdr("x-dyn") "1" }
                use_backend be_disabled if { req.hdr("x-target") "be_disabled" }
                use_backend be
@@ -136,6 +138,18 @@ client c4 -connect ${h1_fe2S_sock} {
        rxresp
        expect resp.status == 200
        expect resp.http.x-be == "be2"
+
+       # Static rule matching on unpublished backend with force-be-switch
+       txreq -hdr "x-force: 1"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-be == "be"
+
+       # Dynamic rule matching on unpublished backend with force-be-switch
+       txreq -hdr "x-dyn: 1" -hdr "x-target: be" -hdr "x-force: 1"
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-be == "be"
 } -run
 
 haproxy h1 -cli {
index 393cac95dffc0bda3522e9b1b6d73b9a01018e13..dd7354352429814995dfcb3e0ec146ddde0ea717 100644 (file)
@@ -1160,6 +1160,56 @@ static int proxy_parse_tcpka_intvl(char **args, int section, struct proxy *proxy
 }
 #endif
 
+static int proxy_parse_force_be_switch(char **args, int section_type, struct proxy *curpx,
+                                       const struct proxy *defpx, const char *file, int line,
+                                       char **err)
+{
+       struct acl_cond *cond = NULL;
+       struct persist_rule *rule;
+
+       if (curpx->cap & PR_CAP_DEF) {
+               memprintf(err, "'%s' not allowed in 'defaults' section.", args[0]);
+               goto err;
+       }
+
+       if (!(curpx->cap & PR_CAP_FE)) {
+               memprintf(err, "'%s' only available in frontend or listen section.", args[0]);
+               goto err;
+       }
+
+       if (strcmp(args[1], "if") != 0 && strcmp(args[1], "unless") != 0) {
+               memprintf(err, "'%s' requires either 'if' or 'unless' followed by a condition.", args[0]);
+               goto err;
+       }
+
+       if (!(cond = build_acl_cond(file, line, &curpx->acl, curpx, (const char **)args + 1, err))) {
+               memprintf(err, "'%s' : %s.", args[0], *err);
+               goto err;
+       }
+
+       if (warnif_cond_conflicts(cond, SMP_VAL_FE_REQ_CNT, err)) {
+               memprintf(err, "'%s' : %s.", args[0], *err);
+               goto err;
+       }
+
+       rule = calloc(1, sizeof(*rule));
+       if (!rule) {
+               memprintf(err, "'%s' : out of memory.", args[0]);
+               goto err;
+       }
+
+       rule->cond = cond;
+       rule->type = PERSIST_TYPE_BE_SWITCH;
+       LIST_INIT(&rule->list);
+       LIST_APPEND(&curpx->persist_rules, &rule->list);
+
+       return 0;
+
+ err:
+       free_acl_cond(cond);
+       return -1;
+}
+
 static int proxy_parse_guid(char **args, int section_type, struct proxy *curpx,
                             const struct proxy *defpx, const char *file, int line,
                             char **err)
@@ -2869,6 +2919,7 @@ static struct cfg_kw_list cfg_kws = {ILH, {
        { CFG_LISTEN, "clitcpka-intvl", proxy_parse_tcpka_intvl },
        { CFG_LISTEN, "srvtcpka-intvl", proxy_parse_tcpka_intvl },
 #endif
+       { CFG_LISTEN, "force-be-switch", proxy_parse_force_be_switch },
        { CFG_LISTEN, "guid", proxy_parse_guid },
        { 0, NULL, NULL },
 }};
index 5bb0c247d12e6c351f3ac60c6cb5a9a8159258d6..f7709072901edfc31579f2974e94fc9bb801f6ac 100644 (file)
@@ -1117,6 +1117,41 @@ enum act_return process_use_service(struct act_rule *rule, struct proxy *px,
        return ACT_RET_STOP;
 }
 
+/* Parses persist-rules attached to <fe> frontend and report the first macthing
+ * entry, using <sess> session and <s> stream as sample source.
+ *
+ * As this function is called several times in the same stream context,
+ * <persist> will act as a caching value to avoid reprocessing of a similar
+ * ruleset. It must be set to a negative value for the first invokation.
+ *
+ * Returns 1 if a rule matches, else 0.
+ */
+static int lookup_fe_persist_rules(struct proxy *fe, struct session *sess,
+                                   struct stream *s, int *persist)
+{
+       struct persist_rule *prst_rule;
+
+       if (*persist >= 0) {
+               /* Rules already processed, use previous computed result. */
+               return *persist;
+       }
+
+       list_for_each_entry(prst_rule, &fe->persist_rules, list) {
+               if (!acl_match_cond(prst_rule->cond, fe, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL))
+                       continue;
+
+               /* force/ignore-persist match */
+               if (prst_rule->type == PERSIST_TYPE_BE_SWITCH) {
+                       *persist = 1;
+                       break;
+               }
+       }
+
+       if (*persist < 0)
+               *persist = 0;
+       return *persist;
+}
+
 /* This stream analyser checks the switching rules and changes the backend
  * if appropriate. The default_backend rule is also considered, then the
  * target backend's forced persistence rules are also evaluated last if any.
@@ -1130,6 +1165,7 @@ static int process_switching_rules(struct stream *s, struct channel *req, int an
        struct session *sess = s->sess;
        struct proxy *fe = sess->fe;
        struct proxy *backend = NULL;
+       int fe_persist = -1;
 
        req->analysers &= ~an_bit;
        req->analyse_exp = TICK_ETERNITY;
@@ -1160,7 +1196,8 @@ static int process_switching_rules(struct stream *s, struct channel *req, int an
                        }
 
                        /* If backend is ineligible, continue rules processing. */
-                       if (backend && !be_is_eligible(backend)) {
+                       if (backend && !be_is_eligible(backend) &&
+                           !lookup_fe_persist_rules(fe, sess, s, &fe_persist)) {
                                backend = NULL;
                                continue;
                        }
@@ -1185,10 +1222,14 @@ static int process_switching_rules(struct stream *s, struct channel *req, int an
                        }
 
                        /* Use default backend if possible or stay on the current proxy. */
-                       if (fe->defbe.be && be_is_eligible(fe->defbe.be))
+                       if (fe->defbe.be &&
+                           (be_is_eligible(fe->defbe.be) ||
+                            lookup_fe_persist_rules(fe, sess, s, &fe_persist))) {
                                backend = fe->defbe.be;
-                       else
+                       }
+                       else {
                                backend = s->be;
+                       }
                }
 
                if (!stream_set_backend(s, backend))