diff options
author | Vladimír Čunát <vladimir.cunat@nic.cz> | 2023-09-03 17:33:39 +0200 |
---|---|---|
committer | Vladimír Čunát <vladimir.cunat@nic.cz> | 2023-10-05 12:30:48 +0200 |
commit | e4e655f32291184d90dcf2fb8baa7d304c499f99 (patch) | |
tree | 812f9f9b4dd81a21c0b3e56dbbaf251851353840 /lib | |
parent | WIP lib/rules: consider multiple tags variants when answering (diff) | |
download | knot-resolver-e4e655f32291184d90dcf2fb8baa7d304c499f99.tar.xz knot-resolver-e4e655f32291184d90dcf2fb8baa7d304c499f99.zip |
/views/*/{dst_subnet,protocols}: add, both backend+config
Examples:
- tagging based on dst_subnet is useful for providing different
filtering setting on different resolver addresses
- tagging based on protocols is useful to signal used transport
(change in DNS data that can be read by the final app)
(docs added in a later commit)
Diffstat (limited to 'lib')
-rw-r--r-- | lib/resolve.h | 2 | ||||
-rw-r--r-- | lib/rules/api.c | 127 | ||||
-rw-r--r-- | lib/rules/api.h | 41 |
3 files changed, 144 insertions, 26 deletions
diff --git a/lib/resolve.h b/lib/resolve.h index 2ead6e26..5fc66541 100644 --- a/lib/resolve.h +++ b/lib/resolve.h @@ -186,7 +186,7 @@ int kr_resolver_init(module_array_t *modules, knot_mm_t *pool); KR_EXPORT void kr_resolver_deinit(void); -/* Kept outside, because kres-gen.lua can't handle this depth +/* Kept outside struct kr_request, because kres-gen.lua can't handle this depth * (and lines here were too long anyway). */ struct kr_request_qsource_flags { bool tcp:1; /**< true if the request is not on UDP; only meaningful if (dst_addr). */ diff --git a/lib/rules/api.c b/lib/rules/api.c index a656eb7e..cdedd2ae 100644 --- a/lib/rules/api.c +++ b/lib/rules/api.c @@ -31,7 +31,7 @@ const uint32_t KR_RULE_TTL_DEFAULT = 300; - 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() + -> conditions + action-rule string; see kr_view_insert_action() */ /*const*/ char RULESET_DEFAULT[] = "d"; @@ -874,8 +874,10 @@ bool subnet_is_prefix(uint8_t a, uint8_t b) memcpy(key.data, arr, sizeof(arr)); \ } while (false) -int kr_view_insert_action(const char *subnet, const char *action) +int kr_view_insert_action(const char *subnet, const char *dst_subnet, + kr_proto_set protos, const char *action) { + if (*dst_subnet == '\0') dst_subnet = NULL; // convenience for the API ENSURE_the_rules; // Parse the subnet string. union kr_sockaddr saddr; @@ -901,15 +903,83 @@ int kr_view_insert_action(const char *subnet, const char *action) memcpy(key.data, RULESET_DEFAULT, rsp_len); } - // Insert. We overwrite, as subnet is the only condition so far and that's in key. - knot_db_val_t val = { - .data = (void *)/*const-cast*/action, - .len = strlen(action), - }; - int ret = ruledb_op(remove, &key, 1); kr_assert(ret == 0 || ret == 1); + // We have the key; start constructing the value to insert. + const int dst_maxlen = 1 + (dst_subnet ? kr_family_len(saddr.ip.sa_family) : 0); + const int action_len = strlen(action); + uint8_t buf[sizeof(protos) + dst_maxlen + action_len]; + uint8_t *data = buf; + int dlen = 0; + + memcpy(data, &protos, sizeof(protos)); + data += sizeof(protos); + dlen += sizeof(protos); + + uint8_t dst_bitlen = 0; + if (dst_subnet) { + // For simplicity, we always write the whole address, + // even if some bytes at the end are useless (keep it iff dst_bitlen > 0). + int ret = kr_straddr_subnet(data + sizeof(dst_bitlen), dst_subnet); + if (ret < 0) { + kr_log_error(RULES, "failed to parse destination subnet: %s\n", + dst_subnet); + return kr_error(ret); + } + if (saddr.ip.sa_family != kr_straddr_family(dst_subnet)) { + kr_log_error(RULES, + "destination subnet mismatching IPv4 vs. IPv6: %s\n", + dst_subnet); + return kr_error(EINVAL); + } + dst_bitlen = ret; + } + memcpy(data, &dst_bitlen, sizeof(dst_bitlen)); + if (dst_bitlen > 0) { + data += dst_maxlen; // address bytes already written above + dlen += dst_maxlen; + } else { + data += sizeof(dst_bitlen); + dlen += sizeof(dst_bitlen); + } + + memcpy(data, action, action_len); + data += action_len; + dlen += action_len; + + kr_require(data <= buf + dlen); + knot_db_val_t val = { .data = buf, .len = dlen }; return ruledb_op(write, &key, &val, 1); } +static enum kr_proto req_proto(const struct kr_request *req) +{ + if (!req->qsource.addr) + return KR_PROTO_INTERNAL; + const struct kr_request_qsource_flags fl = req->qsource.flags; + if (fl.http) + return KR_PROTO_DOH; + if (fl.tcp) + return fl.tls ? KR_PROTO_DOT : KR_PROTO_TCP53; + // UDP in some form + return fl.tls ? KR_PROTO_DOQ : KR_PROTO_UDP53; +} +static bool req_proto_matches(const struct kr_request *req, kr_proto_set proto_set) +{ + if (!proto_set) // empty set always matches + return true; + kr_proto_set mask = 1 << req_proto(req); + return mask & proto_set; +} +static void log_action(const struct kr_request *req, knot_db_val_t act) +{ + if (!kr_log_is_debug(RULES, req)) + return; + // it's complex to get zero-terminated string for the action + char act_0t[act.len + 1]; + memcpy(act_0t, act.data, act.len); + act_0t[act.len] = 0; + VERBOSE_MSG(req->rplan.initial, "=> view selected action: %s\n", act_0t); +} + int kr_view_select_action(const struct kr_request *req, knot_db_val_t *selected) { kr_require(the_rules); @@ -983,18 +1053,37 @@ int kr_view_select_action(const struct kr_request *req, knot_db_val_t *selected) } } // We certainly have a matching key (join of various sub-cases). - // LATER: we'd iterate on this key's entries and find one - // that matches additional conditions (optional ones in future) - 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); + // But multiple variants are possible, and conditions inside values. + for (ret = ruledb_op(it_first, &key_leq, &val); + ret == 0; + ret = ruledb_op(it_next, &val)) { + kr_proto_set protos; + if (deserialize_fails_assert(&val, &protos)) + continue; + if (!req_proto_matches(req, protos)) + continue; + uint8_t dst_bitlen; + if (deserialize_fails_assert(&val, &dst_bitlen)) + continue; + if (dst_bitlen) { + const int abytes = kr_inaddr_len(addr); + const char *dst_a = kr_inaddr(req->qsource.dst_addr); + if (kr_fails_assert(val.len >= abytes)) + continue; + if (kr_bitcmp(val.data, dst_a, dst_bitlen) != 0) + continue; + val.data += abytes; + val.len -= abytes; + } + // we passed everything; `val` contains just the action + log_action(req, val); + *selected = val; + return kr_ok(); } - *selected = val; - return kr_ok(); + // Key matched but none of the condition variants; + // we may still get a match with a wider subnet rule -> continue. + // LATER(optim.): it's possible that something could be made + // somewhat faster in this various jumping around keys. } } return kr_error(ENOENT); diff --git a/lib/rules/api.h b/lib/rules/api.h index 52715c5c..d6bf97a1 100644 --- a/lib/rules/api.h +++ b/lib/rules/api.h @@ -16,6 +16,27 @@ typedef uint64_t kr_rule_tags_t; /// Tags "capacity", i.e. numbered from 0 to _CAP - 1. #define KR_RULE_TAGS_CAP (sizeof(kr_rule_tags_t) * 8) +/** DNS protocol set - mutually exclusive options, contrary to kr_request_qsource_flags + * + * The XDP flag is not discerned here, as it could apply to any protocol. + * (not right now, but libknot does support it for TCP, so that would complete everything) + * + * TODO: probably unify with enum protolayer_grp. + */ +enum kr_proto { + KR_PROTO_INTERNAL = 0, /// no protocol, e.g. useful to mark internal requests + KR_PROTO_UDP53, + KR_PROTO_TCP53, + KR_PROTO_DOT, + KR_PROTO_DOH, + KR_PROTO_DOQ, /// unused for now + KR_PROTO_COUNT, +}; +/** Bitmap of enum kr_proto options. */ +typedef uint8_t kr_proto_set; +static_assert(sizeof(kr_proto_set) * 8 >= KR_PROTO_COUNT, "bad combination of type sizes"); + + /** Open the rule DB. * * You can call this to override the path or size (NULL/0 -> default). @@ -149,16 +170,24 @@ int kr_rule_local_subtree(const knot_dname_t *apex, enum kr_rule_sub_t type, /** Insert a view action into the default ruleset. * * \param subnet String specifying a subnet, e.g. "192.168.0.0/16". + * \param dst_subnet String specifying a subnet to be matched by the destination address. (or empty/NULL) + * \param protos Set of transport protocols. (or 0 to always match) * \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: some way to do multiple actions? Will be useful e.g. with option-setting actions. - * On implementation side this would probably be multi-value LMDB, cf. local_data rules. + * TODO: improve? (return code, warning, ...) Internal queries never get matched. + * + * The concept of chain actions isn't respected; at most one action is chosen. + * The winner needs to fulfill all conditions. Closer subnet match is preferred, + * but otherwise the priority is unspecified (it is deterministic, though). + * + * There's no detection of action rules that clash in this way, + * even if all conditions match exactly. + * TODO we might consider some overwriting semantics, + * but the additional conditions make that harder. */ KR_EXPORT -int kr_view_insert_action(const char *subnet, const char *action); +int kr_view_insert_action(const char *subnet, const char *dst_subnet, + kr_proto_set protos, const char *action); /** Add a tag by name into a tag-set variable. * |