From a67005a1a731b4e45a714135de184397773f2e11 Mon Sep 17 00:00:00 2001
From: Benedict Weis <benedict.weis01@sap.com>
Date: Thu, 24 Sep 2026 08:58:38 +0000
Subject: [PATCH 1/3] MEDIUM: proxy: support "use-server" rules in defaults
 sections

Until now "use-server" rules were only accepted in "listen" and "backend"
sections. This makes it possible to declare them in a "defaults" section so
that a common set of rules can be shared by all backends inheriting from it,
in the same way "http-request" and "tcp-request" rules already are.

A "defaults" section has no servers, so a static server name could never be
resolved there. For this reason only the dynamic (log-format) form is allowed
(e.g. "use-server %[var(txn.srv)] if ..."); a plain server name is rejected at
parse time with a clear error. The authoritative static/dynamic classification
is performed later in the new proxy_resolve_server_rules(), which is called
from check_config_validity() for the defaults proxies that proxy_finalize()
skips.

The parse/classify logic that was previously inlined in proxy_finalize() is
factored into resolve_server_rule_expr() and reused by both call sites. At
request time, process_server_rules() now evaluates the backend's own rules
first and then the ones inherited from its defaults section, the latter being
reached through the existing px->defpx reference so no rule is duplicated.

The inherited rules are owned by the defaults proxy and are released in
defaults_px_free().
---
 include/haproxy/proxy.h |   1 +
 src/cfgparse-listen.c   |  20 +++++--
 src/cfgparse.c          |   7 +++
 src/proxy.c             | 127 +++++++++++++++++++++++++++++++---------
 src/stream.c            |  58 ++++++++++--------
 5 files changed, 155 insertions(+), 58 deletions(-)

diff --git a/include/haproxy/proxy.h b/include/haproxy/proxy.h
index 77589317d..2c7d58f48 100644
--- a/include/haproxy/proxy.h
+++ b/include/haproxy/proxy.h
@@ -102,6 +102,7 @@ void free_stick_rules(struct list *rules);
 void free_server_rules(struct list *srules);
 int proxy_init_per_thr(struct proxy *px);
 int proxy_finalize(struct proxy *px, int *err_code);
+int proxy_resolve_server_rules(struct proxy *px, int *err_code);
 
 int be_check_for_deletion(const char *bename, struct proxy **pb, const char **pm);
 
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index fe5337fe8..7a5148266 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -1569,12 +1569,6 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
 	else if (strcmp(args[0], "use-server") == 0) {
 		struct server_rule *rule;
 
-		if (curproxy->cap & PR_CAP_DEF) {
-			ha_alert("parsing [%s:%d] : '%s' not allowed in 'defaults' section.\n", file, linenum, args[0]);
-			err_code |= ERR_ALERT | ERR_FATAL;
-			goto out;
-		}
-
 		if (warnifnotcap(curproxy, PR_CAP_BE, file, linenum, args[0], NULL))
 			err_code |= ERR_WARN;
 
@@ -1584,6 +1578,20 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
 			goto out;
 		}
 
+		/* Conservative pre-filter for a common mistake: a static server name
+		 * cannot be resolved in a defaults section (no servers there), so only
+		 * log-format expressions are allowed. This fails early with a clear
+		 * error; the authoritative dynamic/static classification happens later
+		 * in proxy_resolve_server_rules().
+		 */
+		if ((curproxy->cap & PR_CAP_DEF) && !strchr(args[1], '%')) {
+			ha_alert("parsing [%s:%d] : '%s' in a 'defaults' section requires a dynamic "
+			         "log-format expression (e.g. %%[var(...)]), not a static server name.\n",
+			         file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
 		if (strcmp(args[2], "if") != 0 && strcmp(args[2], "unless") != 0) {
 			ha_alert("parsing [%s:%d] : '%s' requires either 'if' or 'unless' followed by a condition.\n",
 				 file, linenum, args[0]);
diff --git a/src/cfgparse.c b/src/cfgparse.c
index bf5d46e39..e193f6606 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -2402,6 +2402,13 @@ int check_config_validity()
 		}
 
 		err_code |= proxy_check_http_errors(defpx);
+
+		/* Resolve log-format expressions in use-server rules so that
+		 * dynamic rules (e.g. %[var(...)]) get srule->dynamic=1 set.
+		 * This normally happens in proxy_finalize(), which skips defaults.
+		 */
+		if (!LIST_ISEMPTY(&defpx->server_rules))
+			cfgerr += proxy_resolve_server_rules(defpx, &err_code);
 	}
 
 	/* starting to initialize the main proxies list */
diff --git a/src/proxy.c b/src/proxy.c
index 3d8834222..56f983d14 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -1689,6 +1689,87 @@ int proxy_init_per_thr(struct proxy *px)
 	return 0;
 }
 
+/* Parse the server name of use-server rule <srule> as a log-format expression.
+ * If it resolves to a single static string, the expression is freed and
+ * srule->dynamic is left at 0 (the caller resolves srule->srv.name to a
+ * server); otherwise srule->dynamic is set to 1 and srule->srv.name is freed.
+ * Returns one of the SRV_RULE_RESOLVE_* values below.
+ */
+enum {
+	SRV_RULE_RESOLVE_ERR    = -1, /* parse error, an alert was emitted */
+	SRV_RULE_RESOLVE_STATIC =  0, /* static server name, srule->srv.name kept */
+	SRV_RULE_RESOLVE_DYN    =  1, /* dynamic log-format expr, srule->srv.name freed */
+};
+
+static int resolve_server_rule_expr(struct proxy *px, struct server_rule *srule)
+{
+	char *server_name = srule->srv.name;
+	char *err = NULL;
+
+	lf_expr_init(&srule->expr);
+	px->conf.args.ctx = ARGC_USRV;
+	px->conf.args.file = srule->file;
+	px->conf.args.line = srule->line;
+	if (!parse_logformat_string(server_name, px, &srule->expr, 0, SMP_VAL_FE_HRQ_HDR, &err)) {
+		ha_alert("Parsing [%s:%d]; use-server rule failed to parse log-format '%s' : %s.\n",
+		         srule->file, srule->line, server_name, err);
+		free(err);
+		return SRV_RULE_RESOLVE_ERR;
+	}
+
+	if (!lf_expr_isempty(&srule->expr)) {
+		struct logformat_node *node;
+
+		node = LIST_NEXT(&srule->expr.nodes.list, struct logformat_node *, list);
+		if (node->type != LOG_FMT_TEXT || node->list.n != &srule->expr.nodes.list) {
+			srule->dynamic = 1;
+			free(server_name);
+			return SRV_RULE_RESOLVE_DYN;
+		}
+		/* Only one element in the list, a simple string: free the expression and
+		 * fall back to static rule
+		 */
+		lf_expr_deinit(&srule->expr);
+	}
+
+	srule->dynamic = 0;
+	return SRV_RULE_RESOLVE_STATIC;
+}
+
+/* Resolve log-format expressions in use-server rules of <px>. This is called
+ * for defaults proxies, which are skipped by proxy_finalize(). A dynamic
+ * log-format rule (e.g. %[var(...)]) sets srule->dynamic=1 and populates
+ * srule->expr so process_server_rules() can evaluate it at request time.
+ * Returns the number of errors.
+ */
+int proxy_resolve_server_rules(struct proxy *px, int *err_code)
+{
+	struct server_rule *srule;
+	int cfgerr = 0;
+
+	list_for_each_entry(srule, &px->server_rules, list) {
+		int ret = resolve_server_rule_expr(px, srule);
+
+		if (ret == SRV_RULE_RESOLVE_ERR)
+			cfgerr++;
+		if (ret != SRV_RULE_RESOLVE_STATIC)
+			continue;
+
+		/* A static server name in a defaults section cannot be resolved:
+		 * defaults sections have no servers. Only dynamic log-format
+		 * expressions (e.g. %[var(...)]) are allowed here.
+		 */
+		ha_alert("Parsing [%s:%d]: use-server rule in a 'defaults' section requires a dynamic "
+		         "log-format expression (e.g. %%[var(...)]), not a static server name '%s'.\n",
+		         srule->file, srule->line, srule->srv.name);
+		cfgerr++;
+	}
+
+	if (cfgerr)
+		*err_code |= ERR_ALERT | ERR_FATAL;
+	return cfgerr;
+}
+
 int proxy_finalize(struct proxy *px, int *err_code)
 {
 	struct list tmp_list = LIST_HEAD_INIT(tmp_list);
@@ -2124,40 +2205,16 @@ int proxy_finalize(struct proxy *px, int *err_code)
 	/* find the target server for 'use_server' rules */
 	list_for_each_entry(srule, &px->server_rules, list) {
 		struct server *target;
-		struct logformat_node *node;
-		char *server_name;
+		int ret;
 
-		/* We try to parse the string as a log format expression. If the result of the parsing
-		 * is only one entry containing a single string, then it's a standard string corresponding
-		 * to a static rule, thus the parsing is cancelled and we fall back to setting srv.ptr.
-		 */
-		server_name = srule->srv.name;
-		lf_expr_init(&srule->expr);
-		px->conf.args.ctx = ARGC_USRV;
-		err = NULL;
-		if (!parse_logformat_string(server_name, px, &srule->expr, 0, SMP_VAL_FE_HRQ_HDR, &err)) {
-			ha_alert("Parsing [%s:%d]; use-server rule failed to parse log-format '%s' : %s.\n",
-			         srule->file, srule->line, server_name, err);
-			free(err);
+		ret = resolve_server_rule_expr(px, srule);
+		if (ret == SRV_RULE_RESOLVE_ERR) {
 			cfgerr++;
 			continue;
 		}
-		node = LIST_NEXT(&srule->expr.nodes.list, struct logformat_node *, list);
-
-		if (!lf_expr_isempty(&srule->expr)) {
-			if (node->type != LOG_FMT_TEXT || node->list.n != &srule->expr.nodes.list) {
-				srule->dynamic = 1;
-				free(server_name);
-				continue;
-			}
-			/* Only one element in the list, a simple string: free the expression and
-			 * fall back to static rule
-			 */
-			lf_expr_deinit(&srule->expr);
-		}
+		if (ret == SRV_RULE_RESOLVE_DYN) /* resolved at request time */
+			continue;
 
-		srule->dynamic = 0;
-		srule->srv.name = server_name;
 		target = server_find_by_name(px, srule->srv.name);
 		*err_code |= warnif_tcp_http_cond(px, srule->cond);
 
@@ -2822,6 +2879,10 @@ int proxy_finalize(struct proxy *px, int *err_code)
 		    (px->defpx && !LIST_ISEMPTY(&px->defpx->tcp_req.inspect_rules)))
 			px->be_req_ana |= AN_REQ_INSPECT_BE;
 
+		if (!LIST_ISEMPTY(&px->server_rules) ||
+		    (px->defpx && !LIST_ISEMPTY(&px->defpx->server_rules)))
+			px->be_req_ana |= AN_REQ_SRV_RULES;
+
 		if (!LIST_ISEMPTY(&px->tcp_rep.inspect_rules) ||
 		    (px->defpx && !LIST_ISEMPTY(&px->defpx->tcp_rep.inspect_rules)))
                         px->be_rsp_ana |= AN_RES_INSPECT;
@@ -2938,6 +2999,7 @@ static void defaults_px_free(struct proxy *defproxy)
 	proxy_free_common(defproxy);
 
 	/* default proxy specific cleanup */
+	free_server_rules(&defproxy->server_rules);
 	if (defproxy->defsrv)
 		srv_free_params(defproxy->defsrv);
 	ha_free(&defproxy->defbe.name);
@@ -3149,6 +3211,13 @@ int proxy_ref_defaults(struct proxy *px, struct proxy *defpx, char **errmsg)
 		defaults_px_ref(defpx, px);
 	}
 
+	/* server_rules (use-server) in a defaults section: backends that inherit
+	 * from it need a live reference to defpx so process_server_rules() can
+	 * walk defpx->server_rules at request time.
+	 */
+	if (!LIST_ISEMPTY(&defpx->server_rules) && (px->cap & PR_CAP_BE))
+		defaults_px_ref(defpx, px);
+
 	if (defpx->tcpcheck.rs && (defpx->tcpcheck.rs->flags & TCPCHK_RULES_PROTO_CHK) &&
 	    (px->cap & PR_CAP_LISTEN) == PR_CAP_BE) {
 		/* If the current default proxy defines tcpcheck rules, the
diff --git a/src/stream.c b/src/stream.c
index d2e3a7099..ee7e3226b 100644
--- a/src/stream.c
+++ b/src/stream.c
@@ -1335,35 +1335,47 @@ static int process_server_rules(struct stream *s, struct channel *req, int an_bi
 	DBG_TRACE_ENTER(STRM_EV_STRM_ANA, s);
 
 	if (!(s->flags & SF_ASSIGNED)) {
-		list_for_each_entry(rule, &px->server_rules, list) {
-			struct server *srv;
+		struct list *rules;
+		int pass;
+
+		for (pass = 0; pass < 2 && !(s->flags & SF_ASSIGNED); pass++) {
+			if (pass == 0)
+				rules = &px->server_rules;
+			else if (px->defpx && !LIST_ISEMPTY(&px->defpx->server_rules))
+				rules = &px->defpx->server_rules;
+			else
+				break;
 
-			if (!acl_match_cond(rule->cond, s->be, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL))
-				continue;
+			list_for_each_entry(rule, rules, list) {
+				struct server *srv;
 
-			if (rule->dynamic) {
-				struct buffer *tmp = get_trash_chunk();
+				if (!acl_match_cond(rule->cond, s->be, sess, s, SMP_OPT_DIR_REQ|SMP_OPT_FINAL))
+					continue;
 
-				if (!build_logline(s, tmp->area, tmp->size, &rule->expr))
-					break;
+				if (rule->dynamic) {
+					struct buffer *tmp = get_trash_chunk();
 
-				srv = server_find_by_name(s->be, tmp->area);
-				if (!srv)
-					break;
-			}
-			else
-				srv = rule->srv.ptr;
+					if (!build_logline(s, tmp->area, tmp->size, &rule->expr))
+						break;
 
-			if ((srv->cur_state != SRV_ST_STOPPED) ||
-			    (px->options & PR_O_PERSIST) ||
-			    (s->flags & SF_FORCE_PRST)) {
-				s->flags |= SF_DIRECT | SF_ASSIGNED;
-				stream_set_srv_target(s, srv);
-				break;
+					srv = server_find_by_name(s->be, tmp->area);
+					if (!srv)
+						break;
+				}
+				else
+					srv = rule->srv.ptr;
+
+				if ((srv->cur_state != SRV_ST_STOPPED) ||
+				    (px->options & PR_O_PERSIST) ||
+				    (s->flags & SF_FORCE_PRST)) {
+					s->flags |= SF_DIRECT | SF_ASSIGNED;
+					stream_set_srv_target(s, srv);
+					break;
+				}
+				/* if the server is not UP, let's go on with next rules
+				 * just in case another one is suited.
+				 */
 			}
-			/* if the server is not UP, let's go on with next rules
-			 * just in case another one is suited.
-			 */
 		}
 	}
 
-- 
2.51.0

