summaryrefslogtreecommitdiffstats
path: root/lib/rules/zonefile.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rules/zonefile.c')
-rw-r--r--lib/rules/zonefile.c272
1 files changed, 272 insertions, 0 deletions
diff --git a/lib/rules/zonefile.c b/lib/rules/zonefile.c
new file mode 100644
index 00000000..fc5ff1f5
--- /dev/null
+++ b/lib/rules/zonefile.c
@@ -0,0 +1,272 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+/** @file
+ *
+ * Code for loading rules from some kinds of zonefile, e.g. RPZ.
+ */
+
+#include "lib/rules/api.h"
+#include "lib/rules/impl.h"
+
+#include "lib/log.h"
+#include "lib/utils.h"
+#include "lib/generic/trie.h"
+
+#include <libzscanner/scanner.h>
+
+/// State used in zs_scanner_t::process.data
+typedef struct {
+ const struct kr_rule_zonefile_config *c; /// owned by the caller
+ trie_t *rrs; /// map: local_data_key() -> knot_rrset_t where we only use .ttl and .rrs
+ knot_mm_t *pool; /// used for everything inside s_data_t (unless noted otherwise)
+
+ // state data for owner_relativize()
+ const knot_dname_t *origin_soa;
+ bool seen_record, warned_soa, warned_bailiwick;
+} s_data_t;
+
+//TODO: logs should better include file name and position within
+
+
+/// Process scanned RR of other types, gather RRsets in a map.
+static void rr_scan2trie(zs_scanner_t *s)
+{
+ s_data_t *s_data = s->process.data;
+ uint8_t key_data[KEY_MAXLEN];
+ knot_rrset_t rrs_for_key = {
+ .owner = s->r_owner,
+ .type = s->r_type,
+ };
+ knot_db_val_t key = local_data_key(&rrs_for_key, key_data, RULESET_DEFAULT);
+ trie_val_t *rr_p = trie_get_ins(s_data->rrs, key.data, key.len);
+ knot_rrset_t *rr;
+ if (*rr_p) {
+ rr = *rr_p;
+ if (s->r_ttl < rr->ttl)
+ rr->ttl = s->r_ttl; // we could also warn here
+ } else {
+ rr = *rr_p = mm_alloc(s_data->pool, sizeof(*rr));
+ knot_rrset_init(rr, NULL, s->r_type, KNOT_CLASS_IN, s->r_ttl);
+ // we don't ^^ need owner so save allocation
+ }
+ knot_rrset_add_rdata(rr, s->r_data, s->r_data_length, s_data->pool);
+}
+/// Process an RRset of other types into a rule
+static int rr_trie2rule(const char *key_data, uint32_t key_len, trie_val_t *rr_p, void *config)
+{
+ const knot_db_val_t key = { .data = (void *)key_data, .len = key_len };
+ const knot_rrset_t *rr = *rr_p;
+ const struct kr_rule_zonefile_config *c = config;
+ return local_data_ins(key, rr, NULL, c->tags);
+ //TODO: check error logging path here (LMDB)
+}
+
+/// Process a scanned CNAME RR into a rule
+static void cname_scan2rule(zs_scanner_t *s)
+{
+ s_data_t *s_data = s->process.data;
+ const struct kr_rule_zonefile_config *c = s_data->c;
+
+ const char *last_label = NULL; // last label of the CNAME
+ for (knot_dname_t *dn = s->r_data; *dn != '\0'; dn += 1 + *dn)
+ last_label = (const char *)dn + 1;
+ if (last_label && strncmp(last_label, "rpz-", 4) == 0) {
+ kr_log_warning(RULES, "skipping unsupported CNAME target .%s\n", last_label);
+ return;
+ }
+ int ret = 0;
+ if (s->r_data[0] == 0) { // "CNAME ." i.e. NXDOMAIN
+ const knot_dname_t *apex = s->r_owner;
+ if (knot_dname_is_wildcard(apex))
+ apex += 2;
+ // RPZ_COMPAT: we NXDOMAIN the whole subtree regardless of being wildcard.
+ // Exact RPZ semantics would be hard here, it makes more sense
+ // to apply also to a subtree, and corresponding wildcard rule
+ // usually accompanies this rule anyway.
+ ret = insert_trivial_zone(VAL_ZLAT_NXDOMAIN, s->r_ttl, apex, c->tags);
+ } else if (knot_dname_is_wildcard(s->r_data) && s->r_data[2] == 0) {
+ // "CNAME *." -> NODATA
+ knot_dname_t *apex = s->r_owner;
+ if (knot_dname_is_wildcard(apex)) {
+ apex += 2;
+ ret = insert_trivial_zone(VAL_ZLAT_NODATA, s->r_ttl, apex, c->tags);
+ } else { // using special kr_rule_ semantics of empty CNAME RRset
+ knot_rrset_t rrs;
+ knot_rrset_init(&rrs, apex, KNOT_RRTYPE_CNAME,
+ KNOT_CLASS_IN, s->r_ttl);
+ ret = kr_rule_local_data_ins(&rrs, NULL, c->tags);
+ }
+ } else {
+ knot_dname_t *target = s->r_owner;
+ knot_rrset_t rrs;
+ knot_rrset_init(&rrs, target, KNOT_RRTYPE_CNAME, KNOT_CLASS_IN, s->r_ttl);
+ // TODO: implement wildcard expansion for target
+ ret = knot_rrset_add_rdata(&rrs, s->r_data, s->r_data_length, NULL);
+ if (!ret) ret = kr_rule_local_data_ins(&rrs, NULL, c->tags);
+ knot_rdataset_clear(&rrs.rrs, NULL);
+ }
+ if (ret)
+ kr_log_warning(RULES, "failure code %d\n", ret);
+}
+
+/// Relativize s->r_owner if suitable. (Also react to SOA.) Return false to skip RR.
+static bool owner_relativize(zs_scanner_t *s)
+{
+ s_data_t *d = s->process.data;
+ if (!d->c->is_rpz)
+ return true;
+
+ // SOA determines the zone apex, but lots of error/warn cases
+ if (s->r_type == KNOT_RRTYPE_SOA) {
+ if (d->seen_record && !knot_dname_is_equal(s->zone_origin, s->r_owner)) {
+ // We most likely inserted some rules wrong already, so abort.
+ kr_log_error(RULES,
+ "SOA encountered late, with unexpected owner; aborting\n");
+ s->state = ZS_STATE_STOP;
+ return false;
+ }
+ if (!d->warned_soa && (d->seen_record || d->origin_soa)) {
+ d->warned_soa = true;
+ kr_log_warning(RULES,
+ "SOA should come as the first record in a RPZ\n");
+ }
+ if (!d->origin_soa) // sticking with the first encountered SOA
+ d->origin_soa = knot_dname_copy(s->r_owner, d->pool);
+ }
+ d->seen_record = true;
+
+ // $ORIGIN as fallback if SOA is missing
+ const knot_dname_t *apex = d->origin_soa;
+ if (!apex)
+ apex = s->zone_origin;
+
+ const int labels = knot_dname_in_bailiwick(s->r_owner, apex);
+ if (labels < 0) {
+ if (!d->warned_bailiwick) {
+ d->warned_bailiwick = true;
+ KR_DNAME_GET_STR(owner_str, s->r_owner);
+ kr_log_warning(RULES,
+ "skipping out-of-zone record(s); first name %s\n",
+ owner_str);
+ }
+ return false;
+ }
+ const int len = knot_dname_prefixlen(s->r_owner, labels, NULL);
+ s->r_owner[len] = '\0'; // not very nice but safe at this point
+ return true;
+}
+
+/// Process a single scanned RR
+static void process_record(zs_scanner_t *s)
+{
+ s_data_t *s_data = s->process.data;
+ if (s->r_class != KNOT_CLASS_IN) {
+ kr_log_warning(RULES, "skipping unsupported RR class\n");
+ return;
+ }
+
+ // inspect the owner name
+ const bool ok = knot_dname_size(s->r_owner) == strlen((const char *)s->r_owner) + 1;
+ if (!ok) {
+ kr_log_warning(RULES, "skipping zero-containing RR owner name\n");
+ return;
+ }
+ // .rpz-* owner; sounds OK to warn and skip even for non-RPZ input
+ // TODO: support "rpz-client-ip"
+ const char *last_label = NULL;
+ for (knot_dname_t *dn = s->r_owner; *dn != '\0'; dn += 1 + *dn)
+ last_label = (const char *)dn + 1;
+ if (last_label && strncmp(last_label, "rpz-", 4) == 0) {
+ kr_log_warning(RULES, "skipping unsupported RR owner .%s\n", last_label);
+ return;
+ }
+ if (!owner_relativize(s))
+ return;
+
+ // RR type: mainly deal with various unsupported cases
+ switch (s->r_type) {
+ case KNOT_RRTYPE_RRSIG:
+ case KNOT_RRTYPE_NSEC:
+ case KNOT_RRTYPE_NSEC3:
+ case KNOT_RRTYPE_DNSKEY:
+ case KNOT_RRTYPE_DS:
+ unsupported_type:
+ (void)0; // C can't have a variable definition following a label
+ KR_RRTYPE_GET_STR(type_str, s->r_type);
+ kr_log_warning(RULES, "skipping unsupported RR type %s\n", type_str);
+ return;
+ }
+ if (knot_rrtype_is_metatype(s->r_type))
+ goto unsupported_type;
+ if (s_data->c->is_rpz && s->r_type == KNOT_RRTYPE_CNAME) {
+ cname_scan2rule(s);
+ return;
+ }
+ // Records in zonefile format generally may not be grouped by name and RR type,
+ // so we accumulate RR sets in a trie and push them as rules at the end.
+ rr_scan2trie(s);
+}
+
+int kr_rule_zonefile(const struct kr_rule_zonefile_config *c)
+{
+ ENSURE_the_rules;
+ zs_scanner_t s_storage, *s = &s_storage;
+ /* zs_init(), zs_set_input_file(), zs_set_processing() returns -1 in case of error,
+ * so don't print error code as it meaningless. */
+ uint32_t ttl = c->ttl ? c->ttl : RULE_TTL_DEFAULT; // 0 would be nonsense
+ int ret = zs_init(s, NULL, KNOT_CLASS_IN, ttl);
+ if (ret) {
+ kr_log_error(RULES, "error initializing zone scanner instance, error: %i (%s)\n",
+ s->error.code, zs_strerror(s->error.code));
+ return ret;
+ }
+
+ s_data_t s_data = { 0 };
+ s_data.c = c;
+ s_data.pool = mm_ctx_mempool2(64 * 1024);
+ s_data.rrs = trie_create(s_data.pool);
+ ret = zs_set_processing(s, process_record, NULL, &s_data);
+ if (kr_fails_assert(ret == 0))
+ goto finish;
+
+ // set the input to parse
+ if (c->filename) {
+ kr_assert(!c->input_str && !c->input_len);
+ ret = zs_set_input_file(s, c->filename);
+ if (ret) {
+ kr_log_error(RULES, "error opening zone file `%s`, error: %i (%s)\n",
+ c->filename, s->error.code, zs_strerror(s->error.code));
+ goto finish;
+ }
+ } else {
+ if (kr_fails_assert(c->input_str)) {
+ ret = kr_error(EINVAL);
+ } else {
+ size_t len = c->input_len ? c->input_len : strlen(c->input_str);
+ ret = zs_set_input_string(s, c->input_str, len);
+ }
+ if (ret) {
+ kr_log_error(RULES, "error %d when opening input with rules\n", ret);
+ goto finish;
+ }
+ }
+
+ /* TODO: disable $INCLUDE? In future RPZones could come from wherever.
+ * Automatic processing will do $INCLUDE, so perhaps use a manual loop instead?
+ */
+ ret = zs_parse_all(s);
+ if (ret != 0) {
+ kr_log_error(RULES, "error parsing zone file `%s`, error %i: %s\n",
+ c->filename, s->error.code, zs_strerror(s->error.code));
+ } else if (s->state == ZS_STATE_STOP) { // interrupted inside
+ ret = kr_error(EINVAL);
+ } else { // no fatal error so far
+ ret = trie_apply_with_key(s_data.rrs, rr_trie2rule, (void *)c);
+ }
+finish:
+ zs_deinit(s);
+ mm_ctx_delete(s_data.pool); // this also deletes whole s_data.rrs
+ return ret;
+}
+