diff options
Diffstat (limited to 'dnsd/server_test.go')
| -rw-r--r-- | dnsd/server_test.go | 172 |
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) + } +} |
