]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: httpcheck/ssl: Base the SNI value on the HTTP host header by default
authorChristopher Faulet <cfaulet@haproxy.com>
Thu, 4 Sep 2025 10:13:54 +0000 (12:13 +0200)
committerChristopher Faulet <cfaulet@haproxy.com>
Fri, 5 Sep 2025 13:56:42 +0000 (15:56 +0200)
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
include/haproxy/server-t.h
include/haproxy/tcpcheck-t.h
src/cfgparse-ssl.c
src/tcpcheck.c

index 4c18eb7f616b08f18822703a9a9d690c4b3cfabc..4274323d46c6b9977267145d56f6002b0f71d842 100644 (file)
@@ -17635,6 +17635,28 @@ check-proto <name>
   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 <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 <addr>[:<pl>[-<ph>]] [usesrc { <addr2>[:<port2>] | client | clientip } ]
 source <addr>[:<port>] [usesrc { <addr2>[:<port2>] | hdr_ip(<hdr>[,<occ>]) } ]
 source <addr>[:<pl>[-<ph>]] [interface <name>] ...
index 3cd1f7bdd3622025689040c1212b4e1836af9a13..1829b9763feb3f0cddb3e57a2ed9475fdca01ac8 100644 (file)
@@ -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 */
index f01d512ebf4be2fd1d9df8b5a65d58ed9ab852d7..c660cd477aa550c8584d3e9b6e78ccd88b9169cf 100644 (file)
@@ -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) */
index 141da23573719e9f4d4c811d45d73c01a78f65dd..d135addcb4ee5bc3ca600f5c224643cb77574898 100644 (file)
@@ -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 */
index b910944d6327640a8e2896f06c6572642dc56609..144ea256cede13eabb6f61d48d7bfe40e766e3cf 100644 (file)
@@ -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).
         */