This is an automated email from the ASF dual-hosted git repository.
bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new d47009360f proxy.config.http.per_client.connection.exempt_list (#12198)
d47009360f is described below
commit d47009360f8a3eac3f7dbf7facd1cf15843ff253
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Dec 10 14:24:44 2025 -0600
proxy.config.http.per_client.connection.exempt_list (#12198)
This implements
proxy.config.http.per_client.connection.exempt_list, a
configuration for the user to be able to provide a set of IP addresses
that are not counted against
proxy.config.net.per_client.max_connections_in.
This also adds the following TS APIs to modify this list via a plugin:
TSConnectionLimitExemptListSet
TSConnectionLimitExemptListAdd
TSConnectionLimitExemptListClear
---
doc/admin-guide/files/records.yaml.en.rst | 36 ++++-
.../functions/TSConnectionLimitExemptList.en.rst | 125 ++++++++++++++++
include/iocore/net/ConnectionTracker.h | 69 ++++++++-
include/ts/ts.h | 30 ++++
src/api/InkAPI.cc | 41 ++++++
src/iocore/net/ConnectionTracker.cc | 160 ++++++++++++++++++++-
src/iocore/net/Net.cc | 3 +-
src/iocore/net/P_Net.h | 1 +
src/iocore/net/UnixNetAccept.cc | 11 +-
src/records/RecordsConfig.cc | 2 +
.../per_client_connection_max.test.py | 125 ++++++++++------
11 files changed, 544 insertions(+), 59 deletions(-)
diff --git a/doc/admin-guide/files/records.yaml.en.rst
b/doc/admin-guide/files/records.yaml.en.rst
index 2336a2de6a..360cfc5fc0 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -549,6 +549,40 @@ 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
+ :reloadable:
+
+ 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 is reloadable via :program:`traffic_ctl config reload`.
+
+ 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
@@ -2123,7 +2157,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 0000000000..684695aa39
--- /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 <ts/ts.h>
+
+.. 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 <ts/ts.h>
+ #include <fstream>
+ #include <string>
+
+ 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 86784b2f38..d41d9ad86f 100644
--- a/include/iocore/net/ConnectionTracker.h
+++ b/include/iocore/net/ConnectionTracker.h
@@ -44,6 +44,7 @@
#include <tscore/MgmtDefs.h>
#include "iocore/net/SessionSharingAPIEnums.h"
#include "tsutil/Metrics.h"
+#include "tsutil/Bravo.h"
/**
* Singleton class to keep track of the number of inbound and outbound
connections.
@@ -82,16 +83,23 @@ public:
/** Static configuration values. */
struct GlobalConfig {
- 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.
+ 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.
+ mutable ts::bravo::shared_mutex client_exempt_list_mutex; ///< Protects
client_exempt_list from concurrent access.
};
// 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 +180,18 @@ public:
std::shared_ptr<Group> _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 +287,42 @@ public:
*/
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 +433,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 8ea727082f..3227f1cf18 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -2958,6 +2958,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 9b6e2e0a7e..1353be4f77 100644
--- a/src/api/InkAPI.cc
+++ b/src/api/InkAPI.cc
@@ -90,6 +90,7 @@
#include "mgmt/rpc/jsonrpc/JsonRPC.h"
#include <swoc/bwf_base.h>
+#include <swoc/IPRange.h>
#include "ts/ts.h"
/****************************************************************
@@ -8941,3 +8942,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 4a2e65eaf3..5a0b1c59ca 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,89 @@
Groups_To_JSON(std::vector<std::shared_ptr<ConnectionTracker::Group const>> 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<ConnectionTracker::GlobalConfig *>(cookie);
+ ink_release_assert(config != nullptr);
+
+ if (data.rec_string == nullptr) {
+ // There is no exempt list configured. Ensure that our exempt list is
empty.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(config->client_exempt_list_mutex);
+ config->client_exempt_list.clear();
+ return true;
+ }
+ std::string_view exempt_list_string{data.rec_string};
+
+ // Parse the comma-separated list of IP ranges into a temporary set first.
+ // This ensures we don't lose the previous configuration if parsing fails.
+ swoc::IPRangeSet new_exempt_list;
+ 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<int>(range_sv.size()), range_sv.data(),
ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data());
+ return false;
+ }
+ new_exempt_list.mark(range);
+ }
+ }
+
+ // Parsing succeeded. Now acquire the lock and replace the global exempt
list.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(config->client_exempt_list_mutex);
+ config->client_exempt_list.clear();
+ for (auto const &ip_range : new_exempt_list) {
+ config->client_exempt_list.mark(ip_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;
+ // Lock the source to safely copy the exempt list.
+ // Note: the mutex itself is not copied; it's default-constructed.
+ ts::bravo::shared_lock<ts::bravo::shared_mutex>
lock(other.client_exempt_list_mutex);
+ 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)
+{
+ if (this != &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;
+ // Lock both source and destination to safely copy the exempt list.
+ // Lock in a consistent order to avoid deadlock (lock 'other' first, then
'this').
+ ts::bravo::shared_lock<ts::bravo::shared_mutex>
lock_src(other.client_exempt_list_mutex);
+ std::lock_guard<ts::bravo::shared_mutex>
lock_dst(this->client_exempt_list_mutex);
+ 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 +303,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 +312,82 @@ 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;
+ }
+
+ // Acquire exclusive lock and replace the exempt list.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(_global_config->client_exempt_list_mutex);
+ _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;
+ }
+
+ // Acquire exclusive lock and add the new range to the existing exempt list.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(_global_config->client_exempt_list_mutex);
+ _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;
+ }
+
+ // Acquire exclusive lock and remove the range from the existing exempt list.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(_global_config->client_exempt_list_mutex);
+ _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;
+ }
+
+ // Acquire exclusive lock and clear all ranges from the exempt list.
+ std::lock_guard<ts::bravo::shared_mutex>
lock(_global_config->client_exempt_list_mutex);
+ _global_config->client_exempt_list.clear();
+}
+
ConnectionTracker::TxnState
ConnectionTracker::obtain_inbound(IpEndpoint const &addr)
{
- TxnState zret;
+ TxnState zret;
+ // Check if the address is in the exempt list with shared (read) lock.
+ {
+ ts::bravo::shared_lock<ts::bravo::shared_mutex>
lock(_global_config->client_exempt_list_mutex);
+ 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<std::mutex> lock(_inbound_table._mutex); // Table lock
diff --git a/src/iocore/net/Net.cc b/src/iocore/net/Net.cc
index 7c8abcb727..1c81eefb1f 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 e43a686d72..eae79f379e 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 63ebb6b21b..94a8864854 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<Connection
{
int const client_max = NetHandler::get_per_client_max_connections_in();
if (client_max > 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<Connection
inbound_tracker.Warn_Blocked(client_max, 0, tracked_count - 1, addr,
dbg_ctl_iocore_net_accept.on() ?
&dbg_ctl_iocore_net_accept : nullptr);
Metrics::Counter::increment(net_rsb.per_client_connections_throttled_in);
+ Dbg(dbg_ctl_iocore_net_accepts, "Blocking a client connection due to per
client connection limit.");
return false;
}
conn_track_group = inbound_tracker.drop();
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index 8cd99ca6a9..7ca85a7553 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -396,6 +396,8 @@ static constexpr RecordElement RecordsConfig[] =
,
{RECT_CONFIG, "proxy.config.net.per_client.max_connections_in", RECD_INT,
"0", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
,
+ {RECT_CONFIG, "proxy.config.http.per_client.connection.exempt_list",
RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+ ,
{RECT_CONFIG, "proxy.config.http.per_client.connection.alert_delay",
RECD_INT, "60", RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
,
{RECT_CONFIG, "proxy.config.net.max_requests_in", RECD_INT, "0",
RECU_DYNAMIC, RR_NULL, RECC_STR, "^[0-9]+$", RECA_NULL}
diff --git
a/tests/gold_tests/client_connection/per_client_connection_max.test.py
b/tests/gold_tests/client_connection/per_client_connection_max.test.py
index 4084a6c611..c2eaa2a128 100644
--- a/tests/gold_tests/client_connection/per_client_connection_max.test.py
+++ b/tests/gold_tests/client_connection/per_client_connection_max.test.py
@@ -47,10 +47,7 @@ class Protocol(Enum):
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 @@ class PerClientConnectionMaxTest:
: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,45 +139,56 @@ class PerClientConnectionMaxTest:
# 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],
run_parallel=True)
- 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."""
@@ -180,10 +197,20 @@ class PerClientConnectionMaxTest:
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.')
@@ -191,3 +218,7 @@ class PerClientConnectionMaxTest:
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)