diff --git a/CHANGES b/CHANGES index 15210d2985..eb54983f9b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +6233. [func] Extend client side support for the EDNS EXPIRE option + to IXFR and AXFR query types. [GL #4170] + 6232. [bug] Following the introduction of krb5-subdomain-self-rhs and ms-subdomain-self-rhs update rules, removal of nonexistent PTR and SRV records via UPDATE could fail. diff --git a/bin/tests/system/ixfr/ixfr-stats.good b/bin/tests/system/ixfr/ixfr-stats-with-expire.good similarity index 67% rename from bin/tests/system/ixfr/ixfr-stats.good rename to bin/tests/system/ixfr/ixfr-stats-with-expire.good index 3d0d2dde32..c02992b14f 100644 --- a/bin/tests/system/ixfr/ixfr-stats.good +++ b/bin/tests/system/ixfr/ixfr-stats-with-expire.good @@ -1,3 +1,3 @@ messages=1 records=5 -bytes=204 +bytes=223 diff --git a/bin/tests/system/ixfr/ixfr-stats-without-expire.good b/bin/tests/system/ixfr/ixfr-stats-without-expire.good new file mode 100644 index 0000000000..0713c0b8ee --- /dev/null +++ b/bin/tests/system/ixfr/ixfr-stats-without-expire.good @@ -0,0 +1,3 @@ +messages=1 +records=5 +bytes=215 diff --git a/bin/tests/system/ixfr/tests.sh b/bin/tests/system/ixfr/tests.sh index f1e3b62241..4b61a2c9e1 100644 --- a/bin/tests/system/ixfr/tests.sh +++ b/bin/tests/system/ixfr/tests.sh @@ -388,9 +388,9 @@ status=$((status+ret)) n=$((n+1)) echo_i "checking whether dig calculates IXFR statistics correctly ($n)" ret=0 -$DIG $DIGOPTS +noedns +stat -b 10.53.0.4 @10.53.0.4 test. ixfr=2 > dig.out1.test$n +$DIG $DIGOPTS +expire +nocookie +stat -b 10.53.0.4 @10.53.0.4 test. ixfr=2 > dig.out1.test$n get_dig_xfer_stats dig.out1.test$n > stats.dig -diff ixfr-stats.good stats.dig > /dev/null || ret=1 +diff ixfr-stats-with-expire.good stats.dig > /dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status+ret)) @@ -400,20 +400,20 @@ status=$((status+ret)) _wait_for_stats () { get_named_xfer_stats ns4/named.run "$1" test "$2" > "$3" - diff ixfr-stats.good "$3" > /dev/null || return 1 + diff "$4" "$3" > /dev/null || return 1 return 0 } n=$((n+1)) echo_i "checking whether named calculates incoming IXFR statistics correctly ($n)" ret=0 -retry_quiet 10 _wait_for_stats 10.53.0.3 "Transfer completed" stats.incoming || ret=1 +retry_quiet 10 _wait_for_stats 10.53.0.3 "Transfer completed" stats.incoming ixfr-stats-without-expire.good || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status+ret)) n=$((n+1)) echo_i "checking whether named calculates outgoing IXFR statistics correctly ($n)" -retry_quiet 10 _wait_for_stats 10.53.0.4 "IXFR ended" stats.outgoing || ret=1 +retry_quiet 10 _wait_for_stats 10.53.0.4 "IXFR ended" stats.outgoing ixfr-stats-with-expire.good || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status+ret)) diff --git a/bin/tests/system/xfer/axfr-stats.good b/bin/tests/system/xfer/axfr-stats.good index 264af09705..616854e646 100644 --- a/bin/tests/system/xfer/axfr-stats.good +++ b/bin/tests/system/xfer/axfr-stats.good @@ -1,3 +1,3 @@ messages=16 records=10003 -bytes=218227 +bytes=218403 diff --git a/bin/tests/system/xfer/tests.sh b/bin/tests/system/xfer/tests.sh index 33eee6ae47..cd1a50a3e4 100755 --- a/bin/tests/system/xfer/tests.sh +++ b/bin/tests/system/xfer/tests.sh @@ -464,7 +464,15 @@ $DIGCMD nil. TXT | grep 'SOA mismatch AXFR' >/dev/null && { } n=$((n+1)) -echo_i "check that we ask for and get a EDNS EXPIRE response ($n)" +echo_i "check that we ask for and got a EDNS EXPIRE response when transfering from a secondary ($n)" +tmp=0 +msg="zone edns-expire/IN: zone transfer finished: success, expire=1814[0-4][0-9][0-9]" +grep "$msg" ns7/named.run > /dev/null || tmp=1 +[ "$tmp" -ne 0 ] && echo_i "failed" +status=$((status+tmp)) + +n=$((n+1)) +echo_i "check that we ask for and get a EDNS EXPIRE response when refreshing ($n)" # force a refresh query $RNDCCMD 10.53.0.7 refresh edns-expire 2>&1 | sed 's/^/ns7 /' | cat_i sleep 10 @@ -541,7 +549,7 @@ tmp=0 # Use -b so that we can discern between incoming and outgoing transfers in ns3 # logs later on. wait_for_xfer() ( - $DIG $DIGOPTS +noedns +stat -b 10.53.0.2 @10.53.0.3 xfer-stats. AXFR > dig.out.ns3.test$n + $DIG $DIGOPTS +edns +nocookie +noexpire +stat -b 10.53.0.2 @10.53.0.3 xfer-stats. AXFR > dig.out.ns3.test$n grep "; Transfer failed" dig.out.ns3.test$n > /dev/null || return 0 return 1 ) diff --git a/doc/notes/notes-current.rst b/doc/notes/notes-current.rst index 6f293436b9..c425485832 100644 --- a/doc/notes/notes-current.rst +++ b/doc/notes/notes-current.rst @@ -35,6 +35,10 @@ Feature Changes - Make :iscman:`nsupdate` honor the ``-v`` option for SOA queries, that is send the request over TCP, only if the server is specified. :gl:`#1181` +- Extend client side support for the EDNS EXPIRE option to IXFR and + AXFR query types. ``named`` will now be making EDNS queries AXFR + and IXFR queries with EDNS options present. :gl:`#4170` + Bug Fixes ~~~~~~~~~ diff --git a/lib/dns/include/dns/types.h b/lib/dns/include/dns/types.h index b334eff0a2..70e0b02274 100644 --- a/lib/dns/include/dns/types.h +++ b/lib/dns/include/dns/types.h @@ -434,7 +434,7 @@ typedef isc_result_t (*dns_additionaldatafunc_t)( typedef isc_result_t (*dns_digestfunc_t)(void *, isc_region_t *); -typedef void (*dns_xfrindone_t)(dns_zone_t *, isc_result_t); +typedef void (*dns_xfrindone_t)(dns_zone_t *, uint32_t *, isc_result_t); typedef void (*dns_updatecallback_t)(void *, isc_result_t, dns_message_t *); diff --git a/lib/dns/xfrin.c b/lib/dns/xfrin.c index fba413cba6..079d75a083 100644 --- a/lib/dns/xfrin.c +++ b/lib/dns/xfrin.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -141,7 +142,8 @@ struct dns_xfrin { xfrin_state_t state; uint32_t end_serial; - bool is_ixfr; + uint32_t expireopt; + bool edns, is_ixfr, expireoptset; unsigned int nmsg; /*%< Number of messages recvd */ unsigned int nrecs; /*%< Number of records recvd */ @@ -833,7 +835,9 @@ xfrin_fail(dns_xfrin_t *xfr, isc_result_t result, const char *msg) { dns_journal_destroy(&xfr->ixfr.journal); } if (xfr->done != NULL) { - (xfr->done)(xfr->zone, result); + (xfr->done)(xfr->zone, + xfr->expireoptset ? &xfr->expireopt : NULL, + result); xfr->done = NULL; } xfr->shutdown_result = result; @@ -860,6 +864,7 @@ xfrin_create(isc_mem_t *mctx, dns_zone_t *zone, dns_db_t *db, .primaryaddr = *primaryaddr, .sourceaddr = *sourceaddr, .firstsoa = DNS_RDATA_INIT, + .edns = true, .magic = XFRIN_MAGIC, }; @@ -1147,6 +1152,38 @@ request_type(dns_xfrin_t *xfr) { } } +static isc_result_t +add_opt(dns_message_t *message, uint16_t udpsize, bool reqnsid, + bool reqexpire) { + isc_result_t result; + dns_rdataset_t *rdataset = NULL; + dns_ednsopt_t ednsopts[DNS_EDNSOPTIONS]; + int count = 0; + + /* Set EDNS options if applicable. */ + if (reqnsid) { + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_NSID; + ednsopts[count].length = 0; + ednsopts[count].value = NULL; + count++; + } + if (reqexpire) { + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_EXPIRE; + ednsopts[count].length = 0; + ednsopts[count].value = NULL; + count++; + } + result = dns_message_buildopt(message, &rdataset, 0, udpsize, 0, + ednsopts, count); + if (result != ISC_R_SUCCESS) { + return (result); + } + + return (dns_message_setopt(message, rdataset)); +} + /* * Build an *XFR request and send its length prefix. */ @@ -1160,6 +1197,10 @@ xfrin_send_request(dns_xfrin_t *xfr) { dns_name_t *qname = NULL; dns_dbversion_t *ver = NULL; dns_name_t *msgsoaname = NULL; + bool edns = xfr->edns; + bool reqnsid = xfr->view->requestnsid; + bool reqexpire = dns_zone_getrequestexpire(xfr->zone); + uint16_t udpsize = dns_view_getudpsize(xfr->view); LIBDNS_XFRIN_RECV_SEND_REQUEST(xfr, xfr->info); @@ -1198,6 +1239,24 @@ xfrin_send_request(dns_xfrin_t *xfr) { &xfr->ixfr.request_serial)); } + if (edns && xfr->view->peers != NULL) { + dns_peer_t *peer = NULL; + isc_netaddr_t primaryip; + isc_netaddr_fromsockaddr(&primaryip, &xfr->primaryaddr); + result = dns_peerlist_peerbyaddr(xfr->view->peers, &primaryip, + &peer); + if (result == ISC_R_SUCCESS) { + (void)dns_peer_getsupportedns(peer, &edns); + (void)dns_peer_getudpsize(peer, &udpsize); + (void)dns_peer_getrequestnsid(peer, &reqnsid); + (void)dns_peer_getrequestexpire(peer, &reqexpire); + } + } + + if (edns) { + CHECK(add_opt(msg, udpsize, reqnsid, reqexpire)); + } + xfr->nmsg = 0; xfr->nrecs = 0; xfr->nbytes = 0; @@ -1267,6 +1326,38 @@ failure: dns_xfrin_detach(&xfr); } +static void +get_edns_expire(dns_xfrin_t *xfr, dns_message_t *msg) { + isc_result_t result; + dns_rdata_t rdata = DNS_RDATA_INIT; + isc_buffer_t optbuf; + uint16_t optcode; + uint16_t optlen; + + result = dns_rdataset_first(msg->opt); + if (result == ISC_R_SUCCESS) { + dns_rdataset_current(msg->opt, &rdata); + isc_buffer_init(&optbuf, rdata.data, rdata.length); + isc_buffer_add(&optbuf, rdata.length); + while (isc_buffer_remaininglength(&optbuf) >= 4) { + optcode = isc_buffer_getuint16(&optbuf); + optlen = isc_buffer_getuint16(&optbuf); + /* + * A EDNS EXPIRE response has a length of 4. + */ + if (optcode != DNS_OPT_EXPIRE || optlen != 4) { + isc_buffer_forward(&optbuf, optlen); + continue; + } + xfr->expireopt = isc_buffer_getuint32(&optbuf); + xfr->expireoptset = true; + dns_zone_log(xfr->zone, ISC_LOG_DEBUG(1), + "got EDNS EXPIRE of %u", xfr->expireopt); + break; + } + } +} + static void xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) { dns_xfrin_t *xfr = (dns_xfrin_t *)arg; @@ -1324,7 +1415,17 @@ xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) { if (result != ISC_R_SUCCESS || msg->rcode != dns_rcode_noerror || msg->opcode != dns_opcode_query || msg->rdclass != xfr->rdclass) { - if (result == ISC_R_SUCCESS && msg->rcode != dns_rcode_noerror) + if (result == ISC_R_SUCCESS && + msg->rcode == dns_rcode_formerr && xfr->edns && + (xfr->state == XFRST_SOAQUERY || + xfr->state == XFRST_INITIALSOA)) + { + xfr->edns = false; + dns_message_detach(&msg); + xfrin_reset(xfr); + goto try_again; + } else if (result == ISC_R_SUCCESS && + msg->rcode != dns_rcode_noerror) { result = dns_result_fromrcode(msg->rcode); } else if (result == ISC_R_SUCCESS && @@ -1353,6 +1454,7 @@ xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) { xfrin_reset(xfr); xfr->reqtype = dns_rdatatype_soa; xfr->state = XFRST_SOAQUERY; + try_again: result = xfrin_start(xfr); if (result != ISC_R_SUCCESS) { xfrin_fail(xfr, result, "failed setting up socket"); @@ -1518,6 +1620,10 @@ xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) { xfr->tsigctx = msg->tsigctx; msg->tsigctx = NULL; + if (!xfr->expireoptset && msg->opt != NULL) { + get_edns_expire(xfr, msg); + } + switch (xfr->state) { case XFRST_GOTSOA: xfr->reqtype = dns_rdatatype_axfr; @@ -1545,7 +1651,9 @@ xfrin_recv_done(isc_result_t result, isc_region_t *region, void *arg) { if (xfr->done != NULL) { LIBDNS_XFRIN_DONE_CALLBACK_BEGIN(xfr, xfr->info, result); - (xfr->done)(xfr->zone, ISC_R_SUCCESS); + (xfr->done)(xfr->zone, + xfr->expireoptset ? &xfr->expireopt : NULL, + ISC_R_SUCCESS); xfr->done = NULL; LIBDNS_XFRIN_DONE_CALLBACK_END(xfr, xfr->info, result); } diff --git a/lib/dns/zone.c b/lib/dns/zone.c index 2ecc27557c..d463490908 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -862,7 +862,7 @@ zone_catz_disable(dns_zone_t *zone); static isc_result_t default_journal(dns_zone_t *zone); static void -zone_xfrdone(dns_zone_t *zone, isc_result_t result); +zone_xfrdone(dns_zone_t *zone, uint32_t *expireopt, isc_result_t result); static isc_result_t zone_postload(dns_zone_t *zone, dns_db_t *db, isc_time_t loadtime, isc_result_t result); @@ -11390,6 +11390,27 @@ dump_done(void *arg, isc_result_t result) { ENTER; + /* + * Adjust modification time of zone file to preserve expire timing. + */ + if ((zone->type == dns_zone_secondary || + zone->type == dns_zone_mirror || + zone->type == dns_zone_redirect) && + result == ISC_R_SUCCESS) + { + LOCK_ZONE(zone); + isc_time_t when; + isc_interval_t i; + isc_interval_set(&i, zone->expire, 0); + result = isc_time_subtract(&zone->expiretime, &i, &when); + if (result == ISC_R_SUCCESS) { + (void)isc_file_settime(zone->masterfile, &when); + } else { + result = ISC_R_SUCCESS; + } + UNLOCK_ZONE(zone); + } + if (result == ISC_R_SUCCESS && zone->journal != NULL) { /* * We don't own these, zone->dctx must stay valid. @@ -11573,6 +11594,22 @@ redo: } else { result = dns_master_dump(zone->mctx, db, version, masterstyle, masterfile, masterformat, &rawdata); + if ((zone->type == dns_zone_secondary || + zone->type == dns_zone_mirror || + zone->type == dns_zone_redirect) && + result == ISC_R_SUCCESS) + { + isc_time_t when; + isc_interval_t i; + isc_interval_set(&i, zone->expire, 0); + result = isc_time_subtract(&zone->expiretime, &i, + &when); + if (result == ISC_R_SUCCESS) { + (void)isc_file_settime(zone->masterfile, &when); + } else { + result = ISC_R_SUCCESS; + } + } } fail: if (version != NULL) { @@ -17079,20 +17116,23 @@ zone_detachdb(dns_zone_t *zone) { } static void -zone_xfrdone(dns_zone_t *zone, isc_result_t result) { - isc_time_t now; +zone_xfrdone(dns_zone_t *zone, uint32_t *expireopt, isc_result_t result) { + isc_time_t now, expiretime; bool again = false; unsigned int soacount; unsigned int nscount; - uint32_t serial, refresh, retry, expire, minimum, soattl; + uint32_t serial, refresh, retry, expire, minimum, soattl, oldexpire; isc_result_t xfrresult = result; bool free_needed; dns_zone_t *secure = NULL; REQUIRE(DNS_ZONE_VALID(zone)); - dns_zone_logc(zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_DEBUG(1), - "zone transfer finished: %s", isc_result_totext(result)); + dns_zone_logc( + zone, DNS_LOGCATEGORY_XFER_IN, ISC_LOG_DEBUG(1), + expireopt == NULL ? "zone transfer finished: %s" + : "zone transfer finished: %s, expire=%u", + isc_result_totext(result), expireopt != NULL ? *expireopt : 0); /* * Obtaining a lock on the zone->secure (see zone_send_secureserial) @@ -17133,6 +17173,8 @@ again: goto same_primary; } + oldexpire = zone->expire; + /* * Update the zone structure's data from the actual * SOA received. @@ -17185,19 +17227,31 @@ again: } /* - * Set our next update/expire times. + * Set our next refresh time. */ if (DNS_ZONE_FLAG(zone, DNS_ZONEFLG_NEEDREFRESH)) { DNS_ZONE_CLRFLAG(zone, DNS_ZONEFLG_NEEDREFRESH); zone->refreshtime = now; - DNS_ZONE_TIME_ADD(&now, zone->expire, - &zone->expiretime); } else { DNS_ZONE_JITTER_ADD(&now, zone->refresh, &zone->refreshtime); - DNS_ZONE_TIME_ADD(&now, zone->expire, - &zone->expiretime); } + + /* + * Set our next expire time. If the parent returned + * an EXPIRE option use that to update zone->expiretime. + */ + expire = zone->expire; + if (expireopt != NULL && *expireopt < expire) { + expire = *expireopt; + } + DNS_ZONE_TIME_ADD(&now, expire, &expiretime); + if (oldexpire != zone->expire || + isc_time_compare(&expiretime, &zone->expiretime) > 0) + { + zone->expiretime = expiretime; + } + if (result == ISC_R_SUCCESS && xfrresult == ISC_R_SUCCESS) { char buf[DNS_NAME_FORMATSIZE + sizeof(": TSIG ''")]; if (zone->tsigkey != NULL) { @@ -17224,15 +17278,27 @@ again: */ if (zone->masterfile != NULL || zone->journal != NULL) { unsigned int delay = DNS_DUMP_DELAY; + isc_interval_t i; + isc_time_t when; + + /* + * Compute effective modification time. + */ + isc_interval_set(&i, zone->expire, 0); + result = isc_time_subtract(&zone->expiretime, &i, + &when); + if (result != ISC_R_SUCCESS) { + when = now; + } result = ISC_R_FAILURE; if (zone->journal != NULL) { - result = isc_file_settime(zone->journal, &now); + result = isc_file_settime(zone->journal, &when); } if (result != ISC_R_SUCCESS && zone->masterfile != NULL) { result = isc_file_settime(zone->masterfile, - &now); + &when); } if ((DNS_ZONE_FLAG(zone, DNS_ZONEFLG_NODELAY) != 0) || @@ -17715,7 +17781,7 @@ failure: * zmgr->xfrin_in_progress. */ if (result != ISC_R_SUCCESS) { - zone_xfrdone(zone, result); + zone_xfrdone(zone, NULL, result); } if (zmgr_tlsctx_cache != NULL) {