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 takes the apex form, so the record covers the // domain and everything under it; see // TestExampleRPZZoneCoversTheApexItNames. {"**.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) } } // TestExampleRPZZoneCoversTheApexItNames pins RPZ semantics on the shipped // policy zone: a record names a domain, and it applies to that domain AND // everything below it. // // This was a real gap, pinned rather than fixed for several rounds: rpzRule // gave a bare owner name the strict-subdomain form ("*.name"), so // `analytics.example.org.` blocked cdn.analytics.example.org. and left // analytics.example.org. resolving normally. DECIDED: rpzRule now uses the // apex form ("**.name") for a bare owner, which is what RFC 7766 says. An // owner written with an explicit "*." is still strict, so a zone that wants // subdomain-only behaviour can ask for it. func TestExampleRPZZoneCoversTheApexItNames(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() // The record for analytics.example.org. was written with a bare owner, so // it covers the apex as well as its children. for _, name := range []string{"analytics.example.org.", "cdn.analytics.example.org."} { 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) } } // The same on the allow side: an rpz-passthru. for a bare owner must // allowlist the apex too, or a name on the blocklist stays blocked. if e := set.Match("metrics.internal."); e == nil || !e.IsAllow() { t.Errorf("metrics.internal. = %v, want the rpz-passthru allow rule to cover the apex", e) } // A bare-owner redirect covers its apex as well, which is the point of // pointing a name at a landing page. if e := set.Match("blocked.example."); e == nil || e.Action != ActionRedirect { t.Errorf("blocked.example. = %v, want the redirect to cover the apex", e) } // The example's other records carry an explicit "*." owner, so they stay // strict and the pin above is not masking a blanket change of behaviour. for _, name := range []string{"doubleclick.net.", "ads.example.com."} { if e := set.Match(name); e != nil { t.Errorf("%s matched %v; an explicit \"*.\" owner must stay strict", name, e) } } }