diff --git a/doc/man/knotc.8in b/doc/man/knotc.8in index 3fcc05699..140a1eba0 100644 --- a/doc/man/knotc.8in +++ b/doc/man/knotc.8in @@ -184,6 +184,12 @@ will be held up until the zone is thawed. (#) \fBzone\-thaw\fP [\fIzone\fP\&...] Trigger dismissal of zone freeze. (#) .TP +\fBzone\-xfr\-freeze\fP [\fIzone\fP\&...] +Temporarily disable outgoing AXFR/IXFR for the zone(s). (#) +.TP +\fBzone\-xfr\-thaw\fP [\fIzone\fP\&...] +Dismiss outgoing XFR freeze. (#) +.TP \fBzone\-read\fP \fIzone\fP [\fIowner\fP [\fItype\fP]] Get zone data that are currently being presented. .TP diff --git a/doc/man_knotc.rst b/doc/man_knotc.rst index ddccc5697..f840f065d 100644 --- a/doc/man_knotc.rst +++ b/doc/man_knotc.rst @@ -161,6 +161,12 @@ Actions **zone-thaw** [*zone*...] Trigger dismissal of zone freeze. (#) +**zone-xfr-freeze** [*zone*...] + Temporarily disable outgoing AXFR/IXFR for the zone(s). (#) + +**zone-xfr-thaw** [*zone*...] + Dismiss outgoing XFR freeze. (#) + **zone-read** *zone* [*owner* [*type*]] Get zone data that are currently being presented. diff --git a/src/knot/ctl/commands.c b/src/knot/ctl/commands.c index e27acf178..f312a253f 100644 --- a/src/knot/ctl/commands.c +++ b/src/knot/ctl/commands.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 CZ.NIC, z.s.p.o. +/* Copyright (C) 2022 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 @@ -635,6 +635,24 @@ static int zone_thaw(zone_t *zone, _unused_ ctl_args_t *args) return schedule_trigger(zone, args, ZONE_EVENT_UTHAW, false); } +static int zone_xfr_freeze(zone_t *zone, _unused_ ctl_args_t *args) +{ + zone_set_flag(zone, ZONE_XFR_FROZEN); + + log_zone_info(zone->name, "outgoing XFR frozen"); + + return KNOT_EOK; +} + +static int zone_xfr_thaw(zone_t *zone, _unused_ ctl_args_t *args) +{ + zone_unset_flag(zone, ZONE_XFR_FROZEN); + + log_zone_info(zone->name, "outgoing XFR unfrozen"); + + return KNOT_EOK; +} + static int zone_txn_begin(zone_t *zone, _unused_ ctl_args_t *args) { if (zone->control_update != NULL) { @@ -1567,6 +1585,10 @@ static int ctl_zone(ctl_args_t *args, ctl_cmd_t cmd) return zones_apply(args, zone_freeze); case CTL_ZONE_THAW: return zones_apply(args, zone_thaw); + case CTL_ZONE_XFR_FREEZE: + return zones_apply(args, zone_xfr_freeze); + case CTL_ZONE_XFR_THAW: + return zones_apply(args, zone_xfr_thaw); case CTL_ZONE_READ: return zones_apply(args, zone_read); case CTL_ZONE_BEGIN: @@ -2009,6 +2031,8 @@ static const desc_t cmd_table[] = { [CTL_ZONE_KSK_SBM] = { "zone-ksk-submitted", ctl_zone }, [CTL_ZONE_FREEZE] = { "zone-freeze", ctl_zone }, [CTL_ZONE_THAW] = { "zone-thaw", ctl_zone }, + [CTL_ZONE_XFR_FREEZE] = { "zone-xfr-freeze", ctl_zone }, + [CTL_ZONE_XFR_THAW] = { "zone-xfr-thaw", ctl_zone }, [CTL_ZONE_READ] = { "zone-read", ctl_zone }, [CTL_ZONE_BEGIN] = { "zone-begin", ctl_zone }, diff --git a/src/knot/ctl/commands.h b/src/knot/ctl/commands.h index fa78cf24f..b1ba4e12b 100644 --- a/src/knot/ctl/commands.h +++ b/src/knot/ctl/commands.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 CZ.NIC, z.s.p.o. +/* Copyright (C) 2022 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 @@ -74,6 +74,8 @@ typedef enum { CTL_ZONE_KSK_SBM, CTL_ZONE_FREEZE, CTL_ZONE_THAW, + CTL_ZONE_XFR_FREEZE, + CTL_ZONE_XFR_THAW, CTL_ZONE_READ, CTL_ZONE_BEGIN, diff --git a/src/knot/events/handlers/update.c b/src/knot/events/handlers/update.c index 8f60f17fc..b998eb57d 100644 --- a/src/knot/events/handlers/update.c +++ b/src/knot/events/handlers/update.c @@ -35,7 +35,7 @@ false, fmt) static void init_qdata_from_request(knotd_qdata_t *qdata, - const zone_t *zone, + zone_t *zone, knot_request_t *req, knotd_qdata_params_t *params, knotd_qdata_extra_t *extra) @@ -312,7 +312,7 @@ static void forward_requests(conf_t *conf, zone_t *zone, list_t *requests) } } -static void send_update_response(conf_t *conf, const zone_t *zone, knot_request_t *req) +static void send_update_response(conf_t *conf, zone_t *zone, knot_request_t *req) { if (req->resp) { if (!zone_is_slave(conf, zone)) { @@ -343,7 +343,7 @@ static void free_request(knot_request_t *req) free(req); } -static void send_update_responses(conf_t *conf, const zone_t *zone, list_t *updates) +static void send_update_responses(conf_t *conf, zone_t *zone, list_t *updates) { ptrnode_t *node, *nxt; WALK_LIST_DELSAFE(node, nxt, *updates) { diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c index 7fd69eb3d..5f46a4c0c 100644 --- a/src/knot/nameserver/axfr.c +++ b/src/knot/nameserver/axfr.c @@ -129,6 +129,12 @@ static int axfr_query_init(knotd_qdata_t *qdata) } } + 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; + } + /* Create transfer processing context. */ knot_mm_t *mm = qdata->mm; struct axfr_proc *axfr = mm_alloc(mm, sizeof(struct axfr_proc)); @@ -186,6 +192,9 @@ int axfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) case KNOT_EMALF: /* Malformed query. */ AXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); return KNOT_STATE_FAIL; + case KNOT_EAGAIN: /* Outgoing AXFR temporarily disabled. */ + AXFROUT_LOG(LOG_INFO, qdata, "outgoing AXFR frozen"); + return KNOT_STATE_FAIL; default: AXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", knot_strerror(ret)); diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c index cc89a2907..98c01b0eb 100644 --- a/src/knot/nameserver/ixfr.c +++ b/src/knot/nameserver/ixfr.c @@ -173,6 +173,12 @@ static int ixfr_answer_init(knotd_qdata_t *qdata, uint32_t *serial_from) } } + 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; + } + 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); @@ -278,6 +284,9 @@ int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) 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)); diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c index 87d7cc187..325c7b18d 100644 --- a/src/knot/nameserver/process_query.c +++ b/src/knot/nameserver/process_query.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 CZ.NIC, z.s.p.o. +/* Copyright (C) 2022 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 @@ -182,12 +182,12 @@ static int query_chaos(knot_pkt_t *pkt, knot_layer_t *ctx) } /*! \brief Find zone for given question. */ -static const zone_t *answer_zone_find(const knot_pkt_t *query, knot_zonedb_t *zonedb) +static zone_t *answer_zone_find(const knot_pkt_t *query, knot_zonedb_t *zonedb) { uint16_t qtype = knot_pkt_qtype(query); uint16_t qclass = knot_pkt_qclass(query); const knot_dname_t *qname = knot_pkt_qname(query); - const zone_t *zone = NULL; + zone_t *zone = NULL; // search for zone only for IN and ANY classes if (qclass != KNOT_CLASS_IN && qclass != KNOT_CLASS_ANY) { diff --git a/src/knot/nameserver/process_query.h b/src/knot/nameserver/process_query.h index c80f1228a..bd7d42af0 100644 --- a/src/knot/nameserver/process_query.h +++ b/src/knot/nameserver/process_query.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2019 CZ.NIC, z.s.p.o. +/* Copyright (C) 2022 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 @@ -26,7 +26,7 @@ const knot_layer_api_t *process_query_layer(void); /*! \brief Query processing intermediate data. */ typedef struct knotd_qdata_extra { - const zone_t *zone; /*!< Zone from which is answered. */ + zone_t *zone; /*!< Zone from which is answered. */ const zone_contents_t *contents; /*!< Zone contents from which is answered. */ list_t wildcards; /*!< Visited wildcards. */ list_t rrsigs; /*!< Section RRSIGs. */ diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c index 8d4126130..f58424f12 100644 --- a/src/knot/zone/zone.c +++ b/src/knot/zone/zone.c @@ -410,14 +410,14 @@ void zone_clear_preferred_master(zone_t *zone) pthread_mutex_unlock(&zone->preferred_lock); } -void zone_set_flag(zone_t *zone, zone_flag_t flag) +static void set_flag(zone_t *zone, zone_flag_t flag, bool remove) { if (zone == NULL) { return; } pthread_mutex_lock(&zone->preferred_lock); // this mutex seems OK to be reused for this - zone->flags |= flag; + zone->flags = remove ? (zone->flags & ~flag) : (zone->flags | flag); pthread_mutex_unlock(&zone->preferred_lock); if (flag & ZONE_IS_CATALOG) { @@ -425,6 +425,16 @@ void zone_set_flag(zone_t *zone, zone_flag_t flag) } } +void zone_set_flag(zone_t *zone, zone_flag_t flag) +{ + return set_flag(zone, flag, false); +} + +void zone_unset_flag(zone_t *zone, zone_flag_t flag) +{ + return set_flag(zone, flag, true); +} + zone_flag_t zone_get_flag(zone_t *zone, zone_flag_t flag, bool clear) { if (zone == NULL) { diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h index 6623c4384..0f8ca5424 100644 --- a/src/knot/zone/zone.h +++ b/src/knot/zone/zone.h @@ -43,6 +43,7 @@ typedef enum { ZONE_FORCE_ZSK_ROLL = 1 << 4, /*!< Force ZSK rollover. */ ZONE_IS_CATALOG = 1 << 5, /*!< This is a catalog. */ ZONE_IS_CAT_MEMBER = 1 << 6, /*!< This zone exists according to a catalog. */ + ZONE_XFR_FROZEN = 1 << 7, /*!< Outgoing AXFR/IXFR temporarily disabled. */ } zone_flag_t; /*! @@ -180,6 +181,9 @@ void zone_clear_preferred_master(zone_t *zone); /*! \brief Sets a zone flag. */ void zone_set_flag(zone_t *zone, zone_flag_t flag); +/*! \brief Unsets a zone flag. */ +void zone_unset_flag(zone_t *zone, zone_flag_t flag); + /*! \brief Returns if a flag is set (and optionally clears it). */ zone_flag_t zone_get_flag(zone_t *zone, zone_flag_t flag, bool clear); diff --git a/src/utils/knotc/commands.c b/src/utils/knotc/commands.c index f0ddd97fd..90b114586 100644 --- a/src/utils/knotc/commands.c +++ b/src/utils/knotc/commands.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 CZ.NIC, z.s.p.o. +/* Copyright (C) 2022 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 @@ -54,6 +54,8 @@ #define CMD_ZONE_KSK_SBM "zone-ksk-submitted" #define CMD_ZONE_FREEZE "zone-freeze" #define CMD_ZONE_THAW "zone-thaw" +#define CMD_ZONE_XFR_FREEZE "zone-xfr-freeze" +#define CMD_ZONE_XFR_THAW "zone-xfr-thaw" #define CMD_ZONE_READ "zone-read" #define CMD_ZONE_BEGIN "zone-begin" @@ -380,6 +382,8 @@ static void format_block(ctl_cmd_t cmd, bool failed, bool empty) case CTL_ZONE_KSK_SBM: case CTL_ZONE_FREEZE: case CTL_ZONE_THAW: + case CTL_ZONE_XFR_FREEZE: + case CTL_ZONE_XFR_THAW: case CTL_ZONE_BEGIN: case CTL_ZONE_COMMIT: case CTL_ZONE_ABORT: @@ -1186,6 +1190,8 @@ const cmd_desc_t cmd_table[] = { { CMD_ZONE_KSK_SBM, cmd_zone_ctl, CTL_ZONE_KSK_SBM, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, { CMD_ZONE_FREEZE, cmd_zone_ctl, CTL_ZONE_FREEZE, CMD_FOPT_ZONE }, { CMD_ZONE_THAW, cmd_zone_ctl, CTL_ZONE_THAW, CMD_FOPT_ZONE }, + { CMD_ZONE_XFR_FREEZE, cmd_zone_ctl, CTL_ZONE_XFR_FREEZE, CMD_FOPT_ZONE }, + { CMD_ZONE_XFR_THAW, cmd_zone_ctl, CTL_ZONE_XFR_THAW, CMD_FOPT_ZONE }, { CMD_ZONE_READ, cmd_zone_node_ctl, CTL_ZONE_READ, CMD_FREQ_ZONE }, { CMD_ZONE_BEGIN, cmd_zone_ctl, CTL_ZONE_BEGIN, CMD_FREQ_ZONE | CMD_FOPT_ZONE }, @@ -1236,6 +1242,8 @@ static const cmd_help_t cmd_help_table[] = { { CMD_ZONE_KSK_SBM, " ...", "When KSK submission, confirm parent's DS presence. (#)" }, { CMD_ZONE_FREEZE, "[...]", "Temporarily postpone automatic zone-changing events. (#)" }, { CMD_ZONE_THAW, "[...]", "Dismiss zone freeze. (#)" }, + { CMD_ZONE_XFR_FREEZE, "[...]", "Temporarily disable outgoing AXFR/IXFR. (#)" }, + { CMD_ZONE_XFR_THAW, "[...]", "Dismiss outgoing XFR freeze. (#)" }, { "", "", "" }, { CMD_ZONE_READ, " [ []]", "Get zone data that are currently being presented." }, { CMD_ZONE_BEGIN, "...", "Begin a zone transaction." }, diff --git a/tests-extra/tests/zone/xfr_freeze/test.py b/tests-extra/tests/zone/xfr_freeze/test.py new file mode 100644 index 000000000..a27a3333b --- /dev/null +++ b/tests-extra/tests/zone/xfr_freeze/test.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +'''Test of IXFR freeze.''' + +from dnstest.test import Test + +t = Test() + +master = t.server("knot") +slave = t.server("knot") +zone = t.zone_rnd(1, records=50) + +t.link(zone, master, slave) + +t.start() + +serial_init = master.zone_wait(zone) +slave.zone_wait(zone) + +master.ctl("zone-xfr-freeze", wait=True) +master.random_ddns(zone, allow_empty=False) +t.sleep(4) + +resp = master.dig(zone[0].name, "AXFR", tries=1) +resp.check_xfr(rcode="REFUSED") + +resp = slave.dig(zone[0].name, "SOA") +serial = resp.soa_serial() +if serial != serial_init: + set_err("SOA serial mismatch") + detail_log("SOA serial mismatch %d != %d" % (serial, serial_init)) + +master.ctl("zone-xfr-thaw", wait=True) +master.ctl("zone-notify") +slave.zone_wait(zone, serial_init) +t.xfr_diff(master, slave, zone) + +t.end() diff --git a/tests-extra/tools/dnstest/zonefile.py b/tests-extra/tools/dnstest/zonefile.py index 76586ce1e..39dc48879 100644 --- a/tests-extra/tools/dnstest/zonefile.py +++ b/tests-extra/tools/dnstest/zonefile.py @@ -287,19 +287,19 @@ class ZoneFile(object): changes = 0 with open(self.path, 'r') as file: for fline in file: - line = fline.split(None, 3) + line = fline.split(None, 4) if len(line) < 3: continue if line[0][0] not in [";", "@"]: dname = line[0] + ttl = 0 if line[1].isnumeric(): ttl = line[1] - rtype = line[2] - rdata = ' '.join(line[3:]) - else: - ttl = 0 - rtype = line[1] - rdata = ' '.join(line[2:]) + del line[1] + if line[1] == "IN": + del line[1] + rtype = line[1] + rdata = ' '.join(line[2:]) if rtype not in ["SOA", "RRSIG", "DNSKEY", "DS", "CDS", "CDNSKEY", "NSEC", "NSEC3", "NSEC3PARAM"]: try: if random.randint(1, 20) in [4, 5]: