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")