diff --git a/doc/admin-guide/files/sni.yaml.en.rst b/doc/admin-guide/files/sni.yaml.en.rst index f7f90089df6..c60aac5542b 100644 --- a/doc/admin-guide/files/sni.yaml.en.rst +++ b/doc/admin-guide/files/sni.yaml.en.rst @@ -27,7 +27,7 @@ Description This file is used to configure aspects of TLS connection handling for both inbound and outbound connections. With the exception of ``host_sni_policy`` (see the description below), the configuration is driven by the SNI values provided by the inbound connection. The -file consists of a set of configuration items, each identified by an SNI value (``fqdn``). +file consists of a set of configuration items, each identified by an SNI value and optionally a port range (``fqdn``, ``inbound_port_range``). When an inbound TLS connection is made, the SNI value from the TLS negotiation is matched against the items specified by this file and if there is a match, the values specified in that item override the defaults. This is done during the inbound connection processing; some outbound properties @@ -52,11 +52,34 @@ for a more detailed description of HTTP/2 connection coalescing. A similar thing .. _override-host-sni-policy: .. _override-h2-properties: +The following fields make up the key for each item in the configuration file. + ========================= ========= ======================================================================================== Key Direction Meaning ========================= ========= ======================================================================================== -fqdn Both Fully Qualified Domain Name. This item is used if the SNI value matches this. +fqdn Both Fully Qualified Domain Name. + +inbound_port_range Inbound The port range for the inbound connection in the form ``port`` or + ``min-max``. + + For example: + + ``443`` + + would match all requests with an SNI for example.com on port 443, and + + ``443-446`` + + would match requests with an SNI for example.com on ports 443 to 446, inclusive. + By default this is all ports. +========================= ========= ======================================================================================== + +The following fields are the directives that determine the behavior of connections matching the key. + +========================= ========= ======================================================================================== +Key Direction Meaning +========================= ========= ======================================================================================== ip_allow Inbound Specify a list of client IP address, subnets, or ranges what are allowed to complete the connection. This list is comma separated. IPv4 and IPv6 addresses can be specified. Here is an example list :: diff --git a/include/tscpp/util/ts_ip.h b/include/tscpp/util/ts_ip.h index 9d2c276f00b..40f577bcccb 100644 --- a/include/tscpp/util/ts_ip.h +++ b/include/tscpp/util/ts_ip.h @@ -22,12 +22,17 @@ #pragma once +#include #include +#include "swoc/DiscreteRange.h" #include "swoc/swoc_ip.h" namespace ts { +inline constexpr in_port_t MAX_PORT_VALUE{std::numeric_limits::max()}; +using port_range_t = swoc::DiscreteRange; + /// Pair of addresses, each optional. /// Used in situations where both an IPv4 and IPv6 may be needed. class IPAddrPair diff --git a/iocore/net/Makefile.am b/iocore/net/Makefile.am index b567a359c62..7bfa944007d 100644 --- a/iocore/net/Makefile.am +++ b/iocore/net/Makefile.am @@ -97,7 +97,10 @@ test_UDPNet_SOURCES = \ test_libinknet_SOURCES = \ libinknet_stub.cc \ - unit_tests/test_ProxyProtocol.cc + unit_tests/unit_test_main.cc \ + unit_tests/test_ProxyProtocol.cc \ + unit_tests/test_SSLSNIConfig.cc \ + unit_tests/test_YamlSNIConfig.cc test_libinknet_CPPFLAGS = \ $(AM_CPPFLAGS) \ @@ -108,6 +111,7 @@ test_libinknet_CPPFLAGS = \ -I$(abs_top_srcdir)/proxy/http \ -I$(abs_top_srcdir)/mgmt \ -I$(abs_top_srcdir)/mgmt/utils \ + -DLIBINKNET_UNIT_TEST_DIR="$(abs_top_srcdir)/iocore/net/unit_tests" \ @OPENSSL_INCLUDES@ test_libinknet_LDFLAGS = \ diff --git a/iocore/net/P_QUICNetVConnection_quiche.h b/iocore/net/P_QUICNetVConnection_quiche.h index d20e9188b75..a8832401bd7 100644 --- a/iocore/net/P_QUICNetVConnection_quiche.h +++ b/iocore/net/P_QUICNetVConnection_quiche.h @@ -50,6 +50,8 @@ #include "quic/QUICContext.h" #include "quic/QUICStreamManager.h" #include "quic/QUICStreamManager_quiche.h" + +#include #include class QUICPacketHandler; @@ -155,6 +157,7 @@ class QUICNetVConnection : public UnixNetVConnection, // TLSSNISupport void _fire_ssl_servername_event() override; + in_port_t _get_local_port() override; // TLSSessionResumptionSupport const IpEndpoint &_getLocalEndpoint() override; diff --git a/iocore/net/P_SSLNetVConnection.h b/iocore/net/P_SSLNetVConnection.h index cb41b9a2fee..9c138da4a05 100644 --- a/iocore/net/P_SSLNetVConnection.h +++ b/iocore/net/P_SSLNetVConnection.h @@ -31,17 +31,8 @@ ****************************************************************************/ #pragma once -#include - #include "tscore/ink_platform.h" #include "ts/apidefs.h" -#include -#include -#include - -#include -#include -#include #include "P_EventSystem.h" #include "P_UnixNetVConnection.h" @@ -56,6 +47,15 @@ #include "P_SSLUtils.h" #include "P_SSLConfig.h" +#include +#include +#include +#include + +#include +#include +#include + // These are included here because older OpenSSL libraries don't have them. // Don't copy these defines, or use their values directly, they are merely // here to avoid compiler errors. @@ -408,7 +408,9 @@ class SSLNetVConnection : public UnixNetVConnection, return local_addr; } + // TLSSNISupport void _fire_ssl_servername_event() override; + in_port_t _get_local_port() override; bool _isTryingRenegotiation() const override; shared_SSL_CTX _lookupContextByName(const std::string &servername, SSLCertContextType ctxType) override; diff --git a/iocore/net/QUICNetVConnection_quiche.cc b/iocore/net/QUICNetVConnection_quiche.cc index 3c5795876d3..2d638f99289 100644 --- a/iocore/net/QUICNetVConnection_quiche.cc +++ b/iocore/net/QUICNetVConnection_quiche.cc @@ -26,6 +26,8 @@ #include "QUICMultiCertConfigLoader.h" #include "quic/QUICStream_quiche.h" #include "quic/QUICGlobals.h" + +#include #include static constexpr ink_hrtime WRITE_READY_INTERVAL = HRTIME_MSECONDS(2); @@ -745,6 +747,12 @@ QUICNetVConnection::_fire_ssl_servername_event() { } +in_port_t +QUICNetVConnection::_get_local_port() +{ + return this->get_local_port(); +} + const IpEndpoint & QUICNetVConnection::_getLocalEndpoint() { diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc index 615324335b5..c40fad61f8a 100644 --- a/iocore/net/SSLNetVConnection.cc +++ b/iocore/net/SSLNetVConnection.cc @@ -42,6 +42,8 @@ #include "SSLStats.h" #include "P_ALPNSupport.h" +#include + #include #include @@ -1966,6 +1968,12 @@ SSLNetVConnection::_fire_ssl_servername_event() this->callHooks(TS_EVENT_SSL_SERVERNAME); } +in_port_t +SSLNetVConnection::_get_local_port() +{ + return this->get_local_port(); +} + bool SSLNetVConnection::_isTryingRenegotiation() const { diff --git a/iocore/net/SSLSNIConfig.cc b/iocore/net/SSLSNIConfig.cc index e7749fa4692..e2224a820fd 100644 --- a/iocore/net/SSLSNIConfig.cc +++ b/iocore/net/SSLSNIConfig.cc @@ -41,6 +41,8 @@ #include "tscpp/util/TextView.h" +#include + #include #include #include @@ -60,6 +62,7 @@ NamedElement::operator=(NamedElement &&other) { if (this != &other) { match = std::move(other.match); + ports = std::move(other.ports); } return *this; } @@ -107,87 +110,73 @@ SNIConfigParams::get_property_config(const std::string &servername) const return nps; } -int +bool SNIConfigParams::load_sni_config() { for (auto &item : yaml_sni.items) { - auto ai = sni_action_list.emplace(sni_action_list.end()); - ai->set_glob_name(item.fqdn); + auto &ai = sni_action_list.emplace_back(); + ai.set_glob_name(item.fqdn); + ai.ports = item.port_range; Debug("ssl", "name: %s", item.fqdn.data()); - // set SNI based actions to be called in the ssl_servername_only callback - if (item.offer_h2.has_value()) { - ai->actions.push_back(std::make_unique(item.offer_h2.value())); - } - if (item.offer_quic.has_value()) { - ai->actions.push_back(std::make_unique(item.offer_quic.value())); - } - if (item.verify_client_level != 255) { - ai->actions.push_back( - std::make_unique(item.verify_client_level, item.verify_client_ca_file, item.verify_client_ca_dir)); - } - if (item.host_sni_policy != 255) { - ai->actions.push_back(std::make_unique(item.host_sni_policy)); - } - if (item.valid_tls_version_min_in >= 0 || item.valid_tls_version_max_in >= 0) { - ai->actions.push_back(std::make_unique(item.valid_tls_version_min_in, item.valid_tls_version_max_in)); - } else { - if (!item.protocol_unset) { - ai->actions.push_back(std::make_unique(item.protocol_mask)); - } - } - if (item.tunnel_destination.length() > 0) { - ai->actions.push_back( - std::make_unique(item.tunnel_destination, item.tunnel_type, item.tunnel_prewarm, item.tunnel_alpn)); - } - if (!item.client_sni_policy.empty()) { - ai->actions.push_back(std::make_unique(item.client_sni_policy)); - } - if (item.http2_buffer_water_mark.has_value()) { - ai->actions.push_back(std::make_unique(item.http2_buffer_water_mark.value())); + item.populate_sni_actions(ai.actions); + if (!set_next_hop_properties(item)) { + return false; } + } - ai->actions.push_back(std::make_unique(item.server_max_early_data)); + return true; +} - ai->actions.push_back(std::make_unique(item.ip_allow, item.fqdn)); +bool +SNIConfigParams::set_next_hop_properties(YamlSNIConfig::Item const &item) +{ + auto &nps = next_hop_list.emplace_back(); + if (!load_certs_if_client_cert_specified(item, nps)) { + return false; + }; - // set the next hop properties - auto nps = next_hop_list.emplace(next_hop_list.end()); + nps.set_glob_name(item.fqdn); + nps.prop.verify_server_policy = item.verify_server_policy; + nps.prop.verify_server_properties = item.verify_server_properties; - SSLConfig::scoped_config params; - // Load if we have at least specified the client certificate - if (!item.client_cert.empty()) { - nps->prop.client_cert_file = Layout::get()->relative_to(params->clientCertPathOnly, item.client_cert.data()); - if (!item.client_key.empty()) { - nps->prop.client_key_file = Layout::get()->relative_to(params->clientKeyPathOnly, item.client_key.data()); - } + return true; +} - auto ctx = params->getCTX(nps->prop.client_cert_file, nps->prop.client_key_file, params->clientCACertFilename, - params->clientCACertPath); - if (ctx.get() == nullptr) { - return 1; - } +bool +SNIConfigParams::load_certs_if_client_cert_specified(YamlSNIConfig::Item const &item, NextHopItem &nps) +{ + if (!item.client_cert.empty()) { + SSLConfig::scoped_config params; + nps.prop.client_cert_file = Layout::get()->relative_to(params->clientCertPathOnly, item.client_cert.data()); + if (!item.client_key.empty()) { + nps.prop.client_key_file = Layout::get()->relative_to(params->clientKeyPathOnly, item.client_key.data()); } - nps->set_glob_name(item.fqdn); - nps->prop.verify_server_policy = item.verify_server_policy; - nps->prop.verify_server_properties = item.verify_server_properties; - } // end for + auto ctx = + params->getCTX(nps.prop.client_cert_file, nps.prop.client_key_file, params->clientCACertFilename, params->clientCACertPath); + if (ctx.get() == nullptr) { + return false; + } + } - return 0; + return true; } std::pair -SNIConfigParams::get(std::string_view servername) const +SNIConfigParams::get(std::string_view servername, in_port_t dest_incoming_port) const { int ovector[OVECSIZE]; - for (const auto &retval : sni_action_list) { + for (auto const &retval : sni_action_list) { int length = servername.length(); if (retval.match == nullptr && length == 0) { return {&retval.actions, {}}; } else if (auto offset = pcre_exec(retval.match.get(), nullptr, servername.data(), length, 0, 0, ovector, OVECSIZE); offset >= 0) { + if (!retval.ports.contains(dest_incoming_port)) { + continue; + } if (offset == 1) { // first pair identify the portion of the subject string matched by the entire pattern if (ovector[0] == 0 && ovector[1] == length) { @@ -217,11 +206,16 @@ SNIConfigParams::get(std::string_view servername) const return {nullptr, {}}; } -int +bool SNIConfigParams::initialize() { std::string sni_filename = RecConfigReadConfigPath("proxy.config.ssl.servername.filename"); + return initialize(sni_filename); +} +bool +SNIConfigParams::initialize(std::string const &sni_filename) +{ Note("%s loading ...", sni_filename.c_str()); struct stat sbuf; @@ -229,7 +223,7 @@ SNIConfigParams::initialize() Note("%s failed to load", sni_filename.c_str()); Warning("Loading SNI configuration - filename: %s doesn't exist", sni_filename.c_str()); - return 0; + return true; } YamlSNIConfig yaml_sni_tmp; @@ -242,7 +236,7 @@ SNIConfigParams::initialize() } else { Error("%s failed to load: %s", sni_filename.c_str(), errMsg.str().c_str()); } - return 1; + return false; } yaml_sni = std::move(yaml_sni_tmp); @@ -274,8 +268,8 @@ SNIConfig::reconfigure() Debug("ssl", "Reload SNI file"); SNIConfigParams *params = new SNIConfigParams; - int retStatus = params->initialize(); - if (!retStatus) { + bool retStatus = params->initialize(); + if (retStatus) { _configid = configProcessor.set(_configid, params); prewarmManager.reconfigure(); } else { @@ -283,13 +277,13 @@ SNIConfig::reconfigure() } std::string sni_filename = RecConfigReadConfigPath("proxy.config.ssl.servername.filename"); - if (!retStatus || TSSystemState::is_initializing()) { + if (retStatus || TSSystemState::is_initializing()) { Note("%s finished loading", sni_filename.c_str()); } else { Error("%s failed to load", sni_filename.c_str()); } - return !retStatus; + return retStatus ? 1 : 0; } SNIConfigParams * @@ -309,12 +303,12 @@ SNIConfig::release(SNIConfigParams *params) // setting proxy.config.http.host_sni_policy and is possibly overridden if the sni policy // contains a host_sni_policy entry bool -SNIConfig::test_client_action(const char *servername, const IpEndpoint &ep, int &host_sni_policy) +SNIConfig::test_client_action(const char *servername, in_port_t dest_incoming_port, const IpEndpoint &ep, int &host_sni_policy) { bool retval = false; SNIConfig::scoped_config params; - const auto &actions = params->get(servername); + auto const &actions = params->get(servername, dest_incoming_port); if (actions.first) { for (auto &&item : *actions.first) { retval |= item->TestClientSNIAction(servername, ep, host_sni_policy); diff --git a/iocore/net/SSLSNIConfig.h b/iocore/net/SSLSNIConfig.h index b00dc68357a..46890ae351c 100644 --- a/iocore/net/SSLSNIConfig.h +++ b/iocore/net/SSLSNIConfig.h @@ -35,6 +35,8 @@ #include #include +#include "tscpp/util/ts_ip.h" + #include "ConfigProcessor.h" #include "SNIActionPerformer.h" #include "YamlSNIConfig.h" @@ -60,12 +62,17 @@ struct PcreFreer { struct NamedElement { NamedElement() {} + NamedElement(NamedElement const &other) = delete; + NamedElement &operator=(NamedElement const &other) = delete; NamedElement(NamedElement &&other); NamedElement &operator=(NamedElement &&other); + ~NamedElement() = default; void set_glob_name(std::string name); void set_regex_name(const std::string ®ex_name); + ts::port_range_t ports{1, ts::MAX_PORT_VALUE}; + std::unique_ptr match; }; @@ -80,21 +87,28 @@ struct NextHopItem : public NamedElement { using SNIList = std::vector; using NextHopPropertyList = std::vector; -struct SNIConfigParams : public ConfigInfo { +class SNIConfigParams : public ConfigInfo +{ +public: SNIConfigParams() = default; ~SNIConfigParams() override; const NextHopProperty *get_property_config(const std::string &servername) const; - int initialize(); + bool initialize(); + bool initialize(const std::string &sni_filename); /** Walk sni.yaml config and populate sni_action_list @return 0 for success, 1 is failure */ - int load_sni_config(); - std::pair get(std::string_view servername) const; + bool load_sni_config(); + std::pair get(std::string_view servername, uint16_t dest_incoming_port) const; SNIList sni_action_list; NextHopPropertyList next_hop_list; YamlSNIConfig yaml_sni; + +private: + bool set_next_hop_properties(YamlSNIConfig::Item const &item); + bool load_certs_if_client_cert_specified(YamlSNIConfig::Item const &item, NextHopItem &nps); }; class SNIConfig @@ -110,7 +124,8 @@ class SNIConfig static SNIConfigParams *acquire(); static void release(SNIConfigParams *params); - static bool test_client_action(const char *servername, const IpEndpoint &ep, int &enforcement_policy); + static bool test_client_action(const char *servername, uint16_t dest_incoming_port, const IpEndpoint &ep, + int &enforcement_policy); private: static int _configid; diff --git a/iocore/net/TLSSNISupport.cc b/iocore/net/TLSSNISupport.cc index b0afe78b976..88378a81418 100644 --- a/iocore/net/TLSSNISupport.cc +++ b/iocore/net/TLSSNISupport.cc @@ -20,10 +20,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +#include "P_SSLNextProtocolAccept.h" +#include "SSLSNIConfig.h" #include "TLSSNISupport.h" #include "tscore/ink_assert.h" +#include "tscore/ink_inet.h" #include "tscore/Diags.h" -#include "SSLSNIConfig.h" int TLSSNISupport::_ex_data_index = -1; @@ -64,8 +66,9 @@ TLSSNISupport::perform_sni_action() } SNIConfig::scoped_config params; - if (const auto &actions = params->get({servername, std::strlen(servername)}); !actions.first) { - Debug("ssl_sni", "%s not available in the map", servername); + auto const port{this->_get_local_port()}; + if (auto const &actions = params->get({servername, std::strlen(servername)}, port); !actions.first) { + Debug("ssl_sni", "%s:%i not available in the map", servername, port); } else { for (auto &&item : *actions.first) { auto ret = item->SNIAction(this, actions.second); diff --git a/iocore/net/TLSSNISupport.h b/iocore/net/TLSSNISupport.h index 7276fd9a691..baee3ae3bf2 100644 --- a/iocore/net/TLSSNISupport.h +++ b/iocore/net/TLSSNISupport.h @@ -23,11 +23,14 @@ */ #pragma once -#include +#include "tscore/ink_config.h" + +#include +#include + #include #include -#include -#include "tscore/ink_config.h" +#include class TLSSNISupport { @@ -60,6 +63,8 @@ class TLSSNISupport protected: virtual void _fire_ssl_servername_event() = 0; + virtual in_port_t _get_local_port() = 0; + void _clear(); private: diff --git a/iocore/net/YamlSNIConfig.cc b/iocore/net/YamlSNIConfig.cc index 314adecc851..6d5c1cf0669 100644 --- a/iocore/net/YamlSNIConfig.cc +++ b/iocore/net/YamlSNIConfig.cc @@ -21,15 +21,28 @@ #include "YamlSNIConfig.h" +#include #include #include #include +#include +#include +#include +#include +#include #include #include +#include + +#include "swoc/TextView.h" +#include "swoc/bwf_base.h" #include "P_SNIActionPerformer.h" +#include "tscpp/util/ts_ip.h" + +#include "tscore/BufferWriterForward.h" #include "tscore/Diags.h" #include "tscore/EnumDescriptor.h" #include "tscore/Errata.h" @@ -58,6 +71,7 @@ load_tunnel_alpn(std::vector &dst, const YAML::Node &node) } } } + } // namespace ts::Errata @@ -115,6 +129,40 @@ YamlSNIConfig::Item::EnableProtocol(YamlSNIConfig::TLSProtocol proto) } } +void +YamlSNIConfig::Item::populate_sni_actions(action_vector_t &actions) +{ + if (offer_h2.has_value()) { + actions.push_back(std::make_unique(offer_h2.value())); + } + if (offer_quic.has_value()) { + actions.push_back(std::make_unique(offer_quic.value())); + } + if (verify_client_level != 255) { + actions.push_back(std::make_unique(verify_client_level, verify_client_ca_file, verify_client_ca_dir)); + } + if (host_sni_policy != 255) { + actions.push_back(std::make_unique(host_sni_policy)); + } + if (valid_tls_version_min_in >= 0 || valid_tls_version_max_in >= 0) { + actions.push_back(std::make_unique(valid_tls_version_min_in, valid_tls_version_max_in)); + } else if (!protocol_unset) { + actions.push_back(std::make_unique(protocol_mask)); + } + if (tunnel_destination.length() > 0) { + actions.push_back(std::make_unique(tunnel_destination, tunnel_type, tunnel_prewarm, tunnel_alpn)); + } + if (!client_sni_policy.empty()) { + actions.push_back(std::make_unique(client_sni_policy)); + } + if (http2_buffer_water_mark.has_value()) { + actions.push_back(std::make_unique(http2_buffer_water_mark.value())); + } + + actions.push_back(std::make_unique(server_max_early_data)); + actions.push_back(std::make_unique(ip_allow, fqdn)); +} + VerifyClient::~VerifyClient() {} TsEnumDescriptor LEVEL_DESCRIPTOR = { @@ -131,6 +179,7 @@ TsEnumDescriptor TLS_PROTOCOLS_DESCRIPTOR = { }; std::set valid_sni_config_keys = {TS_fqdn, + TS_inbound_port_range, TS_verify_client, TS_verify_client_ca_certs, TS_tunnel_route, @@ -179,6 +228,26 @@ template <> struct convert { } else { return false; // servername must be present } + + if (node[TS_inbound_port_range]) { + swoc::TextView port_view{node[TS_inbound_port_range].Scalar()}; + auto min{port_view.split_prefix_at('-')}; + if (!min) { + min = port_view; + } + auto const &max{port_view}; + + swoc::TextView parsed_min; + long min_port{swoc::svtoi(min, &parsed_min)}; + swoc::TextView parsed_max; + long max_port{swoc::svtoi(max, &parsed_max)}; + if (parsed_min != min || min_port < 1 || parsed_max != max || max_port > std::numeric_limits::max() || + max_port < min_port) { + throw YAML::ParserException(node[TS_fqdn].Mark(), swoc::bwprint(ts::bw_dbg, "bad port range: {}-{}", min, max)); + } + + item.port_range = ts::port_range_t{static_cast(min_port), static_cast(max_port)}; + } if (node[TS_http2]) { item.offer_h2 = node[TS_http2].as(); } diff --git a/iocore/net/YamlSNIConfig.h b/iocore/net/YamlSNIConfig.h index 63d9aacd1ba..73839b26e8e 100644 --- a/iocore/net/YamlSNIConfig.h +++ b/iocore/net/YamlSNIConfig.h @@ -22,16 +22,23 @@ #pragma once #include +#include #include +#include #include #include +#include +#include "SNIActionPerformer.h" #include "SSLTypes.h" +#include "tscpp/util/ts_ip.h" + #include "tscore/Errata.h" #define TSDECL(id) constexpr char TS_##id[] = #id TSDECL(fqdn); +TSDECL(inbound_port_range); TSDECL(verify_client); TSDECL(verify_client_ca_certs); TSDECL(tunnel_route); @@ -72,6 +79,9 @@ struct YamlSNIConfig { struct Item { std::string fqdn; + + ts::port_range_t port_range{1, ts::MAX_PORT_VALUE}; + 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; @@ -102,7 +112,10 @@ struct YamlSNIConfig { uint32_t tunnel_prewarm_inactive_timeout = 0; TunnelPreWarm tunnel_prewarm = TunnelPreWarm::UNSET; + using action_vector_t = std::vector>; + void EnableProtocol(YamlSNIConfig::TLSProtocol proto); + void populate_sni_actions(action_vector_t &actions); }; ts::Errata loader(const std::string &cfgFilename); diff --git a/iocore/net/unit_tests/sni_conf_test.yaml b/iocore/net/unit_tests/sni_conf_test.yaml new file mode 100644 index 00000000000..82bc3e278df --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test.yaml @@ -0,0 +1,25 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: allports.com +- fqdn: someport.com + inbound_port_range: 1-433 + http2: true +- fqdn: someport.com + inbound_port_range: 8080-65535 +- fqdn: oneport.com + inbound_port_range: 433 diff --git a/iocore/net/unit_tests/sni_conf_test_bad_port_0-1.yaml b/iocore/net/unit_tests/sni_conf_test_bad_port_0-1.yaml new file mode 100644 index 00000000000..6cc3b07cb79 --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test_bad_port_0-1.yaml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: badport.com + inbound_port_range: 0-1 diff --git a/iocore/net/unit_tests/sni_conf_test_bad_port_1-yowzers2.yaml b/iocore/net/unit_tests/sni_conf_test_bad_port_1-yowzers2.yaml new file mode 100644 index 00000000000..eed8159566d --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test_bad_port_1-yowzers2.yaml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: badport.com + inbound_port_range: 1-yowzers2 diff --git a/iocore/net/unit_tests/sni_conf_test_bad_port_65535-65536.yaml b/iocore/net/unit_tests/sni_conf_test_bad_port_65535-65536.yaml new file mode 100644 index 00000000000..64aef61d966 --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test_bad_port_65535-65536.yaml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: badport.com + inbound_port_range: 65535-65536 diff --git a/iocore/net/unit_tests/sni_conf_test_bad_port_8080-433.yaml b/iocore/net/unit_tests/sni_conf_test_bad_port_8080-433.yaml new file mode 100644 index 00000000000..118b46c6b8d --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test_bad_port_8080-433.yaml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: badport.com + inbound_port_range: 8080-433 diff --git a/iocore/net/unit_tests/sni_conf_test_bad_port_yowzers-1.yaml b/iocore/net/unit_tests/sni_conf_test_bad_port_yowzers-1.yaml new file mode 100644 index 00000000000..c783cfee3dc --- /dev/null +++ b/iocore/net/unit_tests/sni_conf_test_bad_port_yowzers-1.yaml @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +sni: +- fqdn: notaport.com + inbound_port_range: yowzers-1 diff --git a/iocore/net/unit_tests/test_ProxyProtocol.cc b/iocore/net/unit_tests/test_ProxyProtocol.cc index 9f8b7c4c0f4..9ff76cc628d 100644 --- a/iocore/net/unit_tests/test_ProxyProtocol.cc +++ b/iocore/net/unit_tests/test_ProxyProtocol.cc @@ -21,7 +21,6 @@ limitations under the License. */ -#define CATCH_CONFIG_MAIN #include "catch.hpp" #include "ProxyProtocol.h" diff --git a/iocore/net/unit_tests/test_SSLSNIConfig.cc b/iocore/net/unit_tests/test_SSLSNIConfig.cc new file mode 100644 index 00000000000..c1f5672f829 --- /dev/null +++ b/iocore/net/unit_tests/test_SSLSNIConfig.cc @@ -0,0 +1,101 @@ +/** @file + + Catch based unit tests for SSLSNIConfig + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#ifndef LIBINKNET_UNIT_TEST_DIR +#error please set LIBINKNET_UNIT_TEST_DIR +#endif + +#define _STR(s) #s +#define _XSTR(s) _STR(s) + +#include "SSLSNIConfig.h" + +#include "catch.hpp" + +#include + +TEST_CASE("Test SSLSNIConfig") +{ + SNIConfigParams params; + params.initialize(_XSTR(LIBINKNET_UNIT_TEST_DIR) "/sni_conf_test.yaml"); + + SECTION("The config does not match any SNIs for someport.com:577") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 577)}; + CHECK(!actions.first); + } + + SECTION("The config does not match any SNIs for someport.com:808") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 808)}; + CHECK(!actions.first); + } + + SECTION("The config does not match any SNIs for oneport.com:1") + { + auto const &actions{params.get({"oneport.com", std::strlen("oneport.com")}, 1)}; + CHECK(!actions.first); + } + + SECTION("The config does match an SNI for oneport.com:433") + { + auto const &actions{params.get({"oneport.com", std::strlen("oneport.com")}, 433)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 2); + } + + SECTION("The config matches an SNI for allports.com") + { + auto const &actions{params.get({"allports.com", std::strlen("allports.com")}, 1)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 2); + } + + SECTION("The config matches an SNI for someport.com:1") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 1)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 3); + } + + SECTION("The config matches an SNI for someport.com:433") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 433)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 3); + } + + SECTION("The config matches an SNI for someport:8080") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 8080)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 2); + } + + SECTION("The config matches an SNI for someport:65535") + { + auto const &actions{params.get({"someport.com", std::strlen("someport.com")}, 65535)}; + REQUIRE(actions.first); + REQUIRE(actions.first->size() == 2); + } +} diff --git a/iocore/net/unit_tests/test_YamlSNIConfig.cc b/iocore/net/unit_tests/test_YamlSNIConfig.cc new file mode 100644 index 00000000000..4d9b878203c --- /dev/null +++ b/iocore/net/unit_tests/test_YamlSNIConfig.cc @@ -0,0 +1,110 @@ +/** @file + + Catch based unit tests for YamlSNIConfig + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#ifndef LIBINKNET_UNIT_TEST_DIR +#error please set LIBINKNET_UNIT_TEST_DIR +#endif + +#define _STR(s) #s +#define _XSTR(s) _STR(s) + +#include +#include +#include + +#include "swoc/bwf_base.h" +#include +#include "catch.hpp" + +#include "YamlSNIConfig.h" + +static void +check_port_range(const YamlSNIConfig::Item &item, in_port_t min_expected, in_port_t max_expected) +{ + CHECK(item.port_range.min() == min_expected); + CHECK(item.port_range.max() == max_expected); +} + +TEST_CASE("YamlSNIConfig sets port ranges appropriately") +{ + YamlSNIConfig conf{}; + ts::Errata zret{conf.loader(_XSTR(LIBINKNET_UNIT_TEST_DIR) "/sni_conf_test.yaml")}; + if (!zret.isOK()) { + std::stringstream errorstream; + errorstream << zret; + FAIL(errorstream.str()); + } + REQUIRE(zret.isOK()); + REQUIRE(conf.items.size() == 4); + + SECTION("If no ports were specified, port range should contain all ports.") + { + check_port_range(conf.items[0], 1, 65535); + } + + SECTION("If one port range was specified, port range should match.") + { + SECTION("Ports 1-433.") + { + check_port_range(conf.items[1], 1, 433); + } + SECTION("Ports 8080-65535.") + { + check_port_range(conf.items[2], 8080, 65535); + } + SECTION("Port 433.") + { + check_port_range(conf.items[3], 433, 433); + } + } + + SECTION("If a port range was specified, it should not interfere with the fqdn.") + { + auto const &item{conf.items[1]}; + CHECK(item.fqdn == "someport.com"); + } + + SECTION("If no port range was specified, it should not interfere with the fqdn.") + { + auto const &item{conf.items[0]}; + CHECK(item.fqdn == "allports.com"); + } +} + +TEST_CASE("YamlConfig handles bad ports appropriately.") +{ + YamlSNIConfig conf{}; + + std::string port_str{GENERATE("0-1", "65535-65536", "8080-433", "yowzers-1", "1-yowzers2")}; + + std::string filepath; + swoc::bwprint(filepath, "{}/sni_conf_test_bad_port_{}.yaml", _XSTR(LIBINKNET_UNIT_TEST_DIR), port_str); + + ts::Errata zret{conf.loader(filepath)}; + std::stringstream errorstream; + errorstream << zret; + + std::string expected; + swoc::bwprint(expected, "1 [1]: yaml-cpp: error at line 18, column 9: bad port range: {}\n", port_str); + CHECK(errorstream.str() == expected); +} diff --git a/iocore/net/unit_tests/unit_test_main.cc b/iocore/net/unit_tests/unit_test_main.cc new file mode 100644 index 00000000000..701b818c432 --- /dev/null +++ b/iocore/net/unit_tests/unit_test_main.cc @@ -0,0 +1,67 @@ +/** @file + + Catch based unit tests for libinknet + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "I_EventSystem.h" // must be included before I_EThread.h +#include "I_EThread.h" +#include "I_Thread.h" +#include "P_SSLConfig.h" +#include "records/I_RecordsConfig.h" +#include "tscore/BaseLogFile.h" +#include "tscore/Diags.h" +#include "tscore/I_Layout.h" + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +inline static constexpr int test_threads{1}; + +class EventProcessorListener final : public Catch::TestEventListenerBase +{ +public: + using TestEventListenerBase::TestEventListenerBase; + + void + testRunStarting(Catch::TestRunInfo const &testRunInfo) override + { + Layout::create(); + BaseLogFile *base_log_file = new BaseLogFile("stderr"); + DiagsPtr::set(new Diags(testRunInfo.name, "" /* tags */, "" /* actions */, base_log_file)); + RecProcessInit(); + LibRecordsConfigInit(); + + ink_event_system_init(EVENT_SYSTEM_MODULE_PUBLIC_VERSION); + eventProcessor.start(test_threads); + + EThread *main_thread = new EThread; + main_thread->set_specific(); + + SSLConfig::startup(); + } + + void + testRunEnded(Catch::TestRunStats const & /* testRunStats */) override + { + } +}; + +CATCH_REGISTER_LISTENER(EventProcessorListener); diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc index 9b8896fd7ed..ae5a710bb47 100644 --- a/proxy/http/HttpSM.cc +++ b/proxy/http/HttpSM.cc @@ -4447,7 +4447,8 @@ HttpSM::check_sni_host() NetVConnection *netvc = ua_txn->get_netvc(); if (netvc) { IpEndpoint ip = netvc->get_remote_endpoint(); - if (SNIConfig::test_client_action(std::string{host_name, static_cast(host_len)}.c_str(), ip, host_sni_policy) && + if (SNIConfig::test_client_action(std::string{host_name, static_cast(host_len)}.c_str(), netvc->get_local_port(), + ip, host_sni_policy) && host_sni_policy > 0) { // In a SNI/Host mismatch where the Host would have triggered SNI policy, mark the transaction // to be considered for rejection after the remap phase passes. Gives the opportunity to conf_remap diff --git a/proxy/http/PreWarmManager.h b/proxy/http/PreWarmManager.h index adc91080b76..b660194b895 100644 --- a/proxy/http/PreWarmManager.h +++ b/proxy/http/PreWarmManager.h @@ -43,7 +43,7 @@ class PreWarmSM; class PreWarmManager; -struct SNIConfigParams; +class SNIConfigParams; extern ClassAllocator preWarmSMAllocator; extern PreWarmManager prewarmManager; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 8504b333b3f..cb076ddb460 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -100,7 +100,22 @@ add_cache_test(Update_S_to_L ${CMAKE_SOURCE_DIR}/iocore/cache/test/test_Update_S add_executable(test_AIO ${CMAKE_SOURCE_DIR}/iocore/aio/test_AIO.cc) add_test(NAME test_AIO COMMAND $ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/iocore/aio) -add_net_test(test_net ${CMAKE_SOURCE_DIR}/iocore/net/unit_tests/test_ProxyProtocol.cc) +add_net_test(test_net + "${CMAKE_SOURCE_DIR}/iocore/net/unit_tests/test_ProxyProtocol.cc" + "${CMAKE_SOURCE_DIR}/iocore/net/unit_tests/test_SSLSNIConfig.cc" + "${CMAKE_SOURCE_DIR}/iocore/net/unit_tests/test_YamlSNIConfig.cc" + "${CMAKE_SOURCE_DIR}/iocore/net/unit_tests/unit_test_main.cc" +) +set(LIBINKNET_UNIT_TEST_DIR "${CMAKE_SOURCE_DIR}/iocore/net/unit_tests") +target_compile_definitions(test_net + PRIVATE + LIBINKNET_UNIT_TEST_DIR=${LIBINKNET_UNIT_TEST_DIR} +) +target_link_libraries(test_net + PRIVATE + hdrs + proxy +) add_stubbed_test(EventSystem ${CMAKE_SOURCE_DIR}/iocore/eventsystem/unit_tests/test_EventSystem.cc) add_stubbed_test(IOBuffer ${CMAKE_SOURCE_DIR}/iocore/eventsystem/unit_tests/test_IOBuffer.cc) diff --git a/tests/gold_tests/tls/tls_sni_with_port.replay.yaml b/tests/gold_tests/tls/tls_sni_with_port.replay.yaml new file mode 100644 index 00000000000..d114d07b345 --- /dev/null +++ b/tests/gold_tests/tls/tls_sni_with_port.replay.yaml @@ -0,0 +1,70 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +meta: + version: "1.0" + +sessions: +- protocol: + - name: http + version: 1 + - name: tls + sni: yay.example.com + - name: tcp + - name: ip + + transactions: + - client-request: + method: "GET" + version: "1.1" + url: /a/path/conn_remapped + headers: + fields: + - [ Host, yay.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, 16 ] + - [ uuid, "conn_remapped" ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + + proxy-response: + status: 200 + + - client-request: + method: "GET" + version: "1.1" + url: /a/path/conn_accepted + headers: + fields: + - [ Host, yay.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, 16 ] + - [ uuid, "conn_accepted" ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Content-Length, 32 ] + + proxy-response: + status: 200 diff --git a/tests/gold_tests/tls/tls_sni_with_port.test.py b/tests/gold_tests/tls/tls_sni_with_port.test.py new file mode 100644 index 00000000000..68e2709bae6 --- /dev/null +++ b/tests/gold_tests/tls/tls_sni_with_port.test.py @@ -0,0 +1,215 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +from typing import Any, Callable, Optional + +from ports import get_port + +Test.Summary = 'Tests SNI port-based routing' + + +class TestSNIWithPort: + """Configure a test for SNI port-based routing .""" + + replay_filepath: str = "tls_sni_with_port.replay.yaml" + client_counter: int = 0 + server_counter: int = 0 + ts_counter: int = 0 + + def __init__(self, name: str, /, autorun) -> None: + """Initialize the test. + + :param name: The name of the test. + """ + self.name = name + self.autorun = autorun + + def _init_run(self) -> "TestRun": + """Initialize processes for the test run.""" + + tr = Test.AddTestRun(self.name) + server_one = TestSNIWithPort.configure_server(tr, "yay.com") + server_two = TestSNIWithPort.configure_server(tr, "oof.com") + server_three = TestSNIWithPort.configure_server(tr, "wow.com") + self._configure_traffic_server(tr, server_one, server_two, server_three) + + tr.Processes.Default.StartBefore(server_one) + tr.Processes.Default.StartBefore(server_two) + tr.Processes.Default.StartBefore(self._ts) + + return tr, self._ts, server_one, server_two, server_three, self._port_one, self._port_two, self._unspecified_port + + @classmethod + def runner(cls, name: str, autorun: bool = True) -> Optional[Callable]: + """Create a runner for a test case. + + :param autorun: Run the test case once it's set up. Default is True. + """ + test = cls(name, autorun=autorun)._prepare_test_case + return test + + def _prepare_test_case(self, func: Callable) -> Callable: + """Set up a test case and possibly run it. + + :param func: The test case to set up. + """ + functools.wraps(func) + tr, *test_run_args = self._init_run() + + def wrapper(*args, **kwargs) -> Any: + return func(tr, *test_run_args, *args, **kwargs) + + if self.autorun: + wrapper() + return wrapper + + @staticmethod + def configure_server(tr: "TestRun", domain: str): + server = tr.AddVerifierServerProcess( + f"server{TestSNIWithPort.server_counter}.{domain}", + TestSNIWithPort.replay_filepath + ) + TestSNIWithPort.server_counter += 1 + + return server + + def _configure_traffic_server(self, tr: "TestRun", server_one: "Process", server_two: "Process", server_three: "Process"): + """Configure Traffic Server. + + :param tr: The TestRun object to associate the ts process with. + """ + ts = tr.MakeATSProcess(f"ts-{TestSNIWithPort.ts_counter}", select_ports=False, enable_tls=True) + TestSNIWithPort.ts_counter += 1 + + ts.addDefaultSSLFiles() + self._port_one = get_port(ts, "PortOne") + self._port_two = get_port(ts, "PortTwo") + self._unspecified_port = get_port(ts, "UnspecifiedPort") + ts.Disk.records_config.update({ + 'proxy.config.ssl.server.cert.path': f"{ts.Variables.SSLDir}", + 'proxy.config.ssl.server.private_key.path': f"{ts.Variables.SSLDir}", + 'proxy.config.http.server_ports': f"{self._port_one}:ssl {self._port_two}:ssl {self._unspecified_port}:ssl", + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'dns|http|ssl|sni', + }) + + ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{server_three.Variables.http_port}") + + ts.Disk.sni_yaml.AddLines([ + "sni:", + "- fqdn: yay.example.com", + f" inbound_port_range: {self._port_one}-{self._port_one}", + f" tunnel_route: localhost:{server_one.Variables.https_port}", + "- fqdn: yay.example.com", + f" inbound_port_range: {self._port_two}", + f" tunnel_route: localhost:{server_two.Variables.https_port}" + ]) + + ts.Disk.ssl_multicert_config.AddLine( + f"dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key" + ) + + self._ts = ts + + +# Tests start. + +@TestSNIWithPort.runner("Test that a request to a port not in the SNI does not get through.") +def test0( + tr: "TestRun", + ts: "Process", + server_one: "Process", + server_two: "Process", + server_three: "Process", + port_one: int, + port_two: int, + unspecified_port: int): + client = tr.AddVerifierClientProcess( + f"client0", + TestSNIWithPort.replay_filepath, + https_ports=[unspecified_port], + keys="conn_remapped" + ) + + tr.Processes.Default.ReturnCode = 0 + ts.Disk.diags_log.Content += Testers.ExcludesExpression( + "unsupported key 'inbound_port_range'", "we should not warn about the key" + ) + ts.Disk.traffic_out.Content += Testers.IncludesExpression( + "not available in the map", "the request should not match an SNI" + ) + server_one.Streams.All.Content += Testers.ExcludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_remapped", "the request should not go to server one" + ) + server_two.Streams.All.Content += Testers.ExcludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_remapped", "the request should not go to server two" + ) + server_three.Streams.All.Content += Testers.IncludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_remapped", "request was remaped to server three" + ) + + +@TestSNIWithPort.runner("Test that a request to a port one goes to server one.") +def test1( + tr: "TestRun", + ts: "Process", + server_one: "Process", + server_two: "Process", + server_three: "Process", + port_one: int, + port_two: int, + unspecified_port: int): + client = tr.AddVerifierClientProcess( + f"client1", + TestSNIWithPort.replay_filepath, + https_ports=[port_one], + keys="conn_accepted" + ) + + tr.Processes.Default.ReturnCode = 0 + server_one.Streams.All.Content += Testers.IncludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_accepted", "the request should go to server one" + ) + server_two.Streams.All.Content += Testers.ExcludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_accepted", "the request should not go to server two" + ) + + +@TestSNIWithPort.runner("Test that a request to port two goes to server two.") +def test2( + tr: "TestRun", + ts: "Process", + server_one: "Process", + server_two: "Process", + server_three: "Process", + port_one: int, + port_two: int, + unspecified_port: int): + client = tr.AddVerifierClientProcess( + f"client2", + TestSNIWithPort.replay_filepath, + https_ports=[port_two], + keys="conn_accepted" + ) + + tr.Processes.Default.ReturnCode = 0 + server_two.Streams.All.Content += Testers.IncludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_accepted", "the request should go to server two" + ) + server_one.Streams.All.Content += Testers.ExcludesExpression( + "Received an HTTP/1 Content-Length body of 16 bytes for key conn_accepted", "the request should not go to server one" + )