diff --git a/doc/admin-guide/files/sni.yaml.en.rst b/doc/admin-guide/files/sni.yaml.en.rst index 95958943630..480f52863d8 100644 --- a/doc/admin-guide/files/sni.yaml.en.rst +++ b/doc/admin-guide/files/sni.yaml.en.rst @@ -172,6 +172,35 @@ server_groups_list Inbound Specifies an override to the `OpenSSL SSL_CTX_set_groups_list `_ documentation. + Each item contains a group key identifying the server group name and optionally, + a weight that determines how frequently that group is selected when multiple + entries are present. + + Any omitted weight defaults to 100. + For example: + + .. code-block:: yaml + + sni: + - fqdn: example1.com + server_groups_list: + - group: "group_1" + weight: 20 + - group: "group_2" + weight: 80 + - fqdn: example2.com + server_groups_list: + - group: "group_1" + - fqdn: example3.com + server_groups_list: "group_1" + + In this configuration: + - connections with SNI example1.com will be directed to group_1 about 20% + of the time and to group_2 about 80% of the time + - Connections for example2.com and example3.com will always use group_1. + + + host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`. If not specified, the value of :ts:cv:`proxy.config.http.host_sni_policy` is used. diff --git a/include/iocore/net/YamlSNIConfig.h b/include/iocore/net/YamlSNIConfig.h index c8396a76e74..139568e4dd1 100644 --- a/include/iocore/net/YamlSNIConfig.h +++ b/include/iocore/net/YamlSNIConfig.h @@ -78,6 +78,11 @@ TSDECL(server_max_early_data); class ActionItem; +struct SNIServerGroupsList { + std::string group; + int weight = 100; +}; + struct YamlSNIConfig { enum class Policy : uint8_t { DISABLED = 0, PERMISSIVE, ENFORCED, UNSET }; enum class Property : uint8_t { NONE = 0, SIGNATURE_MASK = 0x1, NAME_MASK = 0x2, ALL_MASK = 0x3, UNSET }; @@ -91,36 +96,36 @@ struct YamlSNIConfig { std::vector inbound_port_ranges; - std::optional offer_h2; // Has no value by default, so do not initialize! - std::optional offer_quic; // Has no value by default, so do not initialize! - uint8_t verify_client_level = 255; - std::string verify_client_ca_file; - std::string verify_client_ca_dir; - uint8_t host_sni_policy = 255; - SNIRoutingType tunnel_type = SNIRoutingType::NONE; - std::string tunnel_destination; - Policy verify_server_policy = Policy::UNSET; - Property verify_server_properties = Property::UNSET; - std::string client_cert; - std::string client_key; - std::string client_sni_policy; - std::string server_cipher_suite; - std::string server_TLSv1_3_cipher_suites; - std::string server_groups_list; - std::string ip_allow; - bool protocol_unset = true; - unsigned long protocol_mask; - int valid_tls_version_min_in = -1; - int valid_tls_version_max_in = -1; - std::vector tunnel_alpn{}; - std::optional http2_buffer_water_mark; - std::optional http2_max_settings_frames_per_minute; - std::optional http2_max_ping_frames_per_minute; - std::optional http2_max_priority_frames_per_minute; - std::optional http2_max_rst_stream_frames_per_minute; - std::optional http2_max_continuation_frames_per_minute; - uint32_t server_max_early_data = 0; - std::optional http2_initial_window_size_in; + std::optional offer_h2; // Has no value by default, so do not initialize! + std::optional offer_quic; // Has no value by default, so do not initialize! + uint8_t verify_client_level = 255; + std::string verify_client_ca_file; + std::string verify_client_ca_dir; + uint8_t host_sni_policy = 255; + SNIRoutingType tunnel_type = SNIRoutingType::NONE; + std::string tunnel_destination; + Policy verify_server_policy = Policy::UNSET; + Property verify_server_properties = Property::UNSET; + std::string client_cert; + std::string client_key; + std::string client_sni_policy; + std::string server_cipher_suite; + std::string server_TLSv1_3_cipher_suites; + std::vector server_groups_list; + std::string ip_allow; + bool protocol_unset = true; + unsigned long protocol_mask; + int valid_tls_version_min_in = -1; + int valid_tls_version_max_in = -1; + std::vector tunnel_alpn{}; + std::optional http2_buffer_water_mark; + std::optional http2_max_settings_frames_per_minute; + std::optional http2_max_ping_frames_per_minute; + std::optional http2_max_priority_frames_per_minute; + std::optional http2_max_rst_stream_frames_per_minute; + std::optional http2_max_continuation_frames_per_minute; + uint32_t server_max_early_data = 0; + std::optional http2_initial_window_size_in; bool tunnel_prewarm_srv = false; uint32_t tunnel_prewarm_min = 0; diff --git a/src/iocore/net/SNIActionPerformer.cc b/src/iocore/net/SNIActionPerformer.cc index 3b30db815df..ab80317e3b7 100644 --- a/src/iocore/net/SNIActionPerformer.cc +++ b/src/iocore/net/SNIActionPerformer.cc @@ -516,10 +516,32 @@ ServerGroupsList::SNIAction(SSL &ssl, const Context & /* ctx ATS_UNUSED */) cons if (tbs == nullptr) { return SSL_TLSEXT_ERR_OK; } - Dbg(dbg_ctl_ssl_sni, "Setting groups list from server_groups_list to %s", server_groups_list.c_str()); - if (!tbs->set_groups_list(server_groups_list)) { - Error("Invalid server_groups_list: %s", server_groups_list.c_str()); + int total = 0; + for (auto const &g : server_groups_list) { + total += g.weight; + } + if (total < 1) { + Warning("server_groups_list has invalid weights (sum <= 0)"); + return SSL_TLSEXT_ERR_ALERT_WARNING; + } + + int r = random() % total; + int culmative = 0; + std::string group; + for (auto const &g : server_groups_list) { + int start = culmative; + culmative += g.weight; + if (r >= start && r < culmative) { + group = g.group; + break; + } + } + + Dbg(dbg_ctl_ssl_sni, "selecting server group '%s' (rand=%i, total_sum=%i)", group.c_str(), r, total); + + if (!tbs->set_groups_list(group)) { + Warning("Invalid server group '%s' in SNI configuration", group.c_str()); return SSL_TLSEXT_ERR_ALERT_WARNING; } return SSL_TLSEXT_ERR_OK; diff --git a/src/iocore/net/SNIActionPerformer.h b/src/iocore/net/SNIActionPerformer.h index c173caacaad..b0e441c58ca 100644 --- a/src/iocore/net/SNIActionPerformer.h +++ b/src/iocore/net/SNIActionPerformer.h @@ -349,11 +349,11 @@ class ServerTLSv1_3CipherSuites : public ActionItem class ServerGroupsList : public ActionItem { public: - ServerGroupsList(std::string const &p) : server_groups_list(p) {} + ServerGroupsList(std::vector const &p) : server_groups_list(p) {} ~ServerGroupsList() override {} int SNIAction(SSL &ssl, const Context &ctx) const override; private: - std::string const server_groups_list{}; + std::vector const server_groups_list{}; }; diff --git a/src/iocore/net/YamlSNIConfig.cc b/src/iocore/net/YamlSNIConfig.cc index bbc0eb4ace0..22397dc4660 100644 --- a/src/iocore/net/YamlSNIConfig.cc +++ b/src/iocore/net/YamlSNIConfig.cc @@ -463,7 +463,24 @@ template <> struct convert { item.server_TLSv1_3_cipher_suites = node[TS_server_TLSv1_3_cipher_suites].as(); } if (node[TS_server_groups_list]) { - item.server_groups_list = node[TS_server_groups_list].as(); + SNIServerGroupsList input; + if (node[TS_server_groups_list].IsScalar()) { + input.group = node[TS_server_groups_list].as(); + item.server_groups_list.emplace_back(input); + } else { + for (auto const &it : node[TS_server_groups_list]) { + if (it["group"]) { + input.group = it["group"].as(); + } + if (it["weight"]) { + input.weight = it["weight"].as(); + if (input.weight < 1) { + throw YAML::ParserException(node[TS_server_groups_list].Mark(), "server_groups_list weight must be greater than 0"); + } + } + item.server_groups_list.emplace_back(std::move(input)); + } + } } if (node[TS_ip_allow]) { item.ip_allow = node[TS_ip_allow].as(); diff --git a/tests/gold_tests/tls/tls_sni_groups.test.py b/tests/gold_tests/tls/tls_sni_groups.test.py index 16c1cce280a..454bb452f2d 100644 --- a/tests/gold_tests/tls/tls_sni_groups.test.py +++ b/tests/gold_tests/tls/tls_sni_groups.test.py @@ -74,8 +74,7 @@ ts=ts) tr.ReturnCode = 0 tr.StillRunningAfter = ts -ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Setting groups list from server_groups_list to x25519", "Should log setting the server groups") +ts.Disk.traffic_out.Content += Testers.ContainsExpression("selecting server group 'x25519'", "Should log setting the server groups") tr.Processes.Default.Streams.all = Testers.IncludesExpression( f"SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 / x25519", "Curl should log using x25519 in the SSL connection") @@ -89,7 +88,7 @@ tr.StillRunningAfter = ts tr.StillRunningAfter = server ts.Disk.diags_log.Content = Testers.ContainsExpression( - "ERROR: Invalid server_groups_list: ABC123", "Curl attempt should have failed") + "WARNING: Invalid server group 'ABC123' in SNI configuration", "Curl attempt should have failed") # Hybrid ECDH PQ key exchange TLS groups were added in OpenSSL 3.5 if Condition.HasOpenSSLVersion("3.5.0"): @@ -101,7 +100,7 @@ tr.ReturnCode = 0 tr.StillRunningAfter = ts ts.Disk.traffic_out.Content += Testers.ContainsExpression( - "Setting groups list from server_groups_list to X25519MLKEM768", "Should log setting the server groups") + "selecting server group 'X25519MLKEM768'", "Should log setting the server groups") tr.Processes.Default.Streams.all = Testers.IncludesExpression( f"SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768", f"Curl should log using X25519MLKEM768 in the SSL connection")