Fix block-answer TTLs that were nanoseconds, and stop counting NXDOMAIN as an error
Both found by running the resolver locally against real upstreams. response_ttl is a duration (60s) and a DNS TTL is seconds, but eight places converted with a bare uint32(), which keeps the low 32 bits of the nanosecond count. A blocked name's negative answer carried an SOA TTL and minimum of 997195776 (about 31 years), so any client or downstream cache that honoured it would remember the block, and keep remembering it after the name was unblocked. blocklist.TTLSeconds converts properly (whole seconds, clamped to 2^31-1) and is used by the rule parser and the block, sinkhole and redirect answers. The tests read the TTL off the wire; with the old conversion they fail with 4165425152. An ordinary upstream NXDOMAIN was classified as an error. A name that does not exist is the normal result of a typo, a search-domain expansion or a leaked mDNS query, so on a real network the error metric counted mostly those and, with the access log's errors-only default, every one wrote a line. Only the upstream failing or refusing (SERVFAIL, REFUSED, FORMERR, NOTIMP) is an error now. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
04ebf68c0f
commit
b513501773
6 files changed
+146
-13
No files matched your search
@@ -379,7 +379,7 @@ func (m *Manager) loadOne(ctx context.Context, cfg *config.Config, l config.List
|
||||
format := Format(l.Format)
|
||||
opt := ParseOptions{
|
||||
Subdomains: true,
|
||||
DefaultTTL: uint32(cfg.Block.ResponseTTL.D()),
|
||||
DefaultTTL: TTLSeconds(cfg.Block.ResponseTTL.D()),
|
||||
Categories: l.Categories,
|
||||
}
|
||||
rules, st, perr := Parse(format, data, opt)
|
||||
@@ -624,7 +624,7 @@ func parseInline(entries []string, action Action, ttl config.Duration) ([]Rule,
|
||||
sb.WriteString(e)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
opt := ParseOptions{Subdomains: true, DefaultTTL: uint32(ttl.D())}
|
||||
opt := ParseOptions{Subdomains: true, DefaultTTL: TTLSeconds(ttl.D())}
|
||||
rules, st, err := Parse(FormatPlain, []byte(sb.String()), opt)
|
||||
if err != nil {
|
||||
return nil, st, err
|
||||
@@ -668,3 +668,17 @@ func fraction(name string) int {
|
||||
// insecureTLS is the transport tweak for sources that need it. It is only
|
||||
// reachable when a list explicitly sets insecure_skip_tls_verify.
|
||||
func insecureTLS() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
||||
|
||||
// TTLSeconds converts a duration to a DNS TTL: whole seconds, never negative,
|
||||
// and no more than the 2^31-1 RFC 2181 allows. A TTL is seconds on the wire and
|
||||
// a config duration is nanoseconds in memory; converting with a bare uint32
|
||||
// truncates the nanoseconds into a number nobody meant.
|
||||
func TTLSeconds(d time.Duration) uint32 {
|
||||
switch {
|
||||
case d <= 0:
|
||||
return 0
|
||||
case d >= time.Duration(1<<31-1)*time.Second:
|
||||
return 1<<31 - 1
|
||||
}
|
||||
return uint32(d / time.Second)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package blocklist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
|
||||
)
|
||||
|
||||
// TestInlineRulesGetTheResponseTTLInSeconds: the rule TTL is seconds, the config
|
||||
// value is a duration, and the two were once confused.
|
||||
func TestInlineRulesGetTheResponseTTLInSeconds(t *testing.T) {
|
||||
rules, _, err := parseInline([]string{"a.example"}, ActionBlock, config.Duration(90*time.Second))
|
||||
if err != nil || len(rules) != 1 {
|
||||
t.Fatalf("%v %v", rules, err)
|
||||
}
|
||||
if rules[0].TTL != 90 {
|
||||
t.Errorf("rule TTL = %d, want 90", rules[0].TTL)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user