Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52877ffa3f |
@@ -21,8 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
- 2026-10-01: README has Getting Started, Rationale and TODO sections, and its
|
- 2026-10-01: README has Getting Started, Rationale and TODO sections, and its
|
||||||
Architecture section is now Design, in the order policy sets (closes #173).
|
Architecture section is now Design, in the order policy sets (closes #173).
|
||||||
- 2026-10-01: a zone's server that answers SERVFAIL or a referral leading no
|
|
||||||
closer is passed over for the next, as one that times out is (closes #197).
|
|
||||||
- 2026-10-01: when none of a configured name's nameservers answered, the port
|
- 2026-10-01: when none of a configured name's nameservers answered, the port
|
||||||
state saved for its addresses is kept, not removed (closes #193).
|
state saved for its addresses is kept, not removed (closes #193).
|
||||||
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
||||||
|
|||||||
@@ -15,13 +15,6 @@ var (
|
|||||||
// so whether the name has addresses is unknown.
|
// so whether the name has addresses is unknown.
|
||||||
ErrNoNameserverAnswered = errors.New("no nameserver answered")
|
ErrNoNameserverAnswered = errors.New("no nameserver answered")
|
||||||
|
|
||||||
// ErrUnusableReply is returned when a server replied with an
|
|
||||||
// error such as SERVFAIL, or with a referral that leads no
|
|
||||||
// closer to the name asked about.
|
|
||||||
ErrUnusableReply = errors.New(
|
|
||||||
"reply is an error or a referral that leads no closer",
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
// ErrCNAMEDepthExceeded is returned when a CNAME chain
|
||||||
// exceeds MaxCNAMEDepth.
|
// exceeds MaxCNAMEDepth.
|
||||||
ErrCNAMEDepthExceeded = errors.New(
|
ErrCNAMEDepthExceeded = errors.New(
|
||||||
|
|||||||
@@ -11,11 +11,6 @@ func ExtractRecordValue(rr dns.RR) string {
|
|||||||
return extractRecordValue(rr)
|
return extractRecordValue(rr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UsableReply exports usableReply for testing.
|
|
||||||
func UsableReply(resp *dns.Msg, zone string, name string) bool {
|
|
||||||
return usableReply(resp, zone, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectIPs exports collectIPs for testing.
|
// CollectIPs exports collectIPs for testing.
|
||||||
func CollectIPs(
|
func CollectIPs(
|
||||||
results map[string]*NameserverResponse,
|
results map[string]*NameserverResponse,
|
||||||
|
|||||||
@@ -207,16 +207,13 @@ func (r *Resolver) followDelegation(
|
|||||||
domain string,
|
domain string,
|
||||||
servers []string,
|
servers []string,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
// servers are the root servers, the servers of zone ".".
|
|
||||||
zone := "."
|
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
return nil, ErrContextCanceled
|
return nil, ErrContextCanceled
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, zone, domain, dns.TypeNS,
|
ctx, servers, domain, dns.TypeNS,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -253,19 +250,14 @@ func (r *Resolver) followDelegation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
zone = referralZone(resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNoNameservers
|
return nil, ErrNoNameservers
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryServers asks servers, the servers of zone, about name until one
|
|
||||||
// gives a usable reply. A server that times out, refuses or gives a
|
|
||||||
// reply that is not usable is passed over for the next.
|
|
||||||
func (r *Resolver) queryServers(
|
func (r *Resolver) queryServers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
servers []string,
|
servers []string,
|
||||||
zone string,
|
|
||||||
name string,
|
name string,
|
||||||
qtype uint16,
|
qtype uint16,
|
||||||
) (*dns.Msg, error) {
|
) (*dns.Msg, error) {
|
||||||
@@ -277,12 +269,6 @@ func (r *Resolver) queryServers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryDNS(ctx, ip, name, qtype)
|
resp, err := r.queryDNS(ctx, ip, name, qtype)
|
||||||
if err == nil && !usableReply(resp, zone, name) {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"query %s @%s: %w", name, ip, ErrUnusableReply,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -293,38 +279,6 @@ func (r *Resolver) queryServers(
|
|||||||
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
return nil, fmt.Errorf("all servers failed: %w", lastErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// usableReply reports whether resp, a reply from one of the servers of
|
|
||||||
// zone to a query about name, is usable. An error reply such as SERVFAIL
|
|
||||||
// is not. Nor is a referral, unless it refers the query to a zone below
|
|
||||||
// zone that name is in: a server that refers it back to zone, up or
|
|
||||||
// sideways does not serve zone as it should.
|
|
||||||
func usableReply(resp *dns.Msg, zone string, name string) bool {
|
|
||||||
if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeNameError {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
child := referralZone(resp)
|
|
||||||
if resp.Authoritative || len(resp.Answer) > 0 || child == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return child != zone && dns.IsSubDomain(zone, child) &&
|
|
||||||
dns.IsSubDomain(child, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// referralZone returns the zone a referral refers the query to: the
|
|
||||||
// owner name of the NS records in resp's authority section, or "" when
|
|
||||||
// there are none.
|
|
||||||
func referralZone(resp *dns.Msg) string {
|
|
||||||
for _, rr := range resp.Ns {
|
|
||||||
if ns, ok := rr.(*dns.NS); ok {
|
|
||||||
return strings.ToLower(ns.Hdr.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) resolveNSIPs(
|
func (r *Resolver) resolveNSIPs(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
nsNames []string,
|
nsNames []string,
|
||||||
@@ -358,7 +312,6 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
|
|
||||||
domain = dns.Fqdn(domain)
|
domain = dns.Fqdn(domain)
|
||||||
servers := rootServerList()
|
servers := rootServerList()
|
||||||
zone := "."
|
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
@@ -366,7 +319,7 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, zone, domain, dns.TypeNS,
|
ctx, servers, domain, dns.TypeNS,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -391,7 +344,6 @@ func (r *Resolver) resolveNSIterative(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
zone = referralZone(resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ErrNoNameservers
|
return nil, ErrNoNameservers
|
||||||
@@ -409,7 +361,6 @@ func (r *Resolver) resolveARecord(
|
|||||||
|
|
||||||
hostname = dns.Fqdn(hostname)
|
hostname = dns.Fqdn(hostname)
|
||||||
servers := rootServerList()
|
servers := rootServerList()
|
||||||
zone := "."
|
|
||||||
|
|
||||||
for range maxDelegation {
|
for range maxDelegation {
|
||||||
if checkCtx(ctx) != nil {
|
if checkCtx(ctx) != nil {
|
||||||
@@ -417,7 +368,7 @@ func (r *Resolver) resolveARecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := r.queryServers(
|
resp, err := r.queryServers(
|
||||||
ctx, servers, zone, hostname, dns.TypeA,
|
ctx, servers, hostname, dns.TypeA,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
@@ -455,7 +406,6 @@ func (r *Resolver) resolveARecord(
|
|||||||
}
|
}
|
||||||
|
|
||||||
servers = nextServers
|
servers = nextServers
|
||||||
zone = referralZone(resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
|
|||||||
@@ -41,97 +41,6 @@ func TestCollectIPs_FailedIsNoAnswer(t *testing.T) {
|
|||||||
assert.Empty(t, ips)
|
assert.Empty(t, ips)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exampleCom is the zone most cases of TestUsableReply are about.
|
|
||||||
const exampleCom = "example.com."
|
|
||||||
|
|
||||||
// nsRecord builds an NS record that names a server of zone.
|
|
||||||
func nsRecord(zone string) *dns.NS {
|
|
||||||
return &dns.NS{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET,
|
|
||||||
},
|
|
||||||
Ns: "ns1.example.net.",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// referralTo builds a reply that refers the query to the servers of
|
|
||||||
// zone.
|
|
||||||
func referralTo(zone string) *dns.Msg {
|
|
||||||
msg := new(dns.Msg)
|
|
||||||
msg.Ns = []dns.RR{nsRecord(zone)}
|
|
||||||
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestUsableReply checks which replies from one of a zone's servers are
|
|
||||||
// used. A reply that is not usable moves the query on to the zone's
|
|
||||||
// next server.
|
|
||||||
func TestUsableReply(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
servfail := new(dns.Msg)
|
|
||||||
servfail.Rcode = dns.RcodeServerFailure
|
|
||||||
|
|
||||||
answer := new(dns.Msg)
|
|
||||||
answer.Authoritative = true
|
|
||||||
answer.Answer = []dns.RR{nsRecord(exampleCom)}
|
|
||||||
|
|
||||||
nxdomain := new(dns.Msg)
|
|
||||||
nxdomain.Authoritative = true
|
|
||||||
nxdomain.Rcode = dns.RcodeNameError
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
resp *dns.Msg
|
|
||||||
zone string
|
|
||||||
query string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "SERVFAIL", resp: servfail,
|
|
||||||
zone: exampleCom, query: exampleCom, want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "answer", resp: answer,
|
|
||||||
zone: exampleCom, query: exampleCom, want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "NXDOMAIN", resp: nxdomain,
|
|
||||||
zone: ".", query: exampleCom, want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root refers to com", resp: referralTo("com."),
|
|
||||||
zone: ".", query: exampleCom, want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "com refers to example.com", resp: referralTo(exampleCom),
|
|
||||||
zone: "com.", query: "www.example.com.", want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "referral back to the zone", resp: referralTo(exampleCom),
|
|
||||||
zone: exampleCom, query: exampleCom, want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "referral up to the root", resp: referralTo("."),
|
|
||||||
zone: exampleCom, query: exampleCom, want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "referral sideways", resp: referralTo("net."),
|
|
||||||
zone: ".", query: exampleCom, want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
assert.Equal(t, tt.want,
|
|
||||||
resolver.UsableReply(tt.resp, tt.zone, tt.query),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractRecordValue_LetterCase(t *testing.T) {
|
func TestExtractRecordValue_LetterCase(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user