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/test/BUILD.bazel b/osbase/net/dns/test/BUILD.bazel
new file mode 100644
index 0000000..3cbd61d
--- /dev/null
+++ b/osbase/net/dns/test/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+    name = "test",
+    srcs = ["server.go"],
+    importpath = "source.monogon.dev/osbase/net/dns/test",
+    visibility = ["//osbase/net/dns:__subpackages__"],
+    deps = ["@com_github_miekg_dns//:dns"],
+)
+
+go_test(
+    name = "test_test",
+    srcs = ["server_test.go"],
+    embed = [":test"],
+    deps = ["@com_github_miekg_dns//:dns"],
+)
diff --git a/osbase/net/dns/test/LICENSE-3RD-PARTY.txt b/osbase/net/dns/test/LICENSE-3RD-PARTY.txt
new file mode 100644
index 0000000..98f9935
--- /dev/null
+++ b/osbase/net/dns/test/LICENSE-3RD-PARTY.txt
@@ -0,0 +1,13 @@
+Copyright 2016-2024 The CoreDNS authors and contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/osbase/net/dns/test/server.go b/osbase/net/dns/test/server.go
new file mode 100644
index 0000000..a877820
--- /dev/null
+++ b/osbase/net/dns/test/server.go
@@ -0,0 +1,73 @@
+package test
+
+// Taken and modified from CoreDNS, under Apache 2.0.
+
+import (
+	"fmt"
+	"net"
+
+	"github.com/miekg/dns"
+)
+
+// A Server is an DNS server listening on a system-chosen port on the local
+// loopback interface, for use in end-to-end DNS tests.
+type Server struct {
+	Addr string // Address where the server listening.
+
+	s1 *dns.Server // udp
+	s2 *dns.Server // tcp
+}
+
+// NewServer starts and returns a new Server. The caller should call Close when
+// finished, to shut it down.
+func NewServer(f dns.HandlerFunc) *Server {
+	ch1 := make(chan bool)
+	ch2 := make(chan bool)
+
+	s1 := &dns.Server{Handler: f} // udp
+	s2 := &dns.Server{Handler: f} // tcp
+
+	for i := 0; i < 5; i++ { // 5 attempts
+		s2.Listener, _ = net.Listen("tcp", "[::1]:0")
+		if s2.Listener == nil {
+			continue
+		}
+
+		s1.PacketConn, _ = net.ListenPacket("udp", s2.Listener.Addr().String())
+		if s1.PacketConn != nil {
+			break
+		}
+
+		// perhaps UDP port is in use, try again
+		s2.Listener.Close()
+		s2.Listener = nil
+	}
+	if s2.Listener == nil {
+		panic("dnstest.NewServer(): failed to create new server")
+	}
+
+	s1.NotifyStartedFunc = func() { close(ch1) }
+	s2.NotifyStartedFunc = func() { close(ch2) }
+	go s1.ActivateAndServe()
+	go s2.ActivateAndServe()
+
+	<-ch1
+	<-ch2
+
+	return &Server{s1: s1, s2: s2, Addr: s2.Listener.Addr().String()}
+}
+
+// Close shuts down the server.
+func (s *Server) Close() {
+	s.s1.Shutdown()
+	s.s2.Shutdown()
+}
+
+// RR parses s as a DNS resource record.
+func RR(s string) dns.RR {
+	rr, err := dns.NewRR(s)
+	if err != nil {
+		panic(fmt.Sprintf("Failed to parse rr %q: %v", s, err))
+	}
+	return rr
+}
diff --git a/osbase/net/dns/test/server_test.go b/osbase/net/dns/test/server_test.go
new file mode 100644
index 0000000..ea60845
--- /dev/null
+++ b/osbase/net/dns/test/server_test.go
@@ -0,0 +1,39 @@
+package test
+
+// Taken and modified from CoreDNS, under Apache 2.0.
+
+import (
+	"testing"
+
+	"github.com/miekg/dns"
+)
+
+func TestNewServer(t *testing.T) {
+	s := NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
+		ret := new(dns.Msg)
+		ret.SetReply(r)
+		w.WriteMsg(ret)
+	})
+	defer s.Close()
+
+	c := new(dns.Client)
+	c.Net = "tcp"
+	m := new(dns.Msg)
+	m.SetQuestion("example.org.", dns.TypeSOA)
+	ret, _, err := c.Exchange(m, s.Addr)
+	if err != nil {
+		t.Fatalf("Could not send message to dnstest.Server: %s", err)
+	}
+	if ret.Id != m.Id {
+		t.Fatalf("Msg ID's should match, expected %d, got %d", m.Id, ret.Id)
+	}
+
+	c.Net = "udp"
+	ret, _, err = c.Exchange(m, s.Addr)
+	if err != nil {
+		t.Fatalf("Could not send message to dnstest.Server: %s", err)
+	}
+	if ret.Id != m.Id {
+		t.Fatalf("Msg ID's should match, expected %d, got %d", m.Id, ret.Id)
+	}
+}