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)


Reply via email to