144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|