diff options
Diffstat (limited to 'lib/rules/zonefile.c')
-rw-r--r-- | lib/rules/zonefile.c | 272 |
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; +} + |