From ffc1f096e00def9a69f5898063d03cebf55cdbca Mon Sep 17 00:00:00 2001 From: Christopher Faulet Date: Thu, 4 Sep 2025 12:13:54 +0200 Subject: [PATCH] MEDIUM: httpcheck/ssl: Base the SNI value on the HTTP host header by default Similarly to the automic SNI selection for regulat SSL traffic, the SNI of health-checks HTTPS connection is now automatically set by default by using the host header value. "check-sni-auto" and "no-check-sni-auto" server settings were added to change this behavior. Only implicit HTTPS health-checks can take advantage of this feature. In this case, the host header value from the "option httpchk" directive is used to extract the SNI. It is disabled if http-check rules are used. So, the SNI must still be explicitly specified via a "http-check connect" rule. This patch with should paritally fix the issue #3081. --- doc/configuration.txt | 37 ++++++++++++++++++++ include/haproxy/server-t.h | 2 +- include/haproxy/tcpcheck-t.h | 1 + src/cfgparse-ssl.c | 16 +++++++++ src/tcpcheck.c | 68 ++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 4c18eb7f6..4274323d4 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -17635,6 +17635,28 @@ check-proto protocol for health-check connections established to this server. If not defined, the server one will be used, if set. +check-sni-auto + May be used in the following contexts: tcp, http, log + + This option enables the automatic SNI selection when doing health checks over + SSL, if no value was already set. It is enabled by default but this parameter + may be used as "server" setting to reset any "no-check-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-check-sni-auto" setting. + + For HTTPS connections, the SNI is automatically selected but only if there is + no "http-check connect" rule. In that case, the selected SNI is based on the + host header value, specified via the "option httpchk" directive or a + "http-check send" rule. There is no automatic selection for "http-check + connect" rules. For other protocols, the option is ignored. + + If the automatic selection of the SNI is used for health-checks, the value is + assigned to the connection name if "check-reuse-pool" setting is set, unless + overridden by the "check-pool-conn-name" server keyword. + + See "sni-auto" option to enable automatic SNI selection for proxied traffic. + check-sni May be used in the following contexts: tcp, http, log @@ -18129,6 +18151,15 @@ no-check-reuse-pool This option reverts any previous "check-reuse-pool" possibly inherited from a "default-server". Any checks will be conducted on its dedicated connection. +no-check-sni-auto + May be used in the following contexts: tcp, http, log + + This option may be used as "server" setting to disable the automatic SNI + selection for SSL health checks which is enabled by default. + + See "no-sni-auto" option to disable automatic SNI selection for proxied + traffic. + no-check-ssl May be used in the following contexts: tcp, http, log @@ -18195,6 +18226,9 @@ no-sni-auto This option may be used as "server" setting to disable the automatic SNI selection which is enabled by default. + See "no-check-sni-auto" option to disable automatic SNI selection for SSL + health checks. + no-ssl May be used in the following contexts: tcp, http, log, peers, ring @@ -18780,6 +18814,9 @@ sni-auto connection name for "http-reuse", unless overridden by the "pool-conn-name" server keyword. + See "check-sni-auto" option to enable automatic SNI selection for SSL health + checks. + 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 3cd1f7bdd..1829b9763 100644 --- a/include/haproxy/server-t.h +++ b/include/haproxy/server-t.h @@ -171,7 +171,7 @@ enum srv_init_state { #define SRV_F_DEFSRV_USE_SSL 0x4000 /* default-server uses SSL */ #define SRV_F_DELETED 0x8000 /* srv is deleted but not yet purged */ #define SRV_F_STRICT_MAXCONN 0x10000 /* maxconn is to be strictly enforced, as a limit of outbound connections */ -/* unused: 0x20000 */ +#define SRV_F_CHK_NO_AUTO_SNI 0x20000 /* disable automatic SNI selection for healthcheck */ /* configured server options for send-proxy (server->pp_opts) */ #define SRV_PP_V1 0x0001 /* proxy protocol version 1 */ diff --git a/include/haproxy/tcpcheck-t.h b/include/haproxy/tcpcheck-t.h index f01d512eb..c660cd477 100644 --- a/include/haproxy/tcpcheck-t.h +++ b/include/haproxy/tcpcheck-t.h @@ -125,6 +125,7 @@ enum tcpcheck_rule_type { struct check; struct tcpcheck_connect { char *sni; /* server name to use for SSL connections */ + struct lf_expr *sni_fmt; /* log-format string used for SNI. if defined, point on the following HTTP host header value */ char *alpn; /* ALPN to use for the SSL connection */ int alpn_len; /* ALPN string length */ const struct mux_proto_list *mux_proto; /* the mux to use for all outgoing connections (specified by the "proto" keyword) */ diff --git a/src/cfgparse-ssl.c b/src/cfgparse-ssl.c index 141da2357..d135addcb 100644 --- a/src/cfgparse-ssl.c +++ b/src/cfgparse-ssl.c @@ -1878,6 +1878,20 @@ static int srv_parse_crt(char **args, int *cur_arg, struct proxy *px, struct ser return 0; } +/* parse the "check-sni-auto" server keyword */ +static int srv_parse_check_sni_auto(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) +{ + newsrv->flags &= ~SRV_F_CHK_NO_AUTO_SNI; + return 0; +} + +/* parse the "no-check-sni-auto" server keyword */ +static int srv_parse_no_check_sni_auto(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) +{ + newsrv->flags |= SRV_F_CHK_NO_AUTO_SNI; + return 0; +} + /* parse the "no-check-ssl" server keyword */ static int srv_parse_no_check_ssl(char **args, int *cur_arg, struct proxy *px, struct server *newsrv, char **err) { @@ -2594,6 +2608,7 @@ static struct srv_kw_list srv_kws = { "SSL", { }, { { "ca-file", srv_parse_ca_file, 1, 1, 1 }, /* set CAfile to process verify server cert */ { "check-alpn", srv_parse_check_alpn, 1, 1, 1 }, /* Set ALPN used for checks */ { "check-sni", srv_parse_check_sni, 1, 1, 1 }, /* set SNI */ + { "check-sni-auto", srv_parse_check_sni_auto, 0, 1, 0 }, /* enable automatic SNI selection for health checks */ { "check-ssl", srv_parse_check_ssl, 0, 1, 1 }, /* enable SSL for health checks */ { "ciphers", srv_parse_ciphers, 1, 1, 1 }, /* select the cipher suite */ { "ciphersuites", srv_parse_ciphersuites, 1, 1, 1 }, /* select the cipher suite */ @@ -2607,6 +2622,7 @@ static struct srv_kw_list srv_kws = { "SSL", { }, { { "force-tlsv12", srv_parse_tls_method_options, 0, 1, 1 }, /* force TLSv12 */ { "force-tlsv13", srv_parse_tls_method_options, 0, 1, 1 }, /* force TLSv13 */ { "ktls", srv_parse_ktls, 1, 1, 1 }, /* enable or disable kTLS */ + { "no-check-sni-auto", srv_parse_no_check_sni_auto, 0, 1, 0 }, /* disable automatic SNI selection for health checks */ { "no-check-ssl", srv_parse_no_check_ssl, 0, 1, 0 }, /* disable SSL for health checks */ { "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 */ diff --git a/src/tcpcheck.c b/src/tcpcheck.c index b910944d6..144ea256c 100644 --- a/src/tcpcheck.c +++ b/src/tcpcheck.c @@ -1232,6 +1232,27 @@ static inline int tcpcheck_connect_use_ssl(const struct check *check, return 0; } +static inline void tcpcheck_connect_auto_sni(const struct check *check, + const struct tcpcheck_connect *connect, + struct buffer *chk) +{ + chunk_reset(chk); + chk->data = sess_build_logline(check->sess, NULL, b_orig(chk), b_size(chk), connect->sni_fmt); + if (b_data(chk)) { + char *beg = b_orig(chk); + char *end = b_tail(chk) - 1; + char *p; + + for (p = end; p >= beg; p--) { + if (*p == ':' || *p == ']') + break; + } + if (p >= beg && *p == ':') + b_set_data(chk, p - beg); + *b_tail(chk) = 0; + } +} + /* Evaluates a TCPCHK_ACT_CONNECT rule. Returns TCPCHK_EVAL_WAIT to wait the * connection establishment, TCPCHK_EVAL_CONTINUE to evaluate the next rule or * TCPCHK_EVAL_STOP if an error occurred. @@ -1247,6 +1268,7 @@ enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpchec struct protocol *proto; struct xprt_ops *xprt; struct tcpcheck_rule *next; + struct buffer *auto_sni = NULL; int status, port; TRACE_ENTER(CHK_EV_TCPCHK_CONN, check); @@ -1275,6 +1297,19 @@ enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpchec check_release_buf(check, &check->bi); check_release_buf(check, &check->bo); + /* Deal with automatic SNI selection now because it can be used as connection name */ + if (tcpcheck_connect_use_ssl(check, connect) && s && !(s->flags & SRV_F_CHK_NO_AUTO_SNI) && connect->sni_fmt) { + auto_sni = alloc_trash_chunk(); + if (auto_sni) { + tcpcheck_connect_auto_sni(check, connect, auto_sni); + if (!b_data(auto_sni)) { + free_trash_chunk(auto_sni); + auto_sni = NULL; + } + } + } + + if (!(check->state & CHK_ST_AGENT) && check->reuse_pool && !tcpcheck_use_nondefault_connect(check, connect) && !srv_is_transparent(s)) { @@ -1292,6 +1327,8 @@ enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpchec pool_conn_name = ist(connect->sni); else if ((connect->options & TCPCHK_OPT_DEFAULT_CONNECT) && check->sni) pool_conn_name = ist(check->sni); + else if (auto_sni) + pool_conn_name = ist2(b_orig(auto_sni), b_data(auto_sni)); } if (!(s->flags & SRV_F_RHTTP)) { @@ -1444,6 +1481,8 @@ enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpchec ssl_sock_set_servername(conn, connect->sni); else if ((connect->options & TCPCHK_OPT_DEFAULT_CONNECT) && s && s->check.sni) ssl_sock_set_servername(conn, s->check.sni); + else if (auto_sni) + ssl_sock_set_servername(conn, b_orig(auto_sni)); if (connect->alpn) ssl_sock_set_alpn(conn, (unsigned char *)connect->alpn, connect->alpn_len); @@ -1551,6 +1590,9 @@ enum tcpcheck_eval_ret tcpcheck_eval_connect(struct check *check, struct tcpchec if (ret == TCPCHK_EVAL_CONTINUE && check->proxy->timeout.check) check->task->expire = tick_add_ifset(now_ms, check->proxy->timeout.check); + if (auto_sni) + free_trash_chunk(auto_sni); + TRACE_LEAVE(CHK_EV_TCPCHK_CONN, check, 0, 0, (size_t[]){ret}); return ret; } @@ -3959,6 +4001,32 @@ static int check_proxy_tcpcheck(struct proxy *px) LIST_INSERT(px->tcpcheck_rules.list, &chk->list); } + /* Now, back again on HTTP ruleset. Try to resolve the sni log-format + * string if necessary, but onlu for implicit connect rules, by getting + * it from the following send rule. + */ + if ((px->tcpcheck_rules.flags & TCPCHK_RULES_PROTO_CHK) == TCPCHK_RULES_HTTP_CHK) { + struct tcpcheck_connect *connect = NULL; + + list_for_each_entry(chk, px->tcpcheck_rules.list, list) { + if (chk->action == TCPCHK_ACT_CONNECT && !chk->connect.sni && + (chk->connect.options & TCPCHK_OPT_IMPLICIT)) { + /* Only eval connect rule with no explici SNI */ + connect = &chk->connect; + } + else if (connect && chk->action == TCPCHK_ACT_SEND) { + struct tcpcheck_http_hdr *hdr; + + list_for_each_entry(hdr, &chk->send.http.hdrs, list) { + if (isteqi(hdr->name, ist("host"))) + connect->sni_fmt = &hdr->value; + } + connect = NULL; + } + } + } + + /* Remove all comment rules. To do so, when a such rule is found, the * comment is assigned to the following rule(s). */