Просмотр исходного кода

Return NODATA instead of NXDOMAIN for missing record types (#1668)

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.
John Maguire 6 месяцев назад
Родитель
Сommit
32a7c04498
2 измененных файлов с 47 добавлено и 8 удалено
  1. 22 8
      dns_server.go
  2. 25 0
      dns_server_test.go

+ 22 - 8
dns_server.go

@@ -216,22 +216,28 @@ func (d *dnsServer) Stop() {
 	d.shutdownServer(srv, started, "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)
 	data = strings.ToLower(data)
 	d.RLock()
 	d.RLock()
 	defer d.RUnlock()
 	defer d.RUnlock()
+	addr4, haveV4 := d.dnsMap4[data]
+	addr6, haveV6 := d.dnsMap6[data]
+	nameExists := haveV4 || haveV6
 	switch q {
 	switch q {
 	case dns.TypeA:
 	case dns.TypeA:
-		if r, ok := d.dnsMap4[data]; ok {
-			return r
+		if haveV4 {
+			return addr4, nameExists
 		}
 		}
 	case dns.TypeAAAA:
 	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 {
 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) {
 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 {
 	for _, q := range m.Question {
 		switch q.Qtype {
 		switch q.Qtype {
 		case dns.TypeA, dns.TypeAAAA:
 		case dns.TypeA, dns.TypeAAAA:
 			qType := dns.TypeToString[q.Qtype]
 			qType := dns.TypeToString[q.Qtype]
 			d.l.Debugf("Query for %s %s", qType, q.Name)
 			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() {
 			if ip.IsValid() {
 				rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
 				rr, err := dns.NewRR(fmt.Sprintf("%s %s %s", q.Name, qType, ip))
 				if err == nil {
 				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
 		m.Rcode = dns.RcodeNameError
 	}
 	}
 }
 }

+ 25 - 0
dns_server_test.go

@@ -33,18 +33,43 @@ func TestParsequery(t *testing.T) {
 		netip.MustParseAddr("fd01::25"),
 		netip.MustParseAddr("fd01::25"),
 	}
 	}
 	ds.Add("test.com.com", addrs)
 	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 := &dns.Msg{}
 	m.SetQuestion("test.com.com", dns.TypeA)
 	m.SetQuestion("test.com.com", dns.TypeA)
 	ds.parseQuery(m, nil)
 	ds.parseQuery(m, nil)
 	assert.NotNil(t, m.Answer)
 	assert.NotNil(t, m.Answer)
 	assert.Equal(t, "1.2.3.4", m.Answer[0].(*dns.A).A.String())
 	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 = &dns.Msg{}
 	m.SetQuestion("test.com.com", dns.TypeAAAA)
 	m.SetQuestion("test.com.com", dns.TypeAAAA)
 	ds.parseQuery(m, nil)
 	ds.parseQuery(m, nil)
 	assert.NotNil(t, m.Answer)
 	assert.NotNil(t, m.Answer)
 	assert.Equal(t, "fd01::24", m.Answer[0].(*dns.AAAA).AAAA.String())
 	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) {
 func Test_getDnsServerAddr(t *testing.T) {