1 Commits
Author SHA1 Message Date
sneak 0a0afb822a watcher: warn of an expiring certificate on every TLS check (closes #204)
check / check (push) Successful in 1m44s
An expiry warning was skipped when the last one for that hostname and
address was sent less than DNSWATCHER_TLS_INTERVAL ago. Each TLS check
runs after a DNS pass of varying length, so two checks can be less than
the interval apart, and a certificate about to expire was warned about on
every check or every other check, at random. TLS checks already start
once per interval, so the in-memory record of when each warning was sent
is removed and every check warns, as the README says.

The test that expected the second check to stay silent is replaced by one
that runs TLS checks on state built in the test, with no DNS.

Model: opus-5-5
2026-10-01 23:47:04 +00:00
4 changed files with 12 additions and 88 deletions
-2
View File
@@ -21,8 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
- 2026-10-01: a certificate within the expiry warning period is warned about on - 2026-10-01: a certificate within the expiry warning period is warned about on
every TLS check, where some checks used to skip it at random (closes #204). every TLS check, where some checks used to skip it at random (closes #204).
- 2026-10-01: a domain's NS set is its delegation from the parent zone's
servers, not whichever of its own servers answered first (closes #200).
- 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 - 2026-10-01: a zone's server that answers SERVFAIL or a referral leading no
-5
View File
@@ -16,11 +16,6 @@ func UsableReply(resp *dns.Msg, zone string, name string) bool {
return usableReply(resp, zone, name) return usableReply(resp, zone, name)
} }
// NSSetFrom exports nsSetFrom for testing.
func NSSetFrom(resp *dns.Msg, domain string) []string {
return nsSetFrom(resp, domain)
}
// CollectIPs exports collectIPs for testing. // CollectIPs exports collectIPs for testing.
func CollectIPs( func CollectIPs(
results map[string]*NameserverResponse, results map[string]*NameserverResponse,
+8 -25
View File
@@ -222,9 +222,9 @@ func (r *Resolver) followDelegation(
return nil, err return nil, err
} }
nsSet := nsSetFrom(resp, domain) ansNS := extractNSSet(resp.Answer)
if len(nsSet) > 0 { if len(ansNS) > 0 {
return nsSet, nil return ansNS, nil
} }
// An authoritative reply comes from the servers of the zone // An authoritative reply comes from the servers of the zone
@@ -325,21 +325,6 @@ func referralZone(resp *dns.Msg) string {
return "" return ""
} }
// nsSetFrom returns the NS set of domain that resp, a reply to a query
// for domain's NS records, gives: the delegation in a referral to domain
// itself, or else the NS records in the answer; empty when it gives
// neither. A referral to domain comes from its parent zone's servers,
// which all hold the same delegation, so the set does not depend on
// which of them answered. domain's own servers, which can disagree about
// their NS records, are then not asked.
func nsSetFrom(resp *dns.Msg, domain string) []string {
if referralZone(resp) == domain {
return extractNSSet(resp.Ns)
}
return extractNSSet(resp.Answer)
}
func (r *Resolver) resolveNSIPs( func (r *Resolver) resolveNSIPs(
ctx context.Context, ctx context.Context,
nsNames []string, nsNames []string,
@@ -387,7 +372,7 @@ func (r *Resolver) resolveNSIterative(
return nil, err return nil, err
} }
nsNames := nsSetFrom(resp, domain) nsNames := extractNSSet(resp.Answer)
if len(nsNames) > 0 { if len(nsNames) > 0 {
return nsNames, nil return nsNames, nil
} }
@@ -480,8 +465,7 @@ func (r *Resolver) resolveARecord(
// FindAuthoritativeNameservers traces the delegation chain from // FindAuthoritativeNameservers traces the delegation chain from
// root servers to discover all authoritative nameservers for the // root servers to discover all authoritative nameservers for the
// given domain, as the delegation from its parent zone's servers lists // given domain. For a name that is not a zone apex it tries each
// them. For a name that is not a zone apex it tries each
// parent name in turn, so it returns the nameservers of the zone the // parent name in turn, so it returns the nameservers of the zone the
// name is in. // name is in.
func (r *Resolver) FindAuthoritativeNameservers( func (r *Resolver) FindAuthoritativeNameservers(
@@ -647,10 +631,9 @@ func (r *Resolver) querySingleType(
// A reply with no answer that lists other nameservers, from a server // A reply with no answer that lists other nameservers, from a server
// that does not hold the name's zone, is a referral and says nothing // that does not hold the name's zone, is a referral and says nothing
// about the name's records. A server named in the delegation that // about the name's records. A parent zone's servers send one when
// does not hold the zone may send one, as do a parent zone's servers // every server of the name's own zone failed and
// when FindAuthoritativeNameservers found no delegation for the // FindAuthoritativeNameservers moved on to the parent name.
// name's zone and moved on to a parent name.
if !msg.Authoritative && len(msg.Answer) == 0 && if !msg.Authoritative && len(msg.Answer) == 0 &&
len(extractNSSet(msg.Ns)) > 0 { len(extractNSSet(msg.Ns)) > 0 {
state.gotReferral = true state.gotReferral = true
+4 -56
View File
@@ -41,15 +41,8 @@ func TestCollectIPs_FailedIsNoAnswer(t *testing.T) {
assert.Empty(t, ips) assert.Empty(t, ips)
} }
const ( // exampleCom is the zone most cases of TestUsableReply are about.
// exampleCom is the zone most cases of TestUsableReply and const exampleCom = "example.com."
// TestNSSetFrom are about, and wwwExampleCom a name in it.
exampleCom = "example.com."
wwwExampleCom = "www.example.com."
// exampleNS is the server the NS records nsRecord builds name.
exampleNS = "ns1.example.net."
)
// nsRecord builds an NS record that names a server of zone. // nsRecord builds an NS record that names a server of zone.
func nsRecord(zone string) *dns.NS { func nsRecord(zone string) *dns.NS {
@@ -57,7 +50,7 @@ func nsRecord(zone string) *dns.NS {
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET,
}, },
Ns: exampleNS, Ns: "ns1.example.net.",
} }
} }
@@ -112,7 +105,7 @@ func TestUsableReply(t *testing.T) {
}, },
{ {
name: "com refers to example.com", resp: referralTo(exampleCom), name: "com refers to example.com", resp: referralTo(exampleCom),
zone: "com.", query: wwwExampleCom, want: true, zone: "com.", query: "www.example.com.", want: true,
}, },
{ {
name: "referral back to the zone", resp: referralTo(exampleCom), name: "referral back to the zone", resp: referralTo(exampleCom),
@@ -139,51 +132,6 @@ func TestUsableReply(t *testing.T) {
} }
} }
// TestNSSetFrom checks which NS set a reply gives for a domain; a set
// that is not empty ends the walk. The referral to example.com that
// com's servers all send alike gives its delegation, so the set is the
// same whichever of them answered, and example.com's own servers, which
// can disagree, are not asked.
func TestNSSetFrom(t *testing.T) {
t.Parallel()
answer := new(dns.Msg)
answer.Authoritative = true
answer.Answer = []dns.RR{nsRecord(exampleCom)}
tests := []struct {
name string
resp *dns.Msg
domain string
want []string
}{
{
name: "com refers to example.com", resp: referralTo(exampleCom),
domain: exampleCom, want: []string{exampleNS},
},
{
name: "com refers on, for www.example.com",
resp: referralTo(exampleCom), domain: wwwExampleCom,
want: nil,
},
{
name: "answer from a server that holds example.com",
resp: answer, domain: exampleCom,
want: []string{exampleNS},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.ElementsMatch(t, tt.want,
resolver.NSSetFrom(tt.resp, tt.domain),
)
})
}
}
func TestExtractRecordValue_LetterCase(t *testing.T) { func TestExtractRecordValue_LetterCase(t *testing.T) {
t.Parallel() t.Parallel()