osbase/net/dns: add new DNS server
This adds a new DNS server service, which will replace CoreDNS. The
service has built-in handlers for certain names, but all other names
will be handled by runtime configurable handlers.
Change-Id: I4184d11422496e899794ef658ca1450e7bb01471
Reviewed-on: https://review.monogon.dev/c/monogon/+/3126
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/net/dns/dns_test.go b/osbase/net/dns/dns_test.go
new file mode 100644
index 0000000..7dd670c
--- /dev/null
+++ b/osbase/net/dns/dns_test.go
@@ -0,0 +1,445 @@
+package dns
+
+import (
+ "net"
+ "testing"
+
+ "github.com/miekg/dns"
+
+ "source.monogon.dev/osbase/net/dns/test"
+)
+
+func testQuery(t *testing.T, service *Service, query, wantReply *dns.Msg) {
+ t.Helper()
+ wantReply.RecursionAvailable = true
+ testMsg(t, service, query, wantReply)
+}
+
+func testMsg(t *testing.T, service *Service, query, wantReply *dns.Msg) {
+ sCtx := &serviceCtx{service: service}
+ t.Helper()
+ wantReply.Response = true
+ writer := &testWriter{addr: &net.UDPAddr{}}
+ sCtx.ServeDNS(writer, query)
+ if got, want := writer.msg.String(), wantReply.String(); got != want {
+ t.Errorf("Want reply:\n%s\nGot:\n%s", want, got)
+ }
+}
+
+func TestBuiltinHandlers(t *testing.T) {
+ service := New(nil)
+
+ cases := []struct {
+ name string
+ qtype uint16
+ rcode int
+ answer []dns.RR
+ }{
+ {
+ name: "localhost.",
+ qtype: dns.TypeA,
+ answer: []dns.RR{test.RR("localhost. 300 IN A 127.0.0.1")},
+ },
+ {
+ name: "foo.bar.localhost.",
+ qtype: dns.TypeA,
+ answer: []dns.RR{test.RR("foo.bar.localhost. 300 IN A 127.0.0.1")},
+ },
+ {
+ name: "localhost.",
+ qtype: dns.TypeAAAA,
+ answer: []dns.RR{test.RR("localhost. 300 IN AAAA ::1")},
+ },
+ {
+ name: "localhost.",
+ qtype: dns.TypeANY,
+ answer: []dns.RR{
+ test.RR("localhost. 300 IN A 127.0.0.1"),
+ test.RR("localhost. 300 IN AAAA ::1"),
+ },
+ },
+ {
+ name: "localhost.",
+ qtype: dns.TypeMX,
+ },
+ {
+ name: "1.0.0.127.in-addr.arpa.",
+ qtype: dns.TypePTR,
+ answer: []dns.RR{test.RR("1.0.0.127.in-addr.arpa. 300 IN PTR localhost.")},
+ },
+ {
+ name: "1.0.0.127.in-addr.arpa.",
+ qtype: dns.TypeNS,
+ },
+ {
+ name: "2.127.in-addr.arpa.",
+ qtype: dns.TypePTR,
+ },
+ {
+ name: "foo.127.in-addr.arpa.",
+ qtype: dns.TypePTR,
+ rcode: dns.RcodeNameError,
+ },
+ {
+ name: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.",
+ qtype: dns.TypePTR,
+ answer: []dns.RR{test.RR("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa. 300 IN PTR localhost.")},
+ },
+ {
+ name: "foo.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.",
+ qtype: dns.TypePTR,
+ rcode: dns.RcodeNameError,
+ },
+ {
+ name: "invalid.",
+ qtype: dns.TypeA,
+ rcode: dns.RcodeNameError,
+ },
+ {
+ name: "foo.bar.invalid.",
+ qtype: dns.TypeA,
+ rcode: dns.RcodeNameError,
+ },
+ }
+
+ for _, c := range cases {
+ query := new(dns.Msg)
+ query.SetQuestion(c.name, c.qtype)
+ query.RecursionDesired = false
+ wantReply := query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Rcode = c.rcode
+ wantReply.Answer = c.answer
+ testQuery(t, service, query, wantReply)
+ }
+}
+
+type handlerFunc func(*Request)
+
+func (f handlerFunc) HandleDNS(r *Request) {
+ f(r)
+}
+
+func TestCustomHandlers(t *testing.T) {
+ service := New([]string{"handler1", "handler2"})
+ service.SetHandler("handler2", handlerFunc(func(r *Request) {
+ if IsSubDomain("example.com.", r.QnameCanonical) {
+ r.SetAuthoritative()
+ if r.Qtype == dns.TypeA || r.Qtype == dns.TypeANY {
+ rr := new(dns.A)
+ rr.Hdr = dns.RR_Header{Name: r.Qname, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}
+ rr.A = net.IP{1, 2, 3, 4}
+ r.Reply.Answer = append(r.Reply.Answer, rr)
+ }
+ r.SendReply()
+ }
+ }))
+
+ // Because handler1 is not yet set, this query should fail.
+ query := new(dns.Msg)
+ query.SetQuestion("example.com.", dns.TypeA)
+ wantReply := query.Copy()
+ wantReply.Rcode = dns.RcodeServerFailure
+ testQuery(t, service, query, wantReply)
+
+ service.SetHandler("handler1", EmptyDNSHandler{})
+
+ // Now, we should get the result from handler2.
+ query = new(dns.Msg)
+ query.SetQuestion("example.com.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("example.com. 300 IN A 1.2.3.4")}
+ testQuery(t, service, query, wantReply)
+
+ service.SetHandler("handler1", handlerFunc(func(r *Request) {
+ if IsSubDomain("example.com.", r.QnameCanonical) {
+ r.SetAuthoritative()
+ if r.Qtype == dns.TypeA || r.Qtype == dns.TypeANY {
+ rr := new(dns.A)
+ rr.Hdr = dns.RR_Header{Name: r.Qname, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}
+ rr.A = net.IP{5, 6, 7, 8}
+ r.Reply.Answer = append(r.Reply.Answer, rr)
+ }
+ r.SendReply()
+ }
+ }))
+
+ // Handlers can be updated, and are tried in the order in which they were
+ // declared when creating the Service.
+ query = new(dns.Msg)
+ query.SetQuestion("example.com.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("example.com. 300 IN A 5.6.7.8")}
+ testQuery(t, service, query, wantReply)
+
+ // Names which are not handled by any handler get refused.
+ query = new(dns.Msg)
+ query.SetQuestion("example.net.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeRefused
+ testQuery(t, service, query, wantReply)
+}
+
+func TestRedirect(t *testing.T) {
+ service := New([]string{"handler1", "handler2"})
+ service.SetHandler("handler1", handlerFunc(func(r *Request) {
+ if IsSubDomain("example.net.", r.QnameCanonical) {
+ r.SetAuthoritative()
+ if r.Qtype == dns.TypeA || r.Qtype == dns.TypeANY {
+ rr := new(dns.A)
+ rr.Hdr = dns.RR_Header{Name: r.Qname, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}
+ rr.A = net.IP{1, 2, 3, 4}
+ r.Reply.Answer = append(r.Reply.Answer, rr)
+ }
+ r.SendReply()
+ }
+ }))
+ service.SetHandler("handler2", handlerFunc(func(r *Request) {
+ if IsSubDomain("example.com.", r.QnameCanonical) {
+ switch r.QnameCanonical {
+ case "1.example.com.":
+ r.AddCNAME("2.example.com.", 30)
+ case "2.example.com.":
+ r.AddCNAME("example.net.", 30)
+
+ case "loop.example.com.":
+ r.AddCNAME("loop.example.com.", 30)
+
+ case "loop1.example.com.":
+ r.AddCNAME("loop2.example.com.", 30)
+ case "loop2.example.com.":
+ r.AddCNAME("loop3.example.com.", 30)
+ case "loop3.example.com.":
+ r.AddCNAME("loop1.example.com.", 30)
+
+ case "chain1.example.com.":
+ r.AddCNAME("chain2.example.com.", 30)
+ case "chain2.example.com.":
+ r.AddCNAME("chain3.example.com.", 30)
+ case "chain3.example.com.":
+ r.AddCNAME("chain4.example.com.", 30)
+ case "chain4.example.com.":
+ r.AddCNAME("chain5.example.com.", 30)
+ case "chain5.example.com.":
+ r.AddCNAME("chain6.example.com.", 30)
+ case "chain6.example.com.":
+ r.AddCNAME("chain7.example.com.", 30)
+ case "chain7.example.com.":
+ r.AddCNAME("chain8.example.com.", 30)
+ case "chain8.example.com.":
+ r.AddCNAME("chain9.example.com.", 30)
+ case "chain9.example.com.":
+ r.AddCNAME("chain10.example.com.", 30)
+
+ default:
+ r.SendRcode(dns.RcodeNameError)
+ }
+ }
+ }))
+
+ // CNAME redirects are followed.
+ query := new(dns.Msg)
+ query.SetQuestion("1.example.com.", dns.TypeA)
+ wantReply := query.Copy()
+ wantReply.Answer = []dns.RR{
+ test.RR("1.example.com. 30 IN CNAME 2.example.com."),
+ test.RR("2.example.com. 30 IN CNAME example.net."),
+ test.RR("example.net. 300 IN A 1.2.3.4"),
+ }
+ testQuery(t, service, query, wantReply)
+
+ // Queries of type CNAME or ANY do not follow the redirect.
+ query = new(dns.Msg)
+ query.SetQuestion("1.example.com.", dns.TypeCNAME)
+ wantReply = query.Copy()
+ wantReply.Answer = []dns.RR{test.RR("1.example.com. 30 IN CNAME 2.example.com.")}
+ testQuery(t, service, query, wantReply)
+
+ query = new(dns.Msg)
+ query.SetQuestion("1.example.com.", dns.TypeANY)
+ wantReply = query.Copy()
+ wantReply.Answer = []dns.RR{test.RR("1.example.com. 30 IN CNAME 2.example.com.")}
+ testQuery(t, service, query, wantReply)
+
+ // Loops are detected.
+ query = new(dns.Msg)
+ query.SetQuestion("loop.example.com.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Answer = []dns.RR{test.RR("loop.example.com. 30 IN CNAME loop.example.com.")}
+ wantReply.Rcode = dns.RcodeServerFailure
+ testQuery(t, service, query, wantReply)
+
+ // Loops are detected.
+ query = new(dns.Msg)
+ query.SetQuestion("loop1.example.com.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Answer = []dns.RR{
+ test.RR("loop1.example.com. 30 IN CNAME loop2.example.com."),
+ test.RR("loop2.example.com. 30 IN CNAME loop3.example.com."),
+ test.RR("loop3.example.com. 30 IN CNAME loop1.example.com."),
+ }
+ wantReply.Rcode = dns.RcodeServerFailure
+ testQuery(t, service, query, wantReply)
+
+ // Number of redirects is limited.
+ query = new(dns.Msg)
+ query.SetQuestion("chain1.example.com.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Answer = []dns.RR{
+ test.RR("chain1.example.com. 30 IN CNAME chain2.example.com."),
+ test.RR("chain2.example.com. 30 IN CNAME chain3.example.com."),
+ test.RR("chain3.example.com. 30 IN CNAME chain4.example.com."),
+ test.RR("chain4.example.com. 30 IN CNAME chain5.example.com."),
+ test.RR("chain5.example.com. 30 IN CNAME chain6.example.com."),
+ test.RR("chain6.example.com. 30 IN CNAME chain7.example.com."),
+ test.RR("chain7.example.com. 30 IN CNAME chain8.example.com."),
+ test.RR("chain8.example.com. 30 IN CNAME chain9.example.com."),
+ }
+ wantReply.Rcode = dns.RcodeServerFailure
+ testQuery(t, service, query, wantReply)
+}
+
+func TestFlags(t *testing.T) {
+ service := New(nil)
+
+ query := new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+
+ // Set flags which should be copied to the reply.
+ query.RecursionDesired = true
+ query.CheckingDisabled = true
+
+ wantReply := query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("localhost. 300 IN A 127.0.0.1")}
+
+ // Set flags which should be ignored.
+ query.Authoritative = true
+ query.RecursionAvailable = true
+ query.Zero = true
+ query.AuthenticatedData = true
+ query.Rcode = dns.RcodeRefused
+
+ testQuery(t, service, query, wantReply)
+}
+
+func TestOPT(t *testing.T) {
+ service := New(nil)
+
+ query := new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply := query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("localhost. 300 IN A 127.0.0.1")}
+ wantReply.SetEdns0(advertiseUDPSize, false)
+ query.SetEdns0(512, false)
+ testQuery(t, service, query, wantReply)
+
+ // DNSSEC ok flag.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("localhost. 300 IN A 127.0.0.1")}
+ wantReply.SetEdns0(advertiseUDPSize, true)
+ query.SetEdns0(512, true)
+ testQuery(t, service, query, wantReply)
+}
+
+func TestInvalidQuery(t *testing.T) {
+ service := New(nil)
+
+ // Valid query.
+ query := new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply := query.Copy()
+ wantReply.Authoritative = true
+ wantReply.Answer = []dns.RR{test.RR("localhost. 300 IN A 127.0.0.1")}
+ testQuery(t, service, query, wantReply)
+
+ // Not query opcode.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ query.Opcode = dns.OpcodeNotify
+ wantReply = query.Copy()
+ wantReply.RecursionDesired = false
+ wantReply.Rcode = dns.RcodeNotImplemented
+ testMsg(t, service, query, wantReply)
+
+ // Truncated.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeFormatError
+ query.Truncated = true
+ testQuery(t, service, query, wantReply)
+
+ // Multiple OPTs.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeFormatError
+ query.SetEdns0(512, false)
+ query.SetEdns0(512, false)
+ testQuery(t, service, query, wantReply)
+
+ // Unknown OPT version.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeBadVers
+ wantReply.SetEdns0(advertiseUDPSize, false)
+ query.SetEdns0(512, false)
+ query.Extra[0].(*dns.OPT).SetVersion(1)
+ testQuery(t, service, query, wantReply)
+
+ // Invalid OPT name.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeA)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeFormatError
+ query.SetEdns0(512, false)
+ query.Extra[0].(*dns.OPT).Hdr.Name = "localhost."
+ testQuery(t, service, query, wantReply)
+
+ // No question.
+ query = new(dns.Msg)
+ query.Id = dns.Id()
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeRefused
+ testQuery(t, service, query, wantReply)
+
+ // Multiple questions.
+ query = new(dns.Msg)
+ query.Id = dns.Id()
+ query.Question = []dns.Question{
+ {Name: "localhost.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
+ {Name: "localhost.", Qtype: dns.TypeAAAA, Qclass: dns.ClassINET},
+ }
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeRefused
+ testQuery(t, service, query, wantReply)
+
+ // OPT qtype.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeOPT)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeFormatError
+ testQuery(t, service, query, wantReply)
+
+ // Zone transfer.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeAXFR)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeRefused
+ testQuery(t, service, query, wantReply)
+
+ // Zone transfer.
+ query = new(dns.Msg)
+ query.SetQuestion("localhost.", dns.TypeIXFR)
+ wantReply = query.Copy()
+ wantReply.Rcode = dns.RcodeRefused
+ testQuery(t, service, query, wantReply)
+}