From: Amaury Denoyelle Date: Wed, 7 Jan 2026 13:15:14 +0000 (+0100) Subject: MEDIUM: proxy: force traffic on unpublished/disabled backends X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6870551a572aae8f4e687ae856b337fb3426a2c9;p=thirdparty%2Fhaproxy.git MEDIUM: proxy: force traffic on unpublished/disabled backends 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. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 9a5363f76..e5dd53c1b 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -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
: (deprecated) @@ -7559,6 +7564,19 @@ force-persist { if | unless } and section 7 about ACL usage. +force-be-switch { if | unless } + 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 [param*] Add the filter in the filter list attached to the proxy. diff --git a/include/haproxy/http_ana-t.h b/include/haproxy/http_ana-t.h index b2937e86c..aa496e394 100644 --- a/include/haproxy/http_ana-t.h +++ b/include/haproxy/http_ana-t.h @@ -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 */ diff --git a/reg-tests/stream/test_content_switching.vtc b/reg-tests/stream/test_content_switching.vtc index dd5087edc..ef518690a 100644 --- a/reg-tests/stream/test_content_switching.vtc +++ b/reg-tests/stream/test_content_switching.vtc @@ -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 { diff --git a/src/proxy.c b/src/proxy.c index 393cac95d..dd7354352 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -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 }, }}; diff --git a/src/stream.c b/src/stream.c index 5bb0c247d..f77090729 100644 --- a/src/stream.c +++ b/src/stream.c @@ -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 frontend and report the first macthing + * entry, using session and stream as sample source. + * + * As this function is called several times in the same stream context, + * 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))