diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 920ec5d7d40..c3e001565d0 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -524,6 +524,37 @@ Network below this limit. A value of 0 disables the per client concurrent connection limit. + See :ts:cv:`proxy.config.http.per_client.connection.exempt_list` for a way to + allow (not count) certain client IP addresses when applying this limit. + +.. ts:cv:: CONFIG proxy.config.http.per_client.connection.exempt_list STRING NULL + + A comma-separated list of IP addresses or CIDR ranges to exempt when + counting incoming client connections for per client connection + throttling. Incoming addresses in this specified set will not count + against :ts:cv:`proxy.config.net.per_client.max_connections_in` and + thus will not be blocked by that configuration. This may be useful, + for example, to allow any number of incoming connections from within + an organization's network without blocking them due to the per client + connection max feature. + + This configuration takes a comma-separated list of IP addresses, CIDR + networks, or ranges separated by a dash. + + ============================== =========================================================== + Example Effect + ============================== =========================================================== + ``10.0.2.123`` Exempt a single IP Address. + ``10.0.3.1-10.0.3.254`` Exempt a range of IP address. + ``10.0.4.0/24`` Exempt a range of IP address specified by CIDR notation. + ``10.0.2.123,172.16.0.0/20`` Exempt multiple addresses/ranges. + ============================== =========================================================== + + Here is an example configuration value:: + + 10.0.2.123,172.16.0.0/20,192.168.1.0/24 + + .. ts:cv:: CONFIG proxy.config.http.per_client.connection.alert_delay INT 60 :reloadable: :units: seconds @@ -2041,7 +2072,7 @@ Proxy User Variables by a dash or by using CIDR notation. ======================= =========================================================== - Example Effect + Example Effect ======================= =========================================================== ``10.0.2.123`` A single IP Address. ``10.0.3.1-10.0.3.254`` A range of IP address. diff --git a/doc/developer-guide/api/functions/TSConnectionLimitExemptList.en.rst b/doc/developer-guide/api/functions/TSConnectionLimitExemptList.en.rst new file mode 100644 index 00000000000..684695aa39e --- /dev/null +++ b/doc/developer-guide/api/functions/TSConnectionLimitExemptList.en.rst @@ -0,0 +1,125 @@ +.. 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. + +.. default-domain:: cpp + +TSConnectionLimitExemptList +=========================== + +Synopsis +-------- + +.. code-block:: cpp + + #include + +.. function:: TSReturnCode TSConnectionLimitExemptListAdd(std::string_view ip_ranges) +.. function:: TSReturnCode TSConnectionLimitExemptListRemove(std::string_view ip_ranges) +.. function:: void TSConnectionLimitExemptListClear() + +Description +----------- + +These functions manage the per-client connection limit exempt list, which contains IP addresses +and ranges that are exempt from the connection limits enforced by +:ts:cv:`proxy.config.net.per_client.max_connections_in`. + +:func:`TSConnectionLimitExemptListAdd` adds one or more IP addresses or CIDR ranges specified in +:arg:`ip_ranges` to the existing exempt list. The :arg:`ip_ranges` parameter can be a single +IP address or CIDR range, or a comma-separated string of multiple ranges (e.g., +"192.168.1.10,10.0.0.0/8,172.16.0.0/12"). The ranges are added without removing any existing +entries. Returns :enumerator:`TS_SUCCESS` if all ranges were successfully added, :enumerator:`TS_ERROR` if +any of the IP ranges are invalid or if the operation fails. + +:func:`TSConnectionLimitExemptListRemove` removes one or more IP addresses or CIDR ranges specified in +:arg:`ip_ranges` from the existing exempt list. The :arg:`ip_ranges` parameter can be a single +IP address or CIDR range, or a comma-separated string of multiple ranges. If a range is not present +in the list, it is silently ignored. Returns :enumerator:`TS_SUCCESS` if all ranges were successfully +processed, :enumerator:`TS_ERROR` if any of the IP ranges are invalid or if the operation fails. + +:func:`TSConnectionLimitExemptListClear` removes all entries from the per-client connection +limit exempt list. After calling this function, all clients will be subject to connection +limits. This function does not return a value and never fails. + +All functions are thread-safe and can be called from any plugin context. Changes made through +these functions will override any configuration set via +:ts:cv:`proxy.config.http.per_client.connection.exempt_list`. + +Return Values +------------- + +:func:`TSConnectionLimitExemptListAdd` and :func:`TSConnectionLimitExemptListRemove` return +:enumerator:`TS_SUCCESS` if the operation completed successfully, or :enumerator:`TS_ERROR` if the +operation failed due to invalid input or system errors. + +Examples +-------- + +.. code-block:: cpp + + #include + #include + #include + + void load_exempt_list_from_file(const char *filename) { + std::ifstream file(filename); + if (!file.is_open()) { + TSError("Failed to open exempt list file: %s", filename); + return; + } + + // Clear existing exempt list before loading from file + TSConnectionLimitExemptListClear(); + + std::string line; + int line_num = 0; + while (std::getline(file, line)) { + line_num++; + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') { + continue; + } + + // Add each IP range to the exempt list + TSReturnCode result = TSConnectionLimitExemptListAdd(line.c_str()); + if (result != TS_SUCCESS) { + TSError("Failed to add IP range '%s' from line %d in %s", line.c_str(), line_num, filename); + } else { + TSDebug("exempt_list", "Added IP range: %s", line.c_str()); + } + } + file.close(); + } + + void TSPluginInit(int argc, const char *argv[]) { + const char *exempt_file = "exempt_ips.txt"; + + // Check if custom file specified in plugin arguments + if (argc > 1) { + exempt_file = argv[1]; + } + + // Load exempt list from file + load_exempt_list_from_file(exempt_file); + } + + +See Also +-------- + +:ts:cv:`proxy.config.net.per_client.max_connections_in`, +:ts:cv:`proxy.config.http.per_client.connection.exempt_list` diff --git a/include/iocore/net/ConnectionTracker.h b/include/iocore/net/ConnectionTracker.h index 86784b2f38e..5236917e3b0 100644 --- a/include/iocore/net/ConnectionTracker.h +++ b/include/iocore/net/ConnectionTracker.h @@ -82,16 +82,22 @@ class ConnectionTracker /** Static configuration values. */ struct GlobalConfig { + GlobalConfig() = default; + GlobalConfig(GlobalConfig const &); + GlobalConfig &operator=(GlobalConfig const &); + std::chrono::seconds client_alert_delay{60}; ///< Alert delay in seconds. std::chrono::seconds server_alert_delay{60}; ///< Alert delay in seconds. bool metric_enabled{false}; ///< Enabling per server metrics. std::string metric_prefix; ///< Per server metric prefix. + swoc::IPRangeSet client_exempt_list; ///< The set of IP addresses to not block due client connection counting. }; // The names of the configuration values. // Unfortunately these are not used in RecordsConfig.cc so that must be made consistent by hand. // Note: These need to be @c constexpr or there are static initialization ordering risks. static constexpr std::string_view CONFIG_CLIENT_VAR_ALERT_DELAY{"proxy.config.http.per_client.connection.alert_delay"}; + static constexpr std::string_view CONFIG_CLIENT_VAR_EXEMPT_LIST{"proxy.config.http.per_client.connection.exempt_list"}; static constexpr std::string_view CONFIG_SERVER_VAR_MAX{"proxy.config.http.per_server.connection.max"}; static constexpr std::string_view CONFIG_SERVER_VAR_MIN{"proxy.config.http.per_server.connection.min"}; static constexpr std::string_view CONFIG_SERVER_VAR_MATCH{"proxy.config.http.per_server.connection.match"}; @@ -172,11 +178,18 @@ class ConnectionTracker std::shared_ptr _g; ///< Active group for this transaction. bool _reserved_p{false}; ///< Set if a connection slot has been reserved. bool _queued_p{false}; ///< Set if the connection is delayed / queued. + bool _exempt_p{false}; ///< Set if the peer is in the connection exempt list. /// Check if tracking is active. - bool is_active(); + bool is_active() const; + + /// Whether this group is in the connection max exempt list. + /// @return @c true if this group should not be blocked due to + /// proxy.config.net.per_client.max_connections_in. + bool is_exempt() const; /// Reserve a connection. + /// @return the number of tracked connections. int reserve(); /// Release a connection reservation. void release(); @@ -272,6 +285,42 @@ class ConnectionTracker */ static void config_init(GlobalConfig *global, TxnConfig *txn, RecConfigUpdateCb const &config_cb); + /** Set the client connection exempt list programmatically. + * + * This allows plugins to override the per-client connection exempt list with their own + * IPRangeSet. This will replace the existing exempt list entirely. + * + * @param ip_ranges The IPRangeSet containing the addresses that should be exempt from per-client connection limits. + * @return true if the exempt list was successfully updated, false otherwise. + */ + static bool set_client_exempt_list(swoc::IPRangeSet const &ip_ranges); + + /** Add an IP range to the client connection exempt list. + * + * This allows plugins to add an additional IP range to the existing per-client connection exempt list. + * The new range will be added to any existing ranges in the list. + * + * @param ip_range The IPRange containing the addresses to add to the exempt list. + * @return true if the range was successfully added, false otherwise. + */ + static bool add_client_exempt_range(swoc::IPRange const &ip_range); + + /** Remove an IP range from the client connection exempt list. + * + * This allows plugins to remove an IP range from the existing per-client connection exempt list. + * If the range is not present in the list, the operation succeeds without error. + * + * @param ip_range The IPRange containing the addresses to remove from the exempt list. + * @return true if the operation completed successfully, false otherwise. + */ + static bool remove_client_exempt_range(swoc::IPRange const &ip_range); + + /** Clear all IP ranges from the client connection exempt list. + * + * This allows plugins to remove all entries from the per-client connection exempt list. + */ + static void clear_client_exempt_list(); + /// Debug control used for debugging output. static inline DbgCtl dbg_ctl{"conn_track"}; @@ -382,11 +431,17 @@ ConnectionTracker::Group::metric_name(const Key &key, std::string_view fqdn, std } inline bool -ConnectionTracker::TxnState::is_active() +ConnectionTracker::TxnState::is_active() const { return nullptr != _g; } +inline bool +ConnectionTracker::TxnState::is_exempt() const +{ + return _exempt_p; +} + inline int ConnectionTracker::TxnState::reserve() { diff --git a/include/ts/ts.h b/include/ts/ts.h index 7bfab149827..8e990a7625f 100644 --- a/include/ts/ts.h +++ b/include/ts/ts.h @@ -2897,6 +2897,36 @@ TSReturnCode TSHostStatusGet(const char *hostname, const size_t hostname_len, TS void TSHostStatusSet(const char *hostname, const size_t hostname_len, TSHostStatus status, const unsigned int down_time, const unsigned int reason); +/* + * Add one or more IP addresses or CIDR ranges to the per-client connection limit exempt list. + * This function allows plugins to programmatically add to the list of IP addresses + * that should be exempt from per-client connection limits (see + * proxy.config.net.per_client.max_connections_in). + * + * @param ip_ranges The IP address or CIDR range to exempt, or a comma-separated list of ranges. + * @return TS_SUCCESS if the exempt list was successfully updated, TS_ERROR otherwise. + */ +TSReturnCode TSConnectionLimitExemptListAdd(std::string_view ip_ranges); + +/* + * Remove one or more IP addresses or CIDR ranges from the per-client connection limit exempt list. + * This function allows plugins to programmatically remove from the list of IP addresses + * that should be exempt from per-client connection limits (see + * proxy.config.net.per_client.max_connections_in). + * + * @param ip_ranges The IP address or CIDR range to remove, or a comma-separated list of ranges. + * @return TS_SUCCESS if the exempt list was successfully updated, TS_ERROR otherwise. + */ +TSReturnCode TSConnectionLimitExemptListRemove(std::string_view ip_ranges); + +/* + * Clear the per-client connection limit exempt list. + * This function allows plugins to programmatically clear the list of IP addresses + * that should be exempt from per-client connection limits (see + * proxy.config.net.per_client.max_connections_in). + */ +void TSConnectionLimitExemptListClear(); + /* * Set or get various HTTP Transaction control settings. */ diff --git a/src/api/InkAPI.cc b/src/api/InkAPI.cc index 6c6f34b37e9..4b4f5ecb381 100644 --- a/src/api/InkAPI.cc +++ b/src/api/InkAPI.cc @@ -89,6 +89,7 @@ #include "mgmt/rpc/jsonrpc/JsonRPC.h" #include +#include #include "ts/ts.h" /**************************************************************** @@ -9167,3 +9168,43 @@ TSHttpTxnTypeGet(TSHttpTxn txnp) } return retval; } + +TSReturnCode +TSConnectionLimitExemptListAdd(std::string_view ip_ranges) +{ + swoc::TextView ip_ranges_tv{ip_ranges}; + while (auto ip_range_tv = ip_ranges_tv.take_prefix_at(',')) { + swoc::IPRange ip_range; + if (!ip_range.load(ip_range_tv)) { + return TS_ERROR; + } + bool success = ConnectionTracker::add_client_exempt_range(ip_range); + if (!success) { + return TS_ERROR; + } + } + return TS_SUCCESS; +} + +TSReturnCode +TSConnectionLimitExemptListRemove(std::string_view ip_ranges) +{ + swoc::TextView ip_ranges_tv{ip_ranges}; + while (auto ip_range_tv = ip_ranges_tv.take_prefix_at(',')) { + swoc::IPRange ip_range; + if (!ip_range.load(ip_range_tv)) { + return TS_ERROR; + } + bool success = ConnectionTracker::remove_client_exempt_range(ip_range); + if (!success) { + return TS_ERROR; + } + } + return TS_SUCCESS; +} + +void +TSConnectionLimitExemptListClear() +{ + ConnectionTracker::clear_client_exempt_list(); +} diff --git a/src/iocore/net/ConnectionTracker.cc b/src/iocore/net/ConnectionTracker.cc index 4a2e65eaf3d..90f6c380dcf 100644 --- a/src/iocore/net/ConnectionTracker.cc +++ b/src/iocore/net/ConnectionTracker.cc @@ -24,6 +24,7 @@ #include "P_Net.h" // For Metrics. #include "iocore/net/ConnectionTracker.h" #include "records/RecCore.h" +#include "swoc/IPAddr.h" using namespace std::literals; @@ -211,7 +212,72 @@ Groups_To_JSON(std::vector> cons return text; } -} // namespace +bool +Config_Update_Conntrack_Client_Exempt_List(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie) +{ + if (RECD_STRING != dtype) { + Warning("Invalid type for '%s' - must be 'STRING'", ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data()); + return false; + } + auto *config = static_cast(cookie); + if (data.rec_string == nullptr) { + // There is no exempt list configured. Ensure that our exempt list is empty. + config->client_exempt_list.clear(); + return true; + } + std::string_view exempt_list_string{data.rec_string}; + ink_release_assert(config != nullptr); + + // Clear the existing exempt list. + config->client_exempt_list.clear(); + + // Parse the comma-separated list of IP ranges. + swoc::TextView ranges{exempt_list_string}; + while (!ranges.empty()) { + swoc::TextView range_sv = ranges.take_prefix_at(','); + range_sv.trim_if(&isspace); + + if (!range_sv.empty()) { + swoc::IPRange range; + if (!range.load(range_sv)) { + Warning("%s: '%.*s' is not a valid IP range in configuration '%s'", ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data(), + static_cast(range_sv.size()), range_sv.data(), ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data()); + return false; + } + config->client_exempt_list.mark(range); + } + } + + return true; +} + +} // anonymous namespace + +ConnectionTracker::GlobalConfig::GlobalConfig(GlobalConfig const &other) +{ + this->client_alert_delay = other.client_alert_delay; + this->server_alert_delay = other.server_alert_delay; + this->metric_enabled = other.metric_enabled; + this->metric_prefix = other.metric_prefix; + this->client_exempt_list.clear(); + for (auto const &ip_range : other.client_exempt_list) { + this->client_exempt_list.mark(ip_range); + } +} + +ConnectionTracker::GlobalConfig & +ConnectionTracker::GlobalConfig::operator=(GlobalConfig const &other) +{ + this->client_alert_delay = other.client_alert_delay; + this->server_alert_delay = other.server_alert_delay; + this->metric_enabled = other.metric_enabled; + this->metric_prefix = other.metric_prefix; + this->client_exempt_list.clear(); + for (auto const &ip_range : other.client_exempt_list) { + this->client_exempt_list.mark(ip_range); + } + return *this; +} void ConnectionTracker::config_init(GlobalConfig *global, TxnConfig *txn, RecConfigUpdateCb const &config_cb) @@ -220,6 +286,7 @@ ConnectionTracker::config_init(GlobalConfig *global, TxnConfig *txn, RecConfigUp // Per transaction lookup must be done at call time because it changes. Enable_Config_Var(CONFIG_CLIENT_VAR_ALERT_DELAY, &Config_Update_Conntrack_Client_Alert_Delay, config_cb, global); + Enable_Config_Var(CONFIG_CLIENT_VAR_EXEMPT_LIST, &Config_Update_Conntrack_Client_Exempt_List, config_cb, global); Enable_Config_Var(CONFIG_SERVER_VAR_MIN, &Config_Update_Conntrack_Min, config_cb, txn); Enable_Config_Var(CONFIG_SERVER_VAR_MAX, &Config_Update_Conntrack_Max, config_cb, txn); Enable_Config_Var(CONFIG_SERVER_VAR_MATCH, &Config_Update_Conntrack_Match, config_cb, txn); @@ -228,10 +295,74 @@ ConnectionTracker::config_init(GlobalConfig *global, TxnConfig *txn, RecConfigUp Enable_Config_Var(CONFIG_SERVER_VAR_METRIC_PREFIX, &Config_Update_Conntrack_Metric_Prefix, config_cb, global); } +bool +ConnectionTracker::set_client_exempt_list(swoc::IPRangeSet const &ip_ranges) +{ + if (_global_config == nullptr) { + Warning("ConnectionTracker::set_client_exempt_list called before config_init"); + return false; + } + + // Clear the existing exempt list and copy the new ranges. + _global_config->client_exempt_list.clear(); + for (auto const &ip_range : ip_ranges) { + _global_config->client_exempt_list.mark(ip_range); + } + + return true; +} + +bool +ConnectionTracker::add_client_exempt_range(swoc::IPRange const &ip_range) +{ + if (_global_config == nullptr) { + Warning("ConnectionTracker::add_client_exempt_range called before config_init"); + return false; + } + + // Add the new range to the existing exempt list. + _global_config->client_exempt_list.mark(ip_range); + + return true; +} + +bool +ConnectionTracker::remove_client_exempt_range(swoc::IPRange const &ip_range) +{ + if (_global_config == nullptr) { + Warning("ConnectionTracker::remove_client_exempt_range called before config_init"); + return false; + } + + // Remove the range from the existing exempt list. + _global_config->client_exempt_list.erase(ip_range); + + return true; +} + +void +ConnectionTracker::clear_client_exempt_list() +{ + if (_global_config == nullptr) { + Warning("ConnectionTracker::clear_client_exempt_list called before config_init"); + return; + } + + // Clear all ranges from the exempt list. + _global_config->client_exempt_list.clear(); +} + ConnectionTracker::TxnState ConnectionTracker::obtain_inbound(IpEndpoint const &addr) { - TxnState zret; + TxnState zret; + if (_global_config->client_exempt_list.contains(swoc::IPAddr{addr})) { + // This short-circuits all our connection throttling logic. Save time by + // just setting the flag for the caller to see that connections are exempt + // this address. + zret._exempt_p = true; + return zret; + } CryptoHash hash; Group::Key key{addr, hash, MatchType::MATCH_IP}; std::lock_guard lock(_inbound_table._mutex); // Table lock diff --git a/src/iocore/net/Net.cc b/src/iocore/net/Net.cc index 7c8abcb7272..1c81eefb1fd 100644 --- a/src/iocore/net/Net.cc +++ b/src/iocore/net/Net.cc @@ -79,7 +79,8 @@ register_net_stats() net_rsb.connections_throttled_in = Metrics::Counter::createPtr("proxy.process.net.connections_throttled_in"); net_rsb.per_client_connections_throttled_in = Metrics::Counter::createPtr("proxy.process.net.per_client.connections_throttled_in"); - net_rsb.connections_throttled_out = Metrics::Counter::createPtr("proxy.process.net.connections_throttled_out"); + net_rsb.per_client_connections_exempt_in = Metrics::Counter::createPtr("proxy.process.net.per_client.connections_exempt_in"); + net_rsb.connections_throttled_out = Metrics::Counter::createPtr("proxy.process.net.connections_throttled_out"); net_rsb.tunnel_total_client_connections_blind_tcp = Metrics::Counter::createPtr("proxy.process.tunnel.total_client_connections_blind_tcp"); net_rsb.tunnel_current_client_connections_blind_tcp = diff --git a/src/iocore/net/P_Net.h b/src/iocore/net/P_Net.h index e43a686d72b..eae79f379e9 100644 --- a/src/iocore/net/P_Net.h +++ b/src/iocore/net/P_Net.h @@ -45,6 +45,7 @@ struct NetStatsBlock { Metrics::Gauge::AtomicType *connections_currently_open; Metrics::Counter::AtomicType *connections_throttled_in; Metrics::Counter::AtomicType *per_client_connections_throttled_in; + Metrics::Counter::AtomicType *per_client_connections_exempt_in; Metrics::Counter::AtomicType *connections_throttled_out; Metrics::Counter::AtomicType *default_inactivity_timeout_applied; Metrics::Counter::AtomicType *default_inactivity_timeout_count; diff --git a/src/iocore/net/UnixNetAccept.cc b/src/iocore/net/UnixNetAccept.cc index 63ebb6b21b8..94a88648542 100644 --- a/src/iocore/net/UnixNetAccept.cc +++ b/src/iocore/net/UnixNetAccept.cc @@ -54,8 +54,14 @@ handle_max_client_connections(IpEndpoint const &addr, std::shared_ptr 0) { - auto inbound_tracker = ConnectionTracker::obtain_inbound(addr); - auto const tracked_count = inbound_tracker.reserve(); + auto inbound_tracker = ConnectionTracker::obtain_inbound(addr); + if (inbound_tracker.is_exempt()) { + // The user configured connections like this to not be tracked. Simply exempt it. + Metrics::Counter::increment(net_rsb.per_client_connections_exempt_in); + Dbg(dbg_ctl_iocore_net_accepts, "Ignoring client connection counting for an incoming address in the exempt list."); + return true; + } + auto const tracked_count = inbound_tracker.reserve(); if (tracked_count > client_max) { // close the connection as we are in per client connection throttle state inbound_tracker.release(); @@ -63,6 +69,7 @@ handle_max_client_connections(IpEndpoint const &addr, std::shared_ptr str: class PerClientConnectionMaxTest: """Define an object to test our max client connection behavior.""" - _dns_counter: int = 0 - _server_counter: int = 0 - _ts_counter: int = 0 - _client_counter: int = 0 + _process_counter: int = 0 _max_client_connections: int = 3 _protocol_to_replay_file = { Protocol.HTTP: 'http_slow_origins.replay.yaml', @@ -58,15 +55,28 @@ class PerClientConnectionMaxTest: Protocol.HTTP2: 'http2_slow_origins.replay.yaml', } - def __init__(self, protocol: int) -> None: + def __init__(self, protocol: int, exempt_list: str = '', exempt_list_applies: bool = False) -> None: """Configure the test processes in preparation for the TestRun. :param protocol: The protocol to test. + :param exempt_list: A comma-separated string of IP addresses or ranges to exempt. + The default empty string implies that no exempt list will be configured. + :param exempt_list_applies: If True, the exempt list is assumed to exempt + the test connections. Thus the per client max connections is expected + to be enforced for the connections. """ + self._process_counter = PerClientConnectionMaxTest._process_counter + PerClientConnectionMaxTest._process_counter += 1 self._protocol = protocol protocol_string = Protocol.to_str(protocol) self._replay_file = self._protocol_to_replay_file[protocol] - tr = Test.AddTestRun(f'proxy.config.net.per_client.connection.max: {protocol_string}') + self._exempt_list = exempt_list + self._exempt_list_applies = exempt_list_applies + + exempt_list_description = 'exempted' if exempt_list_applies else 'not exempted' + tr = Test.AddTestRun( + f'proxy.config.net.per_client.connection.max: {protocol_string}, ' + f'exempt_list: {exempt_list_description}') self._configure_dns(tr) self._configure_server(tr) self._configure_trafficserver() @@ -78,37 +88,33 @@ def _configure_dns(self, tr: 'TestRun') -> None: :param tr: The TestRun to add the nameserver to. """ - name = f'dns{PerClientConnectionMaxTest._dns_counter}' + name = f'dns{self._process_counter}' self._dns = tr.MakeDNServer(name, default='127.0.0.1') - PerClientConnectionMaxTest._dns_counter += 1 def _configure_server(self, tr: 'TestRun') -> None: """Configure the server to be used in the test. :param tr: The TestRun to add the server to. """ - name = f'server{PerClientConnectionMaxTest._server_counter}' + name = f'server{self._process_counter}' self._server = tr.AddVerifierServerProcess(name, self._replay_file) - PerClientConnectionMaxTest._server_counter += 1 - self._server.Streams.All += Testers.ContainsExpression( - "first-request", "Verify the first request should have been received.") - self._server.Streams.All += Testers.ContainsExpression( - "second-request", "Verify the second request should have been received.") - self._server.Streams.All += Testers.ContainsExpression( - "third-request", "Verify the third request should have been received.") - self._server.Streams.All += Testers.ContainsExpression( - "fifth-request", "Verify the fifth request should have been received.") - - # The fourth request should be blocked due to too many connections. - self._server.Streams.All += Testers.ExcludesExpression( - "fourth-request", "Verify the fourth request should not be received.") + self._server.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") + self._server.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") + self._server.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") + self._server.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") + + if self._exempt_list_applies: + # The fourth request should be allowed due to the exempt_list. + self._server.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") + else: + # The fourth request should be blocked due to too many connections. + self._server.Streams.All += Testers.ExcludesExpression("fourth-request", "Verify the fourth request was not received.") def _configure_trafficserver(self) -> None: """Configure Traffic Server to be used in the test.""" # Associate ATS with the Test so that metrics can be verified. - name = f'ts{PerClientConnectionMaxTest._ts_counter}' + name = f'ts{self._process_counter}' self._ts = Test.MakeATSProcess(name, enable_cache=False, enable_tls=True) - PerClientConnectionMaxTest._ts_counter += 1 self._ts.addDefaultSSLFiles() self._ts.Disk.ssl_multicert_config.AddLine('dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key') if self._protocol == Protocol.HTTP: @@ -133,41 +139,52 @@ def _configure_trafficserver(self) -> None: # per the ConnectionTracker metrics. 'proxy.config.http.keep_alive_enabled_in': 0, }) - self._ts.Disk.diags_log.Content += Testers.ContainsExpression( - f'WARNING:.*too many connections:.*limit={self._max_client_connections}', - 'Verify the user is warned about the connection limit being hit.') + if self._exempt_list: + self._ts.Disk.records_config.update({ + 'proxy.config.http.per_client.connection.exempt_list': self._exempt_list, + }) + if self._exempt_list_applies: + self._ts.Disk.diags_log.Content += Testers.ExcludesExpression( + f'WARNING:.*too many connections:', 'Connections should not be throttled due to the exempt list.') + else: + self._ts.Disk.diags_log.Content += Testers.ContainsExpression( + f'WARNING:.*too many connections:.*limit={self._max_client_connections}', + 'Verify the user is warned about the connection limit being hit.') def _configure_client(self, tr: 'TestRun') -> None: """Configure the TestRun. :param tr: The TestRun to add the client to. """ - name = f'client{PerClientConnectionMaxTest._client_counter}' + name = f'client{self._process_counter}' p = tr.AddVerifierClientProcess( name, self._replay_file, http_ports=[self._ts.Variables.port], https_ports=[self._ts.Variables.ssl_port]) - PerClientConnectionMaxTest._client_counter += 1 p.StartBefore(self._dns) p.StartBefore(self._server) p.StartBefore(self._ts) - # Because the fourth connection will be aborted, the client will have a - # non-zero return code. - p.ReturnCode = 1 - p.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request should have been received.") - p.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request should have been received.") - p.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request should have been received.") - p.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request should have been received.") - if self._protocol == Protocol.HTTP: - p.Streams.All += Testers.ContainsExpression( - "The peer closed the connection while reading.", - "A connection should be closed due to too many client connections.") - p.Streams.All += Testers.ContainsExpression( - "Failed HTTP/1 transaction with key: fourth-request", "The fourth request should fail.") + p.Streams.All += Testers.ContainsExpression("first-request", "Verify the first request was received.") + p.Streams.All += Testers.ContainsExpression("second-request", "Verify the second request was received.") + p.Streams.All += Testers.ContainsExpression("third-request", "Verify the third request was received.") + p.Streams.All += Testers.ContainsExpression("fifth-request", "Verify the fifth request was received.") + if self._exempt_list_applies: + p.ReturnCode = 0 + p.Streams.All += Testers.ContainsExpression("fourth-request", "Verify the fourth request was received.") else: - p.Streams.All += Testers.ContainsExpression( - "ECONNRESET: Connection reset by peer", "A connection should be closed due to too many client connections.") - p.Streams.All += Testers.ExcludesExpression("fourth-request", "The fourth request should fail.") + # Because the fourth connection will be aborted, the client will have a + # non-zero return code. + p.ReturnCode = 1 + if self._protocol == Protocol.HTTP: + p.Streams.All += Testers.ContainsExpression( + "The peer closed the connection while reading.", + "A connection should be closed due to too many client connections.") + p.Streams.All += Testers.ContainsExpression( + "Failed HTTP/1 transaction with key: fourth-request", "The fourth request should fail.") + else: + p.Streams.All += Testers.ContainsExpression( + "ECONNRESET: Connection reset by peer", "A connection should be closed due to too many client connections.") + p.Streams.All += Testers.ExcludesExpression("fourth-request", "The fourth request should fail.") def _verify_metrics(self) -> None: """Verify the per client connection metrics.""" @@ -176,10 +193,20 @@ def _verify_metrics(self) -> None: tr.Processes.Default.Command = ( 'traffic_ctl metric get ' 'proxy.process.net.per_client.connections_throttled_in ' + 'proxy.process.net.per_client.connections_exempt_in ' 'proxy.process.net.connection_tracker_table_size') tr.Processes.Default.ReturnCode = 0 - tr.Processes.Default.Streams.All += Testers.ContainsExpression( - 'proxy.process.net.per_client.connections_throttled_in 1', 'Verify the per client throttled metric is correct.') + if self._exempt_list_applies: + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_throttled_in 0', 'Verify no connections were recorded as throttled.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_exempt_in 5', + 'Verify that the connections were all recorded as exempted.') + else: + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_throttled_in 1', 'Verify the connection was recorded as throttled.') + tr.Processes.Default.Streams.All += Testers.ContainsExpression( + 'proxy.process.net.per_client.connections_exempt_in 0', 'Verify no connections were recorded as exempt.') tr.Processes.Default.Streams.All += Testers.ContainsExpression( 'proxy.process.net.connection_tracker_table_size 0', 'Verify the table was cleaned up correctly.') @@ -187,3 +214,7 @@ def _verify_metrics(self) -> None: PerClientConnectionMaxTest(Protocol.HTTP) PerClientConnectionMaxTest(Protocol.HTTPS) PerClientConnectionMaxTest(Protocol.HTTP2) + +PerClientConnectionMaxTest(Protocol.HTTP, exempt_list='127.0.0.1,::1', exempt_list_applies=True) +PerClientConnectionMaxTest(Protocol.HTTPS, exempt_list='1.2.3.4,5.6.0.0/16', exempt_list_applies=False) +PerClientConnectionMaxTest(Protocol.HTTP2, exempt_list='0/0,::/0', exempt_list_applies=True)