-
Notifications
You must be signed in to change notification settings - Fork 913
server-state-file silently overrides config port on reload for statically configured servers #3296
Description
Detailed Description of the Problem
When using server-state-file / load-server-state-from-file global, changing a server's port in the configuration and reloading HAProxy causes the old port from the state file to be silently applied instead of the new port from the configuration.
In src/server_state.c, srv_state_srv_update() unconditionally overwrites srv->svc_port with the value from the state file:
if (port_st)
srv->svc_port = port_svc;This is inconsistent with how weight is handled in the same function, where the state file value is only applied if the configuration hasn't changed:
if (srv_iweight == srv->iweight) {
srv->uweight = srv_uweight;
}There is no equivalent guard for the port. Since apply_server_state() runs before srv_init_addr() in src/haproxy.c, and srv_init_addr() only touches the address (not the port), nothing corrects the stale port afterwards.
Expected Behavior
When the server port is changed in the configuration and HAProxy is reloaded, the new config port should take effect. The state file should not silently override it — matching the existing behavior for weight.
Steps to Reproduce the Behavior
Reproducer script (requires socat):
#!/bin/bash
# Repro: state file overrides config port on reload (no config-change guard)
# src/server_state.c: if (port_st) srv->svc_port = port_svc;
set -euo pipefail
HAP="${HAPROXY:-haproxy}"
D=$(mktemp -d /tmp/haproxy-port-bug.XXXXXX)
trap 'kill $(cat "$D/h.pid" 2>/dev/null) 2>/dev/null; rm -rf "$D"' EXIT
mk_cfg() {
cat > "$D/h.cfg" <<EOF
global
stats socket $D/h.sock level admin expose-fd listeners
server-state-file $D/state
defaults
mode http
timeout client 5s
timeout connect 5s
timeout server 5s
load-server-state-from-file global
listen t
bind 127.0.0.1:18080
http-request return status 200
backend b
server s1 127.0.0.1:$1 init-addr 127.0.0.1
EOF
}
port_of() { echo "show servers state" | socat - "$D/h.sock" | awk '/^[0-9].* /{print $19;exit}'; }
$HAP -v | head -1
# start with port 9001, master-worker mode for USR2 reload
mk_cfg 9001
$HAP -f "$D/h.cfg" -p "$D/h.pid" -W 2>"$D/err" &
sleep 0.5
P=$(port_of); echo "1) after start: port=$P (expect 9001)"
# save state, change config to port 9002, reload
echo "show servers state" | socat - "$D/h.sock" > "$D/state"
mk_cfg 9002
kill -USR2 $(cat "$D/h.pid"); sleep 0.5
P=$(port_of); echo "2) after reload: port=$P (expect 9002)"
# dump any warnings for context
grep -i "warning\|ignoring" "$D/err" | sed 's/^/ /' || true
[ "$P" = "9002" ] && echo "OK: config port applied" && exit 0
echo "BUG: state file port 9001 overrode config port 9002"
exit 1Output on unpatched HAProxy:
HAProxy version 3.3.2-72df9192b 2026/01/29 - https://haproxy.org/
1) after start: port=9001 (expect 9001)
2) after reload: port=9001 (expect 9002)
[WARNING] (87686) : Proxy t stopped (cumulated conns: FE: 0, BE: 0).
[WARNING] (87686) : Proxy b stopped (cumulated conns: FE: 0, BE: 0).
[WARNING] (87684) : Former worker (87686) exited with code 0 (Exit)
BUG: state file port 9001 overrode config port 9002
Do you have any idea what may have caused this?
The state file port restore in srv_state_srv_update() was written to support DNS SRV record resolution, where the port is dynamically discovered and needs to persist across reloads. But the code doesn't distinguish between "port came from SRV resolution at runtime" and "port was set statically in the config", so it unconditionally overwrites.
Do you have an idea how to solve the issue?
Gate the state file port restore on srv->srvrq being non-null, which is only set for servers using SRV record resolution. For statically configured servers, srv->svc_port is already set from config parsing and should not be overwritten.
diff --git a/src/server_state.c b/src/server_state.c
index 5d7573104..3ec32b447 100644
--- a/src/server_state.c
+++ b/src/server_state.c
@@ -438,7 +438,13 @@ static void srv_state_srv_update(struct server *srv, int version, char **params)
srv->flags &= ~SRV_F_MAPPORTS;
}
- if (port_st)
+ /* Only restore the port from the state file for SRV-managed servers,
+ * where the port is dynamically resolved from DNS SRV records.
+ * For statically configured servers, the config port (already in
+ * srv->svc_port) must take precedence, otherwise a config change
+ * would be silently ignored on reload.
+ */
+ if (port_st && srv->srvrq)
srv->svc_port = port_svc;What is your configuration?
global
stats socket /tmp/h.sock level admin expose-fd listeners
server-state-file /tmp/state
defaults
mode http
timeout client 5s
timeout connect 5s
timeout server 5s
load-server-state-from-file global
listen t
bind 127.0.0.1:18080
http-request return status 200
backend b
server s1 127.0.0.1:9001Output of haproxy -vv
HAProxy version 3.3.2-72df9192b 2026/01/29 - https://haproxy.org/
Status: stable branch - will stop receiving fixes around Q1 2027.
Known bugs: http://www.haproxy.org/bugs/bugs-3.3.2.html
Running on: Darwin 25.2.0 Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:55 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8103 arm64
Build options :
TARGET = osx
CC = cc
CFLAGS = -O2 -g -fwrapv
OPTIONS = USE_OPENSSL=1 USE_ZLIB=1 USE_PCRE2=1 USE_PCRE2_JIT=1
DEBUG =
Feature list : -51DEGREES -ACCEPT4 -BACKTRACE -CLOSEFROM +CPU_AFFINITY -CRYPT_H -DEVICEATLAS -DL -ECH -ENGINE -EPOLL -EVPORTS +GETADDRINFO +KQUEUE -KTLS -LIBATOMIC +LIBCRYPT -LINUX_CAP -LINUX_SPLICE -LINUX_TPROXY -LUA -MATH -MEMORY_PROFILING -NETFILTER -NS -OBSOLETE_LINKER +OPENSSL -OPENSSL_AWSLC -OPENSSL_WOLFSSL -OT -PCRE +PCRE2 +PCRE2_JIT -PCRE_JIT +POLL -PRCTL -PROCCTL -PROMEX -PTHREAD_EMULATION -QUIC -QUIC_OPENSSL_COMPAT -RT -SHM_OPEN -SLZ +SSL -STATIC_PCRE -STATIC_PCRE2 -TFO +THREAD -THREAD_DUMP +TPROXY -WURFL +ZLIB +ACME
Default settings :
bufsize = 16384, maxrewrite = 1024, maxpollevents = 200
Built with multi-threading support (MAX_TGROUPS=32, MAX_THREADS=1024, default=8).
Built with SSL library version : OpenSSL 3.6.1 27 Jan 2026
Running on SSL library version : OpenSSL 3.6.1 27 Jan 2026
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with zlib version : 1.2.12
Running on zlib version : 1.2.12
Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using:
Built with PCRE2 version : 10.47 2025-10-21
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with clang compiler version 17.0.0 (clang-1700.6.3.2)
Available polling systems :
kqueue : pref=300, test result OK
poll : pref=200, test result OK
select : pref=150, test result OK
Total: 3 (3 usable), will use kqueue.
Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto' keyword)
h2 : mode=HTTP side=FE|BE mux=H2 flags=HTX|HOL_RISK|NO_UPG
h1 : mode=HTTP side=FE|BE mux=H1 flags=HTX|NO_UPG
<default> : mode=HTTP side=FE|BE mux=H1 flags=HTX
fcgi : mode=HTTP side=BE mux=FCGI flags=HTX|HOL_RISK|NO_UPG
spop : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
<default> : mode=SPOP side=BE mux=SPOP flags=HOL_RISK|NO_UPG
none : mode=TCP side=FE|BE mux=PASS flags=NO_UPG
<default> : mode=TCP side=FE|BE mux=PASS flags=
Available services : none
Available filters :
[BWLIM] bwlim-in
[BWLIM] bwlim-out
[CACHE] cache
[COMP] compression
[FCGI] fcgi-app
[SPOE] spoe
[TRACE] trace
Confirmed on 2.8.16, 3.3.4, and current `master` (same code path).
Last Outputs and Backtraces
Additional Information
This bug was discovered while working on server state persistence for GitLab's HAProxy fleet. We had previously implemented init-addr <config-ip> to prevent stale IP addresses from being restored from the state file (see this blog post), but found that port changes in the configuration were still being silently ignored on reload.