summaryrefslogtreecommitdiffstats
path: root/src/knot/modules/synthrecord/synthrecord.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/modules/synthrecord/synthrecord.c')
-rw-r--r--src/knot/modules/synthrecord/synthrecord.c625
1 files changed, 625 insertions, 0 deletions
diff --git a/src/knot/modules/synthrecord/synthrecord.c b/src/knot/modules/synthrecord/synthrecord.c
new file mode 100644
index 0000000..d7af9a1
--- /dev/null
+++ b/src/knot/modules/synthrecord/synthrecord.c
@@ -0,0 +1,625 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "contrib/ctype.h"
+#include "contrib/macros.h"
+#include "contrib/net.h"
+#include "contrib/sockaddr.h"
+#include "contrib/wire_ctx.h"
+#include "knot/include/module.h"
+
+#define MOD_NET "\x07""network"
+#define MOD_ORIGIN "\x06""origin"
+#define MOD_PREFIX "\x06""prefix"
+#define MOD_TTL "\x03""ttl"
+#define MOD_TYPE "\x04""type"
+#define MOD_SHORT "\x0d""reverse-short"
+
+/*! \brief Supported answer synthesis template types. */
+enum synth_template_type {
+ SYNTH_NULL = 0,
+ SYNTH_FORWARD = 1,
+ SYNTH_REVERSE = 2
+};
+
+static const knot_lookup_t synthetic_types[] = {
+ { SYNTH_FORWARD, "forward" },
+ { SYNTH_REVERSE, "reverse" },
+ { 0, NULL }
+};
+
+int check_prefix(knotd_conf_check_args_t *args)
+{
+ if (strchr((const char *)args->data, '.') != NULL) {
+ args->err_str = "dot '.' is not allowed";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+const yp_item_t synth_record_conf[] = {
+ { MOD_TYPE, YP_TOPT, YP_VOPT = { synthetic_types, SYNTH_NULL } },
+ { MOD_PREFIX, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { check_prefix } },
+ { MOD_ORIGIN, YP_TDNAME, YP_VNONE },
+ { MOD_TTL, YP_TINT, YP_VINT = { 0, UINT32_MAX, 3600, YP_STIME } },
+ { MOD_NET, YP_TNET, YP_VNONE, YP_FMULTI },
+ { MOD_SHORT, YP_TBOOL, YP_VBOOL = { true } },
+ { NULL }
+};
+
+int synth_record_conf_check(knotd_conf_check_args_t *args)
+{
+ // Check type.
+ knotd_conf_t type = knotd_conf_check_item(args, MOD_TYPE);
+ if (type.count == 0) {
+ args->err_str = "no synthesis type specified";
+ return KNOT_EINVAL;
+ }
+
+ // Check origin.
+ knotd_conf_t origin = knotd_conf_check_item(args, MOD_ORIGIN);
+ if (origin.count == 0 && type.single.option == SYNTH_REVERSE) {
+ args->err_str = "no origin specified";
+ return KNOT_EINVAL;
+ }
+ if (origin.count != 0 && type.single.option == SYNTH_FORWARD) {
+ args->err_str = "origin not allowed with forward type";
+ return KNOT_EINVAL;
+ }
+
+ // Check network subnet.
+ knotd_conf_t net = knotd_conf_check_item(args, MOD_NET);
+ if (net.count == 0) {
+ args->err_str = "no network subnet specified";
+ return KNOT_EINVAL;
+ }
+ knotd_conf_free(&net);
+
+ // Check reverse-short parameter is only for reverse synthrecord.
+ knotd_conf_t reverse_short = knotd_conf_check_item(args, MOD_SHORT);
+ if (reverse_short.count != 0 && type.single.option == SYNTH_FORWARD) {
+ args->err_str = "reverse-short not allowed with forward type";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+#define ARPA_ZONE_LABELS 2
+#define IPV4_ADDR_LABELS 4
+#define IPV6_ADDR_LABELS 32
+#define IPV4_ARPA_DNAME (uint8_t *)"\x07""in-addr""\x04""arpa"
+#define IPV6_ARPA_DNAME (uint8_t *)"\x03""ip6""\x04""arpa"
+#define IPV4_ARPA_LEN 14
+#define IPV6_ARPA_LEN 10
+
+/*!
+ * \brief Synthetic response template.
+ */
+typedef struct {
+ struct sockaddr_storage addr;
+ struct sockaddr_storage addr_max;
+ int addr_mask;
+} synth_templ_addr_t;
+
+typedef struct {
+ enum synth_template_type type;
+ char *prefix;
+ size_t prefix_len;
+ char *zone;
+ size_t zone_len;
+ uint32_t ttl;
+ size_t addr_count;
+ synth_templ_addr_t *addr;
+ bool reverse_short;
+} synth_template_t;
+
+typedef union {
+ uint32_t b32;
+ uint8_t b4[4];
+} addr_block_t;
+
+/*! \brief Write one IPV4 address block without redundant leading zeros. */
+static unsigned block_write(addr_block_t *block, char *addr_str)
+{
+ unsigned len = 0;
+
+ if (block->b4[0] != '0') {
+ addr_str[len++] = block->b4[0];
+ }
+ if (len > 0 || block->b4[1] != '0') {
+ addr_str[len++] = block->b4[1];
+ }
+ if (len > 0 || block->b4[2] != '0') {
+ addr_str[len++] = block->b4[2];
+ }
+ addr_str[len++] = block->b4[3];
+
+ return len;
+}
+
+/*! \brief Substitute all occurrences of given character. */
+static void str_subst(char *str, size_t len, char from, char to)
+{
+ for (int i = 0; i < len; ++i) {
+ if (str[i] == from) {
+ str[i] = to;
+ }
+ }
+}
+
+/*! \brief Separator character for address family. */
+static char str_separator(int addr_family)
+{
+ return (addr_family == AF_INET6) ? ':' : '.';
+}
+
+/*! \brief Return true if query type is satisfied with provided address family. */
+static bool query_satisfied_by_family(uint16_t qtype, int family)
+{
+ switch (qtype) {
+ case KNOT_RRTYPE_A: return family == AF_INET;
+ case KNOT_RRTYPE_AAAA: return family == AF_INET6;
+ case KNOT_RRTYPE_ANY: return true;
+ default: return false;
+ }
+}
+
+/*! \brief Parse address from reverse query QNAME and return address family. */
+static int reverse_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
+ char *addr_str, int *addr_family, bool *parent)
+{
+ /* QNAME required format is [address].[subnet/zone]
+ * f.e. [1.0...0].[h.g.f.e.0.0.0.0.d.c.b.a.ip6.arpa] represents
+ * [abcd:0:efgh::1] */
+ const knot_dname_t *label = qdata->name; // uncompressed name
+
+ static const char ipv4_zero[] = "0.0.0.0";
+
+ bool can_ipv4 = true;
+ bool can_ipv6 = true;
+ unsigned labels = 0;
+
+ uint8_t buf4[16], *buf4_end = buf4 + sizeof(buf4), *buf4_pos = buf4_end;
+ uint8_t buf6[32], *buf6_end = buf6 + sizeof(buf6), *buf6_pos = buf6_end;
+
+ for ( ; labels < IPV6_ADDR_LABELS; labels++) {
+ if (unlikely(*label == 0)) {
+ return KNOT_EINVAL;
+ }
+ if (label[1] == 'i') {
+ break;
+ }
+ if (labels < IPV4_ADDR_LABELS) {
+ switch (*label) {
+ case 1:
+ assert(buf4 + 1 < buf4_pos && buf6 < buf6_pos);
+ *--buf6_pos = label[1];
+ *--buf4_pos = label[1];
+ *--buf4_pos = '.';
+ break;
+ case 2:
+ case 3:
+ assert(buf4 + *label < buf4_pos);
+ can_ipv6 = false;
+ buf4_pos -= *label;
+ memcpy(buf4_pos, label + 1, *label);
+ *--buf4_pos = '.';
+ break;
+ case 4:
+ case 5:
+ case 6: // Ignore second possibly classless label (e.g. 0/25, 193/26).
+ if (labels-- != 1) {
+ return KNOT_EINVAL;
+ }
+ can_ipv6 = false;
+ break;
+ default:
+ return KNOT_EINVAL;
+ }
+ } else {
+ can_ipv4 = false;
+ if (!can_ipv6 || *label != 1) {
+ return KNOT_EINVAL;
+ }
+ assert(buf6 < buf6_pos);
+ *--buf6_pos = label[1];
+
+ }
+ label += *label + sizeof(*label);
+ }
+
+ if (can_ipv4 && knot_dname_is_equal(label, IPV4_ARPA_DNAME)) {
+ *addr_family = AF_INET;
+ *parent = (labels < IPV4_ADDR_LABELS);
+ int buf4_overweight = (buf4_end - buf4_pos) - (2 * labels);
+ assert(buf4_overweight >= 0);
+ memcpy(addr_str + buf4_overweight, ipv4_zero, sizeof(ipv4_zero));
+ if (labels > 0) {
+ buf4_pos++; // skip leading '.'
+ memcpy(addr_str, buf4_pos, buf4_end - buf4_pos);
+ }
+ return KNOT_EOK;
+ } else if (can_ipv6 && knot_dname_is_equal(label, IPV6_ARPA_DNAME)) {
+ *addr_family = AF_INET6;
+ *parent = (labels < IPV6_ADDR_LABELS);
+
+ addr_block_t blocks[8] = { { 0 } };
+ int compr_start = -1, compr_end = -1;
+
+ unsigned buf6_len = buf6_end - buf6_pos;
+ memcpy(blocks, buf6_pos, buf6_len);
+ memset(((uint8_t *)blocks) + buf6_len, 0x30, sizeof(blocks) - buf6_len);
+
+ for (int i = 0; i < 8; i++) {
+ addr_block_t *block = &blocks[i];
+
+ /* The Unicode string MUST NOT contain "--" in the third and fourth
+ character positions and MUST NOT start or end with a "-".
+ So we will not compress first, second, and last address blocks
+ for simplicity. And we will not compress a single block.
+
+ i: 0 1 2 3 4 5 6 7
+ label block: H:G:F:E:D:C:B:A
+ address block: A B C D E F G H
+ compressibles: 0 0 0 0 0
+ 0 0 0 0
+ 0 0 0
+ 0 0
+ */
+ // Check for trailing zero dual-blocks.
+ if (tpl->reverse_short && i > 1 && i < 6 &&
+ block[0].b32 == 0x30303030UL && block[1].b32 == 0x30303030UL) {
+ if (compr_start == -1) {
+ compr_start = i;
+ }
+ } else {
+ if (compr_start != -1 && compr_end == -1) {
+ compr_end = i;
+ }
+ }
+ }
+
+ // Write address blocks.
+ unsigned addr_len = 0;
+ for (int i = 0; i < 8; i++) {
+ if (compr_start == -1 || i < compr_start || i > compr_end) {
+ // Write regular address block.
+ if (tpl->reverse_short) {
+ addr_len += block_write(&blocks[i], addr_str + addr_len);
+ } else {
+ assert(sizeof(blocks[i]) == 4);
+ memcpy(addr_str + addr_len, &blocks[i], 4);
+ addr_len += 4;
+ }
+ // Write separator
+ if (i < 7) {
+ addr_str[addr_len++] = ':';
+ }
+ } else if (compr_start != -1 && compr_end == i) {
+ // Write compression double colon.
+ addr_str[addr_len++] = ':';
+ }
+ }
+ addr_str[addr_len] = '\0';
+
+ return KNOT_EOK;
+ }
+
+ return KNOT_EINVAL;
+}
+
+static int forward_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
+ char *addr_str, int *addr_family)
+{
+ const knot_dname_t *label = qdata->name;
+
+ // Check for prefix mismatch.
+ if (label[0] <= tpl->prefix_len ||
+ memcmp(label + 1, tpl->prefix, tpl->prefix_len) != 0) {
+ return KNOT_EINVAL;
+ }
+
+ // Copy address part.
+ unsigned addr_len = label[0] - tpl->prefix_len;
+ memcpy(addr_str, label + 1 + tpl->prefix_len, addr_len);
+ addr_str[addr_len] = '\0';
+
+ // Determine address family.
+ unsigned hyphen_cnt = 0;
+ const char *ch = addr_str;
+ while (hyphen_cnt < 4 && ch < addr_str + addr_len) {
+ if (*ch == '-') {
+ hyphen_cnt++;
+ if (*++ch == '-') { // Check for shortened IPv6 notation.
+ hyphen_cnt = 4;
+ break;
+ }
+ }
+ ch++;
+ }
+ // Valid IPv4 address looks like A-B-C-D.
+ *addr_family = (hyphen_cnt == 3) ? AF_INET : AF_INET6;
+
+ // Restore correct address format.
+ const char sep = str_separator(*addr_family);
+ str_subst(addr_str, addr_len, '-', sep);
+
+ return KNOT_EOK;
+}
+
+static int addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl, char *addr_str,
+ int *addr_family, bool *parent)
+{
+ switch (tpl->type) {
+ case SYNTH_REVERSE: return reverse_addr_parse(qdata, tpl, addr_str, addr_family, parent);
+ case SYNTH_FORWARD: return forward_addr_parse(qdata, tpl, addr_str, addr_family);
+ default: return KNOT_EINVAL;
+ }
+}
+
+static knot_dname_t *synth_ptrname(uint8_t *out, const char *addr_str,
+ const synth_template_t *tpl, int addr_family)
+{
+ knot_dname_txt_storage_t ptrname;
+ int addr_len = strlen(addr_str);
+ const char sep = str_separator(addr_family);
+
+ // PTR right-hand value is [prefix][address][zone]
+ wire_ctx_t ctx = wire_ctx_init((uint8_t *)ptrname, sizeof(ptrname));
+ wire_ctx_write(&ctx, tpl->prefix, tpl->prefix_len);
+ wire_ctx_write(&ctx, addr_str, addr_len);
+ wire_ctx_write_u8(&ctx, '.');
+ wire_ctx_write(&ctx, tpl->zone, tpl->zone_len);
+ wire_ctx_write_u8(&ctx, '\0');
+ if (ctx.error != KNOT_EOK) {
+ return NULL;
+ }
+
+ // Substitute address separator by '-'.
+ str_subst(ptrname + tpl->prefix_len, addr_len, sep, '-');
+
+ // Convert to domain name.
+ return knot_dname_from_str(out, ptrname, KNOT_DNAME_MAXLEN);
+}
+
+static int reverse_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knot_rrset_t *rr, int addr_family)
+{
+ // Synthesize PTR record data.
+ knot_dname_storage_t ptrname;
+ if (synth_ptrname(ptrname, addr_str, tpl, addr_family) == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ rr->type = KNOT_RRTYPE_PTR;
+ knot_rrset_add_rdata(rr, ptrname, knot_dname_size(ptrname), &pkt->mm);
+
+ return KNOT_EOK;
+}
+
+static int forward_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knot_rrset_t *rr, int addr_family)
+{
+ struct sockaddr_storage query_addr;
+ sockaddr_set(&query_addr, addr_family, addr_str, 0);
+
+ // Specify address type and data.
+ if (addr_family == AF_INET6) {
+ rr->type = KNOT_RRTYPE_AAAA;
+ const struct sockaddr_in6* ip = (const struct sockaddr_in6*)&query_addr;
+ knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin6_addr,
+ sizeof(struct in6_addr), &pkt->mm);
+ } else if (addr_family == AF_INET) {
+ rr->type = KNOT_RRTYPE_A;
+ const struct sockaddr_in* ip = (const struct sockaddr_in*)&query_addr;
+ knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin_addr,
+ sizeof(struct in_addr), &pkt->mm);
+ } else {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static knot_rrset_t *synth_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, int addr_family)
+{
+ knot_rrset_t *rr = knot_rrset_new(qdata->name, 0, KNOT_CLASS_IN, tpl->ttl,
+ &pkt->mm);
+ if (rr == NULL) {
+ return NULL;
+ }
+
+ // Fill in the specific data.
+ int ret = KNOT_ERROR;
+ switch (tpl->type) {
+ case SYNTH_REVERSE: ret = reverse_rr(addr_str, tpl, pkt, rr, addr_family); break;
+ case SYNTH_FORWARD: ret = forward_rr(addr_str, tpl, pkt, rr, addr_family); break;
+ default: break;
+ }
+
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(rr, &pkt->mm);
+ return NULL;
+ }
+
+ return rr;
+}
+
+/*! \brief Check if query fits the template requirements. */
+static knotd_in_state_t template_match(knotd_in_state_t state, const synth_template_t *tpl,
+ knot_pkt_t *pkt, knotd_qdata_t *qdata)
+{
+ int provided_af = AF_UNSPEC;
+ struct sockaddr_storage query_addr;
+ char addr_str[SOCKADDR_STRLEN];
+ assert(SOCKADDR_STRLEN > KNOT_DNAME_MAXLABELLEN);
+ bool parent = false; // querying empty-non-terminal being (possibly indirect) parent of synthesized name
+
+ // Parse address from query name.
+ if (addr_parse(qdata, tpl, addr_str, &provided_af, &parent) != KNOT_EOK ||
+ sockaddr_set(&query_addr, provided_af, addr_str, 0) != KNOT_EOK) {
+ return state;
+ }
+
+ // Try all available addresses.
+ int i;
+ for (i = 0; i < tpl->addr_count; i++) {
+ if (tpl->addr[i].addr_max.ss_family == AF_UNSPEC) {
+ if (sockaddr_net_match(&query_addr, &tpl->addr[i].addr,
+ tpl->addr[i].addr_mask)) {
+ break;
+ }
+ } else {
+ if (sockaddr_range_match(&query_addr, &tpl->addr[i].addr,
+ &tpl->addr[i].addr_max)) {
+ break;
+ }
+ }
+ }
+ if (i >= tpl->addr_count) {
+ return state;
+ }
+
+ // Check if the request is for an available query type.
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+ switch (tpl->type) {
+ case SYNTH_FORWARD:
+ assert(!parent);
+ if (!query_satisfied_by_family(qtype, provided_af)) {
+ qdata->rcode = KNOT_RCODE_NOERROR;
+ return KNOTD_IN_STATE_NODATA;
+ }
+ break;
+ case SYNTH_REVERSE:
+ if (parent || (qtype != KNOT_RRTYPE_PTR && qtype != KNOT_RRTYPE_ANY)) {
+ qdata->rcode = KNOT_RCODE_NOERROR;
+ return KNOTD_IN_STATE_NODATA;
+ }
+ break;
+ default:
+ return state;
+ }
+
+ // Synthesize record from template.
+ knot_rrset_t *rr = synth_rr(addr_str, tpl, pkt, qdata, provided_af);
+ if (rr == NULL) {
+ qdata->rcode = KNOT_RCODE_SERVFAIL;
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ // Insert synthetic response into packet.
+ if (knot_pkt_put(pkt, 0, rr, KNOT_PF_FREE) != KNOT_EOK) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ // Authoritative response.
+ knot_wire_set_aa(pkt->wire);
+
+ return KNOTD_IN_STATE_HIT;
+}
+
+static knotd_in_state_t solve_synth_record(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ // Applicable when search in zone fails.
+ if (state != KNOTD_IN_STATE_MISS) {
+ return state;
+ }
+
+ // Check if template fits.
+ return template_match(state, knotd_mod_ctx(mod), pkt, qdata);
+}
+
+int synth_record_load(knotd_mod_t *mod)
+{
+ // Create synthesis template.
+ synth_template_t *tpl = calloc(1, sizeof(*tpl));
+ if (tpl == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Set type.
+ knotd_conf_t conf = knotd_conf_mod(mod, MOD_TYPE);
+ tpl->type = conf.single.option;
+
+ /* Set prefix. */
+ conf = knotd_conf_mod(mod, MOD_PREFIX);
+ tpl->prefix = strdup(conf.single.string);
+ tpl->prefix_len = strlen(tpl->prefix);
+
+ // Set origin if generating reverse record.
+ if (tpl->type == SYNTH_REVERSE) {
+ conf = knotd_conf_mod(mod, MOD_ORIGIN);
+ tpl->zone = knot_dname_to_str_alloc(conf.single.dname);
+ if (tpl->zone == NULL) {
+ free(tpl->prefix);
+ free(tpl);
+ return KNOT_ENOMEM;
+ }
+ tpl->zone_len = strlen(tpl->zone);
+ }
+
+ // Set ttl.
+ conf = knotd_conf_mod(mod, MOD_TTL);
+ tpl->ttl = conf.single.integer;
+
+ // Set address.
+ conf = knotd_conf_mod(mod, MOD_NET);
+ tpl->addr_count = conf.count;
+ tpl->addr = calloc(conf.count, sizeof(*tpl->addr));
+ if (tpl->addr == NULL) {
+ knotd_conf_free(&conf);
+ free(tpl->zone);
+ free(tpl->prefix);
+ free(tpl);
+ return KNOT_ENOMEM;
+ }
+ for (size_t i = 0; i < conf.count; i++) {
+ tpl->addr[i].addr = conf.multi[i].addr;
+ tpl->addr[i].addr_max = conf.multi[i].addr_max;
+ tpl->addr[i].addr_mask = conf.multi[i].addr_mask;
+ }
+ knotd_conf_free(&conf);
+
+ // Set address shortening.
+ if (tpl->type == SYNTH_REVERSE) {
+ conf = knotd_conf_mod(mod, MOD_SHORT);
+ tpl->reverse_short = conf.single.boolean;
+ }
+
+ knotd_mod_ctx_set(mod, tpl);
+
+ return knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, solve_synth_record);
+}
+
+void synth_record_unload(knotd_mod_t *mod)
+{
+ synth_template_t *tpl = knotd_mod_ctx(mod);
+
+ free(tpl->addr);
+ free(tpl->zone);
+ free(tpl->prefix);
+ free(tpl);
+}
+
+KNOTD_MOD_API(synthrecord, KNOTD_MOD_FLAG_SCOPE_ZONE,
+ synth_record_load, synth_record_unload, synth_record_conf,
+ synth_record_conf_check);