From 32a7c044985996d83d39313cb2b015cd987d1459 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 21 Apr 2026 16:32:48 -0400 Subject: [PATCH] Return NODATA instead of NXDOMAIN for missing record types (#1668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DNS responder was setting RCODE=NXDOMAIN (Name Error) any time the answer section was empty, including for names that exist in the lighthouse but lack a record of the requested type (e.g. an AAAA query for a v4-only host). Per RFC 2308 §2.1, NXDOMAIN means "the domain referred to by the QNAME does not exist", and per RFC 2308 §2.2 a name that exists with no record of the requested type must be answered with RCODE=NOERROR and an empty answer section (NODATA). The practical fallout: busybox ping in Alpine issues AAAA first, treats NXDOMAIN as a hard failure, and never falls through to A. Returning NODATA lets the resolver continue to the A query as it should. Track whether any queried A/AAAA name is known in either map and only set RcodeNameError when no queried name exists at all. --- dns_server.go | 30 ++++++++++++++++++++++-------- dns_server_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/dns_server.go b/dns_server.go index 75c56f0f..8af88b52 100644 --- a/dns_server.go +++ b/dns_server.go @@ -216,22 +216,28 @@ func (d *dnsServer) Stop() { d.shutdownServer(srv, started, "stop") } -func (d *dnsServer) Query(q uint16, data string) netip.Addr { +// Query returns the address for the given name and query type. The second +// return value reports whether the name is known at all (in either A or AAAA), +// which lets callers distinguish NODATA from NXDOMAIN. +func (d *dnsServer) Query(q uint16, data string) (netip.Addr, bool) { data = strings.ToLower(data) d.RLock() defer d.RUnlock() + addr4, haveV4 := d.dnsMap4[data] + addr6, haveV6 := d.dnsMap6[data] + nameExists := haveV4 || haveV6 switch q { case dns.TypeA: - if r, ok := d.dnsMap4[data]; ok { - return r + if haveV4 { + return addr4, nameExists } case dns.TypeAAAA: - if r, ok := d.dnsMap6[data]; ok { - return r + if haveV6 { + return addr6, nameExists } } - return netip.Addr{} + return netip.Addr{}, nameExists } func (d *dnsServer) QueryCert(data string) string { @@ -305,12 +311,20 @@ func (d *dnsServer) isSelfNebulaOrLocalhost(addr string) bool { } func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) { + // Per RFC 2308 §2.2, a name that exists but has no record of the requested + // type must be answered with NOERROR and an empty answer section (NODATA), + // not NXDOMAIN (RFC 2308 §2.1), which is reserved for names that do not + // exist at all. + anyNameExists := false for _, q := range m.Question { switch q.Qtype { case dns.TypeA, dns.TypeAAAA: qType := dns.TypeToString[q.Qtype] d.l.Debugf("Query for %s %s", qType, q.Name) - ip := d.Query(q.Qtype, q.Name) + ip, nameExists := d.Query(q.Qtype, q.Name) + if nameExists { + anyNameExists = true + } if ip.IsValid() { rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip)) if err == nil { @@ -333,7 +347,7 @@ func (d *dnsServer) parseQuery(m *dns.Msg, w dns.ResponseWriter) { } } - if len(m.Answer) == 0 { + if len(m.Answer) == 0 && !anyNameExists { m.Rcode = dns.RcodeNameError } } diff --git a/dns_server_test.go b/dns_server_test.go index c33c0480..ef8a5a64 100644 --- a/dns_server_test.go +++ b/dns_server_test.go @@ -33,18 +33,43 @@ func TestParsequery(t *testing.T) { netip.MustParseAddr("fd01::25"), } ds.Add("test.com.com", addrs) + ds.Add("v4only.com.com", []netip.Addr{netip.MustParseAddr("1.2.3.6")}) + ds.Add("v6only.com.com", []netip.Addr{netip.MustParseAddr("fd01::26")}) m := &dns.Msg{} m.SetQuestion("test.com.com", dns.TypeA) ds.parseQuery(m, nil) assert.NotNil(t, m.Answer) assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String()) + assert.Equal(t, dns.RcodeSuccess, m.Rcode) m = &dns.Msg{} m.SetQuestion("test.com.com", dns.TypeAAAA) ds.parseQuery(m, nil) assert.NotNil(t, m.Answer) assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String()) + assert.Equal(t, dns.RcodeSuccess, m.Rcode) + + // A known name with no record of the requested type should return NODATA + // (NOERROR with empty answer), not NXDOMAIN. + m = &dns.Msg{} + m.SetQuestion("v4only.com.com", dns.TypeAAAA) + ds.parseQuery(m, nil) + assert.Empty(t, m.Answer) + assert.Equal(t, dns.RcodeSuccess, m.Rcode) + + m = &dns.Msg{} + m.SetQuestion("v6only.com.com", dns.TypeA) + ds.parseQuery(m, nil) + assert.Empty(t, m.Answer) + assert.Equal(t, dns.RcodeSuccess, m.Rcode) + + // An unknown name should still return NXDOMAIN. + m = &dns.Msg{} + m.SetQuestion("unknown.com.com", dns.TypeA) + ds.parseQuery(m, nil) + assert.Empty(t, m.Answer) + assert.Equal(t, dns.RcodeNameError, m.Rcode) } func Test_getDnsServerAddr(t *testing.T) {