diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index 0498be80d22..8e0f8c8cbd5 100644 --- a/doc/admin-guide/plugins/rate_limit.en.rst +++ b/doc/admin-guide/plugins/rate_limit.en.rst @@ -34,9 +34,7 @@ into a single rate limiter. This is still work in progress, in particularly the configuration and the IP reputation system needs some work. In particular: - * We need a proper YAML configuration overall, allowing us to configure - better per service controls as well as sharing resources between remap - rules or SNI. + * The remap configuration needs YAML support. * We need reloadable configurations. * The IP reputation currently only works with the global plugin settings. * There is no support for adding allow listed IPs to the IP reputation. @@ -89,7 +87,7 @@ are available: .. option:: --maxage An optional ``max-age`` for how long a transaction can sit in the delay queue. - The value (default 0) is the age in milliseconds. + The value (default 0) is the age in seconds. .. option:: --prefix @@ -110,9 +108,10 @@ As a global plugin, the rate limiting currently applies only for TLS enabled connections, based on the SNI from the TLS handshake. As a global plugin we also have the support of an IP reputation system, see below for configurations. -The basic use is as:: +In addition, the global plugin must be configured via a reloadable YAML +configuration file. The basic use is as:: - rate_limit.so SNI=www1.example.com,www2.example.com --limit=2 --queue=2 --maxage=10000 + rate_limit.so some_config.yaml .. Note:: @@ -122,43 +121,109 @@ The basic use is as:: done using e.g. the ``conf_remap`` plugin, :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`. -The following options are available: -.. program:: rate-limit - -.. option:: --limit +The YAML configuration can have the following format, where the various sections +and nodes are documented below. + + .. code-block:: yaml + + selector: + - sni: test1.example.com + limit: 1000 + queue: + size: 1000 + max-age: 30 + metrics: + tag: example.com + prefix: ddos + ip-rep: main + exclude: internal + - sni: test2.example.com + aliases: [test3.example.com, test4.example.com] + limit: 100 + ip-rep: + - name: main + buckets: 10 + size: 15 + percentage: 90 + max-age: 300 + perma-block: + limit: 100 + threshold: 1 + max-age: 1800 + lists: + - name: internal + cidr: + - 10.0.0.0/8 + - 192.168.0.0/16 + + +For the top level `selector` node, the following options are available: + +.. option:: sni + + The SNI to match for this rate limiter. + +.. option:: limit The maximum number of active client transactions. -.. option:: --queue +.. option:: aliases - When the limit (above) has been reached, all new connections are placed - on a FIFO queue. This option (optional) sets an upper bound on how many - queued transactions we will allow. When this threshold is reached, all - additional connections are immediately errored out in the TLS handshake. + A list of aliases for the SNI, which will also be matched by this rate limiter. - The queue is effectively disabled if this is set to ``0``, which implies - that when the transaction limit is reached, we immediately start serving - error responses. +.. option:: ip-rep - The default queue size is ``UINT_MAX``, which is essentially unlimited. + The name of the IP reputation node to use for this rate limiter. If not + specified, the IP reputation system is not used for this rate limiter. -.. option:: --maxage +.. option:: exclude - An optional ``max-age`` for how long a transaction can sit in the delay queue. - The value (default 0) is the age in milliseconds. + A list of IP CIDR ranges to exclude from any rate limiting. Any IP matching + this list will not be rate limited, even if the SNI matches. -.. option:: --prefix +.. option:: queue - An optional metric prefix to use instead of the default (plugin.rate_limiter). + If enabled, when the limit (above) has been reached, all new connections + are placed on a FIFO queue. This option sets an upper bound on + how many queued transactions we will allow. When this threshold is reached, + all additional connections are immediately errored out in the TLS handshake. -.. option:: --tag + The queue option can include a `size` and a `max-age` option. The size is + default to ``UINT_MAX``, which is essentially unlimited. The max-age is + default to ``0``, which means no age limit. - An optional metric tag to use instead of the default. When a tag is not specified - the plugin will use the FQDN of the SNI associated with each rate limiter instance - created during plugin initialization. + No queue is enabled without this configuration directive, but it can also be + disabled explicitly if the size is set to ``0``. + +.. option:: metrics + + This is an optional node, which can be used to configure the metrics for + this rate limiter. If not specified, no metrics will be added. + + The metrics node can include a `tag` and a `prefix` option. The tag is + default to the SNI, and the prefix is default to ``plugin.rate_limiter``. + +The `lists` node is used to configure IP lists, which can be used to exclude +certain address ranges from the rate limiting. The following options are used: + +.. option:: name -.. option:: --iprep_buckets + The name of the IP reputation setup, used to refer to it from the rate limiters. + +.. option:: cidr + + A list of CIDR ranges to add to this rule. The format is e.g. `10.0.0.0/8`. + +The `ip-rep`` node is used to configure the IP reputation system, there can be +zero, one or many IP reputation setups. Each setup is configured with a name, +and the following options: + +.. option:: name + + The name of the IP reputation setup, used to refer to it from the rate limiters. + +.. option:: buckets The number of LRU buckets to use for the IP reputation. A good number here is ``10``, which is the default, but can be configured. The reason for the different @@ -166,7 +231,7 @@ The following options are available: few buckets will not be enough to keep such sorting, rendering the algorithm useless. To function in our setup, the number of buckets must be less than ``100``. -.. option:: --iprep_bucketsize +.. option:: size This is the size of the largest LRU bucket (the ``entry bucket``), ``15`` is a good value. This is a power of 2, so ``15`` means the largest LRU can hold ``32768`` entries. @@ -175,32 +240,36 @@ The following options are available: The default here is ``0``, which means the IP reputation filter is not enabled! -.. option:: --iprep_percentage +.. option:: percentage This is the minimum percentage of the ``limit`` that the pressure must be at, before we start blocking IPs. The default is ``0.9`` which means ``90%`` of the limit. -.. option:: --iprep_maxage +.. option:: max-age This is used for aging out entries out of the LRU, the default is ``0`` which means no aging happens. Even with no aging, entries will eventually fall out of buckets because of the LRU mechanism that kicks in. The aging is here to make sure a spike in traffic from an IP doesn't keep the entry for too long in the LRUs. -.. option:: --iprep_permablock_limit +In addition, there's an optional configuration for the permanently blocking buckets, +`perma-block`. This is a special bucket, which is only used for IPs which have been +blocked for a long time. The configuration for this bucket is: + +.. option:: limit The minimum number of hits an IP must reach to get moved to the permanent bucket. In this bucket, entries will stay for 2x -.. option:: --iprep_permablock_pressure +.. option:: threshold This option specifies from which bucket an IP is allowed to move from into the perma block bucket. A good value here is likely ``0`` or ``1``, which is very conservative. -.. option:: --iprep_permablock_maxage +.. option:: max-age - Similar to ``--iprep_maxage`` above, but only applies to the long term (`perma-block`) - bucket. Default is ``0``, which means no aging to this bucket is applied. + Like above, but only applies to the long term (`perma-block`) bucket. Default is + ``0``, which means no aging to this bucket is applied. Metrics ------- diff --git a/plugins/experimental/rate_limit/CMakeLists.txt b/plugins/experimental/rate_limit/CMakeLists.txt index 1ea94e2b95e..8f62e4d29dc 100644 --- a/plugins/experimental/rate_limit/CMakeLists.txt +++ b/plugins/experimental/rate_limit/CMakeLists.txt @@ -15,6 +15,8 @@ # ####################### +project(rate_limit) + add_atsplugin( rate_limit ip_reputation.cc @@ -23,5 +25,7 @@ add_atsplugin( sni_selector.cc txn_limiter.cc utilities.cc + lists.cc ) -target_link_libraries(rate_limit PRIVATE OpenSSL::SSL) + +target_link_libraries(rate_limit PRIVATE libswoc yaml-cpp::yaml-cpp OpenSSL::SSL) diff --git a/plugins/experimental/rate_limit/Makefile.inc b/plugins/experimental/rate_limit/Makefile.inc index 69fa09affe6..b0a40f4001c 100644 --- a/plugins/experimental/rate_limit/Makefile.inc +++ b/plugins/experimental/rate_limit/Makefile.inc @@ -22,4 +22,10 @@ experimental_rate_limit_rate_limit_la_SOURCES = \ experimental/rate_limit/sni_limiter.cc \ experimental/rate_limit/sni_selector.cc \ experimental/rate_limit/ip_reputation.cc \ + experimental/rate_limit/lists.cc \ experimental/rate_limit/utilities.cc + +experimental_rate_limit_rate_limit_la_LDFLAGS = \ + $(AM_LDFLAGS) + +AM_CPPFLAGS += @YAMLCPP_INCLUDES@ diff --git a/plugins/experimental/rate_limit/ip_reputation.cc b/plugins/experimental/rate_limit/ip_reputation.cc index 764093e7750..0bb183785f8 100644 --- a/plugins/experimental/rate_limit/ip_reputation.cc +++ b/plugins/experimental/rate_limit/ip_reputation.cc @@ -34,12 +34,12 @@ SieveLru::hasher(const sockaddr *sock) { switch (sock->sa_family) { case AF_INET: { - const sockaddr_in *sa = reinterpret_cast(sock); + const auto *sa = reinterpret_cast(sock); return (0xffffffff00000000 | sa->sin_addr.s_addr); } break; case AF_INET6: { - const sockaddr_in6 *sa6 = reinterpret_cast(sock); + const auto *sa6 = reinterpret_cast(sock); return (*reinterpret_cast(sa6->sin6_addr.s6_addr) ^ *reinterpret_cast(sa6->sin6_addr.s6_addr + sizeof(uint64_t))); @@ -77,38 +77,65 @@ SieveLru::hasher(const std::string &ip, u_short family) // Mostly a convenience return 0; // Probably can't happen, but have to return something } -// Constructor, setting up the pre-sized LRU buckets etc. -SieveLru::SieveLru(uint32_t num_buckets, uint32_t size) : _lock(TSMutexCreate()) -{ - initialize(num_buckets, size); -} -// Initialize the Sieve LRU object -void -SieveLru::initialize(uint32_t num_buckets, uint32_t size) +bool +SieveLru::parseYaml(const YAML::Node &node) { - TSMutexLock(_lock); - TSAssert(!_initialized); // Don't allow it to be initialized more than once! - TSReleaseAssert(size > num_buckets); // Otherwise we can't half the bucket sizes + if (node["buckets"]) { + _num_buckets = node["buckets"].as(); + } + + if (node["size"]) { + _size = node["size"].as(); + } + + if (node["percentage"]) { + _percentage = node["percentage"].as(); + } + + if (node["max_age"]) { + _max_age = std::chrono::seconds(node["max_age"].as()); + } + + if (node["perma-block"]) { + const YAML::Node &perma = node["perma-block"]; - _initialized = true; - _num_buckets = num_buckets; - _size = size; + if (perma.IsMap()) { + if (perma["limit"]) { + _permablock_limit = perma["limit"].as(); + } + + if (perma["threshold"]) { + _permablock_threshold = perma["threshold"].as(); + } + + if (perma["max_age"]) { + _permablock_max_age = std::chrono::seconds(perma["max_age"].as()); + } + } else { + TSError("[%s] The perma-block node must be a map", PLUGIN_NAME); + return false; + } + } - uint32_t cur_size = pow(2, 1 + _size - num_buckets); + uint32_t cur_size = pow(2, 1 + _size - _num_buckets); - _map.reserve(pow(2, size + 2)); // Allow for all the sieve LRUs, and extra room for the allow list - _buckets.reserve(_num_buckets + 2); // Two extra buckets, for the block list and allow list + _map.reserve(pow(2, _size + 1)); // Allow for all the sieve LRUs + _buckets.reserve(_num_buckets + 1); // One extra bucket, for the deny list // Create the other buckets, in smaller and smaller sizes (power of 2) for (uint32_t i = lastBucket(); i <= entryBucket(); ++i) { _buckets[i] = new SieveBucket(cur_size); cur_size *= 2; } - _buckets[blockBucket()] = new SieveBucket(cur_size / 2); // Block LRU, same size as entry bucket - _buckets[allowBucket()] = new SieveBucket(0); // Allow LRU, this is unlimited - TSMutexUnlock(_lock); + + Dbg(dbg_ctl, "Loaded IP-Reputation rule: %s(%u, %u, %u, %ld)", _name.c_str(), _num_buckets, _size, _percentage, + static_cast(_max_age.count())); + Dbg(dbg_ctl, "\twith perma-block rule: %s(%u, %u, %ld)", _name.c_str(), _permablock_limit, _permablock_threshold, + static_cast(_permablock_max_age.count())); + + return true; } // Increment the count for an element (will be created / added if new). @@ -132,7 +159,8 @@ SieveLru::increment(KeyClass key) _map.erase(l_key); *last = {key, 1, entryBucket(), SystemClock::now()}; } else { - // Create a new entry, the date is not used now (unless perma blocked), but could be useful for aging out stale elements. + // Create a new entry, the date is not used now (unless perma blocked), but could be useful for aging out stale + // elements. lru->push_front({key, 1, entryBucket(), SystemClock::now()}); } _map[key] = lru->begin(); @@ -143,7 +171,7 @@ SieveLru::increment(KeyClass key) auto &[map_key, map_item] = *map_it; auto &[list_key, count, bucket, added] = *map_item; auto lru = _buckets[bucket]; - auto max_age = (bucket == blockBucket() ? _perma_max_age : _max_age); + auto max_age = (bucket == blockBucket() ? _permablock_max_age : _max_age); // Check if the entry is older than max_age (if set), if so just move it to the entry bucket and restart // Yes, this will move likely abusive IPs but they will earn back a bad reputation; The goal here is to @@ -274,8 +302,7 @@ SieveLru::dump() long long cnt = 0, sum = 0; auto lru = _buckets[i]; - std::cout << std::endl - << "Dumping bucket " << i << " (size=" << lru->size() << ", max_size=" << lru->max_size() << ")" << std::endl; + std::cout << '\n' << "Dumping bucket " << i << " (size=" << lru->size() << ", max_size=" << lru->max_size() << ")" << '\n'; for (auto &it : *lru) { auto &[key, count, bucket, added] = it; @@ -288,7 +315,7 @@ SieveLru::dump() #endif } - std::cout << "\tAverage count=" << (cnt > 0 ? sum / cnt : 0) << std::endl; + std::cout << "\tAverage count=" << (cnt > 0 ? sum / cnt : 0) << '\n'; } TSMutexUnlock(_lock); } diff --git a/plugins/experimental/rate_limit/ip_reputation.h b/plugins/experimental/rate_limit/ip_reputation.h index 78987397704..48b0e5e87d6 100644 --- a/plugins/experimental/rate_limit/ip_reputation.h +++ b/plugins/experimental/rate_limit/ip_reputation.h @@ -31,7 +31,9 @@ #include #include +#include #include "ts/ts.h" +#include "utilities.h" namespace IpReputation { @@ -45,9 +47,16 @@ using LruEntry = std::tuple { + using self_type = SieveBucket; + public: SieveBucket(uint32_t max_size) : _max_size(max_size) {} + SieveBucket() = delete; + SieveBucket(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + bool full() const { @@ -83,17 +92,24 @@ using HashMap = std::unordered_map; // The hash // hashed value from the IP as the key (just like the hashed in cache_promote). class SieveLru { + using self_type = SieveLru; + public: - SieveLru() : _lock(TSMutexCreate()){}; // The uninitialized version - SieveLru(uint32_t num_buckets, uint32_t size); + SieveLru(std::string &name) : _lock(TSMutexCreate()) { _name = name; } + + SieveLru() = delete; + SieveLru(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + ~SieveLru() { - for (uint32_t i = 0; i <= _num_buckets + 1; ++i) { // Remember to delete the two special allow/block buckets too - delete _buckets[i]; + for (auto &bucket : _buckets) { + delete bucket; } } - void initialize(uint32_t num_buckets = 10, uint32_t size = 15); + bool parseYaml(const YAML::Node &node); // Return value is the bucket (0 .. num_buckets) that the IP is in, and the // current count of "hits". The lookup version is similar, except it doesn't @@ -113,24 +129,12 @@ class SieveLru return move_bucket(key, blockBucket()); } - uint32_t - allow(KeyClass key) - { - return move_bucket(key, allowBucket()); - } - uint32_t block(const sockaddr *sock) { return move_bucket(hasher(sock), blockBucket()); } - uint32_t - allow(const sockaddr *sock) - { - return move_bucket(hasher(sock), allowBucket()); - } - // Lookup the current state of an IP std::tuple lookup(KeyClass key) const; @@ -149,7 +153,6 @@ class SieveLru // entryBucket == the highest bucket, where new IPs enter (also the biggest bucket) // lastBucket == the last bucket, which is most likely to be abusive // blockBucket == the bucket where we "permanently" block bad IPs - // allowBucket == the bucket where we "permanently" allow good IPs (can not be blocked) uint32_t entryBucket() const { @@ -168,12 +171,6 @@ class SieveLru return 0; } - uint32_t - allowBucket() const - { - return _num_buckets + 1; - } - size_t bucketSize(uint32_t bucket) const { @@ -190,29 +187,52 @@ class SieveLru return _initialized; } - // Aging getters and setters - std::chrono::seconds - maxAge() const + const std::string & + name() const { - return _max_age; + return _name; } - std::chrono::seconds - permaMaxAge() const + uint32_t + numBuckets() const { - return _perma_max_age; + return _num_buckets; } - void - maxAge(std::chrono::seconds maxage) + uint32_t + size() const { - _max_age = maxage; + return _size; } - void - permaMaxAge(std::chrono::seconds maxage) + uint32_t + percentage() const + { + return _percentage; + } + + uint32_t + permablock_count() const + { + return _permablock_limit; + } + + uint32_t + permablock_threshold() const + { + return _permablock_threshold; + } + + std::chrono::seconds + maxAge() const + { + return _max_age; + } + + std::chrono::seconds + permaMaxAge() const { - _perma_max_age = maxage; + return _permablock_max_age; } // Debugging tool, dumps some info around the buckets @@ -225,12 +245,18 @@ class SieveLru private: HashMap _map; std::vector _buckets; - uint32_t _num_buckets = 10; // Leave this at 10 ... - uint32_t _size = 0; // Set this up to initialize - std::chrono::seconds _max_age = std::chrono::seconds::zero(); // Aging time in the SieveLru (default off) - std::chrono::seconds _perma_max_age = std::chrono::seconds::zero(); // Aging time in the SieveLru for perma-blocks - bool _initialized = false; // If this has been properly initialized yet - TSMutex _lock; // The lock around all data access + std::string _name; + bool _initialized = false; // If this has been properly initialized yet + TSMutex _lock; // The lock around all data access + // Standard options + uint32_t _num_buckets = 10; // Leave this at 10 ... + uint32_t _size = 0; // Set this up to initialize + uint32_t _percentage = 90; // At what percentage of limit do we start blocking + std::chrono::seconds _max_age = std::chrono::seconds::zero(); // Aging time in the SieveLru (default off) + // Perma-block options + uint32_t _permablock_limit = 0; // "Hits" limit for blocking permanently + uint32_t _permablock_threshold = 0; // Pressure threshold for permanent block + std::chrono::seconds _permablock_max_age = std::chrono::seconds::zero(); // Aging time in the SieveLru for perma-blocks }; } // namespace IpReputation diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index 3f1d6172ac3..d9fe6f4e02f 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -23,20 +23,17 @@ #include #include #include +#include #include "tscore/ink_config.h" #include "ts/ts.h" +#include #include "utilities.h" -constexpr auto QUEUE_DELAY_TIME = std::chrono::milliseconds{200}; // Examine the queue every 200ms +constexpr auto QUEUE_DELAY_TIME = std::chrono::milliseconds{300}; // Examine the queue every 300ms using QueueTime = std::chrono::time_point; -enum { - RATE_LIMITER_TYPE_SNI = 0, - RATE_LIMITER_TYPE_REMAP, - - RATE_LIMITER_TYPE_MAX -}; +enum { RATE_LIMITER_TYPE_SNI = 0, RATE_LIMITER_TYPE_REMAP, RATE_LIMITER_TYPE_MAX }; // order must align with the above static const char *types[] = { @@ -70,14 +67,83 @@ static const char *RATE_LIMITER_METRIC_PREFIX = "plugin.rate_limiter"; template class RateLimiter { using QueueItem = std::tuple; + using self_type = RateLimiter; public: - RateLimiter() : _queue_lock(TSMutexCreate()), _active_lock(TSMutexCreate()) {} + RateLimiter() = default; + RateLimiter(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + virtual ~RateLimiter() = default; + + virtual bool + parseYaml(const YAML::Node &node) + { + if (node["limit"]) { + _limit = node["limit"].as(); + } else { + // ToDo: Should we require the limit ? + } + + const YAML::Node &queue = node["queue"]; + + // If enabled, we default to UINT32_MAX, but the object default is still 0 (no queue) + if (queue) { + _max_queue = queue["size"] ? queue["size"].as() : UINT32_MAX; + + if (queue["max_age"]) { + _max_age = std::chrono::seconds(queue["max_age"].as()); + } + + const YAML::Node &metrics = node["metrics"]; - virtual ~RateLimiter() + if (metrics) { + std::string prefix = metrics["prefix"] ? metrics["prefix"].as() : RATE_LIMITER_METRIC_PREFIX; + std::string tag = metrics["tag"] ? metrics["tag"].as() : name(); + + Dbg(dbg_ctl, "Metrics for selector rule: %s(%s, %s)", name().c_str(), prefix.c_str(), tag.c_str()); + initializeMetrics(RATE_LIMITER_TYPE_SNI, prefix, tag); + } + } + + return true; + } + + void + initializeMetrics(uint type, std::string tag, std::string prefix = RATE_LIMITER_METRIC_PREFIX) { - TSMutexDestroy(_queue_lock); - TSMutexDestroy(_active_lock); + TSReleaseAssert(type < RATE_LIMITER_TYPE_MAX); + memset(_metrics, 0, sizeof(_metrics)); + + std::string metric_prefix = prefix; + metric_prefix.append("." + std::string(types[type])); + + if (!tag.empty()) { + metric_prefix.append("." + tag); + } else if (!name().empty()) { + metric_prefix.append("." + name()); + } + + for (int i = 0; i < RATE_LIMITER_METRIC_MAX; i++) { + size_t const metricsz = metric_prefix.length() + strlen(suffixes[i]) + 2; // padding for dot+terminator + char *const metric = static_cast(TSmalloc(metricsz)); + snprintf(metric, metricsz, "%s.%s", metric_prefix.data(), suffixes[i]); + + _metrics[i] = TS_ERROR; + + if (TSStatFindName(metric, &_metrics[i]) == TS_ERROR) { + _metrics[i] = TSStatCreate(metric, TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM); + } + + if (_metrics[i] != TS_ERROR) { + Dbg(dbg_ctl, "established metric '%s' as ID %d", metric, _metrics[i]); + } else { + TSError("failed to create metric '%s'", metric); + } + + TSfree(metric); + } } // Reserve / release a slot from the active resource limits. Reserve will return @@ -85,37 +151,38 @@ template class RateLimiter bool reserve() { - TSReleaseAssert(_active <= limit); - TSMutexLock(_active_lock); - if (_active < limit) { + std::lock_guard lock(_active_lock); + + TSReleaseAssert(_active <= limit()); + if (_active < limit()) { ++_active; - TSMutexUnlock(_active_lock); // Reduce the critical section, release early - Dbg(dbg_ctl, "Reserving a slot, active entities == %u", active()); + Dbg(dbg_ctl, "Reserving a slot, active entities == %u", _active.load()); return true; - } else { - TSMutexUnlock(_active_lock); - return false; } + + return false; } void - release() + free() { - TSMutexLock(_active_lock); - --_active; - TSMutexUnlock(_active_lock); - Dbg(dbg_ctl, "Releasing a slot, active entities == %u", active()); + { + std::lock_guard lock(_active_lock); + --_active; + } + + Dbg(dbg_ctl, "Releasing a slot, active entities == %u", _active.load()); } // Current size of the active_in connections - unsigned + uint32_t active() const { return _active.load(); } // Current size of the queue - unsigned + uint32_t size() const { return _size.load(); @@ -125,114 +192,100 @@ template class RateLimiter bool full() const { - return (_size == max_queue); + return (_size >= max_queue()); } void push(T elem, TSCont cont) { QueueTime now = std::chrono::system_clock::now(); + std::lock_guard lock(_queue_lock); - TSMutexLock(_queue_lock); _queue.push_front(std::make_tuple(elem, cont, now)); ++_size; - TSMutexUnlock(_queue_lock); } QueueItem pop() { QueueItem item; + std::lock_guard lock(_queue_lock); - TSMutexLock(_queue_lock); if (!_queue.empty()) { item = std::move(_queue.back()); _queue.pop_back(); --_size; } - TSMutexUnlock(_queue_lock); return item; } + void + incrementMetric(uint metric) + { + if (_metrics[metric] != TS_ERROR) { + TSStatIntIncrement(_metrics[metric], 1); + } + } + bool - hasOldEntity(QueueTime now) const + hasOldEntity(QueueTime now) { - TSMutexLock(_queue_lock); + std::lock_guard lock(_queue_lock); + if (!_queue.empty()) { QueueItem item = _queue.back(); - TSMutexUnlock(_queue_lock); // A little ugly but this reduces the critical section for the lock a little bit. std::chrono::milliseconds age = std::chrono::duration_cast(now - std::get<2>(item)); - return (age >= max_age); - } else { - TSMutexUnlock(_queue_lock); - return false; + return (age >= max_age()); } + + return false; } - void - initializeMetrics(uint type) + const std::string & + name() const { - TSReleaseAssert(type < RATE_LIMITER_TYPE_MAX); - memset(_metrics, 0, sizeof(_metrics)); - - std::string metric_prefix = prefix; - metric_prefix.append("." + std::string(types[type])); - - if (!tag.empty()) { - metric_prefix.append("." + tag); - } else if (!description.empty()) { - metric_prefix.append("." + description); - } - - for (int i = 0; i < RATE_LIMITER_METRIC_MAX; i++) { - size_t const metricsz = metric_prefix.length() + strlen(suffixes[i]) + 2; // padding for dot+terminator - char *const metric = static_cast(TSmalloc(metricsz)); - snprintf(metric, metricsz, "%s.%s", metric_prefix.data(), suffixes[i]); - - _metrics[i] = TS_ERROR; + return _name; + } - if (TSStatFindName(metric, &_metrics[i]) == TS_ERROR) { - _metrics[i] = TSStatCreate(metric, TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM); - } + uint32_t + limit() const + { + return _limit; + } - if (_metrics[i] != TS_ERROR) { - Dbg(dbg_ctl, "established metric '%s' as ID %d", metric, _metrics[i]); - } else { - TSError("failed to create metric '%s'", metric); - } + uint32_t + max_queue() const + { + return _max_queue; + } - TSfree(metric); - } + std::chrono::milliseconds + max_age() const + { + return _max_age; } void - incrementMetric(uint metric) + setName(const std::string &name) { - if (_metrics[metric] != TS_ERROR) { - TSStatIntIncrement(_metrics[metric], 1); - } + _name = name; } - // Initialize a new instance of this rate limiter - bool initialize(int argc, const char *argv[]); - - // These are the configurable portions of this limiter, public so sue me. - unsigned limit = 100; // Arbitrary default, probably should be a required config - unsigned max_queue = UINT_MAX; // No queue limit, but if sets will give an immediate error if at max - std::chrono::milliseconds max_age = std::chrono::milliseconds::zero(); // Max age (ms) in the queue - std::string description = ""; - std::string prefix = RATE_LIMITER_METRIC_PREFIX; // metric prefix, i.e.: plugin.rate_limiter - std::string tag = ""; // optional tag to append to the prefix (prefix.tag) +protected: + std::string _name = "_limiter_"; // The name/descr (e.g. SNI name) of this limiter + uint32_t _limit = UINT32_MAX; // No limit unless specified ... + uint32_t _max_queue = 0; // No queue by default + std::chrono::milliseconds _max_age = std::chrono::milliseconds::zero(); // Max age (ms) in the queue private: - std::atomic _active = 0; // Current active number of txns. This has to always stay <= limit above - std::atomic _size = 0; // Current size of the pending queue of txns. This should aim to be < _max_queue + std::atomic _active = 0; // Current active number of txns. This has to always stay <= limit above + std::atomic _size = 0; // Current size of the pending queue of txns. This should aim to be < _max_queue - TSMutex _queue_lock, _active_lock; // Resource locks - std::deque _queue; // Queue for the pending TXN's. ToDo: Should also move (see below) + std::mutex _queue_lock, _active_lock; // Resource locks + std::deque _queue; // Queue for the pending TXN's. ToDo: Should also move (see below) int _metrics[RATE_LIMITER_METRIC_MAX]; }; diff --git a/plugins/experimental/rate_limit/lists.cc b/plugins/experimental/rate_limit/lists.cc new file mode 100644 index 00000000000..83daa08b5f1 --- /dev/null +++ b/plugins/experimental/rate_limit/lists.cc @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "utilities.h" +#include "lists.h" + +bool +List::IP::parseYaml(const YAML::Node &node) +{ + const YAML::Node &cidr = node["cidr"]; + + if (cidr && cidr.IsSequence()) { + for (const auto &i : cidr) { + auto str = i.as(); + + Dbg(dbg_ctl, "Adding CIDR %s to List %s", str.c_str(), _name.c_str()); + add(str); + } + } else { + TSError("[%s] No 'cidr' list found in Lists rule %s", PLUGIN_NAME, name().c_str()); + return false; + } + + return true; +} diff --git a/plugins/experimental/rate_limit/lists.h b/plugins/experimental/rate_limit/lists.h new file mode 100644 index 00000000000..d9dc09fe357 --- /dev/null +++ b/plugins/experimental/rate_limit/lists.h @@ -0,0 +1,66 @@ +/* + * 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. + */ +#pragma once + +#include + +#include +#include + +#include "ts/ts.h" + +// ToDo: Maybe in the future this should be a template class, but for now it's a simple +// subclass wrapper over swoc::IPRangeSet. +namespace List +{ +class IP : public swoc::IPRangeSet +{ + using self_type = IP; + +public: + explicit IP(std::string &name) : _name(name) {} + + IP() = delete; + IP(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + void + add(std::string &str) + { + if (swoc::IPRange r; r.load(str)) { + mark(r); + } else { + TSReleaseAssert("Bad IP range"); + } + } + + bool parseYaml(const YAML::Node &node); + + const std::string & + name() const + { + return _name; + } + +private: + std::string _name; + +}; // class IpList + +} // namespace List diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index 3bee9558984..f7ba630eb28 100644 --- a/plugins/experimental/rate_limit/rate_limit.cc +++ b/plugins/experimental/rate_limit/rate_limit.cc @@ -33,7 +33,6 @@ // As a global plugin, things works a little different since we don't setup // per transaction or via remap.config. extern int gVCIdx; -SniSelector *gSNISelector = nullptr; void TSPluginInit(int argc, const char *argv[]) @@ -53,37 +52,12 @@ TSPluginInit(int argc, const char *argv[]) TSUserArgIndexReserve(TS_USER_ARGS_VCONN, PLUGIN_NAME, "VConn state information", &gVCIdx); } - if (argc > 1) { - if (!strncasecmp(argv[1], "SNI=", 4)) { - if (gSNISelector == nullptr) { - TSCont sni_cont = TSContCreate(sni_limit_cont, nullptr); - gSNISelector = new SniSelector(); - - TSReleaseAssert(sni_cont); - TSContDataSet(sni_cont, gSNISelector); - - TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont); - TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont); - } - - // Have to skip the first one, which is considered the 'program' name - --argc; - ++argv; - - size_t num_sni = gSNISelector->factory(argv[0] + 4, argc, argv); - Dbg(dbg_ctl, "Finished loading %zu SNIs", num_sni); - - gSNISelector->setupQueueCont(); // Start the queue processing continuation if needed - } else if (!strncasecmp(argv[1], "HOST=", 5)) { - // TODO: Do we need to implement this ?? Or can we just defer this to the remap version? - --argc; // Skip the "HOST" arg of course when parsing the real parameters - ++argv; - // TSCont host_cont = TSContCreate(globalHostCont, nullptr); - } else { - TSError("[%s] unknown global limiter type: %s", PLUGIN_NAME, argv[1]); - } + if (argc == 2) { + // Make sure we start the global SNI selector before we do anything else. + // This selector can be replaced later, during configuration reload. + SniSelector::startup(argv[1]); } else { - TSError("[%s] Usage: rate_limit.so SNI|HOST [option arguments]", PLUGIN_NAME); + TSError("[%s] Usage: rate_limit.so ", PLUGIN_NAME); } } @@ -107,10 +81,10 @@ TSRemapDeleteInstance(void *ih) TSReturnCode TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */) { - TxnRateLimiter *limiter = new TxnRateLimiter(); + auto *limiter = new TxnRateLimiter(); - // set the description based on the pristine remap URL prior to advancing the pointer below - limiter->description = getDescriptionFromUrl(argv[0]); + // set the name based on the pristine remap URL prior to advancing the pointer below + limiter->setName(getDescriptionFromUrl(argv[0])); // argv contains the "to" and "from" URLs. Skip the first so that the // second one poses as the program name. @@ -121,8 +95,8 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE limiter->initialize(argc, const_cast(argv)); *ih = static_cast(limiter); - Dbg(dbg_ctl, "Added active_in limiter rule (limit=%u, queue=%u, max-age=%ldms, error=%u)", limiter->limit, limiter->max_queue, - static_cast(limiter->max_age.count()), limiter->error); + Dbg(dbg_ctl, "Added active_in limiter rule (limit=%u, queue=%u, max-age=%ldms, error=%u)", limiter->limit(), limiter->max_queue(), + static_cast(limiter->max_age().count()), limiter->error()); return TS_SUCCESS; } @@ -133,13 +107,13 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE TSRemapStatus TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) { - TxnRateLimiter *limiter = static_cast(ih); + auto *limiter = static_cast(ih); if (limiter) { if (!limiter->reserve()) { - if (!limiter->max_queue || limiter->full()) { + if (!limiter->max_queue() || limiter->full()) { // We are running at limit, and the queue has reached max capacity, give back an error and be done. - TSHttpTxnStatusSet(txnp, static_cast(limiter->error)); + TSHttpTxnStatusSet(txnp, static_cast(limiter->error())); limiter->setupTxnCont(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK); Dbg(dbg_ctl, "Rejecting request, we're at capacity and queue is full"); } else { diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index 85c9f48d3f3..5e6aa04763b 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -27,6 +27,35 @@ // This holds the VC user arg index for the SNI limiters. int gVCIdx = -1; +bool +SniRateLimiter::parseYaml(const YAML::Node &node) +{ + super_type::parseYaml(node); + + if (node["ip-rep"]) { + auto ipr_name = node["ip-rep"].as(); + + if (!(_iprep = _selector->findIpRep(ipr_name))) { + TSError("[%s] IP Reputation name (%s) not found for SNI=%s", PLUGIN_NAME, ipr_name.c_str(), name().c_str()); + return false; + } + } + + // ToDo: It's unfortunate, but the selector holds the lists (and the ip-reps), so the lookup has to happen here ... :/. + if (node["exclude"]) { + auto excl_name = node["exclude"].as(); + + if (!(_exclude = _selector->findList(excl_name))) { + TSError("[%s] IP Reputation name (%s) not found for SNI=%s", PLUGIN_NAME, excl_name.c_str(), name().c_str()); + return false; + } + } + + Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %ld)", name().c_str(), limit(), max_queue(), static_cast(max_age().count())); + + return true; +} + /////////////////////////////////////////////////////////////////////////////// // These continuations are "helpers" to the SNI limiter object. Putting them // outside the class implementation is just cleaner. @@ -34,30 +63,39 @@ int gVCIdx = -1; int sni_limit_cont(TSCont contp, TSEvent event, void *edata) { - TSVConn vc = static_cast(edata); - SniSelector *selector = static_cast(TSContDataGet(contp)); - TSReleaseAssert(selector); + auto vc = static_cast(edata); switch (event) { case TS_EVENT_SSL_CLIENT_HELLO: { int len; const char *server_name = TSVConnSslSniGet(vc, &len); - std::string_view sni_name(server_name, len); - SniRateLimiter *limiter = selector->find(sni_name); + const std::string sni_name(server_name, len); + SniSelector *selector = SniSelector::instance(); + SniRateLimiter *limiter = selector->findLimiter(sni_name); if (limiter) { + const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); + + // See if this should be excluded from any rate limiting at all. + if (limiter->exclude() && limiter->exclude()->contains(swoc::IPAddr(sock))) { + Dbg(dbg_ctl, "Limiter on %s is excluded via List=%s", sni_name.c_str(), limiter->exclude()->name().c_str()); + TSUserArgSet(vc, gVCIdx, nullptr); + TSVConnReenableEx(vc, TS_EVENT_ERROR); + + return TS_ERROR; + } + // Check if we have an IP reputation for this SNI, and if we should block - if (limiter->iprep.initialized()) { - const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); - int pressure = limiter->pressure(); + if (limiter->iprep() && limiter->iprep()->initialized()) { + int32_t pressure = limiter->pressure(); - Dbg(dbg_ctl, "CLIENT_HELLO on %.*s, pressure=%d", static_cast(sni_name.length()), sni_name.data(), pressure); + Dbg(dbg_ctl, "CLIENT_HELLO on %s, pressure=%d", sni_name.c_str(), pressure); // Dbg(dbg_ctl, "IP Reputation: pressure is currently %d", pressure); if (pressure >= 0) { // When pressure is < 0, we're not yet at a level of pressure to be concerned about char client_ip[INET6_ADDRSTRLEN] = "[unknown]"; - auto [bucket, cur_cnt] = limiter->iprep.increment(sock); + auto [bucket, cur_cnt] = limiter->iprep()->increment(sock); // Get the client IP string if debug is enabled if (dbg_ctl.on()) { @@ -68,16 +106,17 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) } } - if (cur_cnt > limiter->iprep_permablock_count && - bucket <= limiter->iprep_permablock_threshold) { // Mark for long-term blocking + if (cur_cnt > limiter->iprep()->permablock_count() && + bucket <= limiter->iprep()->permablock_threshold()) { // Mark for long-term blocking Dbg(dbg_ctl, "Marking IP=%s for perma-blocking", client_ip); - bucket = limiter->iprep.block(sock); + bucket = limiter->iprep()->block(sock); } if (static_cast(pressure) > bucket) { // Remember the perma-block bucket is always 0, and we are >=0 already // Block this IP from finishing the handshake Dbg(dbg_ctl, "Rejecting connection from IP=%s, we're at pressure and IP was chosen to be blocked", client_ip); TSUserArgSet(vc, gVCIdx, nullptr); + selector->release(); TSVConnReenableEx(vc, TS_EVENT_ERROR); return TS_ERROR; @@ -89,11 +128,12 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) // If we passed the IP reputation filter, continue rate limiting these connections if (!limiter->reserve()) { - if (!limiter->max_queue || limiter->full()) { + if (!limiter->max_queue() || limiter->full()) { // We are running at limit, and the queue has reached max capacity, give back an error and be done. Dbg(dbg_ctl, "Rejecting connection, we're at capacity and queue is full"); TSUserArgSet(vc, gVCIdx, nullptr); limiter->incrementMetric(RATE_LIMITER_METRIC_REJECTED); + selector->release(); TSVConnReenableEx(vc, TS_EVENT_ERROR); return TS_ERROR; @@ -116,11 +156,12 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) } break; case TS_EVENT_VCONN_CLOSE: { - SniRateLimiter *limiter = static_cast(TSUserArgGet(vc, gVCIdx)); + auto *limiter = static_cast(TSUserArgGet(vc, gVCIdx)); if (limiter) { TSUserArgSet(vc, gVCIdx, nullptr); - limiter->release(); + limiter->free(); + limiter->selector()->release(); // Release the selector, such that it can be deleted later } TSVConnReenable(vc); break; @@ -134,100 +175,3 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) return TS_EVENT_CONTINUE; } - -/////////////////////////////////////////////////////////////////////////////// -// Parse the configurations for the TXN limiter. -// -bool -SniRateLimiter::initialize(int argc, const char *argv[]) -{ - static const struct option longopt[] = { - {const_cast("limit"), required_argument, nullptr, 'l' }, - {const_cast("queue"), required_argument, nullptr, 'q' }, - {const_cast("maxage"), required_argument, nullptr, 'm' }, - {const_cast("prefix"), required_argument, nullptr, 'p' }, - {const_cast("tag"), required_argument, nullptr, 't' }, - // These are all for the IP reputation system. ToDo: These should be global rather than per SNI ? - {const_cast("iprep_maxage"), required_argument, nullptr, 'a' }, - {const_cast("iprep_buckets"), required_argument, nullptr, 'B' }, - {const_cast("iprep_bucketsize"), required_argument, nullptr, 'S' }, - {const_cast("iprep_percentage"), required_argument, nullptr, 'C' }, - {const_cast("iprep_permablock_limit"), required_argument, nullptr, 'L' }, - {const_cast("iprep_permablock_pressure"), required_argument, nullptr, 'P' }, - {const_cast("iprep_permablock_maxage"), required_argument, nullptr, 'A' }, - // EOF - {nullptr, no_argument, nullptr, '\0'}, - }; - optind = 1; - - Dbg(dbg_ctl, "Initializing an SNI Rate Limiter"); - - while (true) { - int opt = getopt_long(argc, const_cast(argv), "", longopt, nullptr); - - switch (opt) { - case 'l': - this->limit = strtol(optarg, nullptr, 10); - break; - case 'q': - this->max_queue = strtol(optarg, nullptr, 10); - break; - case 'm': - this->max_age = std::chrono::milliseconds(strtol(optarg, nullptr, 10)); - break; - case 'p': - this->prefix = std::string(optarg); - break; - case 't': - this->tag = std::string(optarg); - break; - case 'a': - this->_iprep_max_age = std::chrono::seconds(strtol(optarg, nullptr, 10)); - break; - case 'B': - this->_iprep_num_buckets = strtol(optarg, nullptr, 10); - if (this->_iprep_num_buckets >= 100) { - TSError("sni_limiter: iprep_num_buckets must be in the range 1 .. 99, IP reputation disabled"); - this->_iprep_num_buckets = 0; - } - break; - case 'S': - this->_iprep_size = strtol(optarg, nullptr, 10); - break; - case 'C': - this->_iprep_percent = strtol(optarg, nullptr, 10); - break; - case 'L': - this->iprep_permablock_count = strtol(optarg, nullptr, 10); - break; - case 'P': - this->iprep_permablock_threshold = strtol(optarg, nullptr, 10); - break; - case 'A': - this->_iprep_perma_max_age = std::chrono::seconds(strtol(optarg, nullptr, 10)); - break; - } - if (opt == -1) { - break; - } - } - - // Enable and initialize the IP reputation if asked for - if (this->_iprep_num_buckets > 0 && this->_iprep_size > 0) { - Dbg(dbg_ctl, "Calling and _initialized is %d\n", this->iprep.initialized()); - this->iprep.initialize(this->_iprep_num_buckets, this->_iprep_size); - Dbg(dbg_ctl, "IP-reputation enabled with %u buckets, max size is 2^%u", this->_iprep_num_buckets, this->_iprep_size); - - Dbg(dbg_ctl, "Called and _initialized is %d\n", this->iprep.initialized()); - - // These settings are optional - if (this->_iprep_max_age != std::chrono::seconds::zero()) { - this->iprep.maxAge(this->_iprep_max_age); - } - if (this->_iprep_perma_max_age != std::chrono::seconds::zero()) { - this->iprep.permaMaxAge(this->_iprep_perma_max_age); - } - } - - return true; -} diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 9cdecb0f6be..42bcba5ae61 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -17,52 +17,72 @@ */ #pragma once +#include "ts/ts.h" +#include + #include "limiter.h" #include "ip_reputation.h" -#include "ts/ts.h" +#include "lists.h" int sni_limit_cont(TSCont contp, TSEvent event, void *edata); +class SniSelector; + /////////////////////////////////////////////////////////////////////////////// -// SNI based limiters, for global (pligin.config) instance(s). +// SNI based limiters, for global (plugin.config) instance(s). // class SniRateLimiter : public RateLimiter { -public: - SniRateLimiter() {} + using super_type = RateLimiter; + using self_type = SniRateLimiter; - SniRateLimiter(const SniRateLimiter &src) - { - limit = src.limit; - max_queue = src.max_queue; - max_age = src.max_age; - prefix = src.prefix; - tag = src.tag; - } +public: + SniRateLimiter() = delete; + SniRateLimiter(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; - bool initialize(int argc, const char *argv[]); + SniRateLimiter(std::string &sni, SniSelector *sel) : _selector(sel) { setName(sni); } - // ToDo: this ought to go into some better global IP reputation pool / settings. Waiting for YAML... - IpReputation::SieveLru iprep; - uint32_t iprep_permablock_count = 0; // "Hits" limit for blocking permanently - uint32_t iprep_permablock_threshold = 0; // Pressure threshold for permanent block + bool parseYaml(const YAML::Node &node) override; // Calculate the pressure, which is either a negative number (ignore), or a number 0-. // 0 == block only perma-blocks. int32_t pressure() const { - int32_t p = ((active() / static_cast(limit) * 100) - _iprep_percent) / (100 - _iprep_percent) * (_iprep_num_buckets + 1); + int32_t p = ((active() / static_cast(limit()) * 100) - _iprep->percentage()) / (100 - _iprep->percentage()) * + (_iprep->numBuckets() + 1); + + return (p >= static_cast(_iprep->numBuckets()) ? _iprep->numBuckets() : p); + } + + void + addIPReputation(IpReputation::SieveLru *iprep) + { + this->_iprep = iprep; + } + + IpReputation::SieveLru * + iprep() const + { + return _iprep; + } - return (p >= static_cast(_iprep_num_buckets) ? _iprep_num_buckets : p); + List::IP * + exclude() const + { + return _exclude; + } + + SniSelector * + selector() const + { + return _selector; } private: - // ToDo: These should be moved to global configurations to have one shared IP Reputation. - // today the configuration of this is so clunky, that there is no easy way to make it "global". - std::chrono::seconds _iprep_max_age = std::chrono::seconds::zero(); // Max age in the SieveLRUs for regular buckets - std::chrono::seconds _iprep_perma_max_age = std::chrono::seconds::zero(); // Max age in the SieveLRUs for perma-block buckets - uint32_t _iprep_num_buckets = 10; // Number of buckets. ToDo: leave this at 10 always - uint32_t _iprep_percent = 90; // At what percentage of limit we start blocking - uint32_t _iprep_size = 0; // Size of the biggest bucket; 15 == 2^15 == 32768 + SniSelector *_selector = nullptr; // The selector we belong to + IpReputation::SieveLru *_iprep = nullptr; // IP reputation for this SNI (if any) + List::IP *_exclude = nullptr; // The list of IPs to exclude (if any). ToDo: belongs in limiter.h :-/. }; diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index bf81a8d87ad..1ff1c4b134c 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -16,117 +16,233 @@ * limitations under the License. */ #include "tscore/ink_config.h" +#include -#include - -#include "sni_limiter.h" #include "sni_selector.h" +std::atomic SniSelector::_instance = nullptr; + /////////////////////////////////////////////////////////////////////////////// -// This is the queue management continuation, which gets called periodically +// YAML parser for the global YAML configuration (via plugin.config) // -static int -sni_queue_cont(TSCont cont, TSEvent event, void *edata) +bool +SniSelector::yamlParser(const std::string &yaml_file) { - SniSelector *selector = static_cast(TSContDataGet(cont)); + YAML::Node config; + + try { + config = YAML::LoadFile(yaml_file); + } catch (YAML::BadFile const &e) { + TSError("[%s] Cannot load configuration file: %s.", PLUGIN_NAME, e.what()); + return false; + } catch (std::exception const &e) { + TSError("[%s] Unknown error while loading configuration file: %s.", PLUGIN_NAME, e.what()); + return false; + } + + _yaml_file = yaml_file; + + // First build the Lists, if any + const YAML::Node &lists = config["lists"]; + + if (lists && lists.IsSequence()) { + for (const auto &i : lists) { + const YAML::Node &list = i; + + if (list.IsMap() && list["name"]) { + auto name = list["name"].as(); + + if (nullptr != findList(name)) { + TSError("[%s] Duplicate List names being added (%s)", PLUGIN_NAME, name.c_str()); + return false; + } + + auto ipl = new List::IP(name); + + if (ipl->parseYaml(list)) { + Dbg(dbg_ctl, "Loaded List rule: %s", name.c_str()); + addList(ipl); + } else { + TSError("[%s] Failed to parse the List YAML node", PLUGIN_NAME); + delete ipl; + return false; + } + } else { + TSError("[%s] List node is not a map or without a name", PLUGIN_NAME); + return false; + } + } + } + + // Next, build the IP reputation (if any) + const YAML::Node &ipreps = config["ip-rep"]; - for (const auto &[key, limiter] : selector->limiters()) { - QueueTime now = std::chrono::system_clock::now(); // Only do this once per limiter + if (ipreps && ipreps.IsSequence()) { + for (const auto &i : ipreps) { + const YAML::Node &ipr = i; - // Try to enable some queued VCs (if any) if there are slots available - while (limiter->size() > 0 && limiter->reserve()) { - auto [vc, contp, start_time] = limiter->pop(); - std::chrono::milliseconds delay = std::chrono::duration_cast(now - start_time); + if (ipr.IsMap() && ipr["name"]) { + auto name = ipr["name"].as(); - (void)contp; // Ugly, but silences some compilers. - Dbg(dbg_ctl, "SNI=%s: Enabling queued VC after %ldms", key.data(), static_cast(delay.count())); - TSVConnReenable(vc); - limiter->incrementMetric(RATE_LIMITER_METRIC_RESUMED); + if (nullptr != findIpRep(name)) { + TSError("[%s] Duplicate IP-Reputation names being added (%s)", PLUGIN_NAME, name.c_str()); + return false; + } + + auto iprep = new IpReputation::SieveLru(name); + + if (iprep->parseYaml(ipr)) { + Dbg(dbg_ctl, "Loaded IP Reputation rule: %s", name.c_str()); + addIPReputation(iprep); + } else { + TSError("[%s] Failed to parse the ip-rep YAML node", PLUGIN_NAME); + delete iprep; + return false; + } + } else { + TSError("[%s] ip-rep node is not a map or without a name", PLUGIN_NAME); + return false; + } } + } - // Kill any queued VCs if they are too old - if (limiter->size() > 0 && limiter->max_age > std::chrono::milliseconds::zero()) { - now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy + // Finally, parse all the SNI selectors (if any) + const YAML::Node &sel = config["selector"]; - while (limiter->size() > 0 && limiter->hasOldEntity(now)) { - // The oldest object on the queue is too old on the queue, so "kill" it. - auto [vc, contp, start_time] = limiter->pop(); - std::chrono::milliseconds age = std::chrono::duration_cast(now - start_time); + if (sel && sel.IsSequence()) { + for (const auto &i : sel) { + const YAML::Node &sni = i; - (void)contp; - Dbg(dbg_ctl, "Queued VC is too old (%ldms), erroring out", static_cast(age.count())); - TSVConnReenableEx(vc, TS_EVENT_ERROR); - limiter->incrementMetric(RATE_LIMITER_METRIC_EXPIRED); + if (sni.IsMap() && !sni["sni"].IsSequence()) { + auto name = sni["sni"].as(); + + if (nullptr != findLimiter(name)) { + TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, name.c_str()); + return false; + } + + auto limiter = new SniRateLimiter(name, this); + + if (limiter->parseYaml(sni)) { + if (name == "*" || name == "default") { + _default = limiter; + } else { + addLimiter(limiter); + } + + // Add aliases, if any + const YAML::Node &aliases = sni["aliases"]; + + if (aliases) { + if (aliases.IsSequence()) { + for (const auto &aliase : aliases) { + auto alias = aliase.as(); + + if (nullptr != findLimiter(alias)) { + TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, alias.c_str()); + return false; + } + Dbg(dbg_ctl, "Adding alias: %s -> %s", alias.c_str(), name.c_str()); + addAlias(alias, limiter); + } + } else { + TSError("[%s] aliases node is not a sequence", PLUGIN_NAME); + return false; + } + } + } else { + TSError("[%s] Failed to parse the selector YAML node", PLUGIN_NAME); + delete limiter; + return false; + } + } else { + TSError("[%s] selector node is not a map or without a name", PLUGIN_NAME); + return false; } } } - return TS_EVENT_NONE; + Dbg(dbg_ctl, "Succesfully loaded YAML file: %s", yaml_file.c_str()); + + return true; } /////////////////////////////////////////////////////////////////////////////// // This is the queue management continuation, which gets called periodically // -bool -SniSelector::insert(std::string_view sni, SniRateLimiter *limiter) +static int +sni_config_cont(TSCont cont, TSEvent event, void *edata) { - if (_limiters.find(sni) == _limiters.end()) { - _limiters[sni] = limiter; - Dbg(dbg_ctl, "Added global limiter for SNI=%s (limit=%u, queue=%u, max_age=%ldms)", sni.data(), limiter->limit, - limiter->max_queue, static_cast(limiter->max_age.count())); - - limiter->initializeMetrics(RATE_LIMITER_TYPE_SNI); + auto selector = SniSelector::instance(); // Also leases the instance + auto old_sel = static_cast(TSContDataGet(cont)); + auto new_sel = new SniSelector(); - return true; + // Delete the previous selector, which releases the lease we got at setup / reload + if (old_sel) { + old_sel->release(); + TSContDataSet(cont, nullptr); } - return false; -} - -SniRateLimiter * -SniSelector::find(std::string_view sni) -{ - if (sni.empty()) { // Likely shouldn't happen, but we can shortcircuit - return nullptr; + if (new_sel->yamlParser(selector->yamlFile())) { + new_sel->acquire(); + new_sel->setupQueueCont(); // Start the queue processing continuation if needed + SniSelector::swap(new_sel); + // Now, save the old selector in the cont data here, such that we do the final release next time + TSContDataSet(cont, selector); + Dbg(dbg_ctl, "Reloading YAML file: %s", new_sel->yamlFile().c_str()); + } else { + delete new_sel; + TSError("[%s] Failed to reload YAML file: %s", PLUGIN_NAME, selector->yamlFile().c_str()); } - auto limiter = _limiters.find(sni); + selector->release(); - if (limiter != _limiters.end()) { - return limiter->second; - } - return nullptr; + return TS_EVENT_NONE; } /////////////////////////////////////////////////////////////////////////////// -// This factory will create a number of SNI limiters based on the input string -// given. The list of SNI's is comma separated. ToDo: This should go away when -// we switch to a proper YAML parser, and we will only use the insert() above. +// This is the queue management continuation, which gets called periodically // -size_t -SniSelector::factory(const char *sni_list, int argc, const char *argv[]) +static int +sni_queue_cont(TSCont cont, TSEvent event, void *edata) { - char *saveptr; - char *sni = strdup(sni_list); // We make a copy of the sni list, to not touch the original string - char *token = strtok_r(sni, ",", &saveptr); + auto *selector = static_cast(TSContDataGet(cont)); - // Todo: We are repeating initializing here with the same configurations, but once we move this to - // YAML, and refactor this, it'll be better. And this is not particularly expensive. - while (nullptr != token) { - SniRateLimiter *limiter = new SniRateLimiter(); - TSReleaseAssert(limiter); + for (const auto &[key, entry] : selector->limiters()) { + auto [owner, limiter] = entry; + QueueTime now = std::chrono::system_clock::now(); // Only do this once per limiter + + if (owner) { // Don't operate on the aliases + // Try to enable some queued VCs (if any) if there are slots available + while (limiter->size() > 0 && limiter->reserve()) { + auto [vc, contp, start_time] = limiter->pop(); + std::chrono::milliseconds delay = std::chrono::duration_cast(now - start_time); + + (void)contp; // Ugly, but silences some compilers. + Dbg(dbg_ctl, "SNI=%s: Enabling queued VC after %ldms", key.data(), static_cast(delay.count())); + TSVConnReenable(vc); + limiter->incrementMetric(RATE_LIMITER_METRIC_RESUMED); + } - limiter->initialize(argc, argv); - limiter->description = token; + // Kill any queued VCs if they are too old + if (limiter->size() > 0 && limiter->max_age() > std::chrono::milliseconds::zero()) { + now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy - _needs_queue_cont = (limiter->max_queue > 0); + while (limiter->size() > 0 && limiter->hasOldEntity(now)) { + // The oldest object on the queue is too old on the queue, so "kill" it. + auto [vc, contp, start_time] = limiter->pop(); + std::chrono::milliseconds age = std::chrono::duration_cast(now - start_time); - insert(std::string_view(limiter->description), limiter); - token = strtok_r(nullptr, ",", &saveptr); + (void)contp; + Dbg(dbg_ctl, "Queued VC is too old (%ldms), erroring out", static_cast(age.count())); + TSVConnReenableEx(vc, TS_EVENT_ERROR); + limiter->incrementMetric(RATE_LIMITER_METRIC_EXPIRED); + } + } + } } - free(sni); - return _limiters.size(); + return TS_EVENT_NONE; } /////////////////////////////////////////////////////////////////////////////// @@ -139,6 +255,35 @@ SniSelector::setupQueueCont() _queue_cont = TSContCreate(sni_queue_cont, TSMutexCreate()); TSReleaseAssert(_queue_cont); TSContDataSet(_queue_cont, this); - _action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK); + _queue_action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK); + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Startup of the SNI selector hooks and config reload continuation and +// instance. This should only be called once, after which the configuration +// continuation takes over any reloads. +// +void +SniSelector::startup(const std::string &yaml_file) +{ + auto sni_cont = TSContCreate(sni_limit_cont, nullptr); + auto config_cont = TSContCreate(sni_config_cont, TSMutexCreate()); + + TSReleaseAssert(sni_cont); + TSReleaseAssert(config_cont); + + _instance.store(new SniSelector()); + TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont); + TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont); + + auto selector = SniSelector::instance(); // Assure that we don't delete this until next config reload + + if (selector->yamlParser(yaml_file)) { + selector->setupQueueCont(); // Start the queue processing continuation if needed + TSMgmtUpdateRegister(config_cont, PLUGIN_NAME, yaml_file.c_str()); + } else { + selector->release(); + TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, yaml_file.c_str()); } } diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 893173f1b64..b361b372f41 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -21,18 +21,88 @@ #include #include #include +#include #include "ts/ts.h" -#include "utilities.h" #include "sni_limiter.h" +#include "utilities.h" +#include "ip_reputation.h" +#include "lists.h" /////////////////////////////////////////////////////////////////////////////// -// SNI based limiter selector +// SNI based limiter selector, this will have one singleton instance. // class SniSelector { + using self_type = SniSelector; + public: - using Limiters = std::unordered_map; + using Limiters = std::unordered_map>; + using IPReputations = std::vector; + using Lists = std::vector; + + SniSelector() = default; + + SniSelector(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + virtual ~SniSelector() + { + if (_queue_action) { + TSActionCancel(_queue_action); + } + + if (_queue_cont) { + TSContDestroy(_queue_cont); + } + + for (auto &iprep : _reputations) { + delete iprep; + } + + for (auto &list : _lists) { + delete list; + } + + delete _default; + for (auto &limiter : _limiters) { + auto &[owner, ptr] = limiter.second; + + if (owner) { + delete ptr; + } + } + } + + static void + swap(self_type *other) + { + _instance.exchange(other); + } + + static SniSelector * + instance() + { + auto sel = _instance.load(); + ++sel->_leases; + return sel; + } + + SniSelector * + acquire() + { + ++_leases; + return this; + } + + void + release() + { + if (0 == --_leases) { + delete this; + } + } Limiters & limiters() @@ -40,16 +110,85 @@ class SniSelector return _limiters; } - size_t factory(const char *sni_list, int argc, const char *argv[]); + SniRateLimiter * + findLimiter(const std::string &sni) + { + auto iter = _limiters.find(sni); + + return ((iter != _limiters.end()) ? std::get<1>(iter->second) : _default); + } + + void + addLimiter(SniRateLimiter *limiter) + { + _needs_queue_cont |= (limiter->max_queue() > 0); + _limiters.emplace(limiter->name(), std::forward_as_tuple(true, limiter)); + } + + void + addAlias(std::string alias, SniRateLimiter *limiter) + { + _limiters.emplace(alias, std::forward_as_tuple(false, limiter)); + } + + const std::string & + yamlFile() const + { + return _yaml_file; + } + + void + addIPReputation(IpReputation::SieveLru *iprep) + { + _reputations.emplace_back(iprep); + } + + IpReputation::SieveLru * + findIpRep(const std::string &name) + { + auto it = std::find_if(_reputations.begin(), _reputations.end(), + [&name](const IpReputation::SieveLru *iprep) { return iprep->name() == name; }); + + if (it != _reputations.end()) { + return *it; + } + + return nullptr; + } + + void + addList(List::IP *list) + { + _lists.emplace_back(list); + } + + List::IP * + findList(const std::string &name) + { + auto it = std::find_if(_lists.begin(), _lists.end(), [&name](const List::IP *list) { return list->name() == name; }); + + if (it != _lists.end()) { + return *it; + } + + return nullptr; + } + void setupQueueCont(); + bool yamlParser(const std::string &yaml_file); - SniRateLimiter *find(std::string_view sni); - bool insert(std::string_view sni, SniRateLimiter *limiter); - bool erase(std::string_view sni); + static void startup(const std::string &yaml_file); private: + std::string _yaml_file; bool _needs_queue_cont = false; - TSCont _queue_cont = nullptr; // Continuation processing the queue periodically - TSAction _action = nullptr; // The action associated with the queue continuation, needed to shut it down - Limiters _limiters; + TSCont _queue_cont = nullptr; // Continuation processing the queue periodically + TSAction _queue_action = nullptr; // The action associated with the queue continuation, needed to shut it down + Limiters _limiters; // The SNI limiters + SniRateLimiter *_default = nullptr; // Default limiter, if any + IPReputations _reputations; // IP-Reputation rules + Lists _lists; // IP lists (for now, could be generalized later) + std::atomic _leases = 0; // Number of leases we have on the current selector, start with one + + static std::atomic _instance; // Holds the singleton instance, initialized in the .cc file }; diff --git a/plugins/experimental/rate_limit/txn_limiter.cc b/plugins/experimental/rate_limit/txn_limiter.cc index 06f0ae77bc7..507c402e0a6 100644 --- a/plugins/experimental/rate_limit/txn_limiter.cc +++ b/plugins/experimental/rate_limit/txn_limiter.cc @@ -28,11 +28,11 @@ static int txn_limit_cont(TSCont cont, TSEvent event, void *edata) { - TxnRateLimiter *limiter = static_cast(TSContDataGet(cont)); + auto *limiter = static_cast(TSContDataGet(cont)); switch (event) { case TS_EVENT_HTTP_TXN_CLOSE: - limiter->release(); + limiter->free(); TSContDestroy(cont); // We are done with this continuation now TSHttpTxnReenable(static_cast(edata), TS_EVENT_HTTP_CONTINUE); return TS_EVENT_CONTINUE; @@ -45,7 +45,7 @@ txn_limit_cont(TSCont cont, TSEvent event, void *edata) break; case TS_EVENT_HTTP_SEND_RESPONSE_HDR: // This is only applicable when we set an error in remap - retryAfter(static_cast(edata), limiter->retry); + retryAfter(static_cast(edata), limiter->retry()); TSContDestroy(cont); // We are done with this continuation now TSHttpTxnReenable(static_cast(edata), TS_EVENT_HTTP_CONTINUE); limiter->incrementMetric(RATE_LIMITER_METRIC_REJECTED); @@ -63,15 +63,15 @@ txn_limit_cont(TSCont cont, TSEvent event, void *edata) static int txn_queue_cont(TSCont cont, TSEvent event, void *edata) { - TxnRateLimiter *limiter = static_cast(TSContDataGet(cont)); - QueueTime now = std::chrono::system_clock::now(); // Only do this once per "loop" + auto *limiter = static_cast(TSContDataGet(cont)); + QueueTime now = std::chrono::system_clock::now(); // Only do this once per "loop" // Try to enable some queued txns (if any) if there are slots available while (limiter->size() > 0 && limiter->reserve()) { auto [txnp, contp, start_time] = limiter->pop(); std::chrono::milliseconds delay = std::chrono::duration_cast(now - start_time); - delayHeader(txnp, limiter->header, delay); + delayHeader(txnp, limiter->header(), delay); Dbg(dbg_ctl, "Enabling queued txn after %ldms", static_cast(delay.count())); // Since this was a delayed transaction, we need to add the TXN_CLOSE hook to free the slot when done TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, contp); @@ -80,7 +80,7 @@ txn_queue_cont(TSCont cont, TSEvent event, void *edata) } // Kill any queued txns if they are too old - if (limiter->size() > 0 && limiter->max_age > std::chrono::milliseconds::zero()) { + if (limiter->size() > 0 && limiter->max_age() > std::chrono::milliseconds::zero()) { now = std::chrono::system_clock::now(); // Update the "now", for some extra accuracy while (limiter->size() > 0 && limiter->hasOldEntity(now)) { @@ -88,9 +88,9 @@ txn_queue_cont(TSCont cont, TSEvent event, void *edata) auto [txnp, contp, start_time] = limiter->pop(); std::chrono::milliseconds age = std::chrono::duration_cast(now - start_time); - delayHeader(txnp, limiter->header, age); + delayHeader(txnp, limiter->header(), age); Dbg(dbg_ctl, "Queued TXN is too old (%ldms), erroring out", static_cast(age.count())); - TSHttpTxnStatusSet(txnp, static_cast(limiter->error)); + TSHttpTxnStatusSet(txnp, static_cast(limiter->error())); TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp); TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR); limiter->incrementMetric(RATE_LIMITER_METRIC_EXPIRED); @@ -118,35 +118,37 @@ TxnRateLimiter::initialize(int argc, const char *argv[]) // EOF {nullptr, no_argument, nullptr, '\0'}, }; - optind = 1; + optind = 1; + std::string prefix = RATE_LIMITER_METRIC_PREFIX; + std::string tag = ""; while (true) { int opt = getopt_long(argc, const_cast(argv), "", longopt, nullptr); switch (opt) { case 'l': - this->limit = strtol(optarg, nullptr, 10); + this->_limit = strtol(optarg, nullptr, 10); break; case 'q': - this->max_queue = strtol(optarg, nullptr, 10); + this->_max_queue = strtol(optarg, nullptr, 10); break; case 'e': - this->error = strtol(optarg, nullptr, 10); + this->_error = strtol(optarg, nullptr, 10); break; case 'r': - this->retry = strtol(optarg, nullptr, 10); + this->_retry = strtol(optarg, nullptr, 10); break; case 'm': - this->max_age = std::chrono::milliseconds(strtol(optarg, nullptr, 10)); + this->_max_age = std::chrono::milliseconds(strtol(optarg, nullptr, 10)); break; case 'h': - this->header = optarg; + this->_header = optarg; break; case 'p': - this->prefix = std::string(optarg); + prefix = optarg; break; case 't': - this->tag = std::string(optarg); + tag = optarg; break; } if (opt == -1) { @@ -154,14 +156,14 @@ TxnRateLimiter::initialize(int argc, const char *argv[]) } } - if (this->max_queue > 0) { + if (this->max_queue() > 0) { _queue_cont = TSContCreate(txn_queue_cont, TSMutexCreate()); TSReleaseAssert(_queue_cont); TSContDataSet(_queue_cont, this); _action = TSContScheduleEveryOnPool(_queue_cont, QUEUE_DELAY_TIME.count(), TS_THREAD_POOL_TASK); } - this->initializeMetrics(RATE_LIMITER_TYPE_REMAP); + this->initializeMetrics(RATE_LIMITER_TYPE_REMAP, tag, prefix); return true; } diff --git a/plugins/experimental/rate_limit/txn_limiter.h b/plugins/experimental/rate_limit/txn_limiter.h index 034db3cb4e0..6c7a6512263 100644 --- a/plugins/experimental/rate_limit/txn_limiter.h +++ b/plugins/experimental/rate_limit/txn_limiter.h @@ -39,11 +39,29 @@ class TxnRateLimiter : public RateLimiter void setupTxnCont(TSHttpTxn txnp, TSHttpHookID hook); bool initialize(int argc, const char *argv[]); - std::string header = ""; // Header to put the latency metrics in, e.g. @RateLimit-Delay - unsigned error = 429; // Error code when we decide not to allow a txn to be processed (e.g. queue full) - unsigned retry = 0; // If > 0, we will also send a Retry-After: header with this retry value + const std::string & + header() const + { + return _header; + } + + unsigned + error() const + { + return _error; + } + + unsigned + retry() const + { + return _retry; + } private: + std::string _header = ""; // Header to put the latency metrics in, e.g. @RateLimit-Delay + unsigned _error = 429; // Error code when we decide not to allow a txn to be processed (e.g. queue full) + unsigned _retry = 0; // If > 0, we will also send a Retry-After: header with this retry value + TSCont _queue_cont = nullptr; // Continuation processing the queue periodically TSAction _action = nullptr; // The action associated with the queue continuation, needed to shut it down }; diff --git a/plugins/experimental/rate_limit/utilities.cc b/plugins/experimental/rate_limit/utilities.cc index 20ee4ff9b5d..84ef691cf77 100644 --- a/plugins/experimental/rate_limit/utilities.cc +++ b/plugins/experimental/rate_limit/utilities.cc @@ -31,7 +31,7 @@ DbgCtl dbg_ctl{PLUGIN_NAME}; // for logging, and other types of metrics. // void -delayHeader(TSHttpTxn txnp, std::string &header, std::chrono::milliseconds delay) +delayHeader(TSHttpTxn txnp, const std::string &header, std::chrono::milliseconds delay) { if (header.size() > 0) { TSMLoc hdr_loc = nullptr; diff --git a/plugins/experimental/rate_limit/utilities.h b/plugins/experimental/rate_limit/utilities.h index f88b3458e67..5bef29c945d 100644 --- a/plugins/experimental/rate_limit/utilities.h +++ b/plugins/experimental/rate_limit/utilities.h @@ -18,13 +18,13 @@ #pragma once #include -#include #include -#include + +#include "ts/ts.h" constexpr char const PLUGIN_NAME[] = "rate_limit"; -void delayHeader(TSHttpTxn txnp, std::string &header, std::chrono::milliseconds delay); +void delayHeader(TSHttpTxn txnp, const std::string &header, std::chrono::milliseconds delay); void retryAfter(TSHttpTxn txnp, unsigned retry); std::string getDescriptionFromUrl(const char *url);