From 5b7326281e1ef94f4d0c539878fcc3661f63dff1 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Mon, 2 Oct 2023 13:59:35 -0600 Subject: [PATCH 01/10] First pass at refactoring, prior to adding YAML configs --- .../experimental/rate_limit/ip_reputation.cc | 15 +- .../experimental/rate_limit/ip_reputation.h | 88 +++++++++--- plugins/experimental/rate_limit/limiter.h | 131 +++++++++--------- plugins/experimental/rate_limit/rate_limit.cc | 41 ++---- .../experimental/rate_limit/sni_limiter.cc | 124 ++--------------- plugins/experimental/rate_limit/sni_limiter.h | 44 +++--- .../experimental/rate_limit/sni_selector.cc | 109 +++++++++------ .../experimental/rate_limit/sni_selector.h | 68 +++++++-- .../experimental/rate_limit/txn_limiter.cc | 12 +- 9 files changed, 315 insertions(+), 317 deletions(-) diff --git a/plugins/experimental/rate_limit/ip_reputation.cc b/plugins/experimental/rate_limit/ip_reputation.cc index 764093e7750..8175628b145 100644 --- a/plugins/experimental/rate_limit/ip_reputation.cc +++ b/plugins/experimental/rate_limit/ip_reputation.cc @@ -77,23 +77,20 @@ 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) +SieveLru::initialize(std::string_view name, uint32_t num_buckets, uint32_t size, uint32_t percentage, std::chrono::seconds max_age) { 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 - _initialized = true; + _name = name; _num_buckets = num_buckets; _size = size; + _percentage = percentage; + _max_age = max_age; uint32_t cur_size = pow(2, 1 + _size - num_buckets); @@ -108,6 +105,8 @@ SieveLru::initialize(uint32_t num_buckets, uint32_t size) _buckets[blockBucket()] = new SieveBucket(cur_size / 2); // Block LRU, same size as entry bucket _buckets[allowBucket()] = new SieveBucket(0); // Allow LRU, this is unlimited + + _initialized = true; TSMutexUnlock(_lock); } @@ -143,7 +142,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 diff --git a/plugins/experimental/rate_limit/ip_reputation.h b/plugins/experimental/rate_limit/ip_reputation.h index 78987397704..0bc5aa94b96 100644 --- a/plugins/experimental/rate_limit/ip_reputation.h +++ b/plugins/experimental/rate_limit/ip_reputation.h @@ -84,8 +84,14 @@ using HashMap = std::unordered_map; // The hash class SieveLru { public: - SieveLru() : _lock(TSMutexCreate()){}; // The uninitialized version - SieveLru(uint32_t num_buckets, uint32_t size); + SieveLru() = delete; + + SieveLru(std::string_view name, uint32_t num_buckets, uint32_t size, uint32_t percentage, std::chrono::seconds max_age) + : _lock(TSMutexCreate()) + { + initialize(name, num_buckets, size, percentage, max_age); + } + ~SieveLru() { for (uint32_t i = 0; i <= _num_buckets + 1; ++i) { // Remember to delete the two special allow/block buckets too @@ -93,7 +99,16 @@ class SieveLru } } - void initialize(uint32_t num_buckets = 10, uint32_t size = 15); + void initialize(std::string_view name, uint32_t num_buckets = 10, uint32_t size = 15, uint32_t percentage = 90, + std::chrono::seconds max_age = std::chrono::seconds::zero()); + + void + initializePerma(uint32_t limit, uint32_t threshold, std::chrono::seconds max_age = std::chrono::seconds::zero()) + { + _permablock_limit = limit; + _permablock_threshold = threshold; + _permablock_max_age = max_age; + } // 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 @@ -190,29 +205,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 +263,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..01d02682590 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -31,12 +31,7 @@ constexpr auto QUEUE_DELAY_TIME = std::chrono::milliseconds{200}; // Examine the queue every 200ms 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[] = { @@ -80,6 +75,55 @@ template class RateLimiter TSMutexDestroy(_active_lock); } + void + initialize(uint32_t limit) + { + this->limit = limit; + } + + void + initializeMetrics(uint type, std::string tag, std::string prefix = RATE_LIMITER_METRIC_PREFIX) + { + 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 = (char *)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); + } + } + + void + initializeQueue(uint32_t max_queue, std::chrono::seconds max_age = std::chrono::seconds::zero()) + { + this->max_queue = max_queue; + this->max_age = max_age; + } + // Reserve / release a slot from the active resource limits. Reserve will return // false if we are unable to reserve a slot. bool @@ -99,7 +143,7 @@ template class RateLimiter } void - release() + free() { TSMutexLock(_active_lock); --_active; @@ -108,14 +152,14 @@ template class RateLimiter } // 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(); @@ -155,6 +199,14 @@ template class RateLimiter return item; } + void + incrementMetric(uint metric) + { + if (_metrics[metric] != TS_ERROR) { + TSStatIntIncrement(_metrics[metric], 1); + } + } + bool hasOldEntity(QueueTime now) const { @@ -172,64 +224,15 @@ template class RateLimiter } } - void - initializeMetrics(uint type) - { - 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; - - 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); - } - } - - void - incrementMetric(uint metric) - { - if (_metrics[metric] != TS_ERROR) { - TSStatIntIncrement(_metrics[metric], 1); - } - } - - // 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 + // ToDo: Probably should be made private... + std::string name = ""; // The name/descr (e.g. SNI name) of this limiter + uint32_t limit = 100; // Arbitrary default, probably should be a required config + uint32_t max_queue = UINT32_MAX; // No queue limit, but if set 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) 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) diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index 3bee9558984..be4d0409c87 100644 --- a/plugins/experimental/rate_limit/rate_limit.cc +++ b/plugins/experimental/rate_limit/rate_limit.cc @@ -33,7 +33,7 @@ // 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; +extern SniSelector *gSNISelector; void TSPluginInit(int argc, const char *argv[]) @@ -53,37 +53,24 @@ 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(); + if (argc == 2) { + TSCont sni_cont = TSContCreate(sni_limit_cont, nullptr); + gSNISelector = new SniSelector(); - TSReleaseAssert(sni_cont); - TSContDataSet(sni_cont, gSNISelector); + TSReleaseAssert(sni_cont); - 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); + TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont); + TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont); + if (gSNISelector->yamlParser(argv[1])) { + gSNISelector->acquire(); // Assure that we don't delete this until config reload 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]); + delete gSNISelector; + TSError("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[0]); } } else { - TSError("[%s] Usage: rate_limit.so SNI|HOST [option arguments]", PLUGIN_NAME); + TSError("[%s] Usage: rate_limit.so ", PLUGIN_NAME); } } @@ -109,8 +96,8 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE { TxnRateLimiter *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->name = getDescriptionFromUrl(argv[0]); // argv contains the "to" and "from" URLs. Skip the first so that the // second one poses as the program name. diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index 85c9f48d3f3..73bc58e93c3 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -25,7 +25,8 @@ #include "sni_limiter.h" // This holds the VC user arg index for the SNI limiters. -int gVCIdx = -1; +int gVCIdx = -1; +SniSelector *gSNISelector = nullptr; /////////////////////////////////////////////////////////////////////////////// // These continuations are "helpers" to the SNI limiter object. Putting them @@ -34,22 +35,21 @@ 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); + TSVConn 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); + SniSelector *selector = gSNISelector->acquire(); + SniRateLimiter *limiter = selector->findSNI(sni_name); if (limiter) { // Check if we have an IP reputation for this SNI, and if we should block - if (limiter->iprep.initialized()) { + if (limiter->iprep && limiter->iprep->initialized()) { const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); - int pressure = limiter->pressure(); + int32_t pressure = limiter->pressure(); Dbg(dbg_ctl, "CLIENT_HELLO on %.*s, pressure=%d", static_cast(sni_name.length()), sni_name.data(), pressure); @@ -57,7 +57,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) 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 +68,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; @@ -94,6 +95,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) 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; @@ -120,7 +122,8 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) 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 +137,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..4be43dd0d19 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -23,46 +23,34 @@ 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() {} - - SniRateLimiter(const SniRateLimiter &src) - { - limit = src.limit; - max_queue = src.max_queue; - max_age = src.max_age; - prefix = src.prefix; - tag = src.tag; - } - - bool initialize(int argc, const char *argv[]); - - // 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 + SniRateLimiter() = delete; + SniRateLimiter(std::string_view sni, SniSelector *sel) : selector(sel) { name = sni; } // 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); + } - return (p >= static_cast(_iprep_num_buckets) ? _iprep_num_buckets : p); + void + addIPReputation(IpReputation::SieveLru *iprep) + { + this->iprep = iprep; } -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) }; diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index bf81a8d87ad..d2b9cf71c31 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -22,6 +22,62 @@ #include "sni_limiter.h" #include "sni_selector.h" +/////////////////////////////////////////////////////////////////////////////// +// YAML parser for the global YAML configuration (via plugin.config) +// +bool +SniSelector::yamlParser(std::string yaml_file) +{ + // Parsed the IP Reputations, so add those. + // ToDo: Obviously need the YAML values here ... + std::string iprep_name = "main"; + uint32_t num_buckets = 10; + uint32_t size = 15; + uint32_t percentage = 90; + std::chrono::seconds max_age(60); + + if (nullptr != findIpRep(iprep_name)) { + TSError("[%s] Duplicate IP-Reputation names being added (%.*s)", PLUGIN_NAME, static_cast(iprep_name.size()), + iprep_name.data()); + return false; + } + + auto iprep = new IpReputation::SieveLru(iprep_name, num_buckets, size, percentage, max_age); + + std::chrono::seconds perma_max_age(1800); + + iprep->initializePerma(100, 1, perma_max_age); + addIPReputation(iprep); + + // ToDo: Add the IP lists + + // ToDo: Iterate over all SNIs. + std::string sni = "hel.ogre.com"; + + if (nullptr != findSNI(sni)) { + TSError("[%s] Duplicate SNIs being added (%.*s)", PLUGIN_NAME, static_cast(sni.size()), sni.data()); + return false; + } + + auto limiter = new SniRateLimiter(sni, this); + auto iprep2 = findIpRep("main"); + + TSReleaseAssert(limiter); + + std::chrono::seconds queue_max_age(60); + + limiter->initialize(5); + limiter->initializeQueue(0, queue_max_age); + limiter->initializeMetrics(RATE_LIMITER_TYPE_SNI, sni); + if (iprep2) { + limiter->addIPReputation(iprep2); + } + + addLimiter(limiter); + + return true; +} + /////////////////////////////////////////////////////////////////////////////// // This is the queue management continuation, which gets called periodically // @@ -64,27 +120,8 @@ sni_queue_cont(TSCont cont, TSEvent event, void *edata) return TS_EVENT_NONE; } -/////////////////////////////////////////////////////////////////////////////// -// This is the queue management continuation, which gets called periodically -// -bool -SniSelector::insert(std::string_view sni, SniRateLimiter *limiter) -{ - 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); - - return true; - } - - return false; -} - SniRateLimiter * -SniSelector::find(std::string_view sni) +SniSelector::findSNI(std::string_view sni) { if (sni.empty()) { // Likely shouldn't happen, but we can shortcircuit return nullptr; @@ -98,35 +135,17 @@ SniSelector::find(std::string_view sni) return nullptr; } -/////////////////////////////////////////////////////////////////////////////// -// 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. -// -size_t -SniSelector::factory(const char *sni_list, int argc, const char *argv[]) +IpReputation::SieveLru * +SniSelector::findIpRep(std::string_view name) { - 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 it = std::find_if(_reputations.begin(), _reputations.end(), + [&name](const IpReputation::SieveLru *iprep) { return iprep->name() == name; }); - // 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); - - limiter->initialize(argc, argv); - limiter->description = token; - - _needs_queue_cont = (limiter->max_queue > 0); - - insert(std::string_view(limiter->description), limiter); - token = strtok_r(nullptr, ",", &saveptr); + if (it != _reputations.end()) { + return *it; } - free(sni); - return _limiters.size(); + return nullptr; } /////////////////////////////////////////////////////////////////////////////// diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 893173f1b64..23a8539fbb6 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -25,14 +25,50 @@ #include "ts/ts.h" #include "utilities.h" #include "sni_limiter.h" +#include "ip_reputation.h" + +// This is the list of things (CIDR's) to exclude from blocking etc. +// ToDo: This has to be finished +class ListType +{ + const std::string_view + name() const + { + return _name; + } + + std::string _name; // Name of the list +}; /////////////////////////////////////////////////////////////////////////////// -// SNI based limiter selector +// SNI based limiter selector, this will have one single instance globally. // class SniSelector { public: - using Limiters = std::unordered_map; + using Limiters = std::unordered_map; + using Lists = std::vector; + using IPReputations = std::vector; + + SniSelector() = default; + virtual ~SniSelector() = default; // ToDo: This has to clean stuff up + + bool yamlParser(const std::string yaml_file); + + SniSelector * + acquire() + { + ++_leases; + return this; + } + + void + release() + { + if (0 == --_leases) { + delete this; + } + } Limiters & limiters() @@ -40,16 +76,30 @@ class SniSelector return _limiters; } - size_t factory(const char *sni_list, int argc, const char *argv[]); + void + addIPReputation(IpReputation::SieveLru *iprep) + { + _reputations.emplace_back(iprep); + } + + void + addLimiter(SniRateLimiter *limiter) + { + _needs_queue_cont = (limiter->max_queue > 0); + _limiters.emplace(limiter->name, limiter); + } void setupQueueCont(); - SniRateLimiter *find(std::string_view sni); - bool insert(std::string_view sni, SniRateLimiter *limiter); - bool erase(std::string_view sni); + IpReputation::SieveLru *findIpRep(std::string_view name); + SniRateLimiter *findSNI(std::string_view sni); 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 _action = nullptr; // The action associated with the queue continuation, needed to shut it down + Limiters _limiters; // The SNI limiters + IPReputations _reputations; // IP-Reputation rules + Lists _lists; // Exclude lists (things not to block) + std::atomic _leases = 0; // Number of leases we have on the current selector }; diff --git a/plugins/experimental/rate_limit/txn_limiter.cc b/plugins/experimental/rate_limit/txn_limiter.cc index 06f0ae77bc7..0a145621810 100644 --- a/plugins/experimental/rate_limit/txn_limiter.cc +++ b/plugins/experimental/rate_limit/txn_limiter.cc @@ -32,7 +32,7 @@ txn_limit_cont(TSCont cont, TSEvent event, void *edata) 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; @@ -118,7 +118,9 @@ 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); @@ -143,10 +145,10 @@ TxnRateLimiter::initialize(int argc, const char *argv[]) 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) { @@ -161,7 +163,7 @@ TxnRateLimiter::initialize(int argc, const char *argv[]) _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; } From 15f018cc4dc28ea89ea45fed166b1ba81d309295 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Tue, 3 Oct 2023 19:46:25 -0600 Subject: [PATCH 02/10] Adding the YAML parser for SNI --- doc/admin-guide/plugins/rate_limit.en.rst | 109 ++++++++++++------ .../experimental/rate_limit/ip_reputation.cc | 63 ++++++---- .../experimental/rate_limit/ip_reputation.h | 19 +-- .../experimental/rate_limit/sni_limiter.cc | 45 ++++++++ plugins/experimental/rate_limit/sni_limiter.h | 2 + .../experimental/rate_limit/sni_selector.cc | 94 ++++++++++----- 6 files changed, 226 insertions(+), 106 deletions(-) diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index 0498be80d22..d8e0916db47 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,75 @@ 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 varies sections +and nodes are documented below:: + selector: + - sni: test1.example.com + limit: 1000 + queue: + size: 1000 + max-age: 30 + metrics: + tag: example.com + prefix: ddos + ip-rep: main + - sni: test2.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 + +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:: ip-rep - 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. + 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. - 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:: queue - The default queue size is ``UINT_MAX``, which is essentially unlimited. + 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:: --maxage + 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 ``max-age`` for how long a transaction can sit in the delay queue. - The value (default 0) is the age in milliseconds. + No queue is enable without this configuration directive, but it can also be + disable explicitly if the size is set to ``0``. -.. option:: --prefix +.. option:: metrics - An optional metric prefix to use instead of the default (plugin.rate_limiter). + 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. -.. option:: --tag + 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``. - 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. +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:: --iprep_buckets +.. 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 +197,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 +206,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/ip_reputation.cc b/plugins/experimental/rate_limit/ip_reputation.cc index 8175628b145..b4c102e02af 100644 --- a/plugins/experimental/rate_limit/ip_reputation.cc +++ b/plugins/experimental/rate_limit/ip_reputation.cc @@ -78,36 +78,52 @@ SieveLru::hasher(const std::string &ip, u_short family) // Mostly a convenience return 0; // Probably can't happen, but have to return something } -// Initialize the Sieve LRU object -void -SieveLru::initialize(std::string_view name, uint32_t num_buckets, uint32_t size, uint32_t percentage, std::chrono::seconds max_age) +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(); + } - _name = name; - _num_buckets = num_buckets; - _size = size; - _percentage = percentage; - _max_age = max_age; + if (node["percentage"]) { + _percentage = node["percentage"].as(); + } - uint32_t cur_size = pow(2, 1 + _size - num_buckets); + if (node["max_age"]) { + _max_age = std::chrono::seconds(node["max_age"].as()); + } - _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 + if (node["perma-block"]) { + const YAML::Node &perma = node["perma-block"]; - // 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; + 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; + } } - _buckets[blockBucket()] = new SieveBucket(cur_size / 2); // Block LRU, same size as entry bucket - _buckets[allowBucket()] = new SieveBucket(0); // Allow LRU, this is unlimited + Dbg(dbg_ctl, "Loaded IP-Reputation rule: %s(%u, %u, %u, %lld)", _name.c_str(), _num_buckets, _size, _percentage, + _max_age.count()); + Dbg(dbg_ctl, "\twith perma-block rule: %s(%u, %u, %lld)", _name.c_str(), _permablock_limit, _permablock_threshold, + _permablock_max_age.count()); - _initialized = true; - TSMutexUnlock(_lock); + return true; } // Increment the count for an element (will be created / added if new). @@ -131,7 +147,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(); diff --git a/plugins/experimental/rate_limit/ip_reputation.h b/plugins/experimental/rate_limit/ip_reputation.h index 0bc5aa94b96..c318dd704ee 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 { @@ -86,11 +88,7 @@ class SieveLru public: SieveLru() = delete; - SieveLru(std::string_view name, uint32_t num_buckets, uint32_t size, uint32_t percentage, std::chrono::seconds max_age) - : _lock(TSMutexCreate()) - { - initialize(name, num_buckets, size, percentage, max_age); - } + SieveLru(std::string_view name) : _lock(TSMutexCreate()) { _name = name; } ~SieveLru() { @@ -99,16 +97,7 @@ class SieveLru } } - void initialize(std::string_view name, uint32_t num_buckets = 10, uint32_t size = 15, uint32_t percentage = 90, - std::chrono::seconds max_age = std::chrono::seconds::zero()); - - void - initializePerma(uint32_t limit, uint32_t threshold, std::chrono::seconds max_age = std::chrono::seconds::zero()) - { - _permablock_limit = limit; - _permablock_threshold = threshold; - _permablock_max_age = max_age; - } + 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 diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index 73bc58e93c3..bd297db1134 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -28,6 +28,51 @@ int gVCIdx = -1; SniSelector *gSNISelector = nullptr; +bool +SniRateLimiter::parseYaml(const YAML::Node &node) +{ + if (node["limit"]) { + limit = node["limit"].as(); + } else { + // ToDo: Should we require the limit ? + } + + if (node["ip-rep"]) { + std::string 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; + } + } + + const YAML::Node &queue = node["queue"]; + + if (queue) { + if (queue["size"]) { + max_queue = queue["size"].as(); + } + + if (queue["max_age"]) { + max_age = std::chrono::seconds(queue["max_age"].as()); + } + } + + const YAML::Node &metrics = node["metrics"]; + + 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); + } + + Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %lld)", name.c_str(), limit, max_queue, max_age.count()); + + return true; +} + /////////////////////////////////////////////////////////////////////////////// // These continuations are "helpers" to the SNI limiter object. Putting them // outside the class implementation is just cleaner. diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 4be43dd0d19..18b814710a5 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -34,6 +34,8 @@ class SniRateLimiter : public RateLimiter SniRateLimiter() = delete; SniRateLimiter(std::string_view sni, SniSelector *sel) : selector(sel) { name = sni; } + bool parseYaml(const YAML::Node &node); + // Calculate the pressure, which is either a negative number (ignore), or a number 0-. // 0 == block only perma-blocks. int32_t diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index d2b9cf71c31..9ba6e69c087 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -16,6 +16,7 @@ * limitations under the License. */ #include "tscore/ink_config.h" +#include #include @@ -28,52 +29,83 @@ bool SniSelector::yamlParser(std::string yaml_file) { - // Parsed the IP Reputations, so add those. - // ToDo: Obviously need the YAML values here ... - std::string iprep_name = "main"; - uint32_t num_buckets = 10; - uint32_t size = 15; - uint32_t percentage = 90; - std::chrono::seconds max_age(60); - - if (nullptr != findIpRep(iprep_name)) { - TSError("[%s] Duplicate IP-Reputation names being added (%.*s)", PLUGIN_NAME, static_cast(iprep_name.size()), - iprep_name.data()); + YAML::Node config; + + try { + config = YAML::LoadFile(yaml_file); + } catch (YAML::BadFile &e) { + TSError("[%s] Cannot load configuration file: %s.", PLUGIN_NAME, e.what()); + return false; + } catch (std::exception &e) { + TSError("[%s] Unknown error while loading configuration file: %s.", PLUGIN_NAME, e.what()); return false; } - auto iprep = new IpReputation::SieveLru(iprep_name, num_buckets, size, percentage, max_age); + const YAML::Node &ipreps = config["ip-rep"]; - std::chrono::seconds perma_max_age(1800); + if (ipreps && ipreps.IsSequence()) { + for (size_t i = 0; i < ipreps.size(); ++i) { + const YAML::Node &ipr = ipreps[i]; - iprep->initializePerma(100, 1, perma_max_age); - addIPReputation(iprep); + if (ipr.IsMap() && ipr["name"]) { + std::string name = ipr["name"].as(); - // ToDo: Add the IP lists + if (nullptr != findIpRep(name)) { + TSError("[%s] Duplicate IP-Reputation names being added (%s)", PLUGIN_NAME, name.c_str()); + return false; + } - // ToDo: Iterate over all SNIs. - std::string sni = "hel.ogre.com"; + auto iprep = new IpReputation::SieveLru(name); - if (nullptr != findSNI(sni)) { - TSError("[%s] Duplicate SNIs being added (%.*s)", PLUGIN_NAME, static_cast(sni.size()), sni.data()); - return false; + if (iprep->parseYaml(ipr)) { + 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; + } + } } - auto limiter = new SniRateLimiter(sni, this); - auto iprep2 = findIpRep("main"); + // ToDo: Add the IP list YAML parsing - TSReleaseAssert(limiter); + // Parse all the SNI selectors + const YAML::Node &sel = config["selector"]; - std::chrono::seconds queue_max_age(60); + if (sel && sel.IsSequence()) { + for (size_t i = 0; i < sel.size(); ++i) { + const YAML::Node &sni = sel[i]; - limiter->initialize(5); - limiter->initializeQueue(0, queue_max_age); - limiter->initializeMetrics(RATE_LIMITER_TYPE_SNI, sni); - if (iprep2) { - limiter->addIPReputation(iprep2); + if (sni.IsMap() && sni["sni"]) { + // ToDo: Allow a sequence of names here + std::string name = sni["sni"].as(); + + if (nullptr != findSNI(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)) { + addLimiter(limiter); + } 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; + } + } } - addLimiter(limiter); + Dbg(dbg_ctl, "Succesfully loaded YAML file: %s", yaml_file.c_str()); return true; } From 3125cea9cdcb68d5186938e96ef47ceb41cf82fd Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Wed, 4 Oct 2023 13:44:19 -0600 Subject: [PATCH 03/10] Cleanup and adding config reloads --- doc/admin-guide/plugins/rate_limit.en.rst | 65 ++++---- .../experimental/rate_limit/ip_reputation.cc | 12 ++ .../experimental/rate_limit/ip_reputation.h | 39 ++--- plugins/experimental/rate_limit/limiter.h | 2 +- plugins/experimental/rate_limit/rate_limit.cc | 20 +-- .../experimental/rate_limit/sni_limiter.cc | 9 +- plugins/experimental/rate_limit/sni_limiter.h | 10 +- .../experimental/rate_limit/sni_selector.cc | 150 +++++++++++++----- .../experimental/rate_limit/sni_selector.h | 108 ++++++++++--- plugins/experimental/rate_limit/utilities.h | 1 - 10 files changed, 276 insertions(+), 140 deletions(-) diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index d8e0916db47..3997a571979 100644 --- a/doc/admin-guide/plugins/rate_limit.en.rst +++ b/doc/admin-guide/plugins/rate_limit.en.rst @@ -123,30 +123,33 @@ configuration file. The basic use is as:: The YAML configuration can have the following format, where the varies sections -and nodes are documented below:: - selector: - - sni: test1.example.com - limit: 1000 - queue: - size: 1000 - max-age: 30 - metrics: - tag: example.com - prefix: ddos - ip-rep: main - - sni: test2.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 +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 + - 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 For the top level `selector` node, the following options are available: @@ -158,6 +161,10 @@ For the top level `selector` node, the following options are available: The maximum number of active client transactions. +.. option:: aliases + + A list of aliases for the SNI, which will also be matched by this rate limiter. + .. option:: ip-rep The name of the IP reputation node to use for this rate limiter. If not @@ -179,13 +186,13 @@ For the top level `selector` node, the following options are available: .. 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. + 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 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 ip-rep node is used to configure the IP reputation system, there can be +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: diff --git a/plugins/experimental/rate_limit/ip_reputation.cc b/plugins/experimental/rate_limit/ip_reputation.cc index b4c102e02af..c0551027877 100644 --- a/plugins/experimental/rate_limit/ip_reputation.cc +++ b/plugins/experimental/rate_limit/ip_reputation.cc @@ -118,6 +118,18 @@ SieveLru::parseYaml(const YAML::Node &node) } } + uint32_t cur_size = pow(2, 1 + _size - _num_buckets); + + _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 + Dbg(dbg_ctl, "Loaded IP-Reputation rule: %s(%u, %u, %u, %lld)", _name.c_str(), _num_buckets, _size, _percentage, _max_age.count()); Dbg(dbg_ctl, "\twith perma-block rule: %s(%u, %u, %lld)", _name.c_str(), _permablock_limit, _permablock_threshold, diff --git a/plugins/experimental/rate_limit/ip_reputation.h b/plugins/experimental/rate_limit/ip_reputation.h index c318dd704ee..48b0e5e87d6 100644 --- a/plugins/experimental/rate_limit/ip_reputation.h +++ b/plugins/experimental/rate_limit/ip_reputation.h @@ -47,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 { @@ -85,15 +92,20 @@ 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() = delete; + SieveLru(std::string &name) : _lock(TSMutexCreate()) { _name = name; } - SieveLru(std::string_view 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; } } @@ -117,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; @@ -153,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 { @@ -172,12 +171,6 @@ class SieveLru return 0; } - uint32_t - allowBucket() const - { - return _num_buckets + 1; - } - size_t bucketSize(uint32_t bucket) const { diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index 01d02682590..e4b66bef8f3 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -28,7 +28,7 @@ #include "ts/ts.h" #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 }; diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index be4d0409c87..540c99a224a 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; -extern SniSelector *gSNISelector; void TSPluginInit(int argc, const char *argv[]) @@ -54,20 +53,17 @@ TSPluginInit(int argc, const char *argv[]) } if (argc == 2) { - TSCont sni_cont = TSContCreate(sni_limit_cont, nullptr); - gSNISelector = new SniSelector(); + // Make sure we start the global SNI selector before we do anything else. + // This selector can be replaced later, during configuration reload. + SniSelector::startup(); - TSReleaseAssert(sni_cont); + auto selector = SniSelector::instance(); // Assure that we don't delete this until config reload - TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont); - TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont); - - if (gSNISelector->yamlParser(argv[1])) { - gSNISelector->acquire(); // Assure that we don't delete this until config reload - gSNISelector->setupQueueCont(); // Start the queue processing continuation if needed + if (selector->yamlParser(argv[1])) { + selector->setupQueueCont(); // Start the queue processing continuation if needed } else { - delete gSNISelector; - TSError("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[0]); + selector->release(); + TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[0]); } } else { TSError("[%s] Usage: rate_limit.so ", PLUGIN_NAME); diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index bd297db1134..427d34a4b2c 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -25,8 +25,7 @@ #include "sni_limiter.h" // This holds the VC user arg index for the SNI limiters. -int gVCIdx = -1; -SniSelector *gSNISelector = nullptr; +int gVCIdx = -1; bool SniRateLimiter::parseYaml(const YAML::Node &node) @@ -86,8 +85,8 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) case TS_EVENT_SSL_CLIENT_HELLO: { int len; const char *server_name = TSVConnSslSniGet(vc, &len); - std::string_view sni_name(server_name, len); - SniSelector *selector = gSNISelector->acquire(); + const std::string sni_name(server_name, len); + SniSelector *selector = SniSelector::instance(); SniRateLimiter *limiter = selector->findSNI(sni_name); if (limiter) { @@ -96,7 +95,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); 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); diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 18b814710a5..208ae44692b 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -30,9 +30,15 @@ class SniSelector; // class SniRateLimiter : public RateLimiter { + using self_type = SniRateLimiter; + public: - SniRateLimiter() = delete; - SniRateLimiter(std::string_view sni, SniSelector *sel) : selector(sel) { name = sni; } + SniRateLimiter() = delete; + SniRateLimiter(self_type &&) = delete; + self_type &operator=(const self_type &) = delete; + self_type &operator=(self_type &&) = delete; + + SniRateLimiter(std::string &sni, SniSelector *sel) : selector(sel) { name = sni; } bool parseYaml(const YAML::Node &node); diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index 9ba6e69c087..ec4ea7c48f2 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -18,11 +18,11 @@ #include "tscore/ink_config.h" #include -#include - #include "sni_limiter.h" #include "sni_selector.h" +std::atomic SniSelector::_instance = nullptr; + /////////////////////////////////////////////////////////////////////////////// // YAML parser for the global YAML configuration (via plugin.config) // @@ -41,6 +41,9 @@ SniSelector::yamlParser(std::string yaml_file) return false; } + _yaml_file = yaml_file; + _yaml_last_write = std::filesystem::last_write_time(_yaml_file); + const YAML::Node &ipreps = config["ip-rep"]; if (ipreps && ipreps.IsSequence()) { @@ -80,7 +83,7 @@ SniSelector::yamlParser(std::string yaml_file) for (size_t i = 0; i < sel.size(); ++i) { const YAML::Node &sni = sel[i]; - if (sni.IsMap() && sni["sni"]) { + if (sni.IsMap()) { // ToDo: Allow a sequence of names here std::string name = sni["sni"].as(); @@ -93,6 +96,22 @@ SniSelector::yamlParser(std::string yaml_file) if (limiter->parseYaml(sni)) { addLimiter(limiter); + + // Add aliases, if any + const YAML::Node &aliases = sni["aliases"]; + + if (aliases && aliases.IsSequence()) { + for (size_t j = 0; j < aliases.size(); ++j) { + std::string alias = aliases[j].as(); + + if (nullptr != findSNI(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] Failed to parse the selector YAML node", PLUGIN_NAME); delete limiter; @@ -110,6 +129,45 @@ SniSelector::yamlParser(std::string yaml_file) return true; } +/////////////////////////////////////////////////////////////////////////////// +// This is the queue management continuation, which gets called periodically +// +static int +sni_config_cont(TSCont cont, TSEvent event, void *edata) +{ + auto selector = SniSelector::instance(); // Also leases the instance + auto current = std::filesystem::last_write_time(selector->yamlFile()); + auto old_sel = static_cast(TSContDataGet(cont)); + + // Delete the previous selector + if (old_sel) { + old_sel->release(); + TSContDataSet(cont, nullptr); + } + + if (current > selector->yamlLastWrite()) { + auto new_sel = new SniSelector(); + + 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()); + } + } else { + Dbg(dbg_ctl, "No change in YAML file: %s", selector->yamlFile().c_str()); + } + + selector->release(); + + return TS_EVENT_NONE; +} + /////////////////////////////////////////////////////////////////////////////// // This is the queue management continuation, which gets called periodically // @@ -118,33 +176,36 @@ sni_queue_cont(TSCont cont, TSEvent event, void *edata) { SniSelector *selector = static_cast(TSContDataGet(cont)); - for (const auto &[key, limiter] : selector->limiters()) { - QueueTime now = std::chrono::system_clock::now(); // Only do this once per 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 - // 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 (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); - } + (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); + } - // 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 + // 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 - 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); + 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); - (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); + (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); + } } } } @@ -152,23 +213,8 @@ sni_queue_cont(TSCont cont, TSEvent event, void *edata) return TS_EVENT_NONE; } -SniRateLimiter * -SniSelector::findSNI(std::string_view sni) -{ - if (sni.empty()) { // Likely shouldn't happen, but we can shortcircuit - return nullptr; - } - - auto limiter = _limiters.find(sni); - - if (limiter != _limiters.end()) { - return limiter->second; - } - return nullptr; -} - IpReputation::SieveLru * -SniSelector::findIpRep(std::string_view name) +SniSelector::findIpRep(const std::string &name) { auto it = std::find_if(_reputations.begin(), _reputations.end(), [&name](const IpReputation::SieveLru *iprep) { return iprep->name() == name; }); @@ -190,6 +236,26 @@ 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 +// +void +SniSelector::startup() +{ + TSCont sni_cont = TSContCreate(sni_limit_cont, nullptr); + + TSReleaseAssert(sni_cont); + + _instance.store(new SniSelector()); + TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, sni_cont); + TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, sni_cont); + + auto config_cont = TSContCreate(sni_config_cont, TSMutexCreate()); + + TSReleaseAssert(config_cont); + TSContScheduleEveryOnPool(config_cont, std::chrono::milliseconds{10000}.count(), TS_THREAD_POOL_TASK); +} diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 23a8539fbb6..2c7622e3582 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -21,39 +21,67 @@ #include #include #include +#include +#include #include "ts/ts.h" #include "utilities.h" #include "sni_limiter.h" #include "ip_reputation.h" -// This is the list of things (CIDR's) to exclude from blocking etc. -// ToDo: This has to be finished -class ListType -{ - const std::string_view - name() const - { - return _name; - } - - std::string _name; // Name of the list -}; - /////////////////////////////////////////////////////////////////////////////// -// SNI based limiter selector, this will have one single instance globally. +// SNI based limiter selector, this will have one singleton instance. // class SniSelector { + using self_type = SniSelector; + public: - using Limiters = std::unordered_map; - using Lists = std::vector; + using Limiters = std::unordered_map>; using IPReputations = std::vector; - SniSelector() = default; - virtual ~SniSelector() = default; // ToDo: This has to clean stuff up + SniSelector() = default; - bool yamlParser(const std::string yaml_file); + 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 &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() @@ -76,6 +104,26 @@ class SniSelector return _limiters; } + SniRateLimiter * + findSNI(const std::string &sni) + { + auto iter = _limiters.find(sni); + + return ((iter != _limiters.end()) ? std::get<1>(iter->second) : nullptr); + } + + const std::string & + yamlFile() const + { + return _yaml_file; + } + + std::filesystem::file_time_type + yamlLastWrite() const + { + return _yaml_last_write; + } + void addIPReputation(IpReputation::SieveLru *iprep) { @@ -86,20 +134,30 @@ class SniSelector addLimiter(SniRateLimiter *limiter) { _needs_queue_cont = (limiter->max_queue > 0); - _limiters.emplace(limiter->name, limiter); + _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)); } + void setupQueueCont(); + bool yamlParser(const std::string yaml_file); + IpReputation::SieveLru *findIpRep(const std::string &name); - IpReputation::SieveLru *findIpRep(std::string_view name); - SniRateLimiter *findSNI(std::string_view sni); + static void startup(); private: std::string _yaml_file; + std::filesystem::file_time_type _yaml_last_write; // Last time the yaml file was written to 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 + TSAction _queue_action = nullptr; // The action associated with the queue continuation, needed to shut it down Limiters _limiters; // The SNI limiters IPReputations _reputations; // IP-Reputation rules - Lists _lists; // Exclude lists (things not to block) - std::atomic _leases = 0; // Number of leases we have on the current selector + 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/utilities.h b/plugins/experimental/rate_limit/utilities.h index f88b3458e67..7954764e8a7 100644 --- a/plugins/experimental/rate_limit/utilities.h +++ b/plugins/experimental/rate_limit/utilities.h @@ -18,7 +18,6 @@ #pragma once #include -#include #include #include From dc42618228b7549893dfe96fd9da35bb3ce45367 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Thu, 5 Oct 2023 12:14:21 -0600 Subject: [PATCH 04/10] Adds a wildcard rule for the SNI --- .../experimental/rate_limit/CMakeLists.txt | 9 +++++++- plugins/experimental/rate_limit/Makefile.inc | 5 +++++ .../experimental/rate_limit/ip_reputation.cc | 8 +++---- plugins/experimental/rate_limit/limiter.h | 8 +++---- .../experimental/rate_limit/sni_limiter.cc | 21 +++++++++---------- .../experimental/rate_limit/sni_selector.cc | 6 +++++- .../experimental/rate_limit/sni_selector.h | 14 +++++++------ 7 files changed, 44 insertions(+), 27 deletions(-) diff --git a/plugins/experimental/rate_limit/CMakeLists.txt b/plugins/experimental/rate_limit/CMakeLists.txt index 1ea94e2b95e..229476c8be3 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 @@ -24,4 +26,9 @@ add_atsplugin( txn_limiter.cc utilities.cc ) -target_link_libraries(rate_limit PRIVATE OpenSSL::SSL) + +target_link_libraries(rate_limit + PRIVATE + yaml-cpp::yaml-cpp + OpenSSL::SSL +) diff --git a/plugins/experimental/rate_limit/Makefile.inc b/plugins/experimental/rate_limit/Makefile.inc index 69fa09affe6..6e100443e7f 100644 --- a/plugins/experimental/rate_limit/Makefile.inc +++ b/plugins/experimental/rate_limit/Makefile.inc @@ -23,3 +23,8 @@ experimental_rate_limit_rate_limit_la_SOURCES = \ experimental/rate_limit/sni_selector.cc \ experimental/rate_limit/ip_reputation.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 c0551027877..a8779e6b953 100644 --- a/plugins/experimental/rate_limit/ip_reputation.cc +++ b/plugins/experimental/rate_limit/ip_reputation.cc @@ -130,10 +130,10 @@ SieveLru::parseYaml(const YAML::Node &node) } _buckets[blockBucket()] = new SieveBucket(cur_size / 2); // Block LRU, same size as entry bucket - Dbg(dbg_ctl, "Loaded IP-Reputation rule: %s(%u, %u, %u, %lld)", _name.c_str(), _num_buckets, _size, _percentage, - _max_age.count()); - Dbg(dbg_ctl, "\twith perma-block rule: %s(%u, %u, %lld)", _name.c_str(), _permablock_limit, _permablock_threshold, - _permablock_max_age.count()); + 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; } diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index e4b66bef8f3..2eba204959f 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -169,7 +169,7 @@ template class RateLimiter bool full() const { - return (_size == max_queue); + return (_size >= max_queue); } void @@ -225,9 +225,9 @@ template class RateLimiter } // ToDo: Probably should be made private... - std::string name = ""; // The name/descr (e.g. SNI name) of this limiter - uint32_t limit = 100; // Arbitrary default, probably should be a required config - uint32_t max_queue = UINT32_MAX; // No queue limit, but if set will give an immediate error if at max + std::string name = ""; // The name/descr (e.g. SNI name) of this limiter + uint32_t limit = 100; // Arbitrary default, probably should be a required config + 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: diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index 427d34a4b2c..02083775839 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -47,27 +47,26 @@ SniRateLimiter::parseYaml(const YAML::Node &node) const YAML::Node &queue = node["queue"]; + // If enabled, we default to UINT32_MAX, but the object default is still 0 (no queue) if (queue) { - if (queue["size"]) { - max_queue = queue["size"].as(); - } + 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"]; + const YAML::Node &metrics = node["metrics"]; - if (metrics) { - std::string prefix = metrics["prefix"] ? metrics["prefix"].as() : RATE_LIMITER_METRIC_PREFIX; - std::string tag = metrics["tag"] ? metrics["tag"].as() : name; + 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); + 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); + } } - Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %lld)", name.c_str(), limit, max_queue, max_age.count()); + Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %ld)", name.c_str(), limit, max_queue, static_cast(max_age.count())); return true; } diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index ec4ea7c48f2..356caf5e7fd 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -95,7 +95,11 @@ SniSelector::yamlParser(std::string yaml_file) auto limiter = new SniRateLimiter(name, this); if (limiter->parseYaml(sni)) { - addLimiter(limiter); + if (name == "*") { + _default = limiter; + } else { + addLimiter(limiter); + } // Add aliases, if any const YAML::Node &aliases = sni["aliases"]; diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 2c7622e3582..e2ebe2b843e 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -60,6 +60,7 @@ class SniSelector delete iprep; } + delete _default; for (auto &limiter : _limiters) { auto &[owner, ptr] = limiter.second; @@ -109,7 +110,7 @@ class SniSelector { auto iter = _limiters.find(sni); - return ((iter != _limiters.end()) ? std::get<1>(iter->second) : nullptr); + return ((iter != _limiters.end()) ? std::get<1>(iter->second) : _default); } const std::string & @@ -153,11 +154,12 @@ class SniSelector std::string _yaml_file; std::filesystem::file_time_type _yaml_last_write; // Last time the yaml file was written to bool _needs_queue_cont = false; - 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 - IPReputations _reputations; // IP-Reputation rules - std::atomic _leases = 0; // Number of leases we have on the current selector, start with one + 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 + 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 }; From a04b00ccd200ddcd3227464a9358e270066a15d9 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Thu, 5 Oct 2023 16:45:46 -0600 Subject: [PATCH 05/10] Some more cleanup --- plugins/experimental/rate_limit/limiter.h | 62 +++++++++++++------ plugins/experimental/rate_limit/rate_limit.cc | 10 +-- .../experimental/rate_limit/sni_limiter.cc | 30 ++++----- plugins/experimental/rate_limit/sni_limiter.h | 27 +++++--- .../experimental/rate_limit/sni_selector.cc | 4 +- .../experimental/rate_limit/sni_selector.h | 4 +- .../experimental/rate_limit/txn_limiter.cc | 24 +++---- plugins/experimental/rate_limit/txn_limiter.h | 24 ++++++- plugins/experimental/rate_limit/utilities.cc | 2 +- plugins/experimental/rate_limit/utilities.h | 2 +- 10 files changed, 122 insertions(+), 67 deletions(-) diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index 2eba204959f..153fe5f21ce 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -75,12 +75,6 @@ template class RateLimiter TSMutexDestroy(_active_lock); } - void - initialize(uint32_t limit) - { - this->limit = limit; - } - void initializeMetrics(uint type, std::string tag, std::string prefix = RATE_LIMITER_METRIC_PREFIX) { @@ -92,8 +86,8 @@ template class RateLimiter if (!tag.empty()) { metric_prefix.append("." + tag); - } else if (!name.empty()) { - metric_prefix.append("." + name); + } else if (!name().empty()) { + metric_prefix.append("." + name()); } for (int i = 0; i < RATE_LIMITER_METRIC_MAX; i++) { @@ -129,12 +123,12 @@ template class RateLimiter bool reserve() { - TSReleaseAssert(_active <= limit); + TSReleaseAssert(_active <= limit()); TSMutexLock(_active_lock); - if (_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); @@ -148,7 +142,7 @@ template class RateLimiter TSMutexLock(_active_lock); --_active; TSMutexUnlock(_active_lock); - Dbg(dbg_ctl, "Releasing a slot, active entities == %u", active()); + Dbg(dbg_ctl, "Releasing a slot, active entities == %u", _active.load()); } // Current size of the active_in connections @@ -169,7 +163,7 @@ template class RateLimiter bool full() const { - return (_size >= max_queue); + return (_size >= max_queue()); } void @@ -217,18 +211,48 @@ template class RateLimiter std::chrono::milliseconds age = std::chrono::duration_cast(now - std::get<2>(item)); - return (age >= max_age); + return (age >= max_age()); } else { TSMutexUnlock(_queue_lock); return false; } } - // ToDo: Probably should be made private... - std::string name = ""; // The name/descr (e.g. SNI name) of this limiter - uint32_t limit = 100; // Arbitrary default, probably should be a required config - uint32_t max_queue = 0; // No queue by default - std::chrono::milliseconds max_age = std::chrono::milliseconds::zero(); // Max age (ms) in the queue + const std::string & + name() + { + return _name; + } + + uint32_t + limit() const + { + return _limit; + } + + uint32_t + max_queue() const + { + return _max_queue; + } + + std::chrono::milliseconds + max_age() const + { + return _max_age; + } + + void + setName(const std::string &name) + { + _name = name; + } + +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 diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index 540c99a224a..860931ba9a0 100644 --- a/plugins/experimental/rate_limit/rate_limit.cc +++ b/plugins/experimental/rate_limit/rate_limit.cc @@ -93,7 +93,7 @@ TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSE TxnRateLimiter *limiter = new TxnRateLimiter(); // set the name based on the pristine remap URL prior to advancing the pointer below - limiter->name = getDescriptionFromUrl(argv[0]); + 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. @@ -104,8 +104,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; } @@ -120,9 +120,9 @@ TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri) 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 02083775839..efa691b5a6a 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -31,7 +31,7 @@ bool SniRateLimiter::parseYaml(const YAML::Node &node) { if (node["limit"]) { - limit = node["limit"].as(); + _limit = node["limit"].as(); } else { // ToDo: Should we require the limit ? } @@ -39,8 +39,8 @@ SniRateLimiter::parseYaml(const YAML::Node &node) if (node["ip-rep"]) { std::string 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()); + 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; } } @@ -49,24 +49,24 @@ SniRateLimiter::parseYaml(const YAML::Node &node) // 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; + _max_queue = queue["size"] ? queue["size"].as() : UINT32_MAX; if (queue["max_age"]) { - max_age = std::chrono::seconds(queue["max_age"].as()); + _max_age = std::chrono::seconds(queue["max_age"].as()); } const YAML::Node &metrics = node["metrics"]; if (metrics) { std::string prefix = metrics["prefix"] ? metrics["prefix"].as() : RATE_LIMITER_METRIC_PREFIX; - std::string tag = metrics["tag"] ? metrics["tag"].as() : name; + 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()); + 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); } } - Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %ld)", name.c_str(), limit, max_queue, static_cast(max_age.count())); + Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %ld)", name().c_str(), limit(), max_queue(), static_cast(max_age().count())); return true; } @@ -90,7 +90,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) if (limiter) { // Check if we have an IP reputation for this SNI, and if we should block - if (limiter->iprep && limiter->iprep->initialized()) { + if (limiter->iprep() && limiter->iprep()->initialized()) { const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); int32_t pressure = limiter->pressure(); @@ -100,7 +100,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) 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()) { @@ -111,10 +111,10 @@ 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 @@ -133,7 +133,7 @@ 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); @@ -166,7 +166,7 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) if (limiter) { TSUserArgSet(vc, gVCIdx, nullptr); limiter->free(); - limiter->selector->release(); // Release the selector, such that it can be deleted later + limiter->selector()->release(); // Release the selector, such that it can be deleted later } TSVConnReenable(vc); break; diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 208ae44692b..5660c181290 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -38,7 +38,7 @@ class SniRateLimiter : public RateLimiter self_type &operator=(const self_type &) = delete; self_type &operator=(self_type &&) = delete; - SniRateLimiter(std::string &sni, SniSelector *sel) : selector(sel) { name = sni; } + SniRateLimiter(std::string &sni, SniSelector *sel) : _selector(sel) { setName(sni); } bool parseYaml(const YAML::Node &node); @@ -47,18 +47,31 @@ class SniRateLimiter : public RateLimiter int32_t pressure() const { - int32_t p = ((active() / static_cast(limit) * 100) - iprep->percentage()) / (100 - iprep->percentage()) * - (iprep->numBuckets() + 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); + return (p >= static_cast(_iprep->numBuckets()) ? _iprep->numBuckets() : p); } void addIPReputation(IpReputation::SieveLru *iprep) { - this->iprep = iprep; + this->_iprep = iprep; } - SniSelector *selector = nullptr; // The selector we belong to - IpReputation::SieveLru *iprep = nullptr; // IP reputation for this SNI (if any) + IpReputation::SieveLru * + iprep() const + { + return _iprep; + } + + SniSelector * + selector() const + { + return _selector; + } + +private: + SniSelector *_selector = nullptr; // The selector we belong to + IpReputation::SieveLru *_iprep = nullptr; // IP reputation for this SNI (if any) }; diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index 356caf5e7fd..f97474aed8a 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -95,7 +95,7 @@ SniSelector::yamlParser(std::string yaml_file) auto limiter = new SniRateLimiter(name, this); if (limiter->parseYaml(sni)) { - if (name == "*") { + if (name == "*" || name == "default") { _default = limiter; } else { addLimiter(limiter); @@ -197,7 +197,7 @@ sni_queue_cont(TSCont cont, TSEvent event, void *edata) } // Kill any queued VCs 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)) { diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index e2ebe2b843e..2ae25dbaea8 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -134,8 +134,8 @@ class SniSelector void addLimiter(SniRateLimiter *limiter) { - _needs_queue_cont = (limiter->max_queue > 0); - _limiters.emplace(limiter->name, std::forward_as_tuple(true, limiter)); + _needs_queue_cont = (limiter->max_queue() > 0); + _limiters.emplace(limiter->name(), std::forward_as_tuple(true, limiter)); } void diff --git a/plugins/experimental/rate_limit/txn_limiter.cc b/plugins/experimental/rate_limit/txn_limiter.cc index 0a145621810..34f351fadf7 100644 --- a/plugins/experimental/rate_limit/txn_limiter.cc +++ b/plugins/experimental/rate_limit/txn_limiter.cc @@ -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); @@ -71,7 +71,7 @@ txn_queue_cont(TSCont cont, TSEvent event, void *edata) 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); @@ -127,22 +127,22 @@ TxnRateLimiter::initialize(int argc, const char *argv[]) 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': prefix = optarg; @@ -156,7 +156,7 @@ 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); 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 7954764e8a7..31f1595217e 100644 --- a/plugins/experimental/rate_limit/utilities.h +++ b/plugins/experimental/rate_limit/utilities.h @@ -23,7 +23,7 @@ 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); From 7d6b42d3c54e1f8c8fcfc4297028038590d51329 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Mon, 9 Oct 2023 10:46:21 -0600 Subject: [PATCH 06/10] Cleanup YAML parsing, and cleanup --- plugins/experimental/rate_limit/limiter.h | 87 ++++++++++++------- .../experimental/rate_limit/sni_limiter.cc | 28 +----- plugins/experimental/rate_limit/sni_limiter.h | 4 +- 3 files changed, 60 insertions(+), 59 deletions(-) diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index 153fe5f21ce..707d90e1153 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -26,6 +26,7 @@ #include "tscore/ink_config.h" #include "ts/ts.h" +#include #include "utilities.h" constexpr auto QUEUE_DELAY_TIME = std::chrono::milliseconds{300}; // Examine the queue every 300ms @@ -65,14 +66,47 @@ 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() + virtual ~RateLimiter() = default; + + virtual bool + parseYaml(const YAML::Node &node) { - TSMutexDestroy(_queue_lock); - TSMutexDestroy(_active_lock); + 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"]; + + 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 @@ -111,37 +145,31 @@ template class RateLimiter } } - void - initializeQueue(uint32_t max_queue, std::chrono::seconds max_age = std::chrono::seconds::zero()) - { - this->max_queue = max_queue; - this->max_age = max_age; - } - // Reserve / release a slot from the active resource limits. Reserve will return // false if we are unable to reserve a slot. bool reserve() { + std::lock_guard lock(_active_lock); + TSReleaseAssert(_active <= limit()); - TSMutexLock(_active_lock); if (_active < limit()) { ++_active; - TSMutexUnlock(_active_lock); // Reduce the critical section, release early Dbg(dbg_ctl, "Reserving a slot, active entities == %u", _active.load()); return true; - } else { - TSMutexUnlock(_active_lock); - return false; } + + return false; } void free() { - TSMutexLock(_active_lock); - --_active; - TSMutexUnlock(_active_lock); + { + std::lock_guard lock(_active_lock); + --_active; + } + Dbg(dbg_ctl, "Releasing a slot, active entities == %u", _active.load()); } @@ -170,25 +198,23 @@ template class RateLimiter 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; } @@ -202,20 +228,19 @@ template class RateLimiter } 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 false; } const std::string & @@ -258,8 +283,8 @@ template class RateLimiter 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/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index efa691b5a6a..2877247a7af 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -30,11 +30,7 @@ int gVCIdx = -1; bool SniRateLimiter::parseYaml(const YAML::Node &node) { - if (node["limit"]) { - _limit = node["limit"].as(); - } else { - // ToDo: Should we require the limit ? - } + super_type::parseYaml(node); if (node["ip-rep"]) { std::string ipr_name = node["ip-rep"].as(); @@ -44,28 +40,6 @@ SniRateLimiter::parseYaml(const YAML::Node &node) return false; } } - - 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"]; - - 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); - } - } - Dbg(dbg_ctl, "Loaded selector rule: %s(%u, %u, %ld)", name().c_str(), limit(), max_queue(), static_cast(max_age().count())); return true; diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 5660c181290..74a92af15b9 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -20,6 +20,7 @@ #include "limiter.h" #include "ip_reputation.h" #include "ts/ts.h" +#include int sni_limit_cont(TSCont contp, TSEvent event, void *edata); @@ -30,7 +31,8 @@ class SniSelector; // class SniRateLimiter : public RateLimiter { - using self_type = SniRateLimiter; + using super_type = RateLimiter; + using self_type = SniRateLimiter; public: SniRateLimiter() = delete; From 2618fcc703c555a9a47ef8d5f46be8964d4b1fd4 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Mon, 9 Oct 2023 17:34:35 -0600 Subject: [PATCH 07/10] Adds a List YAML section, and an exclude directive --- doc/admin-guide/plugins/rate_limit.en.rst | 27 ++++++++ plugins/experimental/rate_limit/Makefile.inc | 1 + plugins/experimental/rate_limit/limiter.h | 5 +- plugins/experimental/rate_limit/lists.cc | 40 +++++++++++ plugins/experimental/rate_limit/lists.h | 66 +++++++++++++++++++ .../experimental/rate_limit/sni_limiter.cc | 27 +++++++- plugins/experimental/rate_limit/sni_limiter.h | 13 +++- .../experimental/rate_limit/sni_selector.cc | 56 ++++++++++------ .../experimental/rate_limit/sni_selector.h | 55 +++++++++++++--- plugins/experimental/rate_limit/utilities.h | 3 +- 10 files changed, 257 insertions(+), 36 deletions(-) create mode 100644 plugins/experimental/rate_limit/lists.cc create mode 100644 plugins/experimental/rate_limit/lists.h diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index 3997a571979..f3816ff705e 100644 --- a/doc/admin-guide/plugins/rate_limit.en.rst +++ b/doc/admin-guide/plugins/rate_limit.en.rst @@ -137,6 +137,7 @@ and nodes are documented below. tag: example.com prefix: ddos ip-rep: main + exclude: internal - sni: test2.example.com aliases: [test3.example.com, test4.example.com] limit: 100 @@ -150,6 +151,12 @@ and nodes are documented below. 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: @@ -170,6 +177,11 @@ For the top level `selector` node, the following options are available: 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:: exclude + + 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:: queue If enabled, when the limit (above) has been reached, all new connections @@ -192,10 +204,25 @@ For the top level `selector` node, the following options are available: 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 + + 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 diff --git a/plugins/experimental/rate_limit/Makefile.inc b/plugins/experimental/rate_limit/Makefile.inc index 6e100443e7f..b0a40f4001c 100644 --- a/plugins/experimental/rate_limit/Makefile.inc +++ b/plugins/experimental/rate_limit/Makefile.inc @@ -22,6 +22,7 @@ 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 = \ diff --git a/plugins/experimental/rate_limit/limiter.h b/plugins/experimental/rate_limit/limiter.h index 707d90e1153..d9fe6f4e02f 100644 --- a/plugins/experimental/rate_limit/limiter.h +++ b/plugins/experimental/rate_limit/limiter.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "tscore/ink_config.h" #include "ts/ts.h" @@ -126,7 +127,7 @@ template class RateLimiter 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 = (char *)TSmalloc(metricsz); + char *const metric = static_cast(TSmalloc(metricsz)); snprintf(metric, metricsz, "%s.%s", metric_prefix.data(), suffixes[i]); _metrics[i] = TS_ERROR; @@ -244,7 +245,7 @@ template class RateLimiter } const std::string & - name() + name() const { return _name; } diff --git a/plugins/experimental/rate_limit/lists.cc b/plugins/experimental/rate_limit/lists.cc new file mode 100644 index 00000000000..8acd64d4a97 --- /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 (size_t i = 0; i < cidr.size(); ++i) { + std::string str = cidr[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/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index 2877247a7af..e45ea6ed776 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -40,6 +40,17 @@ SniRateLimiter::parseYaml(const YAML::Node &node) 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"]) { + std::string 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; @@ -60,13 +71,23 @@ sni_limit_cont(TSCont contp, TSEvent event, void *edata) const char *server_name = TSVConnSslSniGet(vc, &len); const std::string sni_name(server_name, len); SniSelector *selector = SniSelector::instance(); - SniRateLimiter *limiter = selector->findSNI(sni_name); + 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() && limiter->iprep()->initialized()) { - const sockaddr *sock = TSNetVConnRemoteAddrGet(vc); - int32_t pressure = limiter->pressure(); + int32_t pressure = limiter->pressure(); Dbg(dbg_ctl, "CLIENT_HELLO on %s, pressure=%d", sni_name.c_str(), pressure); diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 74a92af15b9..1a04d02514b 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -17,11 +17,13 @@ */ #pragma once -#include "limiter.h" -#include "ip_reputation.h" #include "ts/ts.h" #include +#include "limiter.h" +#include "ip_reputation.h" +#include "lists.h" + int sni_limit_cont(TSCont contp, TSEvent event, void *edata); class SniSelector; @@ -67,6 +69,12 @@ class SniRateLimiter : public RateLimiter return _iprep; } + List::IP * + exclude() const + { + return _exclude; + } + SniSelector * selector() const { @@ -76,4 +84,5 @@ class SniRateLimiter : public RateLimiter private: 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 f97474aed8a..7450646db64 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -18,7 +18,6 @@ #include "tscore/ink_config.h" #include -#include "sni_limiter.h" #include "sni_selector.h" std::atomic SniSelector::_instance = nullptr; @@ -44,6 +43,39 @@ SniSelector::yamlParser(std::string yaml_file) _yaml_file = yaml_file; _yaml_last_write = std::filesystem::last_write_time(_yaml_file); + // First build the Lists, if any + const YAML::Node &lists = config["lists"]; + + if (lists && lists.IsSequence()) { + for (size_t i = 0; i < lists.size(); ++i) { + const YAML::Node &list = lists[i]; + + if (list.IsMap() && list["name"]) { + std::string 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"]; if (ipreps && ipreps.IsSequence()) { @@ -61,6 +93,7 @@ SniSelector::yamlParser(std::string yaml_file) 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); @@ -74,9 +107,7 @@ SniSelector::yamlParser(std::string yaml_file) } } - // ToDo: Add the IP list YAML parsing - - // Parse all the SNI selectors + // Finally, parse all the SNI selectors (if any) const YAML::Node &sel = config["selector"]; if (sel && sel.IsSequence()) { @@ -87,7 +118,7 @@ SniSelector::yamlParser(std::string yaml_file) // ToDo: Allow a sequence of names here std::string name = sni["sni"].as(); - if (nullptr != findSNI(name)) { + if (nullptr != findLimiter(name)) { TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, name.c_str()); return false; } @@ -108,7 +139,7 @@ SniSelector::yamlParser(std::string yaml_file) for (size_t j = 0; j < aliases.size(); ++j) { std::string alias = aliases[j].as(); - if (nullptr != findSNI(alias)) { + if (nullptr != findLimiter(alias)) { TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, alias.c_str()); return false; } @@ -217,19 +248,6 @@ sni_queue_cont(TSCont cont, TSEvent event, void *edata) return TS_EVENT_NONE; } -IpReputation::SieveLru * -SniSelector::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; -} - /////////////////////////////////////////////////////////////////////////////// // If needed, create the queue continuation that needs to run for this selector. // diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 2ae25dbaea8..05cd0108540 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -25,9 +25,10 @@ #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, this will have one singleton instance. @@ -39,6 +40,7 @@ class SniSelector public: using Limiters = std::unordered_map>; using IPReputations = std::vector; + using Lists = std::vector; SniSelector() = default; @@ -60,6 +62,10 @@ class SniSelector delete iprep; } + for (auto &list : _lists) { + delete list; + } + delete _default; for (auto &limiter : _limiters) { auto &[owner, ptr] = limiter.second; @@ -106,13 +112,26 @@ class SniSelector } SniRateLimiter * - findSNI(const std::string &sni) + 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 { @@ -131,22 +150,39 @@ class SniSelector _reputations.emplace_back(iprep); } - void - addLimiter(SniRateLimiter *limiter) + IpReputation::SieveLru * + findIpRep(const std::string &name) { - _needs_queue_cont = (limiter->max_queue() > 0); - _limiters.emplace(limiter->name(), std::forward_as_tuple(true, limiter)); + 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 - addAlias(std::string alias, SniRateLimiter *limiter) + addList(List::IP *list) { - _limiters.emplace(alias, std::forward_as_tuple(false, limiter)); + _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); - IpReputation::SieveLru *findIpRep(const std::string &name); static void startup(); @@ -159,6 +195,7 @@ class SniSelector 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/utilities.h b/plugins/experimental/rate_limit/utilities.h index 31f1595217e..5bef29c945d 100644 --- a/plugins/experimental/rate_limit/utilities.h +++ b/plugins/experimental/rate_limit/utilities.h @@ -19,7 +19,8 @@ #include #include -#include + +#include "ts/ts.h" constexpr char const PLUGIN_NAME[] = "rate_limit"; From 2b663649fe1126553be6c543311a6b299613627b Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Tue, 10 Oct 2023 10:52:40 -0600 Subject: [PATCH 08/10] Run clang-tidy on this code --- .../experimental/rate_limit/CMakeLists.txt | 1 + .../experimental/rate_limit/ip_reputation.cc | 9 ++-- plugins/experimental/rate_limit/lists.cc | 4 +- plugins/experimental/rate_limit/rate_limit.cc | 6 +-- .../experimental/rate_limit/sni_limiter.cc | 8 +-- plugins/experimental/rate_limit/sni_limiter.h | 2 +- .../experimental/rate_limit/sni_selector.cc | 52 ++++++++++--------- .../experimental/rate_limit/sni_selector.h | 2 +- .../experimental/rate_limit/txn_limiter.cc | 6 +-- 9 files changed, 47 insertions(+), 43 deletions(-) diff --git a/plugins/experimental/rate_limit/CMakeLists.txt b/plugins/experimental/rate_limit/CMakeLists.txt index 229476c8be3..66bf1fd4264 100644 --- a/plugins/experimental/rate_limit/CMakeLists.txt +++ b/plugins/experimental/rate_limit/CMakeLists.txt @@ -29,6 +29,7 @@ add_atsplugin( target_link_libraries(rate_limit PRIVATE + libswoc yaml-cpp::yaml-cpp OpenSSL::SSL ) diff --git a/plugins/experimental/rate_limit/ip_reputation.cc b/plugins/experimental/rate_limit/ip_reputation.cc index a8779e6b953..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))); @@ -302,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; @@ -316,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/lists.cc b/plugins/experimental/rate_limit/lists.cc index 8acd64d4a97..83daa08b5f1 100644 --- a/plugins/experimental/rate_limit/lists.cc +++ b/plugins/experimental/rate_limit/lists.cc @@ -25,8 +25,8 @@ List::IP::parseYaml(const YAML::Node &node) const YAML::Node &cidr = node["cidr"]; if (cidr && cidr.IsSequence()) { - for (size_t i = 0; i < cidr.size(); ++i) { - std::string str = cidr[i].as(); + 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); diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index 860931ba9a0..5883037da54 100644 --- a/plugins/experimental/rate_limit/rate_limit.cc +++ b/plugins/experimental/rate_limit/rate_limit.cc @@ -63,7 +63,7 @@ TSPluginInit(int argc, const char *argv[]) selector->setupQueueCont(); // Start the queue processing continuation if needed } else { selector->release(); - TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[0]); + TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[1]); } } else { TSError("[%s] Usage: rate_limit.so ", PLUGIN_NAME); @@ -90,7 +90,7 @@ 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 name based on the pristine remap URL prior to advancing the pointer below limiter->setName(getDescriptionFromUrl(argv[0])); @@ -116,7 +116,7 @@ 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()) { diff --git a/plugins/experimental/rate_limit/sni_limiter.cc b/plugins/experimental/rate_limit/sni_limiter.cc index e45ea6ed776..5e6aa04763b 100644 --- a/plugins/experimental/rate_limit/sni_limiter.cc +++ b/plugins/experimental/rate_limit/sni_limiter.cc @@ -33,7 +33,7 @@ SniRateLimiter::parseYaml(const YAML::Node &node) super_type::parseYaml(node); if (node["ip-rep"]) { - std::string ipr_name = node["ip-rep"].as(); + 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()); @@ -43,7 +43,7 @@ SniRateLimiter::parseYaml(const YAML::Node &node) // ToDo: It's unfortunate, but the selector holds the lists (and the ip-reps), so the lookup has to happen here ... :/. if (node["exclude"]) { - std::string excl_name = node["exclude"].as(); + 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()); @@ -63,7 +63,7 @@ SniRateLimiter::parseYaml(const YAML::Node &node) int sni_limit_cont(TSCont contp, TSEvent event, void *edata) { - TSVConn vc = static_cast(edata); + auto vc = static_cast(edata); switch (event) { case TS_EVENT_SSL_CLIENT_HELLO: { @@ -156,7 +156,7 @@ 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); diff --git a/plugins/experimental/rate_limit/sni_limiter.h b/plugins/experimental/rate_limit/sni_limiter.h index 1a04d02514b..42bcba5ae61 100644 --- a/plugins/experimental/rate_limit/sni_limiter.h +++ b/plugins/experimental/rate_limit/sni_limiter.h @@ -44,7 +44,7 @@ class SniRateLimiter : public RateLimiter SniRateLimiter(std::string &sni, SniSelector *sel) : _selector(sel) { setName(sni); } - bool parseYaml(const YAML::Node &node); + 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. diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index 7450646db64..c652302389b 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -26,16 +26,16 @@ std::atomic SniSelector::_instance = nullptr; // YAML parser for the global YAML configuration (via plugin.config) // bool -SniSelector::yamlParser(std::string yaml_file) +SniSelector::yamlParser(const std::string &yaml_file) { YAML::Node config; try { config = YAML::LoadFile(yaml_file); - } catch (YAML::BadFile &e) { + } catch (YAML::BadFile const &e) { TSError("[%s] Cannot load configuration file: %s.", PLUGIN_NAME, e.what()); return false; - } catch (std::exception &e) { + } catch (std::exception const &e) { TSError("[%s] Unknown error while loading configuration file: %s.", PLUGIN_NAME, e.what()); return false; } @@ -47,11 +47,11 @@ SniSelector::yamlParser(std::string yaml_file) const YAML::Node &lists = config["lists"]; if (lists && lists.IsSequence()) { - for (size_t i = 0; i < lists.size(); ++i) { - const YAML::Node &list = lists[i]; + for (const auto &i : lists) { + const YAML::Node &list = i; if (list.IsMap() && list["name"]) { - std::string name = list["name"].as(); + auto name = list["name"].as(); if (nullptr != findList(name)) { TSError("[%s] Duplicate List names being added (%s)", PLUGIN_NAME, name.c_str()); @@ -79,11 +79,11 @@ SniSelector::yamlParser(std::string yaml_file) const YAML::Node &ipreps = config["ip-rep"]; if (ipreps && ipreps.IsSequence()) { - for (size_t i = 0; i < ipreps.size(); ++i) { - const YAML::Node &ipr = ipreps[i]; + for (const auto &i : ipreps) { + const YAML::Node &ipr = i; if (ipr.IsMap() && ipr["name"]) { - std::string name = ipr["name"].as(); + auto name = ipr["name"].as(); if (nullptr != findIpRep(name)) { TSError("[%s] Duplicate IP-Reputation names being added (%s)", PLUGIN_NAME, name.c_str()); @@ -111,12 +111,11 @@ SniSelector::yamlParser(std::string yaml_file) const YAML::Node &sel = config["selector"]; if (sel && sel.IsSequence()) { - for (size_t i = 0; i < sel.size(); ++i) { - const YAML::Node &sni = sel[i]; + for (const auto &i : sel) { + const YAML::Node &sni = i; - if (sni.IsMap()) { - // ToDo: Allow a sequence of names here - std::string name = sni["sni"].as(); + 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()); @@ -135,16 +134,21 @@ SniSelector::yamlParser(std::string yaml_file) // Add aliases, if any const YAML::Node &aliases = sni["aliases"]; - if (aliases && aliases.IsSequence()) { - for (size_t j = 0; j < aliases.size(); ++j) { - std::string alias = aliases[j].as(); - - if (nullptr != findLimiter(alias)) { - TSError("[%s] Duplicate SNIs being added (%s)", PLUGIN_NAME, alias.c_str()); - return false; + 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); } - 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 { @@ -209,7 +213,7 @@ sni_config_cont(TSCont cont, TSEvent event, void *edata) static int sni_queue_cont(TSCont cont, TSEvent event, void *edata) { - SniSelector *selector = static_cast(TSContDataGet(cont)); + auto *selector = static_cast(TSContDataGet(cont)); for (const auto &[key, entry] : selector->limiters()) { auto [owner, limiter] = entry; diff --git a/plugins/experimental/rate_limit/sni_selector.h b/plugins/experimental/rate_limit/sni_selector.h index 05cd0108540..2cb96c6e9e9 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -182,7 +182,7 @@ class SniSelector } void setupQueueCont(); - bool yamlParser(const std::string yaml_file); + bool yamlParser(const std::string &yaml_file); static void startup(); diff --git a/plugins/experimental/rate_limit/txn_limiter.cc b/plugins/experimental/rate_limit/txn_limiter.cc index 34f351fadf7..507c402e0a6 100644 --- a/plugins/experimental/rate_limit/txn_limiter.cc +++ b/plugins/experimental/rate_limit/txn_limiter.cc @@ -28,7 +28,7 @@ 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: @@ -63,8 +63,8 @@ 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()) { From 6cf956c918c7fbfa8eb4add1bbd95df9fee51c7d Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Mon, 16 Oct 2023 16:45:49 -0600 Subject: [PATCH 09/10] Updated with Kit's concerns --- doc/admin-guide/plugins/rate_limit.en.rst | 4 ++-- plugins/experimental/rate_limit/CMakeLists.txt | 8 ++------ plugins/experimental/rate_limit/sni_selector.cc | 4 +++- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index f3816ff705e..43c2e5fc9ec 100644 --- a/doc/admin-guide/plugins/rate_limit.en.rst +++ b/doc/admin-guide/plugins/rate_limit.en.rst @@ -122,7 +122,7 @@ configuration file. The basic use is as:: :ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`. -The YAML configuration can have the following format, where the varies sections +The YAML configuration can have the following format, where the various sections and nodes are documented below. .. code-block:: yaml @@ -194,7 +194,7 @@ For the top level `selector` node, the following options are available: default to ``0``, which means no age limit. No queue is enable without this configuration directive, but it can also be - disable explicitly if the size is set to ``0``. + disabled explicitly if the size is set to ``0``. .. option:: metrics diff --git a/plugins/experimental/rate_limit/CMakeLists.txt b/plugins/experimental/rate_limit/CMakeLists.txt index 66bf1fd4264..8f62e4d29dc 100644 --- a/plugins/experimental/rate_limit/CMakeLists.txt +++ b/plugins/experimental/rate_limit/CMakeLists.txt @@ -25,11 +25,7 @@ add_atsplugin( sni_selector.cc txn_limiter.cc utilities.cc + lists.cc ) -target_link_libraries(rate_limit - PRIVATE - libswoc - yaml-cpp::yaml-cpp - OpenSSL::SSL -) +target_link_libraries(rate_limit PRIVATE libswoc yaml-cpp::yaml-cpp OpenSSL::SSL) diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index c652302389b..ac1e5f51272 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -283,5 +283,7 @@ SniSelector::startup() auto config_cont = TSContCreate(sni_config_cont, TSMutexCreate()); TSReleaseAssert(config_cont); - TSContScheduleEveryOnPool(config_cont, std::chrono::milliseconds{10000}.count(), TS_THREAD_POOL_TASK); + // ToDo: Should we make schedule reloads configurable? + // TSContScheduleEveryOnPool(config_cont, std::chrono::milliseconds{10000}.count(), TS_THREAD_POOL_TASK); + TSMgmtUpdateRegister(config_cont, PLUGIN_NAME); } From b842c59901dac6b8cf75372e84d5ee2c82618422 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Fri, 20 Oct 2023 13:54:23 -0600 Subject: [PATCH 10/10] Switches to new flavor of TSMgmtUpdateRegister --- doc/admin-guide/plugins/rate_limit.en.rst | 2 +- plugins/experimental/rate_limit/rate_limit.cc | 11 +--- .../experimental/rate_limit/sni_selector.cc | 54 +++++++++---------- .../experimental/rate_limit/sni_selector.h | 10 +--- 4 files changed, 30 insertions(+), 47 deletions(-) diff --git a/doc/admin-guide/plugins/rate_limit.en.rst b/doc/admin-guide/plugins/rate_limit.en.rst index 43c2e5fc9ec..8e0f8c8cbd5 100644 --- a/doc/admin-guide/plugins/rate_limit.en.rst +++ b/doc/admin-guide/plugins/rate_limit.en.rst @@ -193,7 +193,7 @@ For the top level `selector` node, the following options are available: default to ``UINT_MAX``, which is essentially unlimited. The max-age is default to ``0``, which means no age limit. - No queue is enable without this configuration directive, but it can also be + No queue is enabled without this configuration directive, but it can also be disabled explicitly if the size is set to ``0``. .. option:: metrics diff --git a/plugins/experimental/rate_limit/rate_limit.cc b/plugins/experimental/rate_limit/rate_limit.cc index 5883037da54..f7ba630eb28 100644 --- a/plugins/experimental/rate_limit/rate_limit.cc +++ b/plugins/experimental/rate_limit/rate_limit.cc @@ -55,16 +55,7 @@ TSPluginInit(int argc, const char *argv[]) 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(); - - auto selector = SniSelector::instance(); // Assure that we don't delete this until config reload - - if (selector->yamlParser(argv[1])) { - selector->setupQueueCont(); // Start the queue processing continuation if needed - } else { - selector->release(); - TSFatal("[%s] Failed to parse YAML file '%s'", PLUGIN_NAME, argv[1]); - } + SniSelector::startup(argv[1]); } else { TSError("[%s] Usage: rate_limit.so ", PLUGIN_NAME); } diff --git a/plugins/experimental/rate_limit/sni_selector.cc b/plugins/experimental/rate_limit/sni_selector.cc index ac1e5f51272..1ff1c4b134c 100644 --- a/plugins/experimental/rate_limit/sni_selector.cc +++ b/plugins/experimental/rate_limit/sni_selector.cc @@ -40,8 +40,7 @@ SniSelector::yamlParser(const std::string &yaml_file) return false; } - _yaml_file = yaml_file; - _yaml_last_write = std::filesystem::last_write_time(_yaml_file); + _yaml_file = yaml_file; // First build the Lists, if any const YAML::Node &lists = config["lists"]; @@ -175,31 +174,25 @@ static int sni_config_cont(TSCont cont, TSEvent event, void *edata) { auto selector = SniSelector::instance(); // Also leases the instance - auto current = std::filesystem::last_write_time(selector->yamlFile()); auto old_sel = static_cast(TSContDataGet(cont)); + auto new_sel = new SniSelector(); - // Delete the previous selector + // Delete the previous selector, which releases the lease we got at setup / reload if (old_sel) { old_sel->release(); TSContDataSet(cont, nullptr); } - if (current > selector->yamlLastWrite()) { - auto new_sel = new SniSelector(); - - 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()); - } + 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 { - Dbg(dbg_ctl, "No change in YAML file: %s", selector->yamlFile().c_str()); + delete new_sel; + TSError("[%s] Failed to reload YAML file: %s", PLUGIN_NAME, selector->yamlFile().c_str()); } selector->release(); @@ -267,23 +260,30 @@ SniSelector::setupQueueCont() } /////////////////////////////////////////////////////////////////////////////// -// Startup of the SNI selector hooks and config reload continuation and instance +// 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() +SniSelector::startup(const std::string &yaml_file) { - TSCont sni_cont = TSContCreate(sni_limit_cont, nullptr); + 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 config_cont = TSContCreate(sni_config_cont, TSMutexCreate()); + auto selector = SniSelector::instance(); // Assure that we don't delete this until next config reload - TSReleaseAssert(config_cont); - // ToDo: Should we make schedule reloads configurable? - // TSContScheduleEveryOnPool(config_cont, std::chrono::milliseconds{10000}.count(), TS_THREAD_POOL_TASK); - TSMgmtUpdateRegister(config_cont, PLUGIN_NAME); + 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 2cb96c6e9e9..b361b372f41 100644 --- a/plugins/experimental/rate_limit/sni_selector.h +++ b/plugins/experimental/rate_limit/sni_selector.h @@ -21,7 +21,6 @@ #include #include #include -#include #include #include "ts/ts.h" @@ -138,12 +137,6 @@ class SniSelector return _yaml_file; } - std::filesystem::file_time_type - yamlLastWrite() const - { - return _yaml_last_write; - } - void addIPReputation(IpReputation::SieveLru *iprep) { @@ -184,11 +177,10 @@ class SniSelector void setupQueueCont(); bool yamlParser(const std::string &yaml_file); - static void startup(); + static void startup(const std::string &yaml_file); private: std::string _yaml_file; - std::filesystem::file_time_type _yaml_last_write; // Last time the yaml file was written to bool _needs_queue_cont = false; 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