From fca3e3247e7ed781722e7b71928628712db1990f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Ondr=C3=A1=C4=8Dek?= Date: Thu, 31 Oct 2024 17:28:01 +0100 Subject: [PATCH] daemon/ratelimiting: add log-period and dry-run --- daemon/lua/kres-gen-33.lua | 2 +- daemon/ratelimiting.c | 82 +++++++++++++------ daemon/ratelimiting.h | 3 +- daemon/ratelimiting.test/tests.inc.c | 2 +- doc/_static/config.schema.json | 10 +++ .../datamodel/rate_limiting_schema.py | 6 ++ .../datamodel/templates/rate_limiting.lua.j2 | 4 +- 7 files changed, 79 insertions(+), 30 deletions(-) diff --git a/daemon/lua/kres-gen-33.lua b/daemon/lua/kres-gen-33.lua index fcd3eb352..b72d01f7c 100644 --- a/daemon/lua/kres-gen-33.lua +++ b/daemon/lua/kres-gen-33.lua @@ -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[]; diff --git a/daemon/ratelimiting.c b/daemon/ratelimiting.c index a5b8f974e..805206548 100644 --- a/daemon/ratelimiting.c +++ b/daemon/ratelimiting.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +#include #include "daemon/ratelimiting.h" #include "daemon/mmapped.h" #include "lib/kru.h" @@ -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[]; @@ -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; @@ -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() }; @@ -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); @@ -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]; } @@ -115,6 +126,7 @@ 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 @@ -122,33 +134,51 @@ bool ratelimiting_request_begin(struct kr_request *req) 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); diff --git a/daemon/ratelimiting.h b/daemon/ratelimiting.h index 1e609735f..2e9ccc4de 100644 --- a/daemon/ratelimiting.h +++ b/daemon/ratelimiting.h @@ -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 diff --git a/daemon/ratelimiting.test/tests.inc.c b/daemon/ratelimiting.test/tests.inc.c index 27e935438..1434ee47a 100644 --- a/daemon/ratelimiting.test/tests.inc.c +++ b/daemon/ratelimiting.test/tests.inc.c @@ -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; diff --git a/doc/_static/config.schema.json b/doc/_static/config.schema.json index bc24cdd0b..c6934109a 100644 --- a/doc/_static/config.schema.json +++ b/doc/_static/config.schema.json @@ -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 diff --git a/python/knot_resolver/datamodel/rate_limiting_schema.py b/python/knot_resolver/datamodel/rate_limiting_schema.py index 1b6b46f26..d93272da4 100644 --- a/python/knot_resolver/datamodel/rate_limiting_schema.py +++ b/python/knot_resolver/datamodel/rate_limiting_schema.py @@ -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) @@ -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") diff --git a/python/knot_resolver/datamodel/templates/rate_limiting.lua.j2 b/python/knot_resolver/datamodel/templates/rate_limiting.lua.j2 index c25b5bb1a..4f9547f54 100644 --- a/python/knot_resolver/datamodel/templates/rate_limiting.lua.j2 +++ b/python/knot_resolver/datamodel/templates/rate_limiting.lua.j2 @@ -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 %}