package blocklist import ( "path/filepath" "slices" "testing" ) // TestExampleRPZZoneBlocksWhatItSays runs the shipped policy zone through the // exact path Manager.RefreshAll uses (parseRPZFiles, which prepends a per-file // $ORIGIN derived from the file name) and pins the rules it produces. // // The origin is the reason this is a test and not a doc: a file called // local.rpz.zone.example is read with the origin "local.", so the relative owner // `*.doubleclick.net` became `*.doubleclick.net.local.` and the example blocked // nothing. The example now spells every owner absolutely; if someone "tidies it // up" back to relative names, the rules below stop matching and this fails. func TestExampleRPZZoneBlocksWhatItSays(t *testing.T) { path := filepath.Join("..", "..", "deploy", "zones", "local.rpz.zone.example") rules, stats, err := parseRPZFiles([]string{path}) if err != nil { t.Fatalf("parseRPZFiles(%s): %v", path, err) } type ruleWant struct { name string action Action response string } want := []ruleWant{ {"*.doubleclick.net.", ActionNXDomain, ""}, {"*.ads.example.com.", ActionNXDomain, ""}, {"*.tracker.example.net.", ActionNXDomain, ""}, // A bare owner name in a policy zone is given the subdomain form, // which covers the name's children but not the name itself; see // TestExampleRPZZoneApexIsNotCovered. {"*.analytics.example.org.", ActionNXDomain, ""}, {"*.noisy-cdn.example.", ActionDrop, ""}, {"*.metrics.internal.", ActionPassthrough, ""}, {"*.blocked.example.", ActionRedirect, "192.0.2.10"}, } got := make([]ruleWant, 0, len(rules)) for _, r := range rules { got = append(got, ruleWant{r.Name, r.Action, r.Response}) if r.TTL != 60 { t.Errorf("rule %q has TTL %d, want the zone's $TTL 60", r.Name, r.TTL) } } if !slices.Equal(got, want) { t.Fatalf("rules mismatch\n got: %+v\nwant: %+v", got, want) } if stats.Rules != len(want) { t.Errorf("stats.Rules = %d, want %d", stats.Rules, len(want)) } // The A and the AAAA share an owner, so one of them is deduplicated // rather than compiled twice. if stats.Duplicates != 1 { t.Errorf("stats.Duplicates = %d, want 1 (the second blocked.example. record)", stats.Duplicates) } } // TestExampleRPZZoneMatchesTheNamesItNames builds the Set an instance would // actually consult, so the pin above is about lookups and not only about the // parser's output. A rule that exists but does not match is still a blocklist // that does nothing. func TestExampleRPZZoneMatchesTheNamesItNames(t *testing.T) { path := filepath.Join("..", "..", "deploy", "zones", "local.rpz.zone.example") rules, _, err := parseRPZFiles([]string{path}) if err != nil { t.Fatalf("parseRPZFiles(%s): %v", path, err) } b := NewBuilder() for _, r := range rules { b.Add(r, "local-rpz", nil) } set := b.Build() blocked := []string{ "www.doubleclick.net.", "a.b.doubleclick.net.", // These three carry an explicit "*." owner in the example, so only // names below them are covered; the apex gap is pinned separately. "cdn.ads.example.com.", "pixel.tracker.example.net.", "cdn.noisy-cdn.example.", } for _, name := range blocked { e := set.Match(name) if e == nil { t.Errorf("%s is not matched by the example policy zone", name) continue } if e.IsAllow() { t.Errorf("%s matched an allow rule, want a block", name) } } if e := set.Match("x.metrics.internal."); e == nil || !e.IsAllow() { t.Errorf("x.metrics.internal. = %v, want the rpz-passthru allow rule", e) } if e := set.Match("unrelated.example."); e != nil { t.Errorf("unrelated.example. matched %v, want no rule", e) } } // TestExampleRPZZoneApexIsNotCovered pins a real gap rather than an intention. // // An RPZ record names the domain it applies to, and in RPZ semantics that means // the domain and everything below it. rpzRule gives a bare owner name the // subdomain form ("*.name"), and a subdomain-form rule does not match the name // itself — so `analytics.example.org.` in a policy zone blocks // sub.analytics.example.org. and leaves analytics.example.org. resolving // normally. The apex form ("**.name") is what covers both. // // Not changed here: it alters which queries get blocked for anyone already // running a policy zone, and the previous round's decision was to pin the // open questions rather than change behaviour. If this is fixed, the fix is one // character in rpzRule ("*." -> "**.") plus this test and the STATUS.md entry. func TestExampleRPZZoneApexIsNotCovered(t *testing.T) { path := filepath.Join("..", "..", "deploy", "zones", "local.rpz.zone.example") rules, _, err := parseRPZFiles([]string{path}) if err != nil { t.Fatalf("parseRPZFiles(%s): %v", path, err) } b := NewBuilder() for _, r := range rules { b.Add(r, "local-rpz", nil) } set := b.Build() for _, name := range []string{"doubleclick.net.", "ads.example.com.", "analytics.example.org."} { if e := set.Match(name); e != nil { t.Errorf("%s matched %v; a policy record for a domain is expected to cover "+ "the domain itself as well as its subdomains (see STATUS.md)", name, e) } } // The same gap on the other side: an rpz-passthru. for the apex does not // allowlist the apex, so a name on the blocklist stays blocked. if e := set.Match("metrics.internal."); e != nil && e.IsAllow() { t.Errorf("metrics.internal. matched the allow rule %v; the apex of an "+ "rpz-passthru. record is not covered", e) } }