package engine import ( "testing" "time" "github.com/miekg/dns" "git.k8s-git-k6n.net/mats/fancy-dns/internal/accesslog" "git.k8s-git-k6n.net/mats/fancy-dns/internal/blocklist" "git.k8s-git-k6n.net/mats/fancy-dns/internal/config" ) // soaTTLs returns the TTL and minimum of the SOA in a response's authority // section. func soaTTLs(t *testing.T, m *dns.Msg) (ttl, min uint32) { t.Helper() for _, rr := range m.Ns { if soa, ok := rr.(*dns.SOA); ok { return soa.Hdr.Ttl, soa.Minttl } } t.Fatalf("no SOA in %v", m) return 0, 0 } // TestBlockedAnswersCarryTheConfiguredTTLInSeconds: response_ttl is a duration // ("60s"), and the TTL on the wire is seconds. The conversion once took the // duration's nanoseconds as seconds and truncated them, so a blocked name's // negative answer told every client to remember it for about thirty years. func TestBlockedAnswersCarryTheConfiguredTTLInSeconds(t *testing.T) { const want = 60 // config.Default's response_ttl t.Run("nxdomain", func(t *testing.T) { h := newHarness(t, hopt{rules: []blocklist.Rule{{Name: "blocked.example.test"}}}) res := h.ask("blocked.example.test.", dns.TypeA) requireRcode(t, res, dns.RcodeNameError) if ttl, min := soaTTLs(t, res.Msg); ttl != want || min != want { t.Errorf("SOA ttl=%d minimum=%d, want %d", ttl, min, want) } }) t.Run("zero", func(t *testing.T) { h := newHarness(t, hopt{ blockCfg: func(b *config.Block) { b.DefaultAction = "zero" }, rules: []blocklist.Rule{{Name: "blocked.example.test"}}, }) res := h.ask("blocked.example.test.", dns.TypeA) if len(res.Msg.Answer) != 1 || res.Msg.Answer[0].Header().Ttl != want { t.Errorf("answer = %v, want one record with TTL %d", res.Msg.Answer, want) } }) t.Run("a rule's own ttl", func(t *testing.T) { h := newHarness(t, hopt{rules: []blocklist.Rule{{Name: "blocked.example.test", TTL: 120}}}) res := h.ask("blocked.example.test.", dns.TypeA) if ttl, _ := soaTTLs(t, res.Msg); ttl != 120 { t.Errorf("SOA ttl = %d, want the rule's 120", ttl) } }) t.Run("redirect", func(t *testing.T) { h := newHarness(t, hopt{rules: []blocklist.Rule{{Name: "blocked.example.test", Action: blocklist.ActionRedirect, Response: "10.9.9.9"}}}) res := h.ask("blocked.example.test.", dns.TypeA) if len(res.Msg.Answer) != 1 || res.Msg.Answer[0].Header().Ttl != want { t.Errorf("answer = %v, want one record with TTL %d", res.Msg.Answer, want) } }) } func TestTTLSecondsClamps(t *testing.T) { for in, want := range map[time.Duration]uint32{ 0: 0, 60 * time.Second: 60, 1500 * time.Millisecond: 1, 24 * time.Hour: 86400, -time.Second: 0, 1000000 * time.Hour: 1<<31 - 1, } { if got := blocklist.TTLSeconds(in); got != want { t.Errorf("TTLSeconds(%v) = %d, want %d", in, got, want) } } } // An address override is the answer the operator wants for a name, not a policy // action. With the access log logging redirects by default, logging each one // would fill it with every lookup of an internal name. func TestAddressOverridesAreLocalAnswersNotLoggedRedirects(t *testing.T) { h := newHarness(t, hopt{}) b := blocklist.NewBuilder() b.Add(blocklist.Rule{Name: "**.pinned.test", Action: blocklist.ActionRedirect, Response: "10.9.9.9"}, "managed:address-overrides", nil) b.Add(blocklist.Rule{Name: "**.sinkholed.test", Action: blocklist.ActionRedirect, Response: "10.8.8.8"}, "somelist", nil) h.blocks.Set(b.Build()) res := h.ask("www.pinned.test.", dns.TypeA) if res.Action != accesslog.ActionAllow || !res.Local { t.Errorf("override: action=%q local=%v, want allow/local", res.Action, res.Local) } if len(res.Msg.Answer) != 1 { t.Errorf("override answer = %v", res.Msg.Answer) } // A redirect that a list imposes is still a policy action. if res := h.ask("www.sinkholed.test.", dns.TypeA); res.Action != accesslog.ActionRedirect { t.Errorf("list redirect: action=%q, want redirect", res.Action) } }