initial commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/owen/vibedns/internal/blacklist"
|
||||
"github.com/owen/vibedns/internal/models"
|
||||
)
|
||||
|
||||
func buildSet(id int64, name, kind string, domains map[string]bool) *blacklist.Set {
|
||||
b := blacklist.NewBuilder(id, name, kind, len(domains))
|
||||
for d, sub := range domains {
|
||||
b.Add(d, sub)
|
||||
}
|
||||
return b.Build()
|
||||
}
|
||||
|
||||
// testIndex mirrors the example in the brief: a guest network with three
|
||||
// blacklists, a secure LAN with malware only, and a global allowlist.
|
||||
func testIndex(t *testing.T) *Index {
|
||||
t.Helper()
|
||||
|
||||
malware := buildSet(1, "Malware", models.KindBlacklist, map[string]bool{
|
||||
"evil.example": true,
|
||||
"c2.example.net": true,
|
||||
})
|
||||
adult := buildSet(2, "Adult Content", models.KindBlacklist, map[string]bool{
|
||||
"adult.example": true,
|
||||
})
|
||||
gambling := buildSet(3, "Gambling", models.KindBlacklist, map[string]bool{
|
||||
"bet.example": true,
|
||||
})
|
||||
allow := buildSet(4, "Global Allowlist", models.KindAllowlist, map[string]bool{
|
||||
"safe.adult.example": false, // exact match only
|
||||
})
|
||||
|
||||
lists := map[int64]*blacklist.Set{1: malware, 2: adult, 3: gambling, 4: allow}
|
||||
|
||||
policies := []models.Policy{
|
||||
{
|
||||
ID: 10, Name: "Guest Filtering", Enabled: true,
|
||||
BlockAction: models.BlockNXDOMAIN, BlockTTL: 60,
|
||||
BlacklistIDs: []int64{1, 2, 3}, AllowlistIDs: []int64{4},
|
||||
SinkholeIPv4: "0.0.0.0", SinkholeIPv6: "::",
|
||||
},
|
||||
{
|
||||
ID: 11, Name: "Malware Only", Enabled: true,
|
||||
BlockAction: models.BlockSinkhole, BlockTTL: 30,
|
||||
BlacklistIDs: []int64{1},
|
||||
SinkholeIPv4: "192.0.2.1", SinkholeIPv6: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
ID: 12, Name: "Disabled Policy", Enabled: false,
|
||||
BlockAction: models.BlockRefused, BlacklistIDs: []int64{1, 2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
networks := []models.Network{
|
||||
{ID: 100, Name: "Guest Wi-Fi", CIDR: "100.64.30.0/24", Enabled: true},
|
||||
{ID: 101, Name: "SecureLAN", CIDR: "100.64.10.0/24", Enabled: true},
|
||||
{ID: 102, Name: "Broad", CIDR: "100.64.0.0/16", Enabled: true},
|
||||
{ID: 103, Name: "Disabled Net", CIDR: "10.9.0.0/16", Enabled: false},
|
||||
{ID: 104, Name: "IPv6 LAN", CIDR: "2001:db8:1::/48", Enabled: true},
|
||||
}
|
||||
assignments := map[int64][]int64{
|
||||
100: {10},
|
||||
101: {11},
|
||||
102: {12}, // only a disabled policy
|
||||
103: {10},
|
||||
104: {11},
|
||||
}
|
||||
|
||||
return Build(networks, assignments, policies, lists)
|
||||
}
|
||||
|
||||
func addr(t *testing.T, s string) netip.Addr {
|
||||
t.Helper()
|
||||
a, err := netip.ParseAddr(s)
|
||||
if err != nil {
|
||||
t.Fatalf("bad test address %q: %v", s, err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func TestMatchNetworkPrefersMostSpecific(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
|
||||
tests := []struct {
|
||||
ip string
|
||||
want string
|
||||
}{
|
||||
{"100.64.30.5", "Guest Wi-Fi"}, // /24 beats the enclosing /16
|
||||
{"100.64.10.5", "SecureLAN"},
|
||||
{"100.64.99.5", "Broad"}, // only the /16 covers it
|
||||
{"10.9.0.1", ""}, // network is disabled
|
||||
{"203.0.113.1", ""}, // no network covers it
|
||||
{"2001:db8:1::5", "IPv6 LAN"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.ip, func(t *testing.T) {
|
||||
n := idx.MatchNetwork(addr(t, tc.ip))
|
||||
got := ""
|
||||
if n != nil {
|
||||
got = n.Name
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("network for %s = %q, want %q", tc.ip, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateBlocking(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
client string
|
||||
qname string
|
||||
wantBlocked bool
|
||||
wantList string
|
||||
wantAction models.BlockAction
|
||||
}{
|
||||
{"guest blocked by malware", "100.64.30.5", "evil.example", true, "Malware", models.BlockNXDOMAIN},
|
||||
{"guest blocked by adult", "100.64.30.5", "adult.example", true, "Adult Content", models.BlockNXDOMAIN},
|
||||
{"guest blocked by gambling", "100.64.30.5", "bet.example", true, "Gambling", models.BlockNXDOMAIN},
|
||||
{"guest subdomain blocked", "100.64.30.5", "www.adult.example", true, "Adult Content", models.BlockNXDOMAIN},
|
||||
{"guest deep subdomain blocked", "100.64.30.5", "a.b.c.adult.example", true, "Adult Content", models.BlockNXDOMAIN},
|
||||
{"guest clean name allowed", "100.64.30.5", "example.org", false, "", ""},
|
||||
|
||||
{"lan blocked by malware", "100.64.10.5", "evil.example", true, "Malware", models.BlockSinkhole},
|
||||
{"lan not filtered for adult", "100.64.10.5", "adult.example", false, "", ""},
|
||||
{"lan not filtered for gambling", "100.64.10.5", "bet.example", false, "", ""},
|
||||
|
||||
{"disabled policy filters nothing", "100.64.99.5", "evil.example", false, "", ""},
|
||||
{"unknown client is unfiltered", "203.0.113.1", "evil.example", false, "", ""},
|
||||
{"ipv6 client uses its policy", "2001:db8:1::5", "c2.example.net", true, "Malware", models.BlockSinkhole},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
d := idx.Evaluate(addr(t, tc.client), tc.qname)
|
||||
if d.Blocked != tc.wantBlocked {
|
||||
t.Fatalf("blocked = %v, want %v", d.Blocked, tc.wantBlocked)
|
||||
}
|
||||
if !tc.wantBlocked {
|
||||
return
|
||||
}
|
||||
if d.ListName != tc.wantList {
|
||||
t.Errorf("list = %q, want %q", d.ListName, tc.wantList)
|
||||
}
|
||||
if d.Action() != tc.wantAction {
|
||||
t.Errorf("action = %q, want %q", d.Action(), tc.wantAction)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllowlistOverridesBlacklist is the rule that lets an operator carve an
|
||||
// exception out of a large imported blocklist without editing it.
|
||||
func TestAllowlistOverridesBlacklist(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
client := addr(t, "100.64.30.5")
|
||||
|
||||
// safe.adult.example is on the allowlist even though adult.example (and
|
||||
// therefore all of its subdomains) is blacklisted.
|
||||
d := idx.Evaluate(client, "safe.adult.example")
|
||||
if d.Blocked {
|
||||
t.Errorf("allowlisted name was blocked by %q", d.ListName)
|
||||
}
|
||||
if !d.Allowed {
|
||||
t.Error("expected the decision to record an explicit allow")
|
||||
}
|
||||
|
||||
// The allowlist entry is exact-only, so a sibling stays blocked.
|
||||
if d := idx.Evaluate(client, "other.adult.example"); !d.Blocked {
|
||||
t.Error("an exact-only allowlist entry must not cover sibling names")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryNameNormalisation(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
client := addr(t, "100.64.30.5")
|
||||
|
||||
for _, name := range []string{"evil.example", "evil.example.", "EVIL.EXAMPLE", "Evil.Example."} {
|
||||
if d := idx.Evaluate(client, name); !d.Blocked {
|
||||
t.Errorf("%q was not blocked; names must match regardless of case or trailing dot", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPv4MappedClientAddress(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
// A UDP socket on a dual-stack listener reports IPv4 clients in the
|
||||
// ::ffff:a.b.c.d form; it must still match an IPv4 network.
|
||||
mapped := netip.MustParseAddr("::ffff:100.64.30.5")
|
||||
n := idx.MatchNetwork(mapped)
|
||||
if n == nil || n.Name != "Guest Wi-Fi" {
|
||||
t.Errorf("IPv4-mapped address matched %v, want Guest Wi-Fi", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStats(t *testing.T) {
|
||||
idx := testIndex(t)
|
||||
s := idx.Stats()
|
||||
if s.Networks != 4 {
|
||||
t.Errorf("networks = %d, want 4 enabled", s.Networks)
|
||||
}
|
||||
if s.Policies != 2 {
|
||||
t.Errorf("policies = %d, want 2 enabled", s.Policies)
|
||||
}
|
||||
if s.BlockedDomains != 4 {
|
||||
t.Errorf("blocked domains = %d, want 4", s.BlockedDomains)
|
||||
}
|
||||
if s.AllowedDomains != 1 {
|
||||
t.Errorf("allowed domains = %d, want 1", s.AllowedDomains)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilIndexIsSafe(t *testing.T) {
|
||||
var idx *Index
|
||||
if d := idx.Evaluate(netip.MustParseAddr("192.0.2.1"), "example.com"); d.Blocked {
|
||||
t.Error("a nil index must not block anything")
|
||||
}
|
||||
if idx.MatchNetwork(netip.MustParseAddr("192.0.2.1")) != nil {
|
||||
t.Error("a nil index must match no network")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user