diff --git a/doc/ChangeLog b/doc/ChangeLog index 17bea4997..3ed07f3ab 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,9 @@ +26 July 2026: Jared + - Fall back to AXFR when an IXFR response is SERVFAIL (if + allow-axfr-fallback is enabled), matching the existing NOTIMPL + and FORMERR fallback. This recovers automatically when a primary + cannot serve IXFR due to inconsistent history (GitHub #479). + 16 July 2026: Willem - Merge #500: Support for the "oots" SVCB Service Parameter Key diff --git a/doc/README b/doc/README index 6b4f02df1..becbf0982 100644 --- a/doc/README +++ b/doc/README @@ -328,7 +328,8 @@ are made over TCP. If you want the IXFR request be transmitted over UDP, use request-xfr: 168.192.199.2 NOKEY By default, a slave will fallback to AXFR requests if the master told us it does -not support IXFR. You can configure the slave not to do AXFR fallback with: +not support IXFR, or if an IXFR fails with SERVFAIL. You can configure the slave +not to do AXFR fallback with: allow-axfr-fallback: "no" diff --git a/doc/manual/configuration.rst b/doc/manual/configuration.rst index 85e2e5041..10ba19156 100644 --- a/doc/manual/configuration.rst +++ b/doc/manual/configuration.rst @@ -126,8 +126,8 @@ are made over TCP. If you want the IXFR request be transmitted over UDP, use request-xfr: 168.192.199.2 NOKEY By default, a secondary will fallback to AXFR requests if the primary told us it -does not support IXFR. You can configure the secondary not to do AXFR fallback -with: +does not support IXFR, or if an IXFR fails with SERVFAIL. You can configure the +secondary not to do AXFR fallback with: .. code-block:: text diff --git a/nsd.conf.5.in b/nsd.conf.5.in index 261d401f2..b2d69ba5f 100644 --- a/nsd.conf.5.in +++ b/nsd.conf.5.in @@ -1003,7 +1003,8 @@ not be sent. Support for TLS 1.3 is required for XFR-over-TLS. .TP .B allow\-axfr\-fallback:\fR This option should be accompanied by request\-xfr. It (dis)allows NSD (as secondary) -to fallback to AXFR if the primary name server does not support IXFR. Default is yes. +to fallback to AXFR if the primary name server does not support IXFR (NOTIMPL or +FORMERR) or cannot serve the requested IXFR (SERVFAIL). Default is yes. .TP .B size\-limit\-xfr:\fR This option should be accompanied by request\-xfr. It specifies XFR temporary file size limit. It can be used to stop very large zone retrieval, that could otherwise use up a lot of memory and disk space. diff --git a/nsd.conf.sample.in b/nsd.conf.sample.in index b0fc4db73..7f3f1de4c 100644 --- a/nsd.conf.sample.in +++ b/nsd.conf.sample.in @@ -512,7 +512,8 @@ remote-control: #request-xfr: 192.0.2.2 the_tsig_key_name the_tls_auth_name # Attention: You cannot use UDP and AXFR together. AXFR is always over # TCP. If you use UDP, we highly recommend you to deploy TSIG. - # Allow AXFR fallback if the primary does not support IXFR. Default + # Allow AXFR fallback if the primary does not support IXFR + # (NOTIMPL/FORMERR) or cannot serve the IXFR (SERVFAIL). Default # is yes. #allow-axfr-fallback: yes # set local interface for sending zone transfer requests. diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.conf b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.conf new file mode 100644 index 000000000..da630638b --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.conf @@ -0,0 +1,12 @@ +server: + logfile: "nsd.log" + xfrdfile: xfrd.state + zonesdir: "" + zonelistfile: "zone.list" + interface: 127.0.0.1 + +zone: + name: example.net + zonefile: axfr_fallback_servfail.zone + request-xfr: UDP 127.0.0.1@RANDOM NOKEY + allow-notify: 127.0.0.1@RANDOM NOKEY diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.datafile b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.datafile new file mode 100644 index 000000000..2ec47b018 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.datafile @@ -0,0 +1,32 @@ +$ORIGIN example.net. +$TTL 7200 + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QUERY +REPLY SERVFAIL +REPLY AA AD +ADJUST copy_id ; copy_id copies the ID from the query to the answer. +SECTION QUESTION +example.net. IN IXFR +SECTION ANSWER +SECTION AUTHORITY +SECTION ADDITIONAL +ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +MATCH TCP +REPLY QUERY +REPLY NOERROR +REPLY AA AD +ADJUST copy_id ; copy_id copies the ID from the query to the answer. +SECTION QUESTION +example.net. IN AXFR +SECTION ANSWER +example.net. IN SOA nibbler.example.net. leela.example.net. 2 2000 3000 4000 5000 +www.example.net. IN A 127.0.0.2 +example.net. IN SOA nibbler.example.net. leela.example.net. 2 2000 3000 4000 5000 +SECTION AUTHORITY +SECTION ADDITIONAL +ENTRY_END diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.dsc b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.dsc new file mode 100644 index 000000000..dbb7fbe51 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.dsc @@ -0,0 +1,17 @@ +BaseName: axfr_fallback_servfail +Version: 1.0 +Description: Test AXFR fallback when IXFR gets SERVFAIL (GitHub #479) +CreationDate: Sun Jul 26 19:48:00 EDT 2026 +Maintainer: Jared Mauch +Category: running +Component: +CmdDepends: +Depends: 0000_nsd-compile.tpkg +Help: axfr_fallback_servfail.help +Pre: axfr_fallback_servfail.pre +Post: axfr_fallback_servfail.post +Test: axfr_fallback_servfail.test +AuxFiles: axfr_fallback_servfail.known_good, axfr_fallback_servfail.conf, axfr_fallback_servfail.datafile, + axfr_fallback_servfail.zone +Passed: +Failure: diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.help b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.help new file mode 100644 index 000000000..848724540 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.help @@ -0,0 +1,3 @@ +This test makes use of ldns-testns, nsd and dig. +It takes no arguments. +It tests whether NSD falls back to AXFR if IXFR fails with SERVFAIL. diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.known_good b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.known_good new file mode 100644 index 000000000..c239b2f89 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.known_good @@ -0,0 +1 @@ +www.example.net. 7200 IN A 127.0.0.2 diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.post b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.post new file mode 100644 index 000000000..b8f81fd4f --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.post @@ -0,0 +1,21 @@ +# #-- axfr_fallback_servfail.post --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# source the test var file when it's there +[ -f .tpkg.var.test ] && source .tpkg.var.test +# +# do your teardown here +. ../common.sh +kill_pid $TESTNS_PID + +rm -f xfrd.state +rm -f axfr_fallback_servfail.current + +# do your teardown here +if [ -z $TPKG_NSD_PID ]; then + exit 0 +fi + +# kill NSD +NSD_PID=`cat $TPKG_NSD_PID` +kill_pid $NSD_PID diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.pre b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.pre new file mode 100644 index 000000000..939d0e2c5 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.pre @@ -0,0 +1,23 @@ +# #-- axfr_fallback_servfail.pre--# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test +. ../common.sh + +# start NSD +get_random_port 1 +LDNS_PORT=$RND_PORT +echo port: $LDNS_PORT + +# start ldns-testns, be extra verbose +ldns-testns -vvv -p $LDNS_PORT axfr_fallback_servfail.datafile >testns.log 2>&1 & +echo "export TESTNS_PID=$!" >> .tpkg.var.test +echo "export LDNS_PORT=$LDNS_PORT" >> .tpkg.var.test +wait_ldns_testns_up testns.log + +# replace RANDOM with $LDNS_PORT and put it in nsd_axfr.conf +cat axfr_fallback_servfail.conf | sed "s/RANDOM/$LDNS_PORT/g" > nsd_axfr.conf +if [[ $? -ne 0 ]]; then + exit 1 +fi diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.test b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.test new file mode 100644 index 000000000..649737856 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.test @@ -0,0 +1,43 @@ +# #-- axfr_fallback_servfail.test --# +# source the master var file when it's there +[ -f ../.tpkg.var.master ] && source ../.tpkg.var.master +# use .tpkg.var.test for in test variable passing +[ -f .tpkg.var.test ] && source .tpkg.var.test +. ../common.sh + +# start NSD +get_random_port 1 +TPKG_PORT=$RND_PORT + +PRE="../.." +TPKG_NSD_PID="nsd.pid.$$" +TPKG_NSD="$PRE/nsd" +# share the vars +echo "export TPKG_PORT=$TPKG_PORT" >> .tpkg.var.test +echo "export TPKG_NSD_PID=$TPKG_NSD_PID" >> .tpkg.var.test + +$TPKG_NSD -c nsd_axfr.conf -u "" -p $TPKG_PORT -P $TPKG_NSD_PID -V 2 -l nsd.log +wait_nsd_up nsd.log +sleep 3 + +# now query for the update I sent and compare that with a known good one +echo port: $TPKG_PORT +dig @127.0.0.1 -p $TPKG_PORT a www.example.net +dig @127.0.0.1 -p $TPKG_PORT a www.example.net |grep "^www.example.net" > axfr_fallback_servfail.current + +echo "CURRENT:" +cat axfr_fallback_servfail.current + +echo "KNOWN GOOD:" +cat axfr_fallback_servfail.known_good + +diff -u axfr_fallback_servfail.current axfr_fallback_servfail.known_good +if [[ $? -ne 0 ]]; then + echo testns.log + cat testns.log + echo nsd.log + cat nsd.log + exit 1 +else + exit 0 +fi diff --git a/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.zone b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.zone new file mode 100644 index 000000000..4e1ded1f6 --- /dev/null +++ b/tpkg/axfr_fallback_servfail.tdir/axfr_fallback_servfail.zone @@ -0,0 +1,4 @@ +; nothing fancy here +$TTL 3600 +$ORIGIN example.net. +example.net. IN SOA nibbler.example.net. leela.example.net. 1 2000 3000 4000 5000 diff --git a/tpkg/checkconf.tdir/checkconf.nsd11.conf b/tpkg/checkconf.tdir/checkconf.nsd11.conf index 02252d7d7..dca1d89ca 100644 --- a/tpkg/checkconf.tdir/checkconf.nsd11.conf +++ b/tpkg/checkconf.tdir/checkconf.nsd11.conf @@ -428,7 +428,8 @@ pattern: #request-xfr: 192.0.2.2 the_tsig_key_name the_tls_auth_name # Attention: You cannot use UDP and AXFR together. AXFR is always over # TCP. If you use UDP, we highly recommend you to deploy TSIG. - # Allow AXFR fallback if the master does not support IXFR. Default + # Allow AXFR fallback if the master does not support IXFR, or if + # IXFR fails with SERVFAIL. Default # is yes. #allow-axfr-fallback: yes # set local interface for sending zone transfer requests. diff --git a/xfrd.c b/xfrd.c index 2bd2425f2..63afef491 100644 --- a/xfrd.c +++ b/xfrd.c @@ -2296,7 +2296,12 @@ xfrd_parse_received_xfr_packet(xfrd_zone_type* zone, buffer_type* packet, zone->master->ip_address_spec); } if (RCODE(packet) == RCODE_IMPL || - RCODE(packet) == RCODE_FORMAT) { + RCODE(packet) == RCODE_FORMAT || + RCODE(packet) == RCODE_SERVFAIL) { + /* NOTIMPL/FORMERR: primary does not support IXFR. + * SERVFAIL: primary cannot serve this IXFR (for + * example inconsistent IXFR history). Fall back to + * AXFR when allow-axfr-fallback is enabled. */ return xfrd_packet_notimpl; } if (RCODE(packet) != RCODE_NOTAUTH) { diff --git a/xfrd.h b/xfrd.h index 1e586740d..4b9c65f84 100644 --- a/xfrd.h +++ b/xfrd.h @@ -249,7 +249,7 @@ enum xfrd_packet_result { xfrd_packet_bad, /* drop the packet/connection */ xfrd_packet_drop, /* drop the connection, but not report bad */ xfrd_packet_more, /* more packets to follow on tcp */ - xfrd_packet_notimpl, /* server responded with NOTIMPL or FORMATERR */ + xfrd_packet_notimpl, /* NOTIMPL, FORMERR, or SERVFAIL (IXFR->AXFR) */ xfrd_packet_tcp, /* try tcp connection */ xfrd_packet_transfer, /* server responded with transfer*/ xfrd_packet_newlease /* no changes, soa OK */