package dnsserver import ( "context" "errors" "net" "sync" "sync/atomic" "testing" "time" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/pkg/log" "github.com/coredns/coredns/plugin/test" "github.com/miekg/dns" ) type testPlugin struct{} func (tp testPlugin) ServeDNS(_ctx context.Context, _w dns.ResponseWriter, _r *dns.Msg) (int, error) { return 0, nil } func (tp testPlugin) Name() string { return "local" } type updateResponsePlugin struct { called atomic.Bool } func (p *updateResponsePlugin) Name() string { return "update-response" } func (p *updateResponsePlugin) ServeDNS(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { p.called.Store(true) m := new(dns.Msg) m.SetReply(r) if err := w.WriteMsg(m); err != nil { return dns.RcodeServerFailure, err } return dns.RcodeSuccess, nil } func newRFC2136Update(t *testing.T, zone string) *dns.Msg { t.Helper() m := new(dns.Msg).SetUpdate(zone) rr, err := dns.NewRR("host." + zone + " 300 IN A 192.0.2.123") if err != nil { t.Fatalf("dns.NewRR() failed: %v", err) } m.Insert([]dns.RR{rr}) return m } func mustPackRFC2136Update(t *testing.T) []byte { t.Helper() m := newRFC2136Update(t, "example.com.") // DNS-over-QUIC requires the DNS message ID to be zero. m.Id = 0 wire, err := m.Pack() if err != nil { t.Fatalf("dns.Msg.Pack() failed: %v", err) } return wire } // blockingPlugin uses sync.Mutex to simulate extended processing. type blockingPlugin struct { sync.Mutex } func (b *blockingPlugin) Name() string { return "blocking" } func (b *blockingPlugin) ServeDNS(_ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // Respond immediately to avoid waiting in dns.Exchange m := new(dns.Msg) m.SetRcodeFormatError(r) w.WriteMsg(m) b.Lock() defer b.Unlock() return dns.RcodeSuccess, nil } func testConfig(transport string, p plugin.Handler) *Config { c := &Config{ Zone: "example.com.", Transport: transport, ListenHosts: []string{"127.0.0.1"}, Port: "53", Debug: false, Stacktrace: false, } c.AddPlugin(func(_next plugin.Handler) plugin.Handler { return p }) return c } func TestNewServer(t *testing.T) { _, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) if err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } _, err = NewServergRPC("127.0.0.1:53", []*Config{testConfig("grpc", testPlugin{})}) if err != nil { t.Errorf("Expected no error for NewServergRPC, got %s", err) } _, err = NewServerTLS("127.0.0.1:53", []*Config{testConfig("tls", testPlugin{})}) if err != nil { t.Errorf("Expected no error for NewServerTLS, got %s", err) } _, err = NewServerQUIC("127.0.0.1:53", []*Config{testConfig("quic", testPlugin{})}) if err != nil { t.Errorf("Expected no error for NewServerQUIC, got %s", err) } } func TestUpdateAdmission(t *testing.T) { for _, network := range []string{"udp", "tcp"} { for _, allow := range []bool{false, true} { name := network + "/default-reject" if allow { name = network + "/explicit-opt-in" } t.Run(name, func(t *testing.T) { handler := new(updateResponsePlugin) cfg := testConfig("dns", handler) if allow { cfg.AllowOpcode(dns.OpcodeUpdate) } response := exchangeWithTestServer(t, network, []*Config{cfg}, newRFC2136Update(t, "example.com.")) wantRcode := dns.RcodeNotImplemented if allow { wantRcode = dns.RcodeSuccess } if response.Rcode != wantRcode { t.Fatalf("rcode = %s, want %s", dns.RcodeToString[response.Rcode], dns.RcodeToString[wantRcode]) } if handler.called.Load() != allow { t.Fatalf("plugin called = %v, want %v", handler.called.Load(), allow) } }) } } } func TestUpdateAdmissionIsScopedToConfig(t *testing.T) { dynamicHandler := new(updateResponsePlugin) dynamicConfig := testConfig("dns", dynamicHandler) dynamicConfig.Zone = "dynamic.example." dynamicConfig.AllowOpcode(dns.OpcodeUpdate) staticHandler := new(updateResponsePlugin) staticConfig := testConfig("dns", staticHandler) staticConfig.Zone = "static.example." response := exchangeWithTestServer(t, "udp", []*Config{dynamicConfig, staticConfig}, newRFC2136Update(t, "static.example.")) if response.Rcode != dns.RcodeNotImplemented { t.Fatalf("rcode = %s, want NOTIMP", dns.RcodeToString[response.Rcode]) } if dynamicHandler.called.Load() || staticHandler.called.Load() { t.Fatalf("UPDATE reached a plugin: dynamic=%v static=%v", dynamicHandler.called.Load(), staticHandler.called.Load()) } } func TestUpdateAdmissionHeaderChecks(t *testing.T) { s := &Server{allowedOpcodes: map[int]struct{}{dns.OpcodeUpdate: {}}} header := dns.Header{ Bits: uint16(dns.OpcodeUpdate << 11), Qdcount: 1, Ancount: 3, Nscount: 3, Arcount: 3, } if got := s.acceptMessage(header); got != dns.MsgAccept { t.Fatalf("valid UPDATE action = %v, want MsgAccept", got) } header.Qdcount = 0 if got := s.acceptMessage(header); got != dns.MsgReject { t.Fatalf("zero-question UPDATE action = %v, want MsgReject", got) } header.Qdcount = 2 if got := s.acceptMessage(header); got != dns.MsgReject { t.Fatalf("two-question UPDATE action = %v, want MsgReject", got) } header = dns.Header{Bits: uint16(dns.OpcodeUpdate<<11) | 1<<15, Qdcount: 1} if got := s.acceptMessage(header); got != dns.MsgIgnore { t.Fatalf("UPDATE response action = %v, want MsgIgnore", got) } header = dns.Header{Bits: uint16(dns.OpcodeStatus << 11), Qdcount: 1} if got := s.acceptMessage(header); got != dns.MsgRejectNotImplemented { t.Fatalf("unregistered opcode action = %v, want MsgRejectNotImplemented", got) } queryHeader := dns.Header{Qdcount: 1, Nscount: 2} if got := s.acceptMessage(queryHeader); got != dns.MsgReject { t.Fatalf("invalid QUERY action = %v, want MsgReject", got) } } func TestUpdateAdmissionPreservesTSIGStatus(t *testing.T) { const ( keyName = "update-key.example." secret = "MTIzNDU2Nzg5MDEyMzQ1Ng==" ) called := make(chan struct{}, 1) handler := tsigStatusCheckPlugin{ t: t, called: called, check: func(t *testing.T, status error) { t.Helper() if status != nil { t.Fatalf("TsigStatus() = %v, want nil", status) } }, } cfg := testConfig("dns", handler) cfg.AllowOpcode(dns.OpcodeUpdate) cfg.TsigSecret = map[string]string{keyName: secret} request := newRFC2136Update(t, "example.com.") request.SetTsig(keyName, dns.HmacSHA256, 300, time.Now().Unix()) client := &dns.Client{TsigSecret: map[string]string{keyName: secret}} response := exchangeWithTestServerUsingClient(t, "udp", []*Config{cfg}, request, client) if response.Rcode != dns.RcodeSuccess { t.Fatalf("rcode = %s, want NOERROR", dns.RcodeToString[response.Rcode]) } select { case <-called: default: t.Fatal("TSIG status plugin was not called") } } func TestUpdateAdmissionPreservesTSIGFailure(t *testing.T) { const ( keyName = "update-key.example." serverSecret = "MTIzNDU2Nzg5MDEyMzQ1Ng==" clientSecret = "YWJjZGVmZ2hpamtsbW5vcA==" ) status := make(chan error, 1) handler := tsigStatusCheckPlugin{ t: t, called: make(chan struct{}, 1), check: func(_ *testing.T, got error) { status <- got }, } cfg := testConfig("dns", handler) cfg.AllowOpcode(dns.OpcodeUpdate) cfg.TsigSecret = map[string]string{keyName: serverSecret} server, err := NewServer("127.0.0.1:0", []*Config{cfg}) if err != nil { t.Fatalf("NewServer() failed: %v", err) } packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatalf("net.ListenPacket() failed: %v", err) } go func() { _ = server.ServePacket(packetConn) }() defer func() { _ = server.Stop() _ = packetConn.Close() }() request := newRFC2136Update(t, "example.com.") request.SetTsig(keyName, dns.HmacSHA256, 300, time.Now().Unix()) client := &dns.Client{ Net: "udp", Timeout: 2 * time.Second, TsigSecret: map[string]string{keyName: clientSecret}, } _, _, _ = client.Exchange(request, packetConn.LocalAddr().String()) select { case got := <-status: if got == nil { t.Fatal("TsigStatus() = nil, want verification error") } case <-time.After(2 * time.Second): t.Fatal("UPDATE with invalid TSIG did not reach the status-check plugin") } } func exchangeWithTestServer(t *testing.T, network string, configs []*Config, request *dns.Msg) *dns.Msg { t.Helper() return exchangeWithTestServerUsingClient(t, network, configs, request, new(dns.Client)) } func exchangeWithTestServerUsingClient(t *testing.T, network string, configs []*Config, request *dns.Msg, client *dns.Client) *dns.Msg { t.Helper() s, err := NewServer("127.0.0.1:0", configs) if err != nil { t.Fatalf("NewServer() failed: %v", err) } var addr string switch network { case "udp": pc, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatalf("net.ListenPacket() failed: %v", err) } addr = pc.LocalAddr().String() go func() { _ = s.ServePacket(pc) }() t.Cleanup(func() { _ = pc.Close() }) case "tcp": listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("net.Listen() failed: %v", err) } addr = listener.Addr().String() go func() { _ = s.Serve(listener) }() t.Cleanup(func() { _ = listener.Close() }) default: t.Fatalf("unsupported network %q", network) } t.Cleanup(func() { _ = s.Stop() }) client.Net = network client.Timeout = 2 * time.Second response, _, err := client.Exchange(request, addr) if err != nil { t.Fatalf("dns exchange failed: %v", err) } return response } func TestDebug(t *testing.T) { configNoDebug, configDebug := testConfig("dns", testPlugin{}), testConfig("dns", testPlugin{}) configDebug.Debug = true s1, err := NewServer("127.0.0.1:53", []*Config{configDebug, configNoDebug}) if err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } if !s1.debug { t.Errorf("Expected debug mode enabled for server s1") } if !log.D.Value() { t.Errorf("Expected debug logging enabled") } s2, err := NewServer("127.0.0.1:53", []*Config{configNoDebug}) if err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } if s2.debug { t.Errorf("Expected debug mode disabled for server s2") } if log.D.Value() { t.Errorf("Expected debug logging disabled") } } func TestStacktrace(t *testing.T) { configNoStacktrace, configStacktrace := testConfig("dns", testPlugin{}), testConfig("dns", testPlugin{}) configStacktrace.Stacktrace = true s1, err := NewServer("127.0.0.1:53", []*Config{configStacktrace, configStacktrace}) if err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } if !s1.stacktrace { t.Errorf("Expected stacktrace mode enabled for server s1") } s2, err := NewServer("127.0.0.1:53", []*Config{configNoStacktrace}) if err != nil { t.Errorf("Expected no error for NewServer, got %s", err) } if s2.stacktrace { t.Errorf("Expected stacktrace disabled for server s2") } } func TestGracefulStopTimeout_Internal(t *testing.T) { p := new(blockingPlugin) cfg := testConfig("dns", p) s, err := NewServer("127.0.0.1:0", []*Config{cfg}) if err != nil { t.Fatalf("NewServer failed: %v", err) } // Shorten the graceful timeout s.graceTimeout = 500 * time.Millisecond pc, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatalf("ListenPacket failed: %v", err) } defer pc.Close() go s.ServePacket(pc) udp := pc.LocalAddr().String() // Block the handler p.Lock() defer p.Unlock() m := new(dns.Msg) m.SetQuestion("example.com.", dns.TypeA) _, err = dns.Exchange(m, udp) if err != nil { t.Fatalf("dns.Exchange failed: %v", err) } err = s.Stop() if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected context.DeadlineExceeded, got %v", err) } } // TestMaxTCPQueriesBoundary proves the user-visible behavior of MaxTCPQueries: // a persistent TCP connection may serve exactly the configured number of // queries before the server closes it. func TestMaxTCPQueriesBoundary(t *testing.T) { n := 2 config := testConfig("dns", test.ErrorHandler()) config.MaxTCPQueries = &n s, err := NewServer("127.0.0.1:0", []*Config{config}) if err != nil { t.Fatalf("NewServer failed: %v", err) } defer s.Stop() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("net.Listen failed: %v", err) } defer l.Close() go s.Serve(l) conn, err := net.DialTimeout("tcp", l.Addr().String(), 2*time.Second) if err != nil { t.Fatalf("net.DialTimeout failed: %v", err) } defer conn.Close() dnsConn := &dns.Conn{Conn: conn} for i := range n { m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeA) dnsConn.SetWriteDeadline(time.Now().Add(2 * time.Second)) if err := dnsConn.WriteMsg(m); err != nil { t.Fatalf("query %d: WriteMsg failed: %v", i, err) } dnsConn.SetReadDeadline(time.Now().Add(2 * time.Second)) if _, err := dnsConn.ReadMsg(); err != nil { t.Fatalf("query %d: ReadMsg failed: %v", i, err) } } // The connection should be closed by the server after serving n queries; // query n+1 must not succeed on the same connection. m := new(dns.Msg) m.SetQuestion("example.org.", dns.TypeA) dnsConn.SetWriteDeadline(time.Now().Add(2 * time.Second)) if err := dnsConn.WriteMsg(m); err == nil { dnsConn.SetReadDeadline(time.Now().Add(2 * time.Second)) if _, err := dnsConn.ReadMsg(); err == nil { t.Fatal("expected query beyond MaxTCPQueries to fail on the same connection, but it succeeded") } } } func BenchmarkCoreServeDNS(b *testing.B) { s, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) if err != nil { b.Errorf("Expected no error for NewServer, got %s", err) } ctx := context.TODO() w := &test.ResponseWriter{} m := new(dns.Msg) m.SetQuestion("aaa.example.com.", dns.TypeTXT) b.ReportAllocs() for b.Loop() { s.ServeDNS(ctx, w, m) } } // recordingWriter counts the packed frames the decorated writer receives // before forwarding them to the real writer. The decorator mints a fresh // wrapper per packet; the frames counter is shared across them. type recordingWriter struct { dns.Writer frames *atomic.Int64 } func (rw *recordingWriter) Write(b []byte) (int, error) { rw.frames.Add(1) return rw.Writer.Write(b) } func TestUDPDecorateWriterFunc(t *testing.T) { cfg := testConfig("dns", test.ErrorHandler()) frames := new(atomic.Int64) var gotServer atomic.Pointer[Server] var calls atomic.Int64 cfg.UDPDecorateWriterFunc = func(srv *Server) dns.DecorateWriter { calls.Add(1) gotServer.Store(srv) return func(w dns.Writer) dns.Writer { return &recordingWriter{Writer: w, frames: frames} } } s, err := NewServer("127.0.0.1:0", []*Config{cfg}) if err != nil { t.Fatalf("NewServer failed: %v", err) } pc, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { t.Fatalf("ListenPacket failed: %v", err) } defer pc.Close() go s.ServePacket(pc) defer s.Stop() m := new(dns.Msg) m.SetQuestion("example.com.", dns.TypeA) if _, err := dns.Exchange(m, pc.LocalAddr().String()); err != nil { t.Fatalf("dns.Exchange failed: %v", err) } if n := calls.Load(); n != 1 { t.Errorf("expected UDPDecorateWriterFunc to be called once per socket, got %d", n) } if gotServer.Load() != s { t.Errorf("expected UDPDecorateWriterFunc to receive the serving *Server") } if frames.Load() == 0 { t.Errorf("expected the decorated writer to observe the response write") } }