package acl import ( "strings" "testing" "git.k8s-git-k6n.net/mats/fancy-dns/internal/config" ) func named(name, policy string, m config.Match) config.Group { return config.Group{Name: name, Policy: policy, Match: m} } func analyzeAt(idx int, defRate int, groups ...config.Group) Analysis { return Analyze(Context{Groups: groups, Index: idx, DefaultRateLimit: defRate, DefaultPolicy: "allow"}) } func has(fs []Finding, field, contains string) bool { for _, f := range fs { if f.Field == field && strings.Contains(f.Message, contains) { return true } } return false } func TestAnalyzeAllowListIsAllowlistMode(t *testing.T) { g := named("kids", "allow", config.Match{CIDRs: []string{"10.0.5.0/24"}}) g.AllowDomains = []string{"*.school.example"} a := analyzeAt(0, 0, g) if !has(a.Notes, "allow_domains", "allowlist-only") { t.Errorf("notes = %+v, want the allowlist-only explanation", a.Notes) } if !strings.Contains(strings.Join(a.Summary, " "), "ONLY") { t.Errorf("summary = %v, want it to say only the listed names resolve", a.Summary) } } func TestAnalyzeBlockOnlyIsTheSameAsAllow(t *testing.T) { a := analyzeAt(0, 0, named("g", "block_only", config.Match{CIDRs: []string{"10.0.0.0/24"}})) if !has(a.Warnings, "policy", "exactly like allow") { t.Errorf("warnings = %+v, want the block_only warning", a.Warnings) } } func TestAnalyzeRateLimitZeroExemptsFromTheDefault(t *testing.T) { g := named("g", "allow", config.Match{CIDRs: []string{"10.0.0.0/24"}}) if a := analyzeAt(0, 50, g); !has(a.Warnings, "rate_limit", "exempt") { t.Errorf("warnings = %+v, want the exemption warning", a.Warnings) } if a := analyzeAt(0, 0, g); len(a.Warnings) != 0 { t.Errorf("no default limit, yet warnings = %+v", a.Warnings) } g.RateLimit = 20 if a := analyzeAt(0, 50, g); has(a.Warnings, "rate_limit", "exempt") { t.Errorf("an explicit limit must not warn about exemption: %+v", a.Warnings) } } func TestAnalyzeDenyIgnoresTheRest(t *testing.T) { g := named("g", "deny", config.Match{CIDRs: []string{"10.0.0.0/24"}}) g.RateLimit = 5 g.SkipBlocklist = true a := analyzeAt(0, 50, g) if !has(a.Notes, "rate_limit", "Ignored") || !has(a.Notes, "skip_blocklist", "Ignored") { t.Errorf("notes = %+v, want ignored-setting notes", a.Notes) } if has(a.Warnings, "rate_limit", "exempt") { t.Errorf("a denied group has no rate limit to be exempt from: %+v", a.Warnings) } } func TestAnalyzeShadowing(t *testing.T) { wide := named("lan", "allow", config.Match{CIDRs: []string{"10.0.0.0/8"}}) narrow := named("cam", "deny", config.Match{CIDRs: []string{"10.1.2.0/24"}}) a := analyzeAt(1, 0, wide, narrow) if !a.Unreachable || !has(a.Warnings, "match", `"lan"`) { t.Errorf("narrow range after a wider one: unreachable=%v warnings=%+v", a.Unreachable, a.Warnings) } // The other order is the useful one and must stay quiet. if a := analyzeAt(0, 0, narrow, wide); a.Unreachable || len(a.Warnings) > 1 { t.Errorf("specific-before-general flagged: unreachable=%v warnings=%+v", a.Unreachable, a.Warnings) } // Saving the wide group above the narrow one buries the narrow one. a = analyzeAt(0, 0, wide, narrow) if !has(a.Warnings, "match", `makes "cam" unreachable`) { t.Errorf("warnings = %+v, want the newly-unreachable warning", a.Warnings) } } func TestAnalyzePartialOverlapNamesTheClaimedRange(t *testing.T) { first := named("a", "allow", config.Match{CIDRs: []string{"10.0.0.0/24"}}) second := named("b", "allow", config.Match{CIDRs: []string{"10.0.0.128/25", "192.168.0.0/24"}}) a := analyzeAt(1, 0, first, second) if a.Unreachable { t.Fatal("one range is still free; the group is reachable") } if !has(a.Warnings, "match", "10.0.0.128/25 is already claimed by \"a\"") { t.Errorf("warnings = %+v", a.Warnings) } } func TestAnalyzeReverseNameAndTagShadowing(t *testing.T) { first := named("a", "allow", config.Match{PTRSuffixes: []string{"lan.example."}, Tags: []string{"laptop"}}) second := named("b", "allow", config.Match{PTRSuffixes: []string{".lab.lan.example."}, Tags: []string{"Laptop"}}) if a := analyzeAt(1, 0, first, second); !a.Unreachable { t.Errorf("a suffix under an earlier suffix plus the same tag is fully claimed: %+v", a) } } func TestAnalyzeGroupAfterACatchAllIsUnreachable(t *testing.T) { all := named("all", "allow", config.Match{Any: true, CIDRs: []string{"10.0.0.0/8"}}) g := named("g", "allow", config.Match{CIDRs: []string{"192.168.0.0/24"}}) a := analyzeAt(1, 0, all, g) if !a.Unreachable { t.Error("nothing after a match-any group can be reached") } if b := analyzeAt(0, 0, all, g); !has(b.Warnings, "match.any", "every client") { t.Errorf("warnings = %+v, want the match-any warning", b.Warnings) } } func TestAnalyzeCIDRHygiene(t *testing.T) { a := analyzeAt(0, 0, named("g", "allow", config.Match{CIDRs: []string{"192.168.1.5/24", "0.0.0.0/0"}})) if !has(a.Warnings, "match.cidrs", "host bits") || !has(a.Warnings, "match.cidrs", "very wide") { t.Errorf("warnings = %+v", a.Warnings) } } func TestAnalyzeDomainPatternMistakes(t *testing.T) { g := named("g", "allow", config.Match{CIDRs: []string{"10.0.0.0/24"}}) g.AllowDomains = []string{"https://example.com/a", ".example.org", "ads.*.com", "*.fine.example", "*"} g.DenyDomains = []string{"*.fine.example"} a := analyzeAt(0, 0, g) for _, want := range []string{"looks like a URL", "starts with a dot", "not supported", "allows every name", "deny wins"} { if !has(a.Warnings, "allow_domains", want) { t.Errorf("missing allow_domains warning containing %q in %+v", want, a.Warnings) } } } func TestAnalyzeTagWithADotNeverMatches(t *testing.T) { a := analyzeAt(0, 0, named("g", "allow", config.Match{Tags: []string{"lan.example"}})) if !has(a.Warnings, "match.tags", "single label") { t.Errorf("warnings = %+v", a.Warnings) } } func TestAnalyzeUnknownUpstreams(t *testing.T) { g := named("g", "allow", config.Match{CIDRs: []string{"10.0.0.0/24"}}) g.Upstreams = []string{"9.9.9.9:53"} a := Analyze(Context{Groups: []config.Group{g}, Upstreams: []string{"1.1.1.1:53"}}) if !has(a.Warnings, "upstreams", "uses ALL upstreams") { t.Errorf("warnings = %+v", a.Warnings) } g.Upstreams = []string{"1.1.1.1:53"} if a := Analyze(Context{Groups: []config.Group{g}, Upstreams: []string{"1.1.1.1:53"}}); has(a.Warnings, "upstreams", "") { t.Errorf("a configured upstream warned: %+v", a.Warnings) } if a := Analyze(Context{Groups: []config.Group{g}}); has(a.Warnings, "upstreams", "") { t.Errorf("unknown upstream list must skip the check: %+v", a.Warnings) } } func TestAnalyzeToleratesABadIndex(t *testing.T) { if a := Analyze(Context{Index: 3}); len(a.Summary) != 0 { t.Errorf("summary = %v", a.Summary) } }