Fix the negative-caching warning and show warnings in the CLI
The soa_warning for negative caching fired on the SOA minimum alone and blamed BIND's resolver cap (max-ncache-ttl), which doesn't apply to an authoritative zone. Resolvers cache negative answers for the smaller of the SOA minimum and the SOA record's TTL (RFC 2308 section 5), and the SOA record uses the zone's default TTL. The warning now uses that value, explains the effect, and suggests the fix command (with --view when the zone is in a view). Also: - Problem.String shows zone names without the trailing dot (jarld check). - jarl prints the warnings a change introduces when it is applied or staged, not only on dry runs. - jarl check/resync print only named-checkconf errors, not its "loaded serial" lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bd6b8778e4
commit
3c73afe0e1
7 files changed
+89
-11
No files matched your search
@@ -14,6 +14,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"jarl/internal/dnsname"
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
@@ -47,7 +48,7 @@ func (p Problem) Key() string {
|
||||
func (p Problem) String() string {
|
||||
loc := ""
|
||||
if p.Zone != "" {
|
||||
loc = "zone " + p.Zone
|
||||
loc = "zone " + dnsname.Display(p.Zone)
|
||||
if p.View != "" {
|
||||
loc += " [view " + p.View + "]"
|
||||
}
|
||||
|
||||
@@ -344,3 +344,39 @@ func TestRecordLimit(t *testing.T) {
|
||||
t.Error("max-records-per-type 0 (unlimited) not honoured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegativeCachingTTL(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
defaultTTL, minimum uint32
|
||||
warn string // "" for no warning
|
||||
}{
|
||||
{3600, 300, ""},
|
||||
{3600, 86400, ""}, // the SOA's own TTL (1h) limits negative caching
|
||||
{86400, 10800, ""}, // 3 hours is fine
|
||||
{86400, 86400, "1 day"}, // both long: negative answers live a day
|
||||
{43200, 86400, "12 hours"},
|
||||
} {
|
||||
b := newBuilder()
|
||||
z := b.primary("example.com", nil)
|
||||
z.DefaultTTL, z.SOA.Minimum = tc.defaultTTL, tc.minimum
|
||||
var got *Problem
|
||||
for _, p := range State(b.s, Options{}) {
|
||||
if p.Code == "soa_warning" && strings.Contains(p.Message, "negative") {
|
||||
got = &p
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case tc.warn == "" && got != nil:
|
||||
t.Errorf("TTL %d, minimum %d: unexpected warning %q", tc.defaultTTL, tc.minimum, got.Message)
|
||||
case tc.warn != "" && (got == nil || !strings.Contains(got.Message, tc.warn)):
|
||||
t.Errorf("TTL %d, minimum %d: want a warning mentioning %q, got %v", tc.defaultTTL, tc.minimum, tc.warn, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProblemStringDisplaysZone(t *testing.T) {
|
||||
p := Problem{Severity: Warning, Message: "m", Zone: "10.0.10.in-addr.arpa.", View: "internal"}
|
||||
if got, want := p.String(), "warning: zone 10.0.10.in-addr.arpa [view internal]: m"; got != want {
|
||||
t.Errorf("String() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -164,8 +165,16 @@ func (c *checker) checkPrimary(z *model.Zone) {
|
||||
} else if soa.Expire < 604800 {
|
||||
c.zoneProblem(z, Warning, "soa_warning", "SOA expire is under a week (%d); 2-4 weeks is recommended so secondaries survive a primary outage", soa.Expire)
|
||||
}
|
||||
if soa.Minimum > 10800 {
|
||||
c.zoneProblem(z, Warning, "soa_warning", "SOA minimum (negative caching TTL) %d is more than 3 hours; BIND caps negative caching at 3 hours", soa.Minimum)
|
||||
// Resolvers cache "no such name/type" answers for the smaller of the
|
||||
// SOA minimum and the SOA record's own TTL (RFC 2308 section 5); the
|
||||
// SOA record uses the zone's default TTL.
|
||||
if neg := min(soa.Minimum, z.DefaultTTL); neg > 10800 {
|
||||
cmd := "jarl zone update " + dnsname.Display(z.Name)
|
||||
if v := c.s.ViewName(z); v != "" {
|
||||
cmd += " --view " + v
|
||||
}
|
||||
c.zoneProblem(z, Warning, "soa_warning", "resolvers may cache negative answers (\"no such name\") from this zone for %s (the smaller of the SOA minimum, %d, and the zone's default TTL, %d), so a name you add can stay invisible that long to anyone who looked it up before; RFC 2308 recommends 1 to 3 hours (lower it with '%s --minimum 3600')",
|
||||
humanSeconds(neg), soa.Minimum, z.DefaultTTL, cmd)
|
||||
}
|
||||
if z.DefaultTTL > maxTTL {
|
||||
c.zoneProblem(z, Error, "ttl_invalid", "default TTL %d is larger than 2147483647", z.DefaultTTL)
|
||||
@@ -180,3 +189,19 @@ func (c *checker) checkPrimary(z *model.Zone) {
|
||||
}
|
||||
c.checkZoneRecords(z)
|
||||
}
|
||||
|
||||
// humanSeconds formats a duration in seconds as e.g. "1 day" or "12 hours".
|
||||
func humanSeconds(n uint32) string {
|
||||
for _, u := range []struct {
|
||||
secs uint32
|
||||
name string
|
||||
}{{604800, "week"}, {86400, "day"}, {3600, "hour"}, {60, "minute"}} {
|
||||
if n >= u.secs && n%u.secs == 0 {
|
||||
if v := n / u.secs; v != 1 {
|
||||
return fmt.Sprintf("%d %ss", v, u.name)
|
||||
}
|
||||
return "1 " + u.name
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%d seconds", n)
|
||||
}
|
||||
Reference in new issue
Block a user