From: Vladimír Čunát Date: Sun, 3 Jul 2022 13:15:40 +0000 (+0200) Subject: lib/rules: add basic view capability X-Git-Tag: v6.0.1~9^2~20 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=15013db53a59f9c19d887d9efadd429ee222a0b8;p=thirdparty%2Fknot-resolver.git lib/rules: add basic view capability Example: assert(require('ffi').C.kr_view_insert_action( '127.0.0.0/24', 'policy.DENY_MSG("message")' ) == 0) --- diff --git a/daemon/lua/kres-gen-30.lua b/daemon/lua/kres-gen-30.lua index 968748918..1e4a28802 100644 --- a/daemon/lua/kres-gen-30.lua +++ b/daemon/lua/kres-gen-30.lua @@ -30,6 +30,10 @@ typedef struct { uint32_t size; knot_rdata_t *rdata; } knot_rdataset_t; +typedef struct knot_db_val { + void *data; + size_t len; +} knot_db_val_t; typedef struct knot_mm { void *ctx, *alloc, *free; @@ -461,6 +465,8 @@ int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t); int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int); int kr_cache_commit(struct kr_cache *); uint32_t packet_ttl(const knot_pkt_t *); +int kr_view_insert_action(const char *, const char *); +int kr_view_select_action(const struct kr_request *, knot_db_val_t *); typedef struct { int sock_type; _Bool tls; diff --git a/daemon/lua/kres-gen-31.lua b/daemon/lua/kres-gen-31.lua index c07888e9c..810560dd2 100644 --- a/daemon/lua/kres-gen-31.lua +++ b/daemon/lua/kres-gen-31.lua @@ -30,6 +30,10 @@ typedef struct { uint32_t size; knot_rdata_t *rdata; } knot_rdataset_t; +typedef struct knot_db_val { + void *data; + size_t len; +} knot_db_val_t; typedef struct knot_mm { void *ctx, *alloc, *free; @@ -461,6 +465,8 @@ int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t); int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int); int kr_cache_commit(struct kr_cache *); uint32_t packet_ttl(const knot_pkt_t *); +int kr_view_insert_action(const char *, const char *); +int kr_view_select_action(const struct kr_request *, knot_db_val_t *); typedef struct { int sock_type; _Bool tls; diff --git a/daemon/lua/kres-gen-32.lua b/daemon/lua/kres-gen-32.lua index d63fe6660..84156d05f 100644 --- a/daemon/lua/kres-gen-32.lua +++ b/daemon/lua/kres-gen-32.lua @@ -30,6 +30,10 @@ typedef struct { uint32_t size; knot_rdata_t *rdata; } knot_rdataset_t; +typedef struct knot_db_val { + void *data; + size_t len; +} knot_db_val_t; typedef struct knot_mm { void *ctx, *alloc, *free; @@ -472,6 +476,8 @@ int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t); int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int); int kr_cache_commit(struct kr_cache *); uint32_t packet_ttl(const knot_pkt_t *); +int kr_view_insert_action(const char *, const char *); +int kr_view_select_action(const struct kr_request *, knot_db_val_t *); typedef struct { int sock_type; _Bool tls; diff --git a/daemon/lua/kres-gen.sh b/daemon/lua/kres-gen.sh index 3d6fcfaf1..b56a8a38c 100755 --- a/daemon/lua/kres-gen.sh +++ b/daemon/lua/kres-gen.sh @@ -69,12 +69,13 @@ struct kr_cdb_api {}; struct lru {}; " -${CDEFS} ${LIBKRES} types <<-EOF +${CDEFS} libknot types <<-EOF knot_section_t knot_rrinfo_t knot_dname_t knot_rdata_t knot_rdataset_t + knot_db_val_t EOF # The generator doesn't work well with typedefs of functions. @@ -283,6 +284,9 @@ ${CDEFS} ${LIBKRES} functions <<-EOF kr_cache_commit # FIXME: perhaps rename this exported symbol packet_ttl +# New policy + kr_view_insert_action + kr_view_select_action EOF diff --git a/lib/rules/api.c b/lib/rules/api.c index 896eb95e9..b8f27b55c 100644 --- a/lib/rules/api.c +++ b/lib/rules/api.c @@ -34,6 +34,8 @@ struct kr_rules *the_rules = NULL; -> exact-match rule (for the given name) - KEY_ZONELIKE_A + dname_lf (no '\0' at end) -> zone-like apex (on the given name) + - KEY_VIEW_SRC4 or KEY_VIEW_SRC6 + subnet_encode() + -> action-rule string; see kr_view_insert_action() */ #define KEY_RULESET_MAXLEN 16 /**< max. len of ruleset ID + 1(for kind) */ @@ -42,6 +44,9 @@ static /*const*/ char RULESET_DEFAULT[] = "d"; static const uint8_t KEY_EXACT_MATCH[1] = "e"; static const uint8_t KEY_ZONELIKE_A [1] = "a"; +static const uint8_t KEY_VIEW_SRC4[1] = "4"; +static const uint8_t KEY_VIEW_SRC6[1] = "6"; + /** The first byte of zone-like apex value is its type. */ typedef uint8_t val_zla_type_t; enum { @@ -620,3 +625,183 @@ int kr_rule_local_data_redirect(const knot_dname_t *apex, kr_rule_tags_t tags) return insert_trivial_zone(VAL_ZLAT_REDIRECT, apex, tags); } + +/** Encode a subnet into a (longer) string. + * + * The point is to have different encodings for different subnets, + * with using just byte-length strings (e.g. for ::/1 vs. ::/2). + * And we need to preserve order: FIXME description + * - natural partial order on subnets, one included in another + * - partial order on strings, one being a prefix of another + * - implies lexicographical order on the encoded strings + * + * Consequently, given a set of subnets, the t + */ +static int subnet_encode(const struct sockaddr *addr, int sub_len, uint8_t buf[32]) +{ + const int len = kr_inaddr_len(addr); + if (kr_fails_assert(len > 0)) + return kr_error(len); + if (kr_fails_assert(sub_len >= 0 && sub_len <= 8 * len)) + return kr_error(EINVAL); + const uint8_t *a = (const uint8_t *)/*sign*/kr_inaddr(addr); + + // Algo: interleave bits of the address. Bit pairs: + // - 00 -> beyond the subnet's prefix + // - 10 -> zero bit within the subnet's prefix + // - 11 -> one bit within the subnet's prefix + // Multiplying one uint8_t by 01010101 (in binary) will do interleaving. + int i; + // Let's hope that compiler optimizes this into something reasonable. + for (i = 0; sub_len > 0; ++i, sub_len -= 8) { + uint16_t x = a[i] * 85; // interleave by zero bits + uint8_t sub_mask = 255 >> (8 - MIN(sub_len, 8)); + uint16_t r = x | (sub_mask * 85 * 2); + buf[2*i] = r / 256; + buf[2*i + 1] = r % 256; + } + return i * 2; +} + +// Is `a` subnet-prefix of `b`? (a byte format of subnet_encode()) +bool subnet_is_prefix(uint8_t a, uint8_t b) +{ + while (true) { + if (a >> 6 == 0) + return true; + if (a >> 6 != b >> 6) { + kr_assert(b >> 6 != 0); + return false; + } + a = (a << 2) & 0xff; + b = (b << 2) & 0xff; + } +} + +#define KEY_PREPEND(key, arr) do { \ + key.data -= sizeof(arr); \ + key.len += sizeof(arr); \ + memcpy(key.data, arr, sizeof(arr)); \ + } while (false) + +int kr_view_insert_action(const char *subnet, const char *action) +{ + // Parse the subnet string. + union kr_sockaddr saddr; + saddr.ip.sa_family = kr_straddr_family(subnet); + int bitlen = kr_straddr_subnet((char *)/*const-cast*/kr_inaddr(&saddr.ip), subnet); + if (bitlen < 0) return kr_error(bitlen); + + // Init the addr-based part of key. + uint8_t key_data[KEY_MAXLEN]; + knot_db_val_t key; + key.data = &key_data[KEY_RULESET_MAXLEN]; + key.len = subnet_encode(&saddr.ip, bitlen, key.data); + switch (saddr.ip.sa_family) { + case AF_INET: KEY_PREPEND(key, KEY_VIEW_SRC4); break; + case AF_INET6: KEY_PREPEND(key, KEY_VIEW_SRC6); break; + default: kr_assert(false); return kr_error(EINVAL); + } + + { // Write ruleset-specific prefix of the key. + const size_t rsp_len = strlen(RULESET_DEFAULT); + key.data -= rsp_len; + memcpy(key.data, RULESET_DEFAULT, rsp_len); + } + + // Insert & commit. + knot_db_val_t val = { + .data = (void *)/*const-cast*/action, + .len = strlen(action), + }; + int ret = ruledb_op(write, &key, &val, 1); + return ret < 0 ? ret : ruledb_op(commit); +} + +int kr_view_select_action(const struct kr_request *req, knot_db_val_t *selected) +{ + const struct sockaddr * const addr = req->qsource.addr; + if (!addr) return kr_error(ENOENT); // internal request; LATER: act somehow? + + // Init the addr-based part of key; it's pretty static. + uint8_t key_data[KEY_MAXLEN]; + knot_db_val_t key; + key.data = &key_data[KEY_RULESET_MAXLEN]; + key.len = subnet_encode(addr, kr_inaddr_len(addr) * 8, key.data); + switch (kr_inaddr_family(addr)) { + case AF_INET: KEY_PREPEND(key, KEY_VIEW_SRC4); break; + case AF_INET6: KEY_PREPEND(key, KEY_VIEW_SRC6); break; + default: kr_assert(false); return kr_error(EINVAL); + } + + int ret; + + // Init code for managing the ruleset part of the key. + // LATER(optim.): we might cache the ruleset list a bit + uint8_t * const key_data_ruleset_end = key.data; + knot_db_val_t rulesets = { NULL, 0 }; + { + uint8_t key_rs[] = "\0rulesets"; + knot_db_val_t key_rsk = { .data = key_rs, .len = sizeof(key_rs) }; + ret = ruledb_op(read, &key_rsk, &rulesets, 1); + } + if (ret != 0) return ret; // including ENOENT: no rulesets -> no rule used + const char *rulesets_str = rulesets.data; + + // Iterate over all rulesets. + while (rulesets.len > 0) { + { // Write ruleset-specific prefix of the key. + const size_t rsp_len = strnlen(rulesets_str, rulesets.len); + kr_require(rsp_len <= KEY_RULESET_MAXLEN - 1); + key.data = key_data_ruleset_end - rsp_len; + memcpy(key.data, rulesets_str, rsp_len); + rulesets_str += rsp_len + 1; + rulesets.len -= rsp_len + 1; + } + + static_assert(sizeof(KEY_VIEW_SRC4) == sizeof(KEY_VIEW_SRC6), + "bad combination of constants"); + const size_t addr_start_i = key_data_ruleset_end + sizeof(KEY_VIEW_SRC4) + - (const uint8_t *)key.data; + + knot_db_val_t key_leq = { + .data = key.data, + .len = key.len + (key_data_ruleset_end - (uint8_t *)key.data), + }; + knot_db_val_t val; + ret = ruledb_op(read_leq, &key_leq, &val); + for (; true; ret = ruledb_op(read_less, &key_leq, &val)) { + if (ret == -ENOENT) break; + if (ret < 0) return kr_error(ret); + if (ret > 0) { // found a previous key + ssize_t i = key_common_prefix(key, key_leq); + if (i < addr_start_i) // no suitable key can exist in DB + break; + if (i != key_leq.len) { + if (kr_fails_assert(i < key.len && i < key_leq.len)) + break; + if (!subnet_is_prefix(((uint8_t *)key_leq.data)[i], + ((uint8_t *)key.data)[i])) { + // the key doesn't match + // We can shorten the key to potentially + // speed up by skipping over whole subtrees. + key_leq.len = i + 1; + continue; + } + } + } + // We certainly have a matching key (join of various sub-cases). + if (kr_log_is_debug(RULES, NULL)) { + // it's complex to get zero-terminated string for the action + char act_0t[val.len + 1]; + memcpy(act_0t, val.data, val.len); + act_0t[val.len] = 0; + VERBOSE_MSG(req->rplan.initial, "=> view selected action: %s\n", + act_0t); + } + *selected = val; + return kr_ok(); + } + } + return kr_error(ENOENT); +} diff --git a/lib/rules/api.h b/lib/rules/api.h index dd3f2bf68..e9d10277e 100644 --- a/lib/rules/api.h +++ b/lib/rules/api.h @@ -5,7 +5,9 @@ #include "lib/defines.h" struct kr_query; +struct kr_request; struct knot_pkt; +#include typedef uint64_t kr_rule_tags_t; #define KR_RULE_TAGS_ALL ((kr_rule_tags_t)0) @@ -24,8 +26,17 @@ void kr_rules_deinit(void); */ int kr_rule_local_data_answer(struct kr_query *qry, struct knot_pkt *pkt); +/** Select the view action to perform. + * + * \param selected The action string from kr_view_insert_action() + * \return 0 or negative error code, in particular kr_error(ENOENT) + */ +KR_EXPORT +int kr_view_select_action(const struct kr_request *req, knot_db_val_t *selected); + + -/* API to modify the rule DB. +/* APIs to modify the rule DB. * * FIXME: * - what about transactions in this API? @@ -60,3 +71,16 @@ int kr_rule_local_data_emptyzone(const knot_dname_t *apex, kr_rule_tags_t tags); KR_EXPORT int kr_rule_local_data_redirect(const knot_dname_t *apex, kr_rule_tags_t tags); +/** Insert a view action into the default ruleset. + * + * \param subnet String specifying a subnet, e.g. "192.168.0.0/16". + * \param action Currently a string to execute, like in old policies, e.g. `policy.REFUSE` + * + * The concept of chain actions isn't respected; the most prioritized rule wins. + * If exactly the same subnet is specified repeatedly, that rule gets overwritten silently. + * TODO: improve? (return code, warning, ...) + * TODO: a well-usable action that assigns a tag-set + */ +KR_EXPORT +int kr_view_insert_action(const char *subnet, const char *action); + diff --git a/modules/policy/policy.lua b/modules/policy/policy.lua index 64689801d..1990837aa 100644 --- a/modules/policy/policy.lua +++ b/modules/policy/policy.lua @@ -834,6 +834,8 @@ end policy.rules = {} policy.postrules = {} +local view_action_buf = ffi.new('knot_db_val_t[1]') + -- Top-down policy list walk until we hit a match -- the caller is responsible for reordering policy list -- from most specific to least specific. @@ -843,6 +845,12 @@ policy.layer = { begin = function(state, req) -- Don't act on "finished" cases. if bit.band(state, bit.bor(kres.FAIL, kres.DONE)) ~= 0 then return state end + + if ffi.C.kr_view_select_action(req, view_action_buf) == 0 then + local act_str = ffi.string(view_action_buf[0].data, view_action_buf[0].len) + return loadstring('return '..act_str)()(state, req) + end + local qry = req:initial() -- same as :current() but more descriptive return policy.evaluate(policy.rules, req, qry, state) or state