Skip to content

Fix qname-minimisation to use real qtype on terminal label - #1503

Open
phirince wants to merge 1 commit into
NLnetLabs:masterfrom
phirince:qname_minimisation_fix
Open

phirince wants to merge 1 commit into
NLnetLabs:masterfrom
phirince:qname_minimisation_fix

Conversation

@phirince

@phirince phirince commented Sep 7, 2026 •

Copy link
Copy Markdown

Problem

When qname-minimisation is enabled, the terminal (final) query uses a generic A probe instead of the actual requested qtype. If the authoritative zone has a single nameserver and no A record exists at the target name, the NXDOMAIN response exhausts the only available server address, and unbound returns SERVFAIL — even though the record genuinely exists.

Reproduction: dig zurhbjwo-4.t.nessus.org TXT with qname-minimisation enabled. The zone t.nessus.org has exactly one NS (ns.t.nessus.org → 206.205.255.205) and only a wildcard TXT record — no A record.

Vanilla unbound log (broken):

sending query: zurhbjwo-4.t.nessus.org. A IN          ← terminal A probe
incoming scrubbed packet: rcode: NXDOMAIN             ← no A record
DelegationPoint<t.nessus.org.>: 1 addrs (0 result, 0 avail)  ← sole NS exhausted
No more query targets, attempting last resort          ← → SERVFAIL

Closes #1500. Related to #870.

Root cause

In iterator/iterator.c (processQueryTargets, MINIMISE_STATE), the condition that stops minimisation at the terminal label was:

if(labdiff < 1 || (labdiff < 2
    && (iq->qchase.qtype == LDNS_RR_TYPE_DS
    || iq->qchase.qtype == LDNS_RR_TYPE_A)))

When labdiff == 1 (the outgoing qname IS already the full target name), the code only switched to the real qtype for DS and A queries. For all other qtypes (TXT, AAAA, MX, …) it sent the full qname with qtype=A as a type-hiding probe. If that probe fails and the zone has few/no other usable addresses, the real query can be left with nowhere to go.

Fix (revised)

An earlier version of this PR removed the terminal-label A probe entirely for all qtypes. Testing surfaced a real downside to that approach: some authoritative/GSLB-managed zones (e.g. CDN-backed CNAMEs on managed DNS providers) only answer certain record types correctly and return NODATA on others -- a pre-existing upstream misconfiguration, independently confirmed against multiple such zones. The terminal A probe, when it succeeds, incidentally warms the resolver's cache with a CNAME that the real qtype then chases directly to the target's own correctly-configured nameservers, avoiding an otherwise-reproducible NODATA. Removing the probe outright would have turned that pre-existing upstream gap into a user-facing NODATA/outage risk for anyone relying on it, instead of just fixing the actual correctness bug.

This revised version keeps the terminal-label probe, but fixes the real problem: when that probe gets a non-NOERROR response, the existing best-effort qname-minimisation fallback (which already exists in processQueryResponse for exactly this purpose) now also restores the query target's attempt budget via iter_dec_attempts() -- the same idiom already used elsewhere in this file for other fallback scenarios -- before retrying with the real qtype. A new iq->minimise_terminal_probe flag marks which outbound query is the terminal-label probe, so this restoration is scoped precisely to that case and doesn't touch intermediate-label minimisation behavior.

/* iterator.h */
int minimise_terminal_probe;   /* set when the terminal-label A probe is sent */

/* iterator.c, processQueryResponse(), best-effort minimisation fallback */
if(iq->minimise_terminal_probe) {
    iter_dec_attempts(iq->dp, 1, ie->outbound_msg_retry);
    iq->minimise_terminal_probe = 0;
}
iq->minimisation_state = DONOT_MINIMISE_STATE;

Testing

  • testdata/iter_resolve_minimised.rpl and testdata/iter_resolve_minimised_timeout.rpl are unchanged from master -- this fix doesn't alter their code paths (successful-probe and probe-timeout scenarios respectively), confirming the change is additive.
  • testdata/iter_minimise_terminal_qtype.rpl rewritten: single-NS zone, outbound-msg-retry: 1, terminal A probe genuinely NXDOMAINs (no A record exists), asserts the real TXT query is still sent to the same nameserver afterward and succeeds.
  • Full make test regression suite passes.
  • Verified against two independent real-world cases: dig zurhbjwo-4.t.nessus.org TXT (the original single-NS/NXDOMAIN-probe case from the bug report) and dig www.scholastic.ca AAAA (a GSLB-zone case where the probe's cache-warming side effect avoids an unrelated upstream NODATA) both now succeed, including repeated back-to-back queries against the live zone.

Restores the terminal-label type-hiding "A" probe for non-A/DS qtypes
(the original qname-minimisation behavior), but fixes the actual bug:
when that probe gets a non-NOERROR response, the best-effort
minimisation fallback now restores the query target's attempt budget
before retrying with the real qtype, instead of leaving a single-
nameserver zone with no usable target at all for the real query.

Previously, the terminal A probe consumed the zone's only available
query-target attempt. If that probe NXDOMAINed (e.g. a wildcard
TXT-only zone with no A record), the real query had nowhere left to
go, and unbound returned an avoidable SERVFAIL for a record that
genuinely exists (GitHub issue NLnetLabs#1500).

This also preserves an incidental but real-world-relevant benefit of
the probe: some authoritative/GSLB-managed zones (e.g. CDN-backed
CNAMEs on managed DNS providers) only answer certain record types
correctly and NODATA on others (a pre-existing upstream
misconfiguration). The terminal A probe, when it succeeds, can warm
the resolver's cache with a CNAME the real qtype then chases directly
to the target's own correctly-configured nameservers, avoiding an
otherwise-reproducible NODATA. Removing the probe outright (as
originally proposed) would have exposed that class of upstream issue
as user-facing NODATA/outage risk instead of fixing the actual
correctness bug -- so this keeps the probe but makes its failure mode
safe.

Fix: add iter_qstate.minimise_terminal_probe, set when the terminal
probe is sent, and checked in the best-effort minimisation fallback
(processQueryResponse) to call iter_dec_attempts() -- the same
attempt-budget-restoration idiom already used elsewhere in this file
for other fallback scenarios -- before falling back to the real
qtype.

Testing:
- testdata/iter_resolve_minimised.rpl and
  testdata/iter_resolve_minimised_timeout.rpl reverted to their
  original content; both pass unmodified, confirming this change is
  additive and doesn't alter the existing successful-probe and
  probe-timeout scenarios.
- testdata/iter_minimise_terminal_qtype.rpl rewritten: single-NS zone,
  outbound-msg-retry: 1, terminal A probe genuinely NXDOMAINs (no A
  record exists), asserts the real TXT query is still sent to the
  same nameserver afterward and succeeds.
- Full make test regression suite passes.
- Verified against both real-world repros: dig
  zurhbjwo-4.t.nessus.org TXT (the original single-NS/NXDOMAIN-probe
  case) and dig www.scholastic.ca AAAA (the GSLB/CNAME-masking case)
  both now succeed.

Fixes GitHub issue NLnetLabs#1500.
@phirince
phirince force-pushed the qname_minimisation_fix branch from ebb067a to 1cda01c Compare September 8, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qname-minimisation sends A instead of real qtype on terminal label, causing SERVFAIL when zone has a single nameserver

1 participant