]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
daemon/ratelimiting: add log-period and dry-run
authorLukáš Ondráček <lukas.ondracek@nic.cz>
Thu, 31 Oct 2024 16:28:01 +0000 (17:28 +0100)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Mon, 4 Nov 2024 13:39:46 +0000 (14:39 +0100)
daemon/lua/kres-gen-33.lua
daemon/ratelimiting.c
daemon/ratelimiting.h
daemon/ratelimiting.test/tests.inc.c
doc/_static/config.schema.json
python/knot_resolver/datamodel/rate_limiting_schema.py
python/knot_resolver/datamodel/templates/rate_limiting.lua.j2

index fcd3eb3528ff828da3f8fc10350b5efb44194e3e..4312d2184d7dffe8d70856459aa7ca32d9684405 100644 (file)
@@ -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[];
index 8f079004a952379dc2ba44b7ce5a2b7ba0cd84ff..179ce0431bfece62462550456e80e470e0eb2c76 100644 (file)
@@ -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"
@@ -22,8 +23,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[];
@@ -39,7 +43,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;
@@ -51,7 +56,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()
        };
 
@@ -60,7 +67,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);
@@ -80,6 +89,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];
                }
@@ -114,6 +125,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
 
@@ -121,33 +133,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) ?
-                                       ((kr_rand_bytes(1) % ratelimiting->slip == 0) ? 1 : 2) :
-                                       ((ratelimiting->slip == 1) ? 1 : 2);
+       const 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_prefix) return false;  // not limited
+
+       // slip: truncating vs dropping
+       bool tc =
+               (ratelimiting->slip > 1) ?
+                       ((kr_rand_bytes(1) % 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) {
+               while (time_now - log_time_orig + 1024 >= ratelimiting->log_period + 1024) {
+                       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;
+                       }
                }
        }
-       if (!limited) return false;
 
-       if (limited == 1) { // TC=1: return truncated reply to force source IP validation
+       if (ratelimiting->dry_run) return false;
+
+       // 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);
index 1e609735f0ca08fd062d99ba5fb89313fabe8a09..2e9ccc4dea08f98fddc1ce1cce6eed1371e4e207 100644 (file)
@@ -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
index 27e935438ebca7dabdcf034f1a2067caa63976f9..1434ee47ae6a729da47bee8696f33d4d0f26d855 100644 (file)
@@ -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;
index bc24cdd0be62f03338d4de48319b914a0ac89300..c6934109a3a83b2d353c6ee751192ac7dd4d959e 100644 (file)
                     "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
index 1b6b46f269d68a9701f7257c10fe866c29f905ea..d93272da49174f52dfbaa5c36b792b52d6b07620 100644 (file)
@@ -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 @@ class RateLimitingSchema(ConfigSchema):
             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")
index c25b5bb1a659edec5280e2bf2523cbd817d89050..4f9547f54fea55af196182b9314b25c5ff2d4e29 100644 (file)
@@ -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 %}