From 668916c1a2fc2180028ae051aa805bb71c7b690b Mon Sep 17 00:00:00 2001 From: Christopher Faulet Date: Thu, 4 Sep 2025 10:12:19 +0200 Subject: [PATCH] MEDIUM: server/ssl: Base the SNI value to the HTTP host header by default For HTTPS outgoing connections, the SNI is now automatically set using the Host header value if no other value is already set (via the "sni" server keyword). It is now the default behavior. It could be disabled with the "no-sni-auto" server keyword. And eventually "sni-auto" server keyword may be used to reset any previous "no-sni-auto" setting. This option can be inherited from "default-server" settings. Finally, if no connection name is set via "pool-conn-name" setting, the selected value is used. The automatic selection of the SNI is enabled by default for all outgoing connections. But it is concretely used for HTTPS connections only. The expression used is "req.hdr(host),host_only". This patch should paritally fix the issue #3081. It only covers the server part. Another patch will add the feature for HTTP health-checks. --- doc/configuration.txt | 24 ++++++++++++++++++++++++ include/haproxy/server-t.h | 1 + include/haproxy/server.h | 1 + reg-tests/ssl/set_ssl_cafile.vtc | 4 ++-- reg-tests/ssl/set_ssl_crlfile.vtc | 2 +- src/cfgparse-ssl.c | 16 ++++++++++++++++ src/cfgparse.c | 19 +++++++++++++++++++ src/server.c | 9 ++++----- 8 files changed, 68 insertions(+), 8 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 507c2da03..4c18eb7f6 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -18189,6 +18189,12 @@ no-send-proxy-v2-ssl-cn It may also be used as "default-server" setting to reset any previous "default-server" "send-proxy-v2-ssl-cn" setting. +no-sni-auto + May be used in the following contexts: tcp, http, log, peers, ring + + This option may be used as "server" setting to disable the automatic SNI + selection which is enabled by default. + no-ssl May be used in the following contexts: tcp, http, log, peers, ring @@ -18756,6 +18762,24 @@ sni By default, the SNI is assigned to the connection name for "http-reuse", unless overridden by the "pool-conn-name" server keyword. +sni-auto + May be used in the following contexts: tcp, http, log, peers, ring + + The "sni-auto" parameter enables the automatic SNI selection, if no value was + already set. It is enabled by default but this parameter may be used as + "server" setting to reset any "no-sni-auto" setting which would have been + inherited from "default-server" directive as default value. It may also be + used as "default-server" setting to reset any previous "default-server" + "no-sni-auto" setting. + + For HTTPS connections, the selected SNI is based on the request host header + value, if found. Otherwise it remains unset. For other protocols, the option + is ignored. + + If the automatic selection of the SNI is used, the value is assigned to the + connection name for "http-reuse", unless overridden by the "pool-conn-name" + server keyword. + source [:[-]] [usesrc { [:] | client | clientip } ] source [:] [usesrc { [:] | hdr_ip([,]) } ] source [:[-]] [interface ] ... diff --git a/include/haproxy/server-t.h b/include/haproxy/server-t.h index dd76ffac6..3cd1f7bdd 100644 --- a/include/haproxy/server-t.h +++ b/include/haproxy/server-t.h @@ -201,6 +201,7 @@ enum srv_init_state { /* server ssl options */ #define SRV_SSL_O_NONE 0x0000 +#define SRV_SSL_O_NO_AUTO_SNI 0x0001 /* disable automatic SNI */ #define SRV_SSL_O_NO_TLS_TICKETS 0x0100 /* disable session resumption tickets */ #define SRV_SSL_O_NO_REUSE 0x200 /* disable session reuse */ #define SRV_SSL_O_EARLY_DATA 0x400 /* Allow using early data */ diff --git a/include/haproxy/server.h b/include/haproxy/server.h index 7f38cd53b..79cf880b3 100644 --- a/include/haproxy/server.h +++ b/include/haproxy/server.h @@ -52,6 +52,7 @@ int parse_server(const char *file, int linenum, char **args, struct proxy *curpr int srv_update_addr(struct server *s, void *ip, int ip_sin_family, struct server_inetaddr_updater updater); struct sample_expr *_parse_srv_expr(char *expr, struct arg_list *args_px, const char *file, int linenum, char **err); +int server_parse_exprs(struct server *srv, struct proxy *px, char **err); int server_set_inetaddr(struct server *s, const struct server_inetaddr *inetaddr, struct server_inetaddr_updater updater, struct buffer *msg); int server_set_inetaddr_warn(struct server *s, const struct server_inetaddr *inetaddr, struct server_inetaddr_updater updater); void server_get_inetaddr(struct server *s, struct server_inetaddr *inetaddr); diff --git a/reg-tests/ssl/set_ssl_cafile.vtc b/reg-tests/ssl/set_ssl_cafile.vtc index 897fe5e40..66511ded8 100644 --- a/reg-tests/ssl/set_ssl_cafile.vtc +++ b/reg-tests/ssl/set_ssl_cafile.vtc @@ -51,11 +51,11 @@ haproxy h1 -conf { bind "fd@${clearlst}" # dummy bind used to test a change when the same crt is used as server and bind bind "fd@${foobarlst}" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA1.crt verify none - server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA1.crt verify none + server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA1.crt verify none no-sni-auto listen clear-verified-lst bind "fd@${clearverifiedlst}" - server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA1.crt verify required + server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA1.crt verify required no-sni-auto listen ssl-lst # crt: certificate of the server diff --git a/reg-tests/ssl/set_ssl_crlfile.vtc b/reg-tests/ssl/set_ssl_crlfile.vtc index 05ff67382..23537918c 100644 --- a/reg-tests/ssl/set_ssl_crlfile.vtc +++ b/reg-tests/ssl/set_ssl_crlfile.vtc @@ -52,7 +52,7 @@ haproxy h1 -conf { listen clear-lst bind "fd@${clearlst}" - server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA2.crt crl-file ${testdir}/interCA2_crl_empty.pem verify required + server s1 "${tmpdir}/ssl.sock" ssl crt ${testdir}/set_cafile_client.pem ca-file ${testdir}/set_cafile_interCA2.crt crl-file ${testdir}/interCA2_crl_empty.pem verify required no-sni-auto listen ssl-lst # crt: certificate of the server diff --git a/src/cfgparse-ssl.c b/src/cfgparse-ssl.c index 583461d6a..141da2357 100644 --- a/src/cfgparse-ssl.c +++ b/src/cfgparse-ssl.c @@ -2033,6 +2033,20 @@ static int srv_parse_sni(char **args, int *cur_arg, struct proxy *px, struct ser #endif } +/* parse the "sni-auto" server keyword */ +static int srv_parse_sni_auto(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) +{ + newsrv->ssl_ctx.options &= ~SRV_SSL_O_NO_AUTO_SNI; + return 0; +} + +/* parse the "no-sni-auto" server keyword */ +static int srv_parse_no_sni_auto(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) +{ + newsrv->ssl_ctx.options |= SRV_SSL_O_NO_AUTO_SNI; + return 0; +} + /* parse the "ssl" server keyword */ static int srv_parse_ssl(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) { @@ -2597,6 +2611,7 @@ static struct srv_kw_list srv_kws = { "SSL", { }, { { "no-renegotiate", srv_parse_renegotiate, 0, 1, 1 }, /* Disable renegotiation */ { "no-send-proxy-v2-ssl", srv_parse_no_send_proxy_ssl, 0, 1, 0 }, /* do not send PROXY protocol header v2 with SSL info */ { "no-send-proxy-v2-ssl-cn", srv_parse_no_send_proxy_cn, 0, 1, 0 }, /* do not send PROXY protocol header v2 with CN */ + { "no-sni-auto", srv_parse_no_sni_auto, 0, 1, 0 }, /* disable automatic SNI selection */ { "no-ssl", srv_parse_no_ssl, 0, 1, 0 }, /* disable SSL processing */ { "no-ssl-reuse", srv_parse_no_ssl_reuse, 0, 1, 1 }, /* disable session reuse */ { "no-sslv3", srv_parse_tls_method_options, 0, 0, 1 }, /* disable SSLv3 */ @@ -2611,6 +2626,7 @@ static struct srv_kw_list srv_kws = { "SSL", { }, { { "send-proxy-v2-ssl-cn", srv_parse_send_proxy_cn, 0, 1, 1 }, /* send PROXY protocol header v2 with CN */ { "sigalgs", srv_parse_sigalgs, 1, 1, 1 }, /* signature algorithms */ { "sni", srv_parse_sni, 1, 1, 1 }, /* send SNI extension */ + { "sni-auto", srv_parse_sni_auto, 0, 1, 0 }, /* enable automatic SNI selection */ { "ssl", srv_parse_ssl, 0, 1, 1 }, /* enable SSL processing */ { "ssl-min-ver", srv_parse_tls_method_minmax, 1, 1, 1 }, /* minimum version */ { "ssl-max-ver", srv_parse_tls_method_minmax, 1, 1, 1 }, /* maximum version */ diff --git a/src/cfgparse.c b/src/cfgparse.c index e1b4933dc..0bd29feb0 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -3761,6 +3761,25 @@ out_uri_auth_compat: cfgerr += xprt_get(XPRT_QUIC)->prepare_srv(newsrv); } + if (newsrv->use_ssl == 1 || ((newsrv->flags & SRV_F_DEFSRV_USE_SSL) && newsrv->use_ssl != 1)) { + /* In HTTP only, if the SNI not set and we can realy on the host + * header value, fill the sni expression accordingly + */ + if (newsrv->proxy->mode == PR_MODE_HTTP && !(newsrv->ssl_ctx.options & SRV_SSL_O_NO_AUTO_SNI)) { + newsrv->sni_expr = strdup("req.hdr(host),field(1,:)"); + + err = NULL; + if (server_parse_exprs(newsrv, curproxy, &err)) { + ha_alert("parsing [%s:%d]: failed to parse auto SNI expression: %s\n", + newsrv->conf.file, newsrv->conf.line, err); + free(err); + ++cfgerr; + goto next_srv; + } + } + } + + if ((newsrv->flags & SRV_F_FASTOPEN) && ((curproxy->retry_type & (PR_RE_DISCONNECTED | PR_RE_TIMEOUT)) != (PR_RE_DISCONNECTED | PR_RE_TIMEOUT))) diff --git a/src/server.c b/src/server.c index 441b7f831..83257e151 100644 --- a/src/server.c +++ b/src/server.c @@ -54,7 +54,6 @@ #include #include -static inline int _srv_parse_exprs(struct server *srv, struct proxy *px, char **errmsg); static void srv_update_status(struct server *s, int type, int cause); static int srv_apply_lastaddr(struct server *srv, int *err_code); static void srv_cleanup_connections(struct server *srv); @@ -2768,7 +2767,7 @@ int srv_set_ssl(struct server *s, int use_ssl) s->use_ssl = use_ssl; if (s->use_ssl) { - if (_srv_parse_exprs(s, s->proxy, NULL)) + if (server_parse_exprs(s, s->proxy, NULL)) return -1; s->xprt = xprt_get(XPRT_SSL); } @@ -3303,7 +3302,7 @@ static inline void _srv_parse_set_id_from_prefix(struct server *srv, /* Parse the sni and pool-conn-name expressions. Returns 0 on success and non-zero on * error. */ -static inline int _srv_parse_exprs(struct server *srv, struct proxy *px, char **errmsg) +int server_parse_exprs(struct server *srv, struct proxy *px, char **errmsg) { int ret = 0; @@ -3369,7 +3368,7 @@ static int _srv_parse_tmpl_init(struct server *srv, struct proxy *px) srv_settings_cpy(newsrv, srv, 1); srv_prepare_for_resolution(newsrv, srv->hostname); - if (_srv_parse_exprs(newsrv, px, NULL)) + if (server_parse_exprs(newsrv, px, NULL)) goto err; /* append to list of servers available to receive an hostname */ @@ -3876,7 +3875,7 @@ static int _srv_parse_finalize(char **args, int cur_arg, return ERR_ALERT | ERR_FATAL; } - if ((ret = _srv_parse_exprs(srv, px, &errmsg))) { + if ((ret = server_parse_exprs(srv, px, &errmsg))) { if (errmsg) { ha_alert("error detected while parsing sni or pool-conn-name expressions : %s.\n", errmsg); free(errmsg);