Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 106 additions & 37 deletions doc/admin-guide/plugins/rate_limit.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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::

Expand All @@ -122,51 +121,117 @@ The basic use is as::
done using e.g. the ``conf_remap`` plugin,
:ts:cv:`proxy.config.http.keep_alive_no_activity_timeout_in`.

The following options are available:

.. program:: rate-limit

.. option:: --limit
The YAML configuration can have the following format, where the various sections
and nodes are documented below.

.. code-block:: yaml
selector:
- sni: test1.example.com
limit: 1000
queue:
size: 1000
max-age: 30
metrics:
tag: example.com
prefix: ddos
ip-rep: main
exclude: internal
- sni: test2.example.com
aliases: [test3.example.com, test4.example.com]
limit: 100
ip-rep:
- name: main
buckets: 10
size: 15
percentage: 90
max-age: 300
perma-block:
limit: 100
threshold: 1
max-age: 1800
lists:
- name: internal
cidr:
- 10.0.0.0/8
- 192.168.0.0/16
For the top level `selector` node, the following options are available:

.. option:: sni

The SNI to match for this rate limiter.

.. option:: limit

The maximum number of active client transactions.

.. option:: --queue
.. option:: aliases

When the limit (above) has been reached, all new connections are placed
on a FIFO queue. This option (optional) sets an upper bound on how many
queued transactions we will allow. When this threshold is reached, all
additional connections are immediately errored out in the TLS handshake.
A list of aliases for the SNI, which will also be matched by this rate limiter.

The queue is effectively disabled if this is set to ``0``, which implies
that when the transaction limit is reached, we immediately start serving
error responses.
.. option:: ip-rep

The default queue size is ``UINT_MAX``, which is essentially unlimited.
The name of the IP reputation node to use for this rate limiter. If not
specified, the IP reputation system is not used for this rate limiter.

.. option:: --maxage
.. option:: exclude

An optional ``max-age`` for how long a transaction can sit in the delay queue.
The value (default 0) is the age in milliseconds.
A list of IP CIDR ranges to exclude from any rate limiting. Any IP matching
this list will not be rate limited, even if the SNI matches.

.. option:: --prefix
.. option:: queue

An optional metric prefix to use instead of the default (plugin.rate_limiter).
If enabled, when the limit (above) has been reached, all new connections
are placed on a FIFO queue. This option sets an upper bound on
how many queued transactions we will allow. When this threshold is reached,
all additional connections are immediately errored out in the TLS handshake.

.. option:: --tag
The queue option can include a `size` and a `max-age` option. The size is
default to ``UINT_MAX``, which is essentially unlimited. The max-age is
default to ``0``, which means no age limit.

An optional metric tag to use instead of the default. When a tag is not specified
the plugin will use the FQDN of the SNI associated with each rate limiter instance
created during plugin initialization.
No queue is enabled without this configuration directive, but it can also be
disabled explicitly if the size is set to ``0``.

.. option:: metrics

This is an optional node, which can be used to configure the metrics for
this rate limiter. If not specified, no metrics will be added.

The metrics node can include a `tag` and a `prefix` option. The tag is
default to the SNI, and the prefix is default to ``plugin.rate_limiter``.

The `lists` node is used to configure IP lists, which can be used to exclude
certain address ranges from the rate limiting. The following options are used:

.. option:: name

.. option:: --iprep_buckets
The name of the IP reputation setup, used to refer to it from the rate limiters.

.. option:: cidr

A list of CIDR ranges to add to this rule. The format is e.g. `10.0.0.0/8`.

The `ip-rep`` node is used to configure the IP reputation system, there can be
zero, one or many IP reputation setups. Each setup is configured with a name,
and the following options:

.. option:: name

The name of the IP reputation setup, used to refer to it from the rate limiters.

.. option:: buckets

The number of LRU buckets to use for the IP reputation. A good number here
is ``10``, which is the default, but can be configured. The reason for the different
buckets is to account for a pseudo-sorted list of IPs on the frequency seen. Too
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.
Expand All @@ -175,32 +240,36 @@ The following options are available:

The default here is ``0``, which means the IP reputation filter is not enabled!

.. option:: --iprep_percentage
.. option:: percentage

This is the minimum percentage of the ``limit`` that the pressure must be at, before
we start blocking IPs. The default is ``0.9`` which means ``90%`` of the limit.

.. option:: --iprep_maxage
.. option:: max-age

This is used for aging out entries out of the LRU, the default is ``0`` which means
no aging happens. Even with no aging, entries will eventually fall out of buckets
because of the LRU mechanism that kicks in. The aging is here to make sure a spike
in traffic from an IP doesn't keep the entry for too long in the LRUs.

.. option:: --iprep_permablock_limit
In addition, there's an optional configuration for the permanently blocking buckets,
`perma-block`. This is a special bucket, which is only used for IPs which have been
blocked for a long time. The configuration for this bucket is:

.. option:: limit

The minimum number of hits an IP must reach to get moved to the permanent bucket.
In this bucket, entries will stay for 2x

.. option:: --iprep_permablock_pressure
.. option:: threshold

This option specifies from which bucket an IP is allowed to move from into the
perma block bucket. A good value here is likely ``0`` or ``1``, which is very conservative.

.. option:: --iprep_permablock_maxage
.. option:: max-age

Similar to ``--iprep_maxage`` above, but only applies to the long term (`perma-block`)
bucket. Default is ``0``, which means no aging to this bucket is applied.
Like above, but only applies to the long term (`perma-block`) bucket. Default is
``0``, which means no aging to this bucket is applied.

Metrics
-------
Expand Down
6 changes: 5 additions & 1 deletion plugins/experimental/rate_limit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#
#######################

project(rate_limit)

add_atsplugin(
rate_limit
ip_reputation.cc
Expand All @@ -23,5 +25,7 @@ add_atsplugin(
sni_selector.cc
txn_limiter.cc
utilities.cc
lists.cc
)
target_link_libraries(rate_limit PRIVATE OpenSSL::SSL)

target_link_libraries(rate_limit PRIVATE libswoc yaml-cpp::yaml-cpp OpenSSL::SSL)
6 changes: 6 additions & 0 deletions plugins/experimental/rate_limit/Makefile.inc
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ experimental_rate_limit_rate_limit_la_SOURCES = \
experimental/rate_limit/sni_limiter.cc \
experimental/rate_limit/sni_selector.cc \
experimental/rate_limit/ip_reputation.cc \
experimental/rate_limit/lists.cc \
experimental/rate_limit/utilities.cc

experimental_rate_limit_rate_limit_la_LDFLAGS = \
$(AM_LDFLAGS)

AM_CPPFLAGS += @YAMLCPP_INCLUDES@
81 changes: 54 additions & 27 deletions plugins/experimental/rate_limit/ip_reputation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ SieveLru::hasher(const sockaddr *sock)
{
switch (sock->sa_family) {
case AF_INET: {
const sockaddr_in *sa = reinterpret_cast<const sockaddr_in *>(sock);
const auto *sa = reinterpret_cast<const sockaddr_in *>(sock);

return (0xffffffff00000000 | sa->sin_addr.s_addr);
} break;
case AF_INET6: {
const sockaddr_in6 *sa6 = reinterpret_cast<const sockaddr_in6 *>(sock);
const auto *sa6 = reinterpret_cast<const sockaddr_in6 *>(sock);

return (*reinterpret_cast<uint64_t const *>(sa6->sin6_addr.s6_addr) ^
*reinterpret_cast<uint64_t const *>(sa6->sin6_addr.s6_addr + sizeof(uint64_t)));
Expand Down Expand Up @@ -77,38 +77,65 @@ SieveLru::hasher(const std::string &ip, u_short family) // Mostly a convenience

return 0; // Probably can't happen, but have to return something
}
// Constructor, setting up the pre-sized LRU buckets etc.
SieveLru::SieveLru(uint32_t num_buckets, uint32_t size) : _lock(TSMutexCreate())
{
initialize(num_buckets, size);
}

// Initialize the Sieve LRU object
void
SieveLru::initialize(uint32_t num_buckets, uint32_t size)
bool
SieveLru::parseYaml(const YAML::Node &node)
{
TSMutexLock(_lock);
TSAssert(!_initialized); // Don't allow it to be initialized more than once!
TSReleaseAssert(size > num_buckets); // Otherwise we can't half the bucket sizes
if (node["buckets"]) {
_num_buckets = node["buckets"].as<uint32_t>();
}

if (node["size"]) {
_size = node["size"].as<uint32_t>();
}

if (node["percentage"]) {
_percentage = node["percentage"].as<uint32_t>();
}

if (node["max_age"]) {
_max_age = std::chrono::seconds(node["max_age"].as<uint32_t>());
}

if (node["perma-block"]) {
const YAML::Node &perma = node["perma-block"];

_initialized = true;
_num_buckets = num_buckets;
_size = size;
if (perma.IsMap()) {
if (perma["limit"]) {
_permablock_limit = perma["limit"].as<uint32_t>();
}

if (perma["threshold"]) {
_permablock_threshold = perma["threshold"].as<uint32_t>();
}

if (perma["max_age"]) {
_permablock_max_age = std::chrono::seconds(perma["max_age"].as<uint32_t>());
}
} else {
TSError("[%s] The perma-block node must be a map", PLUGIN_NAME);
return false;
}
}

uint32_t cur_size = pow(2, 1 + _size - num_buckets);
uint32_t cur_size = pow(2, 1 + _size - _num_buckets);

_map.reserve(pow(2, size + 2)); // Allow for all the sieve LRUs, and extra room for the allow list
_buckets.reserve(_num_buckets + 2); // Two extra buckets, for the block list and allow list
_map.reserve(pow(2, _size + 1)); // Allow for all the sieve LRUs
_buckets.reserve(_num_buckets + 1); // One extra bucket, for the deny list

// Create the other buckets, in smaller and smaller sizes (power of 2)
for (uint32_t i = lastBucket(); i <= entryBucket(); ++i) {
_buckets[i] = new SieveBucket(cur_size);
cur_size *= 2;
}

_buckets[blockBucket()] = new SieveBucket(cur_size / 2); // Block LRU, same size as entry bucket
_buckets[allowBucket()] = new SieveBucket(0); // Allow LRU, this is unlimited
TSMutexUnlock(_lock);

Dbg(dbg_ctl, "Loaded IP-Reputation rule: %s(%u, %u, %u, %ld)", _name.c_str(), _num_buckets, _size, _percentage,
static_cast<long>(_max_age.count()));
Dbg(dbg_ctl, "\twith perma-block rule: %s(%u, %u, %ld)", _name.c_str(), _permablock_limit, _permablock_threshold,
static_cast<long>(_permablock_max_age.count()));

return true;
}

// Increment the count for an element (will be created / added if new).
Expand All @@ -132,7 +159,8 @@ SieveLru::increment(KeyClass key)
_map.erase(l_key);
*last = {key, 1, entryBucket(), SystemClock::now()};
} else {
// Create a new entry, the date is not used now (unless perma blocked), but could be useful for aging out stale elements.
// Create a new entry, the date is not used now (unless perma blocked), but could be useful for aging out stale
// elements.
lru->push_front({key, 1, entryBucket(), SystemClock::now()});
}
_map[key] = lru->begin();
Expand All @@ -143,7 +171,7 @@ SieveLru::increment(KeyClass key)
auto &[map_key, map_item] = *map_it;
auto &[list_key, count, bucket, added] = *map_item;
auto lru = _buckets[bucket];
auto max_age = (bucket == blockBucket() ? _perma_max_age : _max_age);
auto max_age = (bucket == blockBucket() ? _permablock_max_age : _max_age);

// Check if the entry is older than max_age (if set), if so just move it to the entry bucket and restart
// Yes, this will move likely abusive IPs but they will earn back a bad reputation; The goal here is to
Expand Down Expand Up @@ -274,8 +302,7 @@ SieveLru::dump()
long long cnt = 0, sum = 0;
auto lru = _buckets[i];

std::cout << std::endl
<< "Dumping bucket " << i << " (size=" << lru->size() << ", max_size=" << lru->max_size() << ")" << std::endl;
std::cout << '\n' << "Dumping bucket " << i << " (size=" << lru->size() << ", max_size=" << lru->max_size() << ")" << '\n';
for (auto &it : *lru) {
auto &[key, count, bucket, added] = it;

Expand All @@ -288,7 +315,7 @@ SieveLru::dump()
#endif
}

std::cout << "\tAverage count=" << (cnt > 0 ? sum / cnt : 0) << std::endl;
std::cout << "\tAverage count=" << (cnt > 0 ? sum / cnt : 0) << '\n';
}
TSMutexUnlock(_lock);
}
Expand Down
Loading