/* Copyright (C) 2023 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 "contrib/mempattern.h" #include "contrib/sockaddr.h" #include "knot/journal/journal_metadata.h" #include "knot/nameserver/axfr.h" #include "knot/nameserver/internet.h" #include "knot/nameserver/ixfr.h" #include "knot/nameserver/log.h" #include "knot/nameserver/xfr.h" #include "knot/zone/serial.h" #include "libknot/libknot.h" #define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query) #define REMOTE(qdata) (struct sockaddr *)knotd_qdata_remote_addr(qdata) #define IXFROUT_LOG(priority, qdata, fmt...) \ ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_IXFR, \ LOG_DIRECTION_OUT, REMOTE(qdata), false, fmt) /*! \brief Helper macro for putting RRs into packet. */ #define IXFR_SAFE_PUT(pkt, rr) \ int ret = knot_pkt_put((pkt), 0, (rr), KNOT_PF_NOTRUNC | KNOT_PF_ORIGTTL); \ if (ret != KNOT_EOK) { \ return ret; \ } /*! \brief Puts current RR into packet, stores state for retries. */ static int ixfr_put_chg_part(knot_pkt_t *pkt, struct ixfr_proc *ixfr, journal_read_t *read) { assert(pkt); assert(ixfr); assert(read); if (!knot_rrset_empty(&ixfr->cur_rr)) { IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); journal_read_clear_rrset(&ixfr->cur_rr); } while (journal_read_rrset(read, &ixfr->cur_rr, true)) { if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { ixfr->in_remove_section = !ixfr->in_remove_section; if (ixfr->in_remove_section) { if (knot_soa_serial(ixfr->cur_rr.rrs.rdata) == ixfr->soa_to) { break; } } else { ixfr->soa_last = knot_soa_serial(ixfr->cur_rr.rrs.rdata); } } if (pkt->size > KNOT_WIRE_PTR_MAX) { // optimization: once the XFR DNS message is > 16 KiB, compression // is limited. Better wrap to next message. return KNOT_ESPACE; } IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); journal_read_clear_rrset(&ixfr->cur_rr); } return journal_read_get_error(read, KNOT_EOK); } /*! * \brief Process the changes from journal. * \note Keep in mind that this function must be able to resume processing, * for example if it fills a packet and returns ESPACE, it is called again * with next empty answer and it must resume the processing exactly where * it's left off. */ static int ixfr_process_journal(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer) { int ret = KNOT_EOK; struct ixfr_proc *ixfr = (struct ixfr_proc *)xfer; journal_read_t *read = (journal_read_t *)item; ret = ixfr_put_chg_part(pkt, ixfr, read); return ret; } #undef IXFR_SAFE_PUT static int ixfr_load_chsets(journal_read_t **journal_read, zone_t *zone, const zone_contents_t *contents, const knot_rrset_t *their_soa) { assert(journal_read); assert(zone); /* Compare serials. */ uint32_t serial_to = zone_contents_serial(contents), j_serial_to; uint32_t serial_from = knot_soa_serial(their_soa->rrs.rdata); if (serial_compare(serial_to, serial_from) & SERIAL_MASK_LEQ) { /* We have older/same age zone. */ return KNOT_EUPTODATE; } zone_journal_t j = zone_journal(zone); bool j_exists = false; int ret = journal_info(j, &j_exists, NULL, NULL, &j_serial_to, NULL, NULL, NULL, NULL); if (ret != KNOT_EOK) { return ret; } else if (!j_exists) { return KNOT_ENOENT; } // please note that the journal serial_to might differ from zone SOA serial // it is because RCU lock is made at different moment than LMDB txn begin return journal_read_begin(zone_journal(zone), false, serial_from, journal_read); } static int ixfr_query_check(knotd_qdata_t *qdata) { NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); NS_NEED_AUTH(qdata, ACL_ACTION_TRANSFER); NS_NEED_ZONE_CONTENTS(qdata); /* Need SOA authority record. */ const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); if (authority->count < 1 || their_soa->type != KNOT_RRTYPE_SOA) { qdata->rcode = KNOT_RCODE_FORMERR; return KNOT_STATE_FAIL; } /* SOA needs to match QNAME. */ NS_NEED_QNAME(qdata, their_soa->owner, KNOT_RCODE_FORMERR); return KNOT_STATE_DONE; } static void ixfr_answer_cleanup(knotd_qdata_t *qdata) { struct ixfr_proc *ixfr = (struct ixfr_proc *)qdata->extra->ext; knot_mm_t *mm = qdata->mm; knot_rrset_clear(&ixfr->cur_rr, NULL); ptrlist_free(&ixfr->proc.nodes, mm); journal_read_end(ixfr->journal_ctx); mm_free(mm, qdata->extra->ext); /* Allow zone changes (finished). */ rcu_read_unlock(); } static int ixfr_answer_init(knotd_qdata_t *qdata, uint32_t *serial_from) { assert(qdata); if (ixfr_query_check(qdata) == KNOT_STATE_FAIL) { if (qdata->rcode == KNOT_RCODE_FORMERR) { return KNOT_EMALF; } else { return KNOT_EDENIED; } } if (zone_get_flag(qdata->extra->zone, ZONE_XFR_FROZEN, false)) { qdata->rcode = KNOT_RCODE_REFUSED; qdata->rcode_ede = KNOT_EDNS_EDE_NOT_READY; return KNOT_EAGAIN; } conf_val_t provide = conf_zone_get(conf(), C_PROVIDE_IXFR, qdata->extra->zone->name); if (!conf_bool(&provide)) { return KNOT_ENOTSUP; } const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); *serial_from = knot_soa_serial(their_soa->rrs.rdata); knot_mm_t *mm = qdata->mm; struct ixfr_proc *xfer = mm_alloc(mm, sizeof(*xfer)); if (xfer == NULL) { return KNOT_ENOMEM; } memset(xfer, 0, sizeof(*xfer)); int ret = ixfr_load_chsets(&xfer->journal_ctx, (zone_t *)qdata->extra->zone, qdata->extra->contents, their_soa); if (ret != KNOT_EOK) { mm_free(mm, xfer); return ret; } xfr_stats_begin(&xfer->proc.stats); xfer->state = IXFR_SOA_DEL; init_list(&xfer->proc.nodes); knot_rrset_init_empty(&xfer->cur_rr); xfer->qdata = qdata; ptrlist_add(&xfer->proc.nodes, xfer->journal_ctx, mm); xfer->soa_from = knot_soa_serial(their_soa->rrs.rdata); xfer->soa_to = zone_contents_serial(qdata->extra->contents); xfer->soa_last = xfer->soa_from; qdata->extra->ext = xfer; qdata->extra->ext_cleanup = &ixfr_answer_cleanup; /* No zone changes during multipacket answer (unlocked in ixfr_answer_cleanup) */ rcu_read_lock(); return KNOT_EOK; } static int ixfr_answer_soa(knot_pkt_t *pkt, knotd_qdata_t *qdata) { assert(pkt); assert(qdata); /* Check query. */ int state = ixfr_query_check(qdata); if (state == KNOT_STATE_FAIL) { return state; /* Malformed query. */ } /* Reserve space for TSIG. */ int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); if (ret != KNOT_EOK) { return KNOT_STATE_FAIL; } /* Guaranteed to have zone contents. */ const zone_node_t *apex = qdata->extra->contents->apex; knot_rrset_t soa_rr = node_rrset(apex, KNOT_RRTYPE_SOA); if (knot_rrset_empty(&soa_rr)) { return KNOT_STATE_FAIL; } ret = knot_pkt_put(pkt, 0, &soa_rr, 0); if (ret != KNOT_EOK) { qdata->rcode = KNOT_RCODE_SERVFAIL; return KNOT_STATE_FAIL; } return KNOT_STATE_DONE; } int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) { if (pkt == NULL || qdata == NULL) { return KNOT_STATE_FAIL; } /* IXFR over UDP is responded with SOA. */ if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { return ixfr_answer_soa(pkt, qdata); } /* Initialize on first call. */ struct ixfr_proc *ixfr = qdata->extra->ext; if (ixfr == NULL) { uint32_t soa_from = 0; int ret = ixfr_answer_init(qdata, &soa_from); ixfr = qdata->extra->ext; switch (ret) { case KNOT_EOK: /* OK */ IXFROUT_LOG(LOG_INFO, qdata, "started, serial %u -> %u", ixfr->soa_from, ixfr->soa_to); break; case KNOT_EUPTODATE: /* Our zone is same age/older, send SOA. */ IXFROUT_LOG(LOG_INFO, qdata, "zone is up-to-date, serial %u", soa_from); return ixfr_answer_soa(pkt, qdata); case KNOT_ENOTSUP: IXFROUT_LOG(LOG_INFO, qdata, "cannot provide, fallback to AXFR"); qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ return axfr_process_query(pkt, qdata); case KNOT_ERANGE: /* No history -> AXFR. */ case KNOT_ENOENT: IXFROUT_LOG(LOG_INFO, qdata, "incomplete history, serial %u, fallback to AXFR", soa_from); qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ return axfr_process_query(pkt, qdata); case KNOT_EDENIED: /* Not authorized, already logged. */ return KNOT_STATE_FAIL; case KNOT_EMALF: /* Malformed query. */ IXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); return KNOT_STATE_FAIL; case KNOT_EAGAIN: /* Outgoing IXFR temporarily disabled. */ IXFROUT_LOG(LOG_INFO, qdata, "outgoing IXFR frozen"); return KNOT_STATE_FAIL; default: /* Server errors. */ IXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", knot_strerror(ret)); return KNOT_STATE_FAIL; } } /* Reserve space for TSIG. */ int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); if (ret != KNOT_EOK) { return KNOT_STATE_FAIL; } /* Answer current packet (or continue). */ ret = xfr_process_list(pkt, &ixfr_process_journal, qdata); switch (ret) { case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ return KNOT_STATE_PRODUCE; /* Check for more. */ case KNOT_EOK: /* Last response. */ if (ixfr->soa_last != ixfr->soa_to) { IXFROUT_LOG(LOG_ERR, qdata, "failed (inconsistent history)"); return KNOT_STATE_FAIL; } xfr_stats_end(&ixfr->proc.stats); xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_IXFR, LOG_DIRECTION_OUT, REMOTE(qdata), false, &ixfr->proc.stats); return KNOT_STATE_DONE; default: /* Generic error. */ IXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret)); return KNOT_STATE_FAIL; } }