From c95128ed47e62951b464d36149f84aa32d1a4f68 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Thu, 1 May 2025 11:43:21 +0200 Subject: [PATCH 1/3] Remove duplicate check in serve-stale test --- bin/tests/system/serve_stale/tests.sh | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bin/tests/system/serve_stale/tests.sh b/bin/tests/system/serve_stale/tests.sh index 865bea731b..71d7468c8b 100755 --- a/bin/tests/system/serve_stale/tests.sh +++ b/bin/tests/system/serve_stale/tests.sh @@ -2038,17 +2038,6 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) -n=$((n + 1)) -echo_i "check server is alive or restart ($n)" -ret=0 -$RNDCCMD 10.53.0.3 status >rndc.out.test$n 2>&1 || ret=1 -if [ $ret != 0 ]; then - echo_i "failed" - echo_i "restart ns3" - start_server --noclean --restart --port ${PORT} serve-stale ns3 -fi -status=$((status + ret)) - ############################################# # Test for stale-answer-client-timeout 0. # ############################################# From 4ee526cb6d3c34ea2736154e15dbc19211e08321 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 27 Mar 2026 10:32:48 +0100 Subject: [PATCH 2/3] Add serve-stale test case for CNAME to A Add a serve-stale system test case where the authority changes a CNAME RRset to A (at cname2.stale.test). The CNAME that is in the cache is stale and should be refreshed. The target A record (at a2.stale.test) has a longer TTL and is also still in the cache. The next query should return the refreshed A RRset to the client. Then the authority changes back the A RRset to CNAME. The A RRset has become stale and should be refreshed. The next query should return the refreshed CNAME RRset plus the already cached a2.stale.test A record. This test requires ns1 to allow dynamic updates to stale.test, and prefetch to be disabled. The latter is to ensure the record is not prefetched, but only refreshed when stale (and logs the expected "an attempt to refresh the RRset" messages). --- .../system/serve_stale/ns1/named.conf.j2 | 1 + .../system/serve_stale/ns1/named4.conf.in | 1 + .../system/serve_stale/ns3/named.conf.j2 | 1 + .../system/serve_stale/ns3/named1.conf.j2 | 1 + .../system/serve_stale/ns3/named2.conf.j2 | 1 + .../system/serve_stale/ns3/named3.conf.j2 | 1 + .../system/serve_stale/ns3/named4.conf.j2 | 1 + .../system/serve_stale/ns3/named5.conf.j2 | 1 + .../system/serve_stale/ns3/named6.conf.j2 | 1 + .../system/serve_stale/ns3/named7.conf.j2 | 1 + .../system/serve_stale/ns3/named9.conf.j2 | 1 + bin/tests/system/serve_stale/tests.sh | 83 +++++++++++++++++++ .../serve_stale/tests_sh_serve_stale.py | 1 + 13 files changed, 95 insertions(+) diff --git a/bin/tests/system/serve_stale/ns1/named.conf.j2 b/bin/tests/system/serve_stale/ns1/named.conf.j2 index 3dff62358c..17b09dd400 100644 --- a/bin/tests/system/serve_stale/ns1/named.conf.j2 +++ b/bin/tests/system/serve_stale/ns1/named.conf.j2 @@ -42,5 +42,6 @@ zone "." { zone "stale.test" { type primary; file "stale.test.db"; + allow-update { any; }; }; {% endif %} diff --git a/bin/tests/system/serve_stale/ns1/named4.conf.in b/bin/tests/system/serve_stale/ns1/named4.conf.in index 92c485542c..28eb0fd67b 100644 --- a/bin/tests/system/serve_stale/ns1/named4.conf.in +++ b/bin/tests/system/serve_stale/ns1/named4.conf.in @@ -34,4 +34,5 @@ zone "." { zone "stale.test" { type primary; file "stale.test.db"; + allow-update { any; }; }; diff --git a/bin/tests/system/serve_stale/ns3/named.conf.j2 b/bin/tests/system/serve_stale/ns3/named.conf.j2 index aff0b00327..227daa0703 100644 --- a/bin/tests/system/serve_stale/ns3/named.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named.conf.j2 @@ -18,6 +18,7 @@ options { recursion yes; dnssec-validation no; qname-minimization off; + prefetch 0; stale-answer-enable yes; stale-cache-enable yes; diff --git a/bin/tests/system/serve_stale/ns3/named1.conf.j2 b/bin/tests/system/serve_stale/ns3/named1.conf.j2 index 165dacfe1b..c67313e0bf 100644 --- a/bin/tests/system/serve_stale/ns3/named1.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named1.conf.j2 @@ -19,6 +19,7 @@ options { dump-file "named_dump3.db"; stale-cache-enable yes; dnssec-validation no; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named2.conf.j2 b/bin/tests/system/serve_stale/ns3/named2.conf.j2 index 6537de9fa5..f6ad7954f3 100644 --- a/bin/tests/system/serve_stale/ns3/named2.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named2.conf.j2 @@ -29,6 +29,7 @@ options { max-stale-ttl 3600; resolver-query-timeout 30000; # 30 seconds qname-minimization disabled; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named3.conf.j2 b/bin/tests/system/serve_stale/ns3/named3.conf.j2 index 65bddea352..0661a7cc5e 100644 --- a/bin/tests/system/serve_stale/ns3/named3.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named3.conf.j2 @@ -27,6 +27,7 @@ options { stale-refresh-time 0; max-stale-ttl 3600; resolver-query-timeout 10000; # 10 seconds + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named4.conf.j2 b/bin/tests/system/serve_stale/ns3/named4.conf.j2 index dbca82b3a4..85da1d1bf9 100644 --- a/bin/tests/system/serve_stale/ns3/named4.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named4.conf.j2 @@ -29,6 +29,7 @@ options { resolver-query-timeout 10000; # 10 seconds max-stale-ttl 3600; recursive-clients 10; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named5.conf.j2 b/bin/tests/system/serve_stale/ns3/named5.conf.j2 index 753a84ba3f..845ee820b6 100644 --- a/bin/tests/system/serve_stale/ns3/named5.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named5.conf.j2 @@ -28,6 +28,7 @@ options { stale-refresh-time 4; resolver-query-timeout 10000; # 10 seconds max-stale-ttl 3600; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named6.conf.j2 b/bin/tests/system/serve_stale/ns3/named6.conf.j2 index d23ec8515a..59a3266af3 100644 --- a/bin/tests/system/serve_stale/ns3/named6.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named6.conf.j2 @@ -25,6 +25,7 @@ options { fetches-per-zone 1 fail; fetches-per-server 1 fail; max-stale-ttl 3600; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named7.conf.j2 b/bin/tests/system/serve_stale/ns3/named7.conf.j2 index 8558640109..79b197bbd1 100644 --- a/bin/tests/system/serve_stale/ns3/named7.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named7.conf.j2 @@ -33,6 +33,7 @@ options { fetches-per-zone 1 fail; fetches-per-server 1 fail; max-stale-ttl 3600; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/ns3/named9.conf.j2 b/bin/tests/system/serve_stale/ns3/named9.conf.j2 index cac3de04bf..dd46102be7 100644 --- a/bin/tests/system/serve_stale/ns3/named9.conf.j2 +++ b/bin/tests/system/serve_stale/ns3/named9.conf.j2 @@ -21,6 +21,7 @@ options { stale-cache-enable yes; stale-answer-ttl 3; stale-answer-client-timeout 0; + prefetch 0; }; zone "." { diff --git a/bin/tests/system/serve_stale/tests.sh b/bin/tests/system/serve_stale/tests.sh index 71d7468c8b..90841afe6a 100755 --- a/bin/tests/system/serve_stale/tests.sh +++ b/bin/tests/system/serve_stale/tests.sh @@ -2359,6 +2359,89 @@ grep "cname-b3\.stale\.test\..*3.*IN.*A.*192\.0\.2\.2" dig.out.test$n >/dev/null if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) +# Change CNAME to A. +n=$((n + 1)) +echo_i "change cname2.stale.test. CNAME to A (stale-answer-client-timeout 0) ($n)" +ret=0 +( + echo zone stale.test. + echo server 10.53.0.1 "${PORT}" + echo update del cname2.stale.test. CNAME a2.stale.test. + echo update add cname2.stale.test. 1 A 192.0.0.2 + echo send +) | $NSUPDATE || ret=1 +test "$ret" -eq 0 || echo_i "failed" +status=$((status + ret)) + +n=$((n + 1)) +ret=0 +echo_i "check stale cname2.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)" +nextpart ns3/named.run >/dev/null +$DIG -p ${PORT} @10.53.0.3 cname2.stale.test A >dig.out.test$n || ret=1 +wait_for_log 5 "cname2.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1 +grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1 +grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1 +grep "cname2\.stale\.test\..*3.*IN.*CNAME.*a2\.stale\.test\." dig.out.test$n >/dev/null || ret=1 +# We can't reliably test the TTL of the a2.stale.test A record. +grep "a2\.stale\.test\..*IN.*A.*192\.0\.2\.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +ret=0 +echo_i "check stale cname2.stale.test A is refreshed (stale-answer-client-timeout 0) ($n)" +nextpart ns3/named.run >/dev/null +$DIG -p ${PORT} @10.53.0.3 cname2.stale.test A >dig.out.test$n || ret=1 +grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1 +grep "cname2\.stale\.test\..*IN.*A.*192\.0\.0\.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +# Change A to CNAME. +n=$((n + 1)) +echo_i "change cname2.stale.test. A to CNAME (stale-answer-client-timeout 0) ($n)" +ret=0 +( + echo zone stale.test. + echo server 10.53.0.1 "${PORT}" + echo update del cname2.stale.test. A 192.0.0.2 + echo update add cname2.stale.test. 1 CNAME a2.stale.test. + echo send +) | $NSUPDATE || ret=1 +test "$ret" -eq 0 || echo_i "failed" +status=$((status + ret)) + +# Allow A record to become stale. +sleep 1 + +n=$((n + 1)) +ret=0 +echo_i "check stale cname2.stale.test A comes from cache (stale-answer-client-timeout 0) ($n)" +nextpart ns3/named.run >/dev/null +$DIG -p ${PORT} @10.53.0.3 cname2.stale.test A >dig.out.test$n || ret=1 +wait_for_log 5 "cname2.stale.test A stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1 +grep "EDE: 3 (Stale Answer): (stale data prioritized over lookup)" dig.out.test$n >/dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1 +grep "cname2\.stale\.test\..*3.*IN.*A.*192\.0\.0\.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +ret=0 +echo_i "check stale cname2.stale.test A is refreshed (stale-answer-client-timeout 0) ($n)" +nextpart ns3/named.run >/dev/null +$DIG -p ${PORT} @10.53.0.3 cname2.stale.test A >dig.out.test$n || ret=1 +grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1 +grep "ANSWER: 2," dig.out.test$n >/dev/null || ret=1 +# We can't reliably test the TTL of the these records. +grep "cname2\.stale\.test\..*IN.*CNAME.*a2\.stale\.test\." dig.out.test$n >/dev/null || ret=1 +grep "a2\.stale\.test\..*IN.*A.*192\.0\.2\.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + #################################################################### # Test for stale-answer-client-timeout 0 and stale-refresh-time 4. # #################################################################### diff --git a/bin/tests/system/serve_stale/tests_sh_serve_stale.py b/bin/tests/system/serve_stale/tests_sh_serve_stale.py index 1f96485cd2..b7bb320a5e 100644 --- a/bin/tests/system/serve_stale/tests_sh_serve_stale.py +++ b/bin/tests/system/serve_stale/tests_sh_serve_stale.py @@ -22,6 +22,7 @@ pytestmark = pytest.mark.extra_artifacts( "ns*/named_dump*", "ns*/named.stats*", "ns*/root.bk", + "ns1/stale.test.db.jnl", ] ) From 69a560fff189f3295a4501a51676ad7e7d0479e3 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Fri, 27 Mar 2026 10:48:21 +0100 Subject: [PATCH 3/3] When caching names, check for CNAME RRsets CNAME and other record types cannot coexist. DNSSEC records are the exceptions to this rule. If the answer contains a name with a CNAME, remove existing RRsets at the same name from the cache. If the answer contains a name without a CNAME, remove the CNAME RRset at the same name from the cache. --- lib/dns/resolver.c | 133 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 117 insertions(+), 16 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index e0575ab8a8..faf69ba588 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -69,6 +69,7 @@ #include #include #include +#include #include #include #include @@ -5521,6 +5522,99 @@ getrrsig(dns_name_t *name, dns_rdatatype_t type) { return NULL; } +static void +delete_rrset(fetchctx_t *fctx, dns_name_t *name, dns_rdatatype_t type) { + isc_result_t result; + dns_dbnode_t *node = NULL; + + result = dns_db_findnode(fctx->cache, name, false, &node); + if (result != ISC_R_SUCCESS) { + return; + } + + dns_db_deleterdataset(fctx->cache, node, NULL, type, 0); + dns_db_deleterdataset(fctx->cache, node, NULL, dns_rdatatype_rrsig, + type); + dns_db_detachnode(&node); +} + +/* + * When caching a CNAME, evict other RRsets at the same owner name, + * according to the RFC specifications. + * + * RFC 1034, 3.6.2: Aliases and canonical names + * If a CNAME RR is present at a node, no other data should be + * present. + * RFC 2181, 10.1: CNAME resource records + * An alias name (label of a CNAME record) may, + * if DNSSEC is in use, have SIG, NXT, and KEY RRs, but may have no + * other data. + * RFC 2535, 2.3.5: Special Considerations with CNAME + * RFC 4034, 3: The RRSIG Resource Record + * Because every authoritative RRset in a zone must be protected by a + * digital signature, RRSIG RRs must be present for names containing a + * CNAME RR. This is a change to the traditional DNS specification + * [RFC1034], which stated that if a CNAME is present for a name, it is + * the only type allowed at that name. + * RFC 4034, 4: The NSEC Resource Record + * Because every authoritative name in a zone must be part of the NSEC + * chain, NSEC RRs must be present for names containing a CNAME RR. + * This is a change to the traditional DNS specification [RFC1034], + * which stated that if a CNAME is present for a name, it is the only + * type allowed at that name. + * + * So types allowed next to CNAME are: KEY, SIG, NXT, RRSIG, and NSEC. + */ +static void +evict_cname_other(fetchctx_t *fctx, dns_name_t *name) { + isc_result_t result; + dns_dbnode_t *node = NULL; + dns_rdatasetiter_t *rdsiter = NULL; + + result = dns_db_findnode(fctx->cache, name, false, &node); + if (result != ISC_R_SUCCESS) { + return; + } + + result = dns_db_allrdatasets(fctx->cache, node, NULL, 0, 0, &rdsiter); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(&node); + return; + } + + DNS_RDATASETITER_FOREACH(rdsiter) { + dns_rdataset_t rdataset = DNS_RDATASET_INIT; + dns_rdatasetiter_current(rdsiter, &rdataset); + if (rdataset.type == dns_rdatatype_nsec || + rdataset.type == dns_rdatatype_nxt || + rdataset.type == dns_rdatatype_key) + { + /* KEY, NSEC and NXT records are allowed */ + dns_rdataset_disassociate(&rdataset); + continue; + } + if (dns_rdatatype_issig(rdataset.type)) { + /* Signatures will be deleted together below */ + dns_rdataset_disassociate(&rdataset); + continue; + } + if (rdataset.type == dns_rdatatype_none) { + /* Negative type. */ + dns_rdataset_disassociate(&rdataset); + continue; + } + + dns_db_deleterdataset(fctx->cache, node, NULL, rdataset.type, + 0); + dns_db_deleterdataset(fctx->cache, node, NULL, + dns_rdatatype_rrsig, rdataset.type); + dns_rdataset_disassociate(&rdataset); + } + + dns_rdatasetiter_destroy(&rdsiter); + dns_db_detachnode(&node); +} + static isc_result_t cache_rrset(fetchctx_t *fctx, isc_stdtime_t now, dns_name_t *name, dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset, @@ -5575,6 +5669,21 @@ cache_rrset(fetchctx_t *fctx, isc_stdtime_t now, dns_name_t *name, result = dns_db_findnode(fctx->cache, name, true, &node); } + /* + * Evict CNAME records, according to the RFC rules (see + * evict_cname_other). + * + * Note that a signature is tied to the type it covers and is deleted + * along with the covered RRset in 'delete_rrset()'. + */ + if (!dns_rdataset_matchestype(rdataset, dns_rdatatype_cname) && + !dns_rdataset_matchestype(rdataset, dns_rdatatype_key) && + !dns_rdataset_matchestype(rdataset, dns_rdatatype_nsec) && + !dns_rdataset_matchestype(rdataset, dns_rdatatype_nxt)) + { + delete_rrset(fctx, name, dns_rdatatype_cname); + } + if (result == ISC_R_SUCCESS) { result = dns_db_addrdataset(fctx->cache, node, NULL, now, rdataset, options | equalok, added); @@ -5611,22 +5720,6 @@ cache_rrset(fetchctx_t *fctx, isc_stdtime_t now, dns_name_t *name, return result; } -static void -delete_rrset(fetchctx_t *fctx, dns_name_t *name, dns_rdatatype_t type) { - isc_result_t result; - dns_dbnode_t *node = NULL; - - result = dns_db_findnode(fctx->cache, name, false, &node); - if (result != ISC_R_SUCCESS) { - return; - } - - dns_db_deleterdataset(fctx->cache, node, NULL, type, 0); - dns_db_deleterdataset(fctx->cache, node, NULL, dns_rdatatype_rrsig, - type); - dns_db_detachnode(&node); -} - static void fctx_cacheauthority(fetchctx_t *fctx, dns_message_t *message, isc_stdtime_t now) { @@ -6271,6 +6364,14 @@ rctx_cachename(respctx_t *rctx, dns_message_t *message, dns_name_t *name) { goto cleanup; } + /* + * If CNAME, delete other RRsets at the same name + * from the cache. + */ + if (rdataset->type == dns_rdatatype_cname) { + evict_cname_other(fctx, name); + } + /* Find the signature for this rdataset */ sigrdataset = getrrsig(name, rdataset->type);