- Fix lame server detection, for selfpointed glue records.

Thanks to Shuhan Zhang, Dan Li, and Baojun Liu from Tsinghua
  University for the report.
This commit is contained in:
W.C.A. Wijngaards
2026-05-20 15:01:42 +02:00
parent 9ce52de6c1
commit bc703c9129
17 changed files with 331 additions and 32 deletions
+3
View File
@@ -38,6 +38,9 @@
- Fix in depth for serve-expired responses from cachedb, that it
does not store bogus. Thanks to Qifan Zhang, Palo Alto Networks,
for the report.
- Fix lame server detection, for selfpointed glue records.
Thanks to Shuhan Zhang, Dan Li, and Baojun Liu from Tsinghua
University for the report.
18 May 2026: Wouter
- Fix for mixed class referrals, the resolver uses the query
+18 -7
View File
@@ -107,7 +107,7 @@ response_type_from_cache(struct dns_msg* msg,
enum response_type
response_type_from_server(int rdset,
struct dns_msg* msg, struct query_info* request, struct delegpt* dp,
int* empty_nodata_found)
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral)
{
uint8_t* origzone = (uint8_t*)"\000"; /* the default */
struct ub_packed_rrset_key* s;
@@ -122,6 +122,10 @@ response_type_from_server(int rdset,
/* If the message is NXDOMAIN, then it answers the question. */
if(FLAGS_GET_RCODE(msg->rep->flags) == LDNS_RCODE_NXDOMAIN) {
if(msg->rep->an_numrrsets == 0 &&
msg->rep->ns_numrrsets == 0 &&
msg_lame_empty)
return RESPONSE_TYPE_LAME;
/* make sure its not recursive when we don't want it to */
if( (msg->rep->flags&BIT_RA) &&
!(msg->rep->flags&BIT_AA) && !rdset)
@@ -143,6 +147,10 @@ response_type_from_server(int rdset,
if(FLAGS_GET_RCODE(msg->rep->flags) != LDNS_RCODE_NOERROR)
return RESPONSE_TYPE_THROWAWAY;
if(msg->rep->an_numrrsets == 0 && msg->rep->ns_numrrsets == 0 &&
msg_lame_empty)
return RESPONSE_TYPE_LAME;
/* Note: TC bit has already been handled */
if(dp) {
@@ -249,13 +257,16 @@ response_type_from_server(int rdset,
* which gives ns==zone delegation from cache
* without AA bit as well, with nodata nosoa*/
/* real answer must be +AA and SOA RFC(2308),
* so this is wrong, and we SERVFAIL it if
* this is the only possible reply, if it
* is misdeployed the THROWAWAY makes us pick
* the next server from the selection */
if(msg->rep->an_numrrsets==0 &&
* this is picked up as lame_referral by the
* sanitize step, so it can spot if there
* was data in the answer section before
* removal. If such data is then removed we
* do not want to turn that answer into lame.
* But if it was not there, it can be lame. */
if(msg_lame_referral &&
msg->rep->an_numrrsets==0 &&
!(msg->rep->flags&BIT_AA) && !rdset)
return RESPONSE_TYPE_THROWAWAY;
return RESPONSE_TYPE_LAME;
return RESPONSE_TYPE_ANSWER;
}
/* If we are getting a referral upwards (or to
+5 -1
View File
@@ -120,10 +120,14 @@ enum response_type response_type_from_cache(struct dns_msg* msg,
* @param dp: The delegation point that was being queried
* when the response was returned.
* @param empty_nodata_found: flag to keep track of empty nodata detection.
* @param msg_lame_empty: The scrubber indicates that this empty message
* is lame, before it became empty.
* @param msg_lame_referral: returned true if the reply has a referral before
* scrub.
* @return the response type (CNAME or ANSWER).
*/
enum response_type response_type_from_server(int rdset,
struct dns_msg* msg, struct query_info* request, struct delegpt* dp,
int* empty_nodata_found);
int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral);
#endif /* ITERATOR_ITER_RESPTYPE_H */
+54 -3
View File
@@ -316,6 +316,20 @@ synth_cname_rrset(uint8_t** sname, size_t* snamelen, uint8_t* alias,
return cn;
}
/** Check if the packet has type NS in answer or authority section */
static int
pkt_contains_ns(struct msg_parse* msg)
{
struct rrset_parse* rrset;
for(rrset = msg->rrset_first; rrset; rrset = rrset->rrset_all_next) {
if(rrset->type == LDNS_RR_TYPE_NS &&
(rrset->section == LDNS_SECTION_ANSWER ||
rrset->section == LDNS_SECTION_AUTHORITY))
return 1;
}
return 0;
}
/** check if DNAME applies to a name */
static int
pkt_strict_sub(sldns_buffer* pkt, uint8_t* sname, uint8_t* dr)
@@ -973,12 +987,20 @@ scrub_sanitize_rr_length(sldns_buffer* pkt, struct msg_parse* msg,
* @param env: module environment with config and cache.
* @param ie: iterator environment with private address data.
* @param qstate: for setting errinf for EDE error messages.
* @param pkt_before_NS: if the packet had type NS before scrub. If that
* is removed now, that indicates this may have been lame.
* @param msg_lame_empty: returned true if the empty packet is lame.
* @param msg_lame_referral: returned true if the reply has a referral before
* scrub.
* @param rdset: if RD bit was sent in query sent by unbound.
* @return 0 on error.
*/
static int
scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
struct query_info* qinfo, uint8_t* zonename, struct module_env* env,
struct iter_env* ie, struct module_qstate* qstate)
struct iter_env* ie, struct module_qstate* qstate,
int pkt_before_NS, int* msg_lame_empty, int* msg_lame_referral,
int rdset)
{
int del_addi = 0; /* if additional-holding rrsets are deleted, we
do not trust the normalized additional-A-AAAA any more */
@@ -1135,6 +1157,21 @@ scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
prev = rrset;
rrset = rrset->rrset_all_next;
}
/* If the packet is empty now, but it was not before. And there
* was type NS in authority, then that indicates the answer is lame. */
if(msg->rrset_first == NULL && pkt_before_NS) {
*msg_lame_empty = 1;
verbose(VERB_ALGO, "sanitize: empty message had referral to NS before, marked as lame");
} else if(pkt_before_NS && msg->an_rrsets==0 &&
!(msg->flags&BIT_AA) && !rdset) {
/* If the packet is now a referral, not really a nodata,
* then if it was also with an empty answer section before,
* it is also lame. */
*msg_lame_referral = 1;
verbose(VERB_ALGO, "sanitize: message has referral not answer, marked as lame");
}
return 1;
}
@@ -1142,11 +1179,15 @@ int
scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
struct query_info* qinfo, uint8_t* zonename, struct regional* region,
struct module_env* env, struct module_qstate* qstate,
struct iter_env* ie)
struct iter_env* ie, int* msg_lame_empty, int* msg_lame_referral,
int rdset)
{
int pkt_before_NS;
/* basic sanity checks */
log_nametypeclass(VERB_ALGO, "scrub for", zonename, LDNS_RR_TYPE_NS,
qinfo->qclass);
*msg_lame_empty = 0;
*msg_lame_referral = 0;
if(msg->qdcount > 1)
return 0;
if( !(msg->flags&BIT_QR) )
@@ -1171,11 +1212,21 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
return 0;
}
/* If the packet contains type NS in authority before scrub,
* like a self referral. With the answer section empty, it
* was not AA, the query was not sent with RD, with NS in auth,
* and no SOA in auth. For a negative answer, type SOA is present.
* This detects certain lameness if after has removed that. */
pkt_before_NS = msg->an_rrsets == 0 &&
!(msg->flags&BIT_AA) && !rdset &&
pkt_contains_ns(msg) && !soa_in_auth(msg);
/* normalize the response, this cleans up the additional. */
if(!scrub_normalize(pkt, msg, qinfo, region, env, zonename))
return 0;
/* delete all out-of-zone information */
if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate))
if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate,
pkt_before_NS, msg_lame_empty, msg_lame_referral, rdset))
return 0;
return 1;
}
+6 -1
View File
@@ -62,11 +62,16 @@ struct module_qstate;
* @param env: module environment with config settings and cache.
* @param qstate: for setting errinf for EDE error messages.
* @param ie: iterator module environment data.
* @param msg_lame_empty: returned true if the empty packet is lame.
* @param msg_lame_referral: returned true if the reply has a referral before
* scrub.
* @param rdset: if RD bit was sent in query sent by unbound.
* @return: false if the message is total waste. true if scrubbed with success.
*/
int scrub_message(struct sldns_buffer* pkt, struct msg_parse* msg,
struct query_info* qinfo, uint8_t* zonename, struct regional* regional,
struct module_env* env, struct module_qstate* qstate,
struct iter_env* ie);
struct iter_env* ie, int* msg_lame_empty, int* msg_lame_referral,
int rdset);
#endif /* ITERATOR_ITER_SCRUB_H */
+8 -3
View File
@@ -3154,7 +3154,8 @@ processQueryResponse(struct module_qstate* qstate, struct iter_qstate* iq,
orig_empty_nodata_found = iq->empty_nodata_found;
type = response_type_from_server(
(int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
iq->response, &iq->qinfo_out, iq->dp, &iq->empty_nodata_found);
iq->response, &iq->qinfo_out, iq->dp, &iq->empty_nodata_found,
iq->msg_lame_empty, iq->msg_lame_referral);
iq->chase_to_rd = 0;
/* remove TC flag, if this is erroneously set by TCP upstream */
iq->response->rep->flags &= ~BIT_TC;
@@ -3760,7 +3761,8 @@ processPrimeResponse(struct module_qstate* qstate, int id)
iq->response->rep->flags &= ~(BIT_RD|BIT_RA); /* ignore rec-lame */
type = response_type_from_server(
(int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
iq->response, &iq->qchase, iq->dp, NULL);
iq->response, &iq->qchase, iq->dp, NULL, iq->msg_lame_empty,
iq->msg_lame_referral);
if(type == RESPONSE_TYPE_ANSWER) {
qstate->return_rcode = LDNS_RCODE_NOERROR;
qstate->return_msg = iq->response;
@@ -4369,7 +4371,10 @@ process_response(struct module_qstate* qstate, struct iter_qstate* iq,
/* normalize and sanitize: easy to delete items from linked lists */
if(!scrub_message(pkt, prs, &iq->qinfo_out, iq->dp->name,
qstate->env->scratch, qstate->env, qstate, ie)) {
qstate->env->scratch, qstate->env, qstate, ie,
&iq->msg_lame_empty, &iq->msg_lame_referral,
(int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd)
)) {
/* if 0x20 enabled, start fallback, but we have no message */
if(event == module_event_capsfail && !iq->caps_fallback) {
iq->caps_fallback = 1;
+7
View File
@@ -434,6 +434,13 @@ struct iter_qstate {
* already so that it is accepted later. */
int empty_nodata_found;
/** Store if the answer was empty, but lame, before it became empty.*/
int msg_lame_empty;
/** Store if the answer was a referral, to self, before scrub. So the
* it is not some sort of answer. */
int msg_lame_referral;
/** list of pending queries to authoritative servers. */
struct outbound_list outlist;
+17 -9
View File
@@ -95,7 +95,7 @@ RANGE_BEGIN 0 100
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
example.com. IN NS
SECTION ANSWER
@@ -107,7 +107,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN A
SECTION ANSWER
@@ -119,7 +119,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN AAAA
SECTION ANSWER
@@ -130,7 +130,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
www.example.com. IN A
SECTION ANSWER
@@ -144,7 +144,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
mail.example.com. IN AAAA
SECTION ANSWER
@@ -158,7 +158,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
foo.example.com. IN A
SECTION ANSWER
@@ -172,7 +172,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
toss.example.com. IN A
SECTION ANSWER
@@ -221,12 +221,16 @@ ENTRY_END
STEP 10 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR RD RA SERVFAIL
REPLY QR RD RA NOERROR
SECTION QUESTION
foo.example.com. IN A
SECTION ANSWER
; scrubbed away
;foo.example.com. IN A 10.20.30.40
SECTION AUTHORITY
example.com. IN NS ns.example.com.
SECTION ADDITIONAL
ns.example.com. IN A 1.2.3.4
ENTRY_END
; IPv6 address is scrubbed
@@ -240,10 +244,14 @@ ENTRY_END
STEP 30 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR RD RA SERVFAIL
REPLY QR RD RA NOERROR
SECTION QUESTION
mail.example.com. IN AAAA
SECTION ANSWER
SECTION AUTHORITY
example.com. IN NS ns.example.com.
SECTION ADDITIONAL
ns.example.com. IN A 1.2.3.4
ENTRY_END
; allowed domain is not scrubbed.
+3 -3
View File
@@ -73,7 +73,7 @@ RANGE_BEGIN 0 100
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
example.com. IN A
SECTION AUTHORITY
@@ -85,7 +85,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
www.example.com. IN A
SECTION AUTHORITY
@@ -97,7 +97,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
www.example.com. IN AAAA
SECTION ANSWER
+109
View File
@@ -0,0 +1,109 @@
; config options
server:
target-fetch-policy: "0 0 0 0 0"
qname-minimisation: no
iter-scrub-promiscuous: yes
stub-zone:
name: "."
stub-addr: 1.2.3.0 # ns.root
CONFIG_END
SCENARIO_BEGIN Test iterator with self glue response that becomes empty.
; ns.root
RANGE_BEGIN 0 400
ADDRESS 1.2.3.0
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR AA NOERROR
SECTION QUESTION
. IN NS
SECTION ANSWER
. IN NS NS.ROOT.
SECTION ADDITIONAL
NS.ROOT. IN A 1.2.3.0
ENTRY_END
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
mesa. IN NS
SECTION AUTHORITY
mesa. IN NS ns.mesa.
SECTION ADDITIONAL
ns.mesa. IN A 1.2.7.7
ENTRY_END
RANGE_END
; ns.mesa
RANGE_BEGIN 0 400
ADDRESS 1.2.7.7
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
example.mesa. IN NS
SECTION AUTHORITY
example.mesa. IN NS ns.example.mesa.
SECTION ADDITIONAL
ns.example.mesa. IN A 1.2.4.3
ENTRY_END
RANGE_END
; ns.example.mesa
RANGE_BEGIN 0 400
ADDRESS 1.2.4.3
; This answer contains an NS record that points to itself.
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
sub.example.mesa. IN NS
SECTION AUTHORITY
sub.example.mesa. IN NS ns.nonce1.sub.example.mesa.
SECTION ADDITIONAL
ns.nonce1.sub.example.mesa. IN A 1.2.4.3
ENTRY_END
RANGE_END
; Test query with authority section MX glue.
STEP 10 QUERY
ENTRY_BEGIN
REPLY RD
SECTION QUESTION
test1.sub.example.mesa. IN A
ENTRY_END
STEP 20 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR RD RA SERVFAIL
SECTION QUESTION
test1.sub.example.mesa. IN A
SECTION ANSWER
ENTRY_END
STEP 30 QUERY
ENTRY_BEGIN
REPLY RD
SECTION QUESTION
test2.sub.example.mesa. IN A
ENTRY_END
STEP 40 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR RD RA SERVFAIL
SECTION QUESTION
test2.sub.example.mesa. IN A
SECTION ANSWER
ENTRY_END
SCENARIO_END
+94
View File
@@ -0,0 +1,94 @@
; config options
server:
target-fetch-policy: "0 0 0 0 0"
qname-minimisation: no
iter-scrub-promiscuous: no
stub-zone:
name: "."
stub-addr: 1.2.3.0 # ns.root
CONFIG_END
SCENARIO_BEGIN Test iterator with self glue response that becomes empty.
; It allows promiscuous NS records.
; ns.root
RANGE_BEGIN 0 400
ADDRESS 1.2.3.0
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR AA NOERROR
SECTION QUESTION
. IN NS
SECTION ANSWER
. IN NS NS.ROOT.
SECTION ADDITIONAL
NS.ROOT. IN A 1.2.3.0
ENTRY_END
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
mesa. IN NS
SECTION AUTHORITY
mesa. IN NS ns.mesa.
SECTION ADDITIONAL
ns.mesa. IN A 1.2.7.7
ENTRY_END
RANGE_END
; ns.mesa
RANGE_BEGIN 0 400
ADDRESS 1.2.7.7
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
example.mesa. IN NS
SECTION AUTHORITY
example.mesa. IN NS ns.example.mesa.
SECTION ADDITIONAL
ns.example.mesa. IN A 1.2.4.3
ENTRY_END
RANGE_END
; ns.example.mesa
RANGE_BEGIN 0 400
ADDRESS 1.2.4.3
; This answer contains an NS record that points to itself.
ENTRY_BEGIN
MATCH opcode subdomain
ADJUST copy_id copy_query
REPLY QR NOERROR
SECTION QUESTION
sub.example.mesa. IN NS
SECTION AUTHORITY
sub.example.mesa. IN NS ns.sub.example.mesa.
SECTION ADDITIONAL
ns.sub.example.mesa. IN A 1.2.4.3
ENTRY_END
RANGE_END
; Test query with authority section MX glue.
STEP 10 QUERY
ENTRY_BEGIN
REPLY RD
SECTION QUESTION
test1.sub.example.mesa. IN A
ENTRY_END
STEP 20 CHECK_ANSWER
ENTRY_BEGIN
MATCH all
REPLY QR RD RA SERVFAIL
SECTION QUESTION
test1.sub.example.mesa. IN A
SECTION ANSWER
ENTRY_END
SCENARIO_END
+2
View File
@@ -32,6 +32,7 @@ SECTION QUESTION
arpa. IN NS
SECTION ANSWER
SECTION AUTHORITY
. IN SOA invalid. invalid. 1 2 3 4 5
. IN NS K.ROOT-SERVERS.NET.
SECTION ADDITIONAL
K.ROOT-SERVERS.NET. IN A 193.0.14.129
@@ -45,6 +46,7 @@ SECTION QUESTION
in-addr.arpa. IN NS
SECTION ANSWER
SECTION AUTHORITY
. IN SOA invalid. invalid. 1 2 3 4 5
. IN NS K.ROOT-SERVERS.NET.
SECTION ADDITIONAL
K.ROOT-SERVERS.NET. IN A 193.0.14.129
+1 -1
View File
@@ -91,7 +91,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN AAAA
SECTION ANSWER
+1 -1
View File
@@ -91,7 +91,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN AAAA
SECTION ANSWER
+1 -1
View File
@@ -90,7 +90,7 @@ RANGE_BEGIN 0 100
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN AAAA
SECTION ANSWER
+1 -1
View File
@@ -92,7 +92,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.example.com. IN AAAA
SECTION ANSWER
+1 -1
View File
@@ -159,7 +159,7 @@ ENTRY_END
ENTRY_BEGIN
MATCH opcode qtype qname
ADJUST copy_id
REPLY QR NOERROR
REPLY QR AA NOERROR
SECTION QUESTION
ns.sub.example.com. IN AAAA
SECTION AUTHORITY