Skip to content

Commit 4fb28b2

Browse files
committed
awldns: return NODATA for AAAA queries on .awl names
The local-domain handler answered A, AAAA and ANY queries identically, always emitting an A (IPv4) record. On IPv6-enabled networks clients such as Windows' ping/getaddrinfo query AAAA first; a NOERROR response carrying a mismatched A record (instead of NODATA) made them fail with "no such host" without falling back to an A query. Split the handler by qtype: A/ANY return the A record, AAAA returns NODATA (NOERROR + empty answer) so clients fall back to A, and unknown names now return a clean NXDOMAIN instead of NXDOMAIN plus an empty A record. Full IPv6 support remains a separate task. Fixes #150
1 parent 018315a commit 4fb28b2

2 files changed

Lines changed: 61 additions & 13 deletions

File tree

awldns/awldns.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,13 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
164164
hostnameLower := strings.ToLower(hostname)
165165
mappedIP, found := cfg.directMapping[hostnameLower]
166166

167+
if !found {
168+
m.SetRcode(req, dns.RcodeNameError)
169+
continue
170+
}
171+
167172
switch qtype {
168-
case dns.TypeA, dns.TypeAAAA, dns.TypeANY:
173+
case dns.TypeA, dns.TypeANY:
169174
aRec := &dns.A{
170175
Hdr: dns.RR_Header{
171176
// we should return original name from the request as some clients expect that
@@ -174,14 +179,11 @@ func (r *Resolver) dnsLocalDomainHandler(resp dns.ResponseWriter, req *dns.Msg)
174179
Class: dns.ClassINET,
175180
Ttl: defaultTTLSeconds,
176181
},
177-
}
178-
if found {
179-
// TODO: support ipv6
180-
aRec.A = net.ParseIP(mappedIP).To4()
181-
} else {
182-
m.SetRcode(req, dns.RcodeNameError)
182+
A: net.ParseIP(mappedIP).To4(),
183183
}
184184
m.Answer = append(m.Answer, aRec)
185+
case dns.TypeAAAA:
186+
// TODO: support ipv6
185187
}
186188
}
187189

awldns/awldns_test.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"net"
7-
"runtime"
87
"testing"
98
"time"
109

@@ -57,15 +56,62 @@ func TestDNS(t *testing.T) {
5756
assertAddr(name2+".awl", addr2)
5857
assertAddr(name2Capitalized+".awl", addr2)
5958

60-
addrs, err := client.LookupHost(ctx, "unknown.awl")
59+
addrs, err := client.LookupHost(ctx, "unknown.awl.")
6160
a.Error(err)
6261
a.Empty(addrs)
6362
dnsErr := err.(*net.DNSError)
64-
// TODO: investigate why macos and linux in CI return `lookup unknown.awl on 127.0.0.53:53: server misbehaving`
65-
// it should use only our resolver, but somehow it tries to use system resolver afterwards
66-
if runtime.GOOS == "windows" {
67-
a.Equalf(true, dnsErr.IsNotFound, "actual error: %v", err)
63+
a.Equalf(true, dnsErr.IsNotFound, "actual error: %v", err)
64+
}
65+
66+
// TestLocalDomainQueryTypes verifies the per-qtype behaviour of the .awl
67+
// handler at the DNS message level (which net.Resolver.LookupHost hides):
68+
// - A for a known name -> NOERROR + one A record
69+
// - AAAA for a known name -> NODATA (NOERROR + empty answer), so IPv6-first
70+
// clients fall back to A instead of failing. Regression test for #150.
71+
// - A/AAAA for an unknown name -> NXDOMAIN
72+
func TestLocalDomainQueryTypes(t *testing.T) {
73+
a := require.New(t)
74+
port := FindFreePort()
75+
addr := fmt.Sprintf("127.0.0.1:%d", port)
76+
77+
resolver := NewResolver(addr)
78+
defer resolver.Close()
79+
// TODO: remove sleep. We need it because NewResolver starts servers in goroutines
80+
time.Sleep(50 * time.Millisecond)
81+
82+
const knownIP = "127.0.0.66"
83+
resolver.ReceiveConfiguration("", map[string]string{"admin": knownIP})
84+
85+
query := func(name string, qtype uint16) *dns.Msg {
86+
m := new(dns.Msg)
87+
m.SetQuestion(dns.CanonicalName(name), qtype)
88+
client := &dns.Client{Net: "udp"}
89+
resp, _, err := client.Exchange(m, addr)
90+
a.NoError(err)
91+
return resp
6892
}
93+
94+
// A for a known name -> single A record
95+
respA := query("admin.awl", dns.TypeA)
96+
a.Equal(dns.RcodeSuccess, respA.Rcode)
97+
a.Len(respA.Answer, 1)
98+
aRec, ok := respA.Answer[0].(*dns.A)
99+
a.True(ok, "expected an A record, got %T", respA.Answer[0])
100+
a.Equal(knownIP, aRec.A.String())
101+
102+
// AAAA for a known name -> NODATA (NOERROR, empty answer)
103+
respAAAA := query("admin.awl", dns.TypeAAAA)
104+
a.Equal(dns.RcodeSuccess, respAAAA.Rcode)
105+
a.Empty(respAAAA.Answer)
106+
107+
// unknown name -> NXDOMAIN for both A and AAAA
108+
respUnknownA := query("nope.awl", dns.TypeA)
109+
a.Equal(dns.RcodeNameError, respUnknownA.Rcode)
110+
a.Empty(respUnknownA.Answer)
111+
112+
respUnknownAAAA := query("nope.awl", dns.TypeAAAA)
113+
a.Equal(dns.RcodeNameError, respUnknownAAAA.Rcode)
114+
a.Empty(respUnknownAAAA.Answer)
69115
}
70116

71117
func NewResolverClient(address string) *net.Resolver {

0 commit comments

Comments
 (0)