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.
This commit is contained in:
Amaury Denoyelle 2026-01-07 14:15:14 +01:00
parent 16f035d555
commit 6870551a57
5 changed files with 129 additions and 4 deletions

View 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.

View 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 */

View 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 {

View 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 },
}};

View 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))