/* Copyright (C) 2024 CZ.NIC, z.s.p.o. 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 . */ #include #include "libknot/attribute.h" #include "libknot/consts.h" #include "libknot/descriptor.h" #include "libknot/packet/pkt.h" #include "libknot/packet/rrset-wire.h" #include "libknot/rrtype/naptr.h" #include "libknot/rrtype/rrsig.h" #include "libknot/rrtype/soa.h" #include "contrib/macros.h" #include "contrib/mempattern.h" #include "contrib/tolower.h" #include "contrib/wire_ctx.h" /*! * \brief Get maximal size of a domain name in a wire with given capacity. */ static uint16_t dname_max(size_t wire_avail) { return MIN(wire_avail, KNOT_DNAME_MAXLEN); } /*! * \brief Compares two domain name labels. * * \param label1 First label. * \param label2 Second label (may be in upper-case). * * \retval true if the labels are identical * \retval false if the labels are NOT identical */ static bool label_is_equal(const uint8_t *label1, const uint8_t *label2) { assert(label1 && label2); if (*label1 != *label2) { return false; } uint8_t len = *label1; for (uint8_t i = 1; i <= len; i++) { if (label1[i] != knot_tolower(label2[i])) { return false; } } return true; } /*! * Case insensitive comparison of two dnames in wire format. * The second name may be compressed in a supplied wire. */ static bool dname_equal_wire(const knot_dname_t *d1, const knot_dname_t *d2, const uint8_t *wire) { assert(d1); assert(d2); assert(wire); d2 = knot_wire_seek_label(d2, wire); while (*d1 != '\0' || *d2 != '\0') { if (!label_is_equal(d1, d2)) { return false; } d1 = knot_dname_next_label(d1); d2 = knot_wire_next_label(d2, wire); } return true; } static uint16_t compr_get_ptr(knot_compr_t *compr, uint16_t hint) { if (compr == NULL) { return 0; } return knot_compr_hint(compr->rrinfo, hint); } static void compr_set_ptr(knot_compr_t *compr, uint16_t hint, const uint8_t *written_at, uint16_t written_size) { if (compr == NULL) { return; } assert(written_at >= compr->wire); uint16_t offset = written_at - compr->wire; if (knot_wire_is_pointer(written_at)) { offset = knot_wire_get_pointer(written_at); } knot_compr_hint_set(compr->rrinfo, hint, offset, written_size); } static int write_rdata_fixed(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail, size_t size) { assert(src && *src); assert(src_avail); assert(dst && *dst); assert(dst_avail); // Check input/output buffer boundaries. if (size > *src_avail) { return KNOT_EMALF; } if (size > *dst_avail) { return KNOT_ESPACE; } // Data binary copy. memcpy(*dst, *src, size); // Update buffers. *src += size; *src_avail -= size; *dst += size; *dst_avail -= size; return KNOT_EOK; } static int write_rdata_naptr_header(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail) { assert(src && *src); assert(src_avail); assert(dst && *dst); assert(dst_avail); int ret = knot_naptr_header_size(*src, *src + *src_avail); if (ret < 0) { return ret; } // Copy the data. return write_rdata_fixed(src, src_avail, dst, dst_avail, ret); } /*! \brief Helper for \ref compr_put_dname, writes label(s) with size checks. */ #define WRITE_LABEL(dst, written, label, max, len) \ if ((written) + (len) > (max)) { \ return KNOT_ESPACE; \ } else { \ memcpy((dst) + (written), (label), (len)); \ written += (len); \ } #define CHECK_WIRE_NEXT_LABEL(res) \ if (res == NULL) { return KNOT_EINVAL; } /*! * \brief Write compressed domain name to the destination wire. * * \param dname Name to be written. * \param dst Destination wire. * \param max Maximum number of bytes available. * \param compr Compression context (NULL for no compression) * \return Number of written bytes or an error. */ static int compr_put_dname(const knot_dname_t *dname, uint8_t *dst, uint16_t max, knot_compr_t *compr) { assert(dname && dst); // Write uncompressible names directly (zero label dname). if (compr == NULL || *dname == '\0') { return knot_dname_to_wire(dst, dname, max); } // Get number of labels (should not be a zero label dname). size_t name_labels = knot_dname_labels(dname, NULL); assert(name_labels > 0); // Suffix must not be longer than whole name. const knot_dname_t *suffix = compr->wire + compr->suffix.pos; int suffix_labels = compr->suffix.labels; while (suffix_labels > name_labels) { suffix = knot_wire_next_label(suffix, compr->wire); CHECK_WIRE_NEXT_LABEL(suffix); --suffix_labels; } // Suffix is shorter than name, write labels until aligned. uint8_t orig_labels = name_labels; uint16_t written = 0; while (name_labels > suffix_labels) { WRITE_LABEL(dst, written, dname, max, (*dname + 1)); dname = knot_dname_next_label(dname); --name_labels; } // Label count is now equal. assert(name_labels == suffix_labels); const knot_dname_t *match_begin = dname; const knot_dname_t *compr_ptr = suffix; while (dname[0] != '\0') { // Next labels. const knot_dname_t *next_dname = knot_dname_next_label(dname); const knot_dname_t *next_suffix = knot_wire_next_label(suffix, compr->wire); CHECK_WIRE_NEXT_LABEL(next_suffix); // Two labels match, extend suffix length. if (!label_is_equal(dname, suffix)) { // If they don't match, write unmatched labels. uint16_t mismatch_len = (dname - match_begin) + (*dname + 1); WRITE_LABEL(dst, written, match_begin, max, mismatch_len); // Start new potential match. match_begin = next_dname; compr_ptr = next_suffix; } // Jump to next labels. dname = next_dname; suffix = next_suffix; } // If match begins at the end of the name, write '\0' label. if (match_begin == dname) { WRITE_LABEL(dst, written, dname, max, 1); } else { // Match covers >0 labels, write out compression pointer. if (written + sizeof(uint16_t) > max) { return KNOT_ESPACE; } knot_wire_put_pointer(compr->wire, dst + written, compr_ptr - compr->wire); written += sizeof(uint16_t); } assert(dst >= compr->wire); size_t wire_pos = dst - compr->wire; assert(wire_pos < KNOT_WIRE_MAX_PKTSIZE); // Heuristics - expect similar names are grouped together. if (written > sizeof(uint16_t) && wire_pos + written < KNOT_WIRE_PTR_MAX) { compr->suffix.pos = wire_pos; compr->suffix.labels = orig_labels; } return written; } #define WRITE_OWNER_CHECK(size, dst_avail) \ if ((size) > *(dst_avail)) { \ return KNOT_ESPACE; \ } #define WRITE_OWNER_INCR(dst, dst_avail, size) \ *(dst) += (size); \ *(dst_avail) -= (size); static int write_owner(const knot_rrset_t *rrset, uint8_t **dst, size_t *dst_avail, knot_compr_t *compr) { assert(rrset); assert(dst && *dst); assert(dst_avail); // Check for zero label owner (don't compress). uint16_t owner_pointer = 0; if (*rrset->owner != '\0') { owner_pointer = compr_get_ptr(compr, KNOT_COMPR_HINT_OWNER); } // Write result. if (owner_pointer > 0) { WRITE_OWNER_CHECK(sizeof(uint16_t), dst_avail); knot_wire_put_pointer(compr->wire, *dst, owner_pointer); WRITE_OWNER_INCR(dst, dst_avail, sizeof(uint16_t)); // Check for coincidence with previous RR set. } else if (compr != NULL && compr->suffix.pos != 0 && *rrset->owner != '\0' && dname_equal_wire(rrset->owner, compr->wire + compr->suffix.pos, compr->wire)) { WRITE_OWNER_CHECK(sizeof(uint16_t), dst_avail); knot_wire_put_pointer(compr->wire, *dst, compr->suffix.pos); compr_set_ptr(compr, KNOT_COMPR_HINT_OWNER, compr->wire + compr->suffix.pos, knot_dname_size(rrset->owner)); WRITE_OWNER_INCR(dst, dst_avail, sizeof(uint16_t)); } else { if (compr != NULL) { compr->suffix.pos = KNOT_WIRE_HEADER_SIZE; compr->suffix.labels = knot_dname_labels(compr->wire + compr->suffix.pos, compr->wire); } // WRITE_OWNER_CHECK not needed, compr_put_dname has a check. int written = compr_put_dname(rrset->owner, *dst, dname_max(*dst_avail), compr); if (written < 0) { return written; } compr_set_ptr(compr, KNOT_COMPR_HINT_OWNER, *dst, written); WRITE_OWNER_INCR(dst, dst_avail, written); } return KNOT_EOK; } static int write_fixed_header(const knot_rrset_t *rrset, const knot_rdata_t *rdata, uint8_t **dst, size_t *dst_avail, uint16_t flags) { assert(rrset); assert(rdata); assert(dst && *dst); assert(dst_avail); // Write header. wire_ctx_t write = wire_ctx_init(*dst, *dst_avail); wire_ctx_write_u16(&write, rrset->type); wire_ctx_write_u16(&write, rrset->rclass); if ((flags & KNOT_PF_ORIGTTL) && rrset->type == KNOT_RRTYPE_RRSIG) { wire_ctx_write_u32(&write, knot_rrsig_original_ttl(rdata)); } else if ((flags & KNOT_PF_SOAMINTTL) && rrset->type == KNOT_RRTYPE_SOA) { wire_ctx_write_u32(&write, MIN(knot_soa_minimum(rdata), rrset->ttl)); } else { wire_ctx_write_u32(&write, rrset->ttl); } // Check write. if (write.error != KNOT_EOK) { return write.error; } // Update buffer. *dst = write.position; *dst_avail = wire_ctx_available(&write); return KNOT_EOK; } static int compress_rdata_dname(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail, knot_compr_t *put_compr, knot_compr_t *compr, uint16_t hint) { assert(src && *src); assert(src_avail); assert(dst && *dst); assert(dst_avail); // Source domain name. const knot_dname_t *dname = *src; size_t dname_size = knot_dname_size(dname); // Output domain name. int written = compr_put_dname(dname, *dst, dname_max(*dst_avail), put_compr); if (written < 0) { return written; } // Update compression hints. if (compr_get_ptr(compr, hint) == 0) { compr_set_ptr(compr, hint, *dst, written); } // Update buffers. *dst += written; *dst_avail -= written; *src += dname_size; *src_avail -= dname_size; return KNOT_EOK; } static int rdata_traverse_write(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail, const knot_rdata_descriptor_t *desc, knot_compr_t *compr, uint16_t hint) { for (const int *type = desc->block_types; *type != KNOT_RDATA_WF_END; type++) { int ret; knot_compr_t *put_compr = NULL; switch (*type) { case KNOT_RDATA_WF_COMPRESSIBLE_DNAME: put_compr = compr; // FALLTHROUGH case KNOT_RDATA_WF_DECOMPRESSIBLE_DNAME: case KNOT_RDATA_WF_FIXED_DNAME: ret = compress_rdata_dname(src, src_avail, dst, dst_avail, put_compr, compr, hint); break; case KNOT_RDATA_WF_NAPTR_HEADER: ret = write_rdata_naptr_header(src, src_avail, dst, dst_avail); break; case KNOT_RDATA_WF_REMAINDER: ret = write_rdata_fixed(src, src_avail, dst, dst_avail, *src_avail); break; default: // Fixed size block. assert(*type > 0); ret = write_rdata_fixed(src, src_avail, dst, dst_avail, *type); break; } if (ret != KNOT_EOK) { return ret; } } return KNOT_EOK; } static int write_rdata(const knot_rrset_t *rrset, uint16_t rrset_index, const knot_rdata_t *rdata, uint8_t **dst, size_t *dst_avail, knot_compr_t *compr) { assert(rrset); assert(rrset_index < rrset->rrs.count); assert(rdata); assert(dst && *dst); assert(dst_avail); // Reserve space for RDLENGTH. if (sizeof(uint16_t) > *dst_avail) { return KNOT_ESPACE; } uint8_t *wire_rdlength = *dst; *dst += sizeof(uint16_t); *dst_avail -= sizeof(uint16_t); uint8_t *wire_rdata_begin = *dst; // Write RDATA. const uint8_t *src = rdata->data; size_t src_avail = rdata->len; if (src_avail > 0) { // Only write non-empty data. const knot_rdata_descriptor_t *desc = knot_get_rdata_descriptor(rrset->type); int ret = rdata_traverse_write(&src, &src_avail, dst, dst_avail, desc, compr, KNOT_COMPR_HINT_RDATA + rrset_index); if (ret != KNOT_EOK) { return ret; } } // Check for trailing data in the message. if (src_avail > 0) { return KNOT_EMALF; } // Write final RDLENGTH. size_t rdlength = *dst - wire_rdata_begin; knot_wire_write_u16(wire_rdlength, rdlength); return KNOT_EOK; } static int write_rr(const knot_rrset_t *rrset, uint16_t rrset_index, const knot_rdata_t *rdata, uint8_t **dst, size_t *dst_avail, knot_compr_t *compr, uint16_t flags) { int ret = write_owner(rrset, dst, dst_avail, compr); if (ret != KNOT_EOK) { return ret; } ret = write_fixed_header(rrset, rdata, dst, dst_avail, flags); if (ret != KNOT_EOK) { return ret; } return write_rdata(rrset, rrset_index, rdata, dst, dst_avail, compr); } _public_ int knot_rrset_to_wire_extra(const knot_rrset_t *rrset, uint8_t *wire, uint32_t max_size, uint16_t rotate, knot_compr_t *compr, uint16_t flags) { if (rrset == NULL || wire == NULL) { return KNOT_EINVAL; } if (rrset->rrs.count == 0) { return 0; } if (rotate != 0) { rotate %= rrset->rrs.count; } uint8_t *write = wire; size_t capacity = max_size; uint16_t count = rrset->rrs.count; knot_rdata_t *rdata = rotate > 1 ? knot_rdataset_at(&rrset->rrs, rotate - 1) : rrset->rrs.rdata; for (int i = rotate; i < count + rotate; i++) { uint16_t pos = (i < count) ? i : (i - count); rdata = pos ? knot_rdataset_next(rdata) : rrset->rrs.rdata; int ret = write_rr(rrset, pos, rdata, &write, &capacity, compr, flags); if (ret != KNOT_EOK) { return ret; } } return write - wire; } static int parse_header(const uint8_t *wire, size_t *pos, size_t pkt_size, knot_mm_t *mm, knot_rrset_t *rrset, uint16_t *rdlen) { assert(wire); assert(pos); assert(rrset); assert(rdlen); wire_ctx_t src = wire_ctx_init_const(wire, pkt_size); wire_ctx_set_offset(&src, *pos); int compr_size = knot_dname_wire_check(src.position, wire + pkt_size, wire); if (compr_size <= 0) { return KNOT_EMALF; } knot_dname_storage_t buff; int decompr_size = knot_dname_unpack(buff, src.position, sizeof(buff), wire); if (decompr_size <= 0) { return KNOT_EMALF; } knot_dname_t *owner = mm_alloc(mm, decompr_size); if (owner == NULL) { return KNOT_ENOMEM; } memcpy(owner, buff, decompr_size); wire_ctx_skip(&src, compr_size); uint16_t type = wire_ctx_read_u16(&src); uint16_t rclass = wire_ctx_read_u16(&src); uint32_t ttl = wire_ctx_read_u32(&src); *rdlen = wire_ctx_read_u16(&src); if (src.error != KNOT_EOK) { knot_dname_free(owner, mm); return KNOT_EMALF; } if (wire_ctx_available(&src) < *rdlen) { knot_dname_free(owner, mm); return KNOT_EMALF; } *pos = wire_ctx_offset(&src); knot_rrset_init(rrset, owner, type, rclass, ttl); return KNOT_EOK; } static int decompress_rdata_dname(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail, const uint8_t *pkt_wire) { assert(src && *src); assert(src_avail); assert(dst && *dst); assert(dst_avail); int compr_size = knot_dname_wire_check(*src, *src + *src_avail, pkt_wire); if (compr_size <= 0) { return compr_size; } int decompr_size = knot_dname_unpack(*dst, *src, *dst_avail, pkt_wire); if (decompr_size <= 0) { return decompr_size; } // Update buffers. *dst += decompr_size; *dst_avail -= decompr_size; *src += compr_size; *src_avail -= compr_size; return KNOT_EOK; } static int rdata_traverse_parse(const uint8_t **src, size_t *src_avail, uint8_t **dst, size_t *dst_avail, const knot_rdata_descriptor_t *desc, const uint8_t *pkt_wire) { for (const int *type = desc->block_types; *type != KNOT_RDATA_WF_END; type++) { int ret; switch (*type) { case KNOT_RDATA_WF_COMPRESSIBLE_DNAME: case KNOT_RDATA_WF_DECOMPRESSIBLE_DNAME: case KNOT_RDATA_WF_FIXED_DNAME: ret = decompress_rdata_dname(src, src_avail, dst, dst_avail, pkt_wire); break; case KNOT_RDATA_WF_NAPTR_HEADER: ret = write_rdata_naptr_header(src, src_avail, dst, dst_avail); break; case KNOT_RDATA_WF_REMAINDER: ret = write_rdata_fixed(src, src_avail, dst, dst_avail, *src_avail); break; default: /* Fixed size block */ assert(*type > 0); ret = write_rdata_fixed(src, src_avail, dst, dst_avail, *type); break; } if (ret != KNOT_EOK) { return ret; } } return KNOT_EOK; } static bool allow_zero_rdata(const knot_rrset_t *rr, const knot_rdata_descriptor_t *desc) { return rr->rclass != KNOT_CLASS_IN || // NONE and ANY for DDNS rr->type == KNOT_RRTYPE_APL || // APL RR type rr->type == KNOT_RRTYPE_NULL || // NULL RR type desc->type_name == NULL; // Unknown RR type } static int parse_rdata(const uint8_t *pkt_wire, size_t *pos, size_t pkt_size, knot_mm_t *mm, uint16_t rdlength, knot_rrset_t *rrset) { assert(pkt_wire); assert(pos); assert(rrset); const knot_rdata_descriptor_t *desc = knot_get_rdata_descriptor(rrset->type); if (desc->type_name == NULL) { desc = knot_get_obsolete_rdata_descriptor(rrset->type); } if (rdlength == 0) { if (allow_zero_rdata(rrset, desc)) { return knot_rrset_add_rdata(rrset, NULL, 0, mm); } else { return KNOT_EMALF; } } else if (pkt_size - *pos < rdlength) { return KNOT_EMALF; } // Buffer for parsed rdata (decompression extends rdata length). const size_t max_rdata_len = UINT16_MAX; uint8_t buf[knot_rdata_size(max_rdata_len)]; knot_rdata_t *rdata = (knot_rdata_t *)buf; const uint8_t *src = pkt_wire + *pos; size_t src_avail = rdlength; uint8_t *dst = rdata->data; size_t dst_avail = max_rdata_len; // Parse RDATA. int ret = rdata_traverse_parse(&src, &src_avail, &dst, &dst_avail, desc, pkt_wire); if (ret != KNOT_EOK) { return KNOT_EMALF; } // Check for trailing data. size_t real_len = max_rdata_len - dst_avail; if (real_len < rdlength) { return KNOT_EMALF; } rdata->len = real_len; ret = knot_rdataset_add(&rrset->rrs, rdata, mm); if (ret != KNOT_EOK) { return ret; } // Update position pointer. *pos += rdlength; return KNOT_EOK; } _public_ int knot_rrset_rr_from_wire(const uint8_t *wire, size_t *pos, size_t max_size, knot_rrset_t *rrset, knot_mm_t *mm, bool canonical) { if (wire == NULL || pos == NULL || *pos > max_size || rrset == NULL) { return KNOT_EINVAL; } uint16_t rdlen = 0; int ret = parse_header(wire, pos, max_size, mm, rrset, &rdlen); if (ret != KNOT_EOK) { return ret; } ret = parse_rdata(wire, pos, max_size, mm, rdlen, rrset); if (ret != KNOT_EOK) { knot_rrset_clear(rrset, mm); return ret; } // Convert RR to the canonical format. if (canonical) { ret = knot_rrset_rr_to_canonical(rrset); if (ret != KNOT_EOK) { knot_rrset_clear(rrset, mm); } } return KNOT_EOK; }