initial commit

This commit is contained in:
2026-08-16 21:18:45 -05:00
commit 1e05a01bcf
122 changed files with 29178 additions and 0 deletions
+229
View File
@@ -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")
}
}