summaryrefslogtreecommitdiff
path: root/dnsd/server_test.go
diff options
context:
space:
mode:
authorAlexander Hunt <me@liz.coffee>2026-06-21 12:43:31 -0700
committerAlexander Hunt <me@liz.coffee>2026-06-21 12:43:31 -0700
commitcfc0d34a584254a9ed76620951c810771aff6f3b (patch)
tree7fba034f7347ce0003e7a3a56adcaf862c834907 /dnsd/server_test.go
parentfb653292612eddca5b088ed88466c546d1597107 (diff)
downloadpenguins.lan-cfc0d34a584254a9ed76620951c810771aff6f3b.tar.gz
penguins.lan-cfc0d34a584254a9ed76620951c810771aff6f3b.zip
STUFF
Diffstat (limited to 'dnsd/server_test.go')
-rw-r--r--dnsd/server_test.go172
1 files changed, 172 insertions, 0 deletions
diff --git a/dnsd/server_test.go b/dnsd/server_test.go
new file mode 100644
index 0000000..a9cb726
--- /dev/null
+++ b/dnsd/server_test.go
@@ -0,0 +1,172 @@
+package dnsd
+
+import (
+ "database/sql"
+ "net"
+ "path/filepath"
+ "testing"
+
+ "github.com/miekg/dns"
+
+ "penguins.lan/database"
+ "penguins.lan/utils"
+)
+
+const (
+ testUser = "tux"
+ testMac = "aa:bb:cc:dd:ee:ff"
+ testMacLbl = "aabbccddeeff"
+ testIP = "192.168.8.50"
+ testZone = "penguins.lan."
+ testApexIP = "10.0.0.1"
+ testDevName = testMacLbl + "." + testUser + "." + testZone
+)
+
+// capWriter captures the reply a Handler writes instead of sending it.
+type capWriter struct{ msg *dns.Msg }
+
+func (c *capWriter) WriteMsg(m *dns.Msg) error { c.msg = m; return nil }
+func (c *capWriter) Write([]byte) (int, error) { return 0, nil }
+func (c *capWriter) Close() error { return nil }
+func (c *capWriter) TsigStatus() error { return nil }
+func (c *capWriter) TsigTimersOnly(bool) {}
+func (c *capWriter) Hijack() {}
+func (c *capWriter) LocalAddr() net.Addr { return &net.UDPAddr{} }
+func (c *capWriter) RemoteAddr() net.Addr { return &net.UDPAddr{} }
+
+func testDB(t *testing.T) *sql.DB {
+ t.Helper()
+ db, err := sql.Open("sqlite3", filepath.Join(t.TempDir(), "d.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { db.Close() })
+ if _, err := database.Migrate(db); err != nil {
+ t.Fatal(err)
+ }
+ return db
+}
+
+func testHandler(db *sql.DB) *Handler {
+ return &Handler{DB: db, Zone: testZone, ServerIP: testApexIP}
+}
+
+// query runs one question through the handler and returns the reply.
+func query(t *testing.T, h *Handler, name string, qtype uint16) *dns.Msg {
+ t.Helper()
+ m := &dns.Msg{}
+ m.SetQuestion(dns.Fqdn(name), qtype)
+ w := &capWriter{}
+ h.ServeDNS(w, m)
+ if w.msg == nil {
+ t.Fatal("handler wrote no reply")
+ }
+ return w.msg
+}
+
+// seedDevice creates the user, binds the MAC, and marks it live at testIP.
+func seedDevice(t *testing.T, db *sql.DB) {
+ t.Helper()
+ user, err := database.CreateUser(db, "u1", testUser)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := database.AssociateMAC(db, testMac, user.ID); err != nil {
+ t.Fatal(err)
+ }
+ if err := database.RefreshConnections(db, map[string]string{testIP: testMac}); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestDeviceRecordSynthesized(t *testing.T) {
+ db := testDB(t)
+ seedDevice(t, db)
+
+ reply := query(t, testHandler(db), testDevName, dns.TypeA)
+ if len(reply.Answer) != 1 {
+ t.Fatalf("answers = %d, want 1: %v", len(reply.Answer), reply.Answer)
+ }
+ a, ok := reply.Answer[0].(*dns.A)
+ if !ok || a.A.String() != testIP {
+ t.Fatalf("device A = %v, want %s", reply.Answer[0], testIP)
+ }
+}
+
+func TestPrimaryCNAMEFollowsToDeviceA(t *testing.T) {
+ db := testDB(t)
+ seedDevice(t, db)
+
+ // the wizard stores this: <user>.zone CNAME -> <mac>.<user>.zone
+ if _, err := database.SaveDNSRecord(db, &database.DNSRecord{
+ ID: "r1", UserID: "u1", Name: testUser + "." + testZone,
+ Type: "CNAME", Content: testDevName, TTL: 300,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ reply := query(t, testHandler(db), testUser+"."+testZone, dns.TypeA)
+ var sawCNAME, sawA bool
+ for _, rr := range reply.Answer {
+ switch v := rr.(type) {
+ case *dns.CNAME:
+ sawCNAME = v.Target == testDevName
+ case *dns.A:
+ sawA = v.A.String() == testIP
+ }
+ }
+ if !sawCNAME || !sawA {
+ t.Fatalf("want CNAME->device + A=%s, got %v", testIP, reply.Answer)
+ }
+}
+
+func TestApexAnswersServerIP(t *testing.T) {
+ db := testDB(t)
+ reply := query(t, testHandler(db), testZone, dns.TypeA)
+ if len(reply.Answer) != 1 {
+ t.Fatalf("apex answers = %v", reply.Answer)
+ }
+ if a, ok := reply.Answer[0].(*dns.A); !ok || a.A.String() != testApexIP {
+ t.Fatalf("apex A = %v, want %s", reply.Answer[0], testApexIP)
+ }
+}
+
+func TestCustomTXTRecord(t *testing.T) {
+ db := testDB(t)
+ seedDevice(t, db)
+ name := "hello." + testUser + "." + testZone
+ if _, err := database.SaveDNSRecord(db, &database.DNSRecord{
+ ID: "r2", UserID: "u1", Name: name, Type: "TXT", Content: "\"waddle\"", TTL: 60,
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ reply := query(t, testHandler(db), name, dns.TypeTXT)
+ if len(reply.Answer) != 1 {
+ t.Fatalf("txt answers = %v", reply.Answer)
+ }
+ txt, ok := reply.Answer[0].(*dns.TXT)
+ if !ok || len(txt.Txt) != 1 || txt.Txt[0] != "waddle" {
+ t.Fatalf("txt = %v, want [waddle]", reply.Answer[0])
+ }
+}
+
+func TestUnknownNameIsNXDOMAIN(t *testing.T) {
+ db := testDB(t)
+ seedDevice(t, db)
+ reply := query(t, testHandler(db), "nope."+testZone, dns.TypeA)
+ if reply.Rcode != dns.RcodeNameError {
+ t.Fatalf("rcode = %d, want NXDOMAIN(%d)", reply.Rcode, dns.RcodeNameError)
+ }
+}
+
+// guard: a foreign MAC label under someone else's username must not resolve.
+func TestDeviceRecordRejectsForeignMAC(t *testing.T) {
+ db := testDB(t)
+ seedDevice(t, db)
+ other, _ := utils.ParseMac("11:22:33:44:55:66")
+ reply := query(t, testHandler(db), other.Compact()+"."+testUser+"."+testZone, dns.TypeA)
+ if reply.Rcode != dns.RcodeNameError {
+ t.Fatalf("foreign mac rcode = %d, want NXDOMAIN", reply.Rcode)
+ }
+}