Skip to content

Commit

Permalink
daemon/ratelimiting: add log-period and dry-run
Browse files Browse the repository at this point in the history
  • Loading branch information
Lukáš Ondráček committed Oct 31, 2024
1 parent 94cafb8 commit fca3e32
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 30 deletions.
2 changes: 1 addition & 1 deletion daemon/lua/kres-gen-33.lua
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct
struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
int zi_zone_import(const zi_config_t);
_Bool ratelimiting_request_begin(struct kr_request *);
int ratelimiting_init(const char *, size_t, uint32_t, uint32_t, uint16_t);
int ratelimiting_init(const char *, size_t, uint32_t, uint32_t, uint16_t, uint32_t, bool);
int defer_init(const char *, int);
struct engine {
char _stub[];
Expand Down
82 changes: 56 additions & 26 deletions daemon/ratelimiting.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

#include <stdatomic.h>
#include "daemon/ratelimiting.h"
#include "daemon/mmapped.h"
#include "lib/kru.h"
Expand All @@ -23,8 +24,11 @@ struct ratelimiting {
size_t capacity;
uint32_t instant_limit;
uint32_t rate_limit;
uint32_t log_period;
uint16_t slip;
bool dry_run;
bool using_avx2;
_Atomic uint32_t log_time;
kru_price_t v4_prices[V4_PREFIXES_CNT];
kru_price_t v6_prices[V6_PREFIXES_CNT];
_Alignas(64) uint8_t kru[];
Expand All @@ -40,7 +44,8 @@ static bool using_avx2(void)
return result;
}

int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_limit, uint32_t rate_limit, uint16_t slip)
int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_limit,
uint32_t rate_limit, uint16_t slip, uint32_t log_period, bool dry_run)
{

size_t capacity_log = 0;
Expand All @@ -52,7 +57,9 @@ int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_l
.capacity = capacity,
.instant_limit = instant_limit,
.rate_limit = rate_limit,
.log_period = log_period,
.slip = slip,
.dry_run = dry_run,
.using_avx2 = using_avx2()
};

Expand All @@ -61,7 +68,9 @@ int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_l
sizeof(header.capacity) +
sizeof(header.instant_limit) +
sizeof(header.rate_limit) +
sizeof(header.log_period) +
sizeof(header.slip) +
sizeof(header.dry_run) +
sizeof(header.using_avx2)); // no undefined padding inside

int ret = mmapped_init(&ratelimiting_mmapped, mmap_file, size, &header, header_size);
Expand All @@ -81,6 +90,8 @@ int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_l
goto fail;
}

ratelimiting->log_time = kr_now() - log_period;

for (size_t i = 0; i < V4_PREFIXES_CNT; i++) {
ratelimiting->v4_prices[i] = base_price / V4_RATE_MULT[i];
}
Expand Down Expand Up @@ -115,40 +126,59 @@ void ratelimiting_deinit(void)

bool ratelimiting_request_begin(struct kr_request *req)
{
if (!ratelimiting) return false;
if (!req->qsource.addr)
return false; // don't consider internal requests

// We only do this on pure UDP. (also TODO if cookies get implemented)
const bool ip_validated = req->qsource.flags.tcp || req->qsource.flags.tls;
if (ip_validated) return false;

uint8_t limited = 0; // 0: not limited, 1: truncated, 2: no answer
if (ratelimiting) {
_Alignas(16) uint8_t key[16] = {0, };
uint8_t limited_prefix;
if (req->qsource.addr->sa_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)req->qsource.addr;
memcpy(key, &ipv6->sin6_addr, 16);

limited_prefix = KRU.limited_multi_prefix_or((struct kru *)ratelimiting->kru, kr_now(),
1, key, V6_PREFIXES, ratelimiting->v6_prices, V6_PREFIXES_CNT, NULL);
} else {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)req->qsource.addr;
memcpy(key, &ipv4->sin_addr, 4); // TODO append port?

limited_prefix = KRU.limited_multi_prefix_or((struct kru *)ratelimiting->kru, kr_now(),
0, key, V4_PREFIXES, ratelimiting->v4_prices, V4_PREFIXES_CNT, NULL);
}
if (limited_prefix) {
limited =
(ratelimiting->slip > 1) ?
((dnssec_random_uint16_t() % ratelimiting->slip == 0) ? 1 : 2) :
((ratelimiting->slip == 1) ? 1 : 2);
}
uint32_t time_now = kr_now();

// classify
_Alignas(16) uint8_t key[16] = {0, };
uint8_t limited_prefix;
if (req->qsource.addr->sa_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)req->qsource.addr;
memcpy(key, &ipv6->sin6_addr, 16);

limited_prefix = KRU.limited_multi_prefix_or((struct kru *)ratelimiting->kru, time_now,
1, key, V6_PREFIXES, ratelimiting->v6_prices, V6_PREFIXES_CNT, NULL);
} else {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)req->qsource.addr;
memcpy(key, &ipv4->sin_addr, 4); // TODO append port?

limited_prefix = KRU.limited_multi_prefix_or((struct kru *)ratelimiting->kru, time_now,
0, key, V4_PREFIXES, ratelimiting->v4_prices, V4_PREFIXES_CNT, NULL);
}
if (!limited) return false;
if (!limited_prefix) return false; // not limited

// slip: truncating vs dropping
bool tc =
(ratelimiting->slip > 1) ?
((dnssec_random_uint16_t() % ratelimiting->slip == 0) ? true : false) :
((ratelimiting->slip == 1) ? true : false);

// logging
uint32_t log_time_orig = atomic_load_explicit(&ratelimiting->log_time, memory_order_relaxed);
if (ratelimiting->log_period && (time_now - log_time_orig + 1024 >= ratelimiting->log_period + 1024)) {
do {
if (atomic_compare_exchange_weak_explicit(&ratelimiting->log_time, &log_time_orig, time_now,
memory_order_relaxed, memory_order_relaxed)) {
kr_log_notice(SYSTEM, "address %s rate-limited on /%d (%s%s)\n",
kr_straddr(req->qsource.addr), limited_prefix,
ratelimiting->dry_run ? "dry-run, " : "",
tc ? "truncated" : "dropped");
break;
}
} while (time_now - log_time_orig + 1024 >= ratelimiting->log_period + 1024);
}

if (ratelimiting->dry_run) return false;

if (limited == 1) { // TC=1: return truncated reply to force source IP validation
// perform limiting
if (tc) { // TC=1: return truncated reply to force source IP validation
knot_pkt_t *answer = kr_request_ensure_answer(req);
if (!answer) { // something bad; TODO: perhaps improve recovery from this
kr_assert(false);
Expand Down
3 changes: 2 additions & 1 deletion daemon/ratelimiting.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ struct kr_request;
* The existing data are used if another instance is already using the file
* and it was initialized with the same parameters; it fails on mismatch. */
KR_EXPORT
int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_limit, uint32_t rate_limit, uint16_t slip);
int ratelimiting_init(const char *mmap_file, size_t capacity, uint32_t instant_limit,
uint32_t rate_limit, uint16_t slip, uint32_t log_period, bool dry_run);

/** Do rate-limiting, during knot_layer_api::begin. */
KR_EXPORT
Expand Down
2 changes: 1 addition & 1 deletion daemon/ratelimiting.test/tests.inc.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ static void test_rrl(void **state) {
const char *tmpdir = test_tmpdir_create();
char mmap_file[64];
stpcpy(stpcpy(mmap_file, tmpdir), "/ratelimiting");
ratelimiting_init(mmap_file, RRL_TABLE_SIZE, RRL_INSTANT_LIMIT, RRL_RATE_LIMIT, 0);
ratelimiting_init(mmap_file, RRL_TABLE_SIZE, RRL_INSTANT_LIMIT, RRL_RATE_LIMIT, 0, 0, false);

if (KRU.initialize == KRU_GENERIC.initialize) {
struct kru_generic *kru = (struct kru_generic *) ratelimiting->kru;
Expand Down
10 changes: 10 additions & 0 deletions doc/_static/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,16 @@
"type": "integer",
"description": "Number of restricted responses out of which one is sent as truncated, the others are dropped.",
"default": 2
},
"log-period": {
"type": "integer",
"description": "Minimal time in msec between two log messages, or zero to disable.",
"default": 0
},
"dry-run": {
"type": "boolean",
"description": "Perform only classification and logging but no restrictions.",
"default": false
}
},
"default": null
Expand Down
6 changes: 6 additions & 0 deletions python/knot_resolver/datamodel/rate_limiting_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ class RateLimitingSchema(ConfigSchema):
rate_limit: Maximal number of allowed queries per second from a single host.
instant_limit: Maximal number of allowed queries at a single point in time from a single host.
slip: Number of restricted responses out of which one is sent as truncated, the others are dropped.
log_period: Minimal time in msec between two log messages, or zero to disable.
dry_run: Perform only classification and logging but no restrictions.
"""

capacity: int = 524288
rate_limit: int
instant_limit: int = 50
slip: int = 2
log_period: int = 0
dry_run: bool = False

def _validate(self) -> None:
max_instant_limit = int(2**32 / 768 - 1)
Expand All @@ -27,3 +31,5 @@ def _validate(self) -> None:
raise ValueError("'capacity' has to be positive")
if not 0 <= self.slip <= 100:
raise ValueError("'slip' has to be in range 0..100")
if not 0 <= self.log_period:
raise ValueError("'log-period' has to be non-negative")
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ assert(C.ratelimiting_init(
{{ cfg.rate_limiting.capacity }},
{{ cfg.rate_limiting.instant_limit }},
{{ cfg.rate_limiting.rate_limit }},
{{ cfg.rate_limiting.slip }}) == 0)
{{ cfg.rate_limiting.slip }},
{{ cfg.rate_limiting.log_period }},
{{ boolean(cfg.rate_limiting.dry_run) }}) == 0)
{%- endif %}

0 comments on commit fca3e32

Please sign in to comment.