summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/resolver/named.root92
-rw-r--r--internal/resolver/resolver.go192
-rw-r--r--internal/resolver/resolver_test.go149
-rw-r--r--internal/resolver/root.go55
-rw-r--r--internal/server/doh.go2
-rw-r--r--internal/server/handler.go37
-rw-r--r--internal/server/server.go17
-rw-r--r--internal/server/server_test.go83
-rw-r--r--main.go5
9 files changed, 557 insertions, 75 deletions
diff --git a/internal/resolver/named.root b/internal/resolver/named.root
new file mode 100644
index 0000000..0a04ecf
--- /dev/null
+++ b/internal/resolver/named.root
@@ -0,0 +1,92 @@
+; This file holds the information on root name servers needed to
+; initialize cache of Internet domain name servers
+; (e.g. reference this file in the "cache . <file>"
+; configuration file of BIND domain name servers).
+;
+; This file is made available by InterNIC
+; under anonymous FTP as
+; file /domain/named.cache
+; on server FTP.INTERNIC.NET
+; -OR- RS.INTERNIC.NET
+;
+; last update: May 21, 2026
+; related version of root zone: 2026052101
+;
+; FORMERLY NS.INTERNIC.NET
+;
+. 3600000 NS A.ROOT-SERVERS.NET.
+A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
+A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
+;
+; FORMERLY NS1.ISI.EDU
+;
+. 3600000 NS B.ROOT-SERVERS.NET.
+B.ROOT-SERVERS.NET. 3600000 A 170.247.170.2
+B.ROOT-SERVERS.NET. 3600000 AAAA 2801:1b8:10::b
+;
+; FORMERLY C.PSI.NET
+;
+. 3600000 NS C.ROOT-SERVERS.NET.
+C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
+C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
+;
+; FORMERLY TERP.UMD.EDU
+;
+. 3600000 NS D.ROOT-SERVERS.NET.
+D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13
+D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d
+;
+; FORMERLY NS.NASA.GOV
+;
+. 3600000 NS E.ROOT-SERVERS.NET.
+E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10
+E.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:a8::e
+;
+; FORMERLY NS.ISC.ORG
+;
+. 3600000 NS F.ROOT-SERVERS.NET.
+F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241
+F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f
+;
+; FORMERLY NS.NIC.DDN.MIL
+;
+. 3600000 NS G.ROOT-SERVERS.NET.
+G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4
+G.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:12::d0d
+;
+; FORMERLY AOS.ARL.ARMY.MIL
+;
+. 3600000 NS H.ROOT-SERVERS.NET.
+H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53
+H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53
+;
+; FORMERLY NIC.NORDU.NET
+;
+. 3600000 NS I.ROOT-SERVERS.NET.
+I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17
+I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53
+;
+; OPERATED BY VERISIGN, INC.
+;
+. 3600000 NS J.ROOT-SERVERS.NET.
+J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30
+J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30
+;
+; OPERATED BY RIPE NCC
+;
+. 3600000 NS K.ROOT-SERVERS.NET.
+K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129
+K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1
+;
+; OPERATED BY ICANN
+;
+. 3600000 NS L.ROOT-SERVERS.NET.
+L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42
+L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42
+;
+; OPERATED BY WIDE
+;
+. 3600000 NS M.ROOT-SERVERS.NET.
+M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33
+M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35
+; End of file \ No newline at end of file
diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go
new file mode 100644
index 0000000..4ad023a
--- /dev/null
+++ b/internal/resolver/resolver.go
@@ -0,0 +1,192 @@
+package resolver
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/miekg/dns"
+)
+
+var (
+ ErrMaxDelegations = errors.New("max delegations exceeded")
+ ErrNoServers = errors.New("no nameservers available")
+)
+
+type Resolver struct {
+ roots []string
+ maxDelegations int
+ timeout time.Duration
+ retries int
+}
+
+type Option func(*Resolver)
+
+func New(opts ...Option) *Resolver {
+ r := &Resolver{
+ roots: loadRootServers(),
+ maxDelegations: 30,
+ timeout: 2 * time.Second,
+ retries: 2,
+ }
+ for _, opt := range opts {
+ opt(r)
+ }
+ return r
+}
+
+func WithRootAddresses(addrs []string) Option {
+ return func(r *Resolver) {
+ r.roots = addrs
+ }
+}
+
+func WithTimeout(d time.Duration) Option {
+ return func(r *Resolver) {
+ r.timeout = d
+ }
+}
+
+func (r *Resolver) Lookup(ctx context.Context, qname string, qtype uint16) (*dns.Msg, error) {
+ if ctx == nil {
+ ctx = context.Background()
+ }
+ return r.resolve(ctx, qname, qtype)
+}
+
+func (r *Resolver) resolve(ctx context.Context, qname string, qtype uint16) (*dns.Msg, error) {
+ servers := make([]string, len(r.roots))
+ copy(servers, r.roots)
+ for depth := 0; depth < r.maxDelegations; depth++ {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+ if len(servers) == 0 {
+ return nil, ErrNoServers
+ }
+ reply, err := r.exchangeWithRetries(ctx, servers, qname, qtype)
+ if err != nil {
+ return nil, fmt.Errorf("resolve %s %s: %w",
+ qname, dns.TypeToString[qtype], err)
+ }
+ switch {
+ case reply.Rcode == dns.RcodeSuccess && len(reply.Answer) > 0:
+ return reply, nil
+ case reply.Rcode == dns.RcodeNameError:
+ return reply, nil
+ case reply.Rcode == dns.RcodeSuccess && isReferral(reply):
+ next, err := r.nextServers(ctx, reply)
+ if err != nil {
+ return nil, err
+ }
+ servers = next
+ default:
+ if len(servers) > 1 {
+ servers = servers[1:]
+ } else {
+ return reply, nil
+ }
+ }
+
+ }
+ return nil, ErrMaxDelegations
+}
+
+func isReferral(msg *dns.Msg) bool {
+ return !msg.Authoritative && len(msg.Ns) > 0
+}
+
+func (r *Resolver) nextServers(ctx context.Context, msg *dns.Msg) ([]string, error) {
+ var targets []string
+ glue := make(map[string]string)
+
+ for _, rr := range msg.Ns {
+ ns, ok := rr.(*dns.NS)
+ if !ok {
+ continue
+ }
+ targets = append(targets, ns.Ns)
+ }
+ for _, rr := range msg.Extra {
+ switch v := rr.(type) {
+ case *dns.A:
+ if _, exists := glue[v.Hdr.Name]; !exists {
+ glue[v.Hdr.Name] = v.A.String()
+ }
+ case *dns.AAAA:
+ if _, exists := glue[v.Hdr.Name]; !exists {
+ glue[v.Hdr.Name] = v.AAAA.String()
+ }
+ }
+ }
+
+ var addrs []string
+ var unresolved []string
+ for _, t := range targets {
+ if ip, ok := glue[t]; ok {
+ addrs = append(addrs, ip)
+ } else {
+ unresolved = append(unresolved, t)
+ }
+ }
+
+ if len(addrs) > 0 {
+ return addrs, nil
+ }
+
+ for _, name := range unresolved {
+ reply, err := r.resolve(ctx, name, dns.TypeA)
+ if err != nil {
+ continue
+ }
+ if reply.Rcode != dns.RcodeSuccess {
+ continue
+ }
+ for _, rr := range reply.Answer {
+ if a, ok := rr.(*dns.A); ok {
+ addrs = append(addrs, a.A.String())
+ break
+ }
+ }
+ }
+
+ if len(addrs) == 0 {
+ return nil, ErrNoServers
+ }
+ return addrs, nil
+}
+
+func (r *Resolver) exchangeWithRetries(ctx context.Context, servers []string,
+ qname string, qtype uint16) (*dns.Msg, error) {
+ msg := new(dns.Msg)
+ msg.SetQuestion(qname, qtype)
+ msg.SetEdns0(4096, false)
+ msg.RecursionDesired = false
+
+ client := &dns.Client{
+ Net: "udp",
+ UDPSize: 4096,
+ Timeout: r.timeout,
+ }
+
+ var lastErr error
+ for _, srv := range servers {
+ addr := srv
+ if _, _, err := net.SplitHostPort(srv); err != nil {
+ addr = net.JoinHostPort(srv, "53")
+ }
+ for attempt := 0; attempt < r.retries; attempt++ {
+ reply, _, err := client.ExchangeContext(ctx, msg, addr)
+ if err == nil {
+ return reply, nil
+ }
+ lastErr = err
+ time.Sleep(time.Duration(attempt+1) * 200 * time.Millisecond)
+ }
+ }
+ return nil, lastErr
+}
diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go
new file mode 100644
index 0000000..0bd0402
--- /dev/null
+++ b/internal/resolver/resolver_test.go
@@ -0,0 +1,149 @@
+package resolver
+
+import (
+ "context"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/miekg/dns"
+)
+
+func startTestServer(t *testing.T, addr string, handler dns.Handler) *dns.Server {
+ t.Helper()
+ srv := &dns.Server{
+ Addr: addr,
+ Net: "udp",
+ Handler: handler,
+ UDPSize: 4096,
+ }
+ go func() {
+ if err := srv.ListenAndServe(); err != nil {
+ }
+ }()
+ return srv
+}
+
+func TestLookupDirectAnswer(t *testing.T) {
+ mux := dns.NewServeMux()
+ mux.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := new(dns.Msg)
+ resp.SetReply(req)
+ resp.Authoritative = true
+ if req.Question[0].Name == "example.com." &&
+ req.Question[0].Qtype == dns.TypeA {
+ resp.Answer = append(resp.Answer, &dns.A{
+ Hdr: dns.RR_Header{
+ Name: "example.com.",
+ Rrtype: dns.TypeA,
+ Class: dns.ClassINET,
+ Ttl: 60,
+ },
+ A: net.IPv4(127, 0, 0, 1),
+ })
+ } else {
+ resp.Rcode = dns.RcodeNameError
+ }
+ w.WriteMsg(resp)
+ })
+
+ srv := startTestServer(t, "127.0.0.1:15353", mux)
+ defer srv.Shutdown()
+ time.Sleep(50 * time.Millisecond)
+
+ r := New(
+ WithRootAddresses([]string{"127.0.0.1:15353"}),
+ WithTimeout(500*time.Millisecond),
+ )
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ resp, err := r.Lookup(ctx, "example.com.", dns.TypeA)
+ if err != nil {
+ t.Fatalf("Lookup failed: %v", err)
+ }
+ if resp.Rcode != dns.RcodeSuccess {
+ t.Fatalf("expected NOERROR, got %d (%s)",
+ resp.Rcode, dns.RcodeToString[resp.Rcode])
+ }
+ if len(resp.Answer) != 1 {
+ t.Fatalf("expected 1 answer, got %d", len(resp.Answer))
+ }
+ a, ok := resp.Answer[0].(*dns.A)
+ if !ok {
+ t.Fatal("expected A record")
+ }
+ if !a.A.Equal(net.IPv4(127, 0, 0, 1)) {
+ t.Fatalf("expected 127.0.0.1, got %s", a.A)
+ }
+}
+
+func TestLookupNXDOMAIN(t *testing.T) {
+ mux := dns.NewServeMux()
+ mux.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) {
+ resp := new(dns.Msg)
+ resp.SetReply(req)
+ resp.Rcode = dns.RcodeNameError
+ w.WriteMsg(resp)
+ })
+
+ srv := startTestServer(t, "127.0.0.1:15354", mux)
+ defer srv.Shutdown()
+ time.Sleep(50 * time.Millisecond)
+
+ r := New(
+ WithRootAddresses([]string{"127.0.0.1:15354"}),
+ WithTimeout(500*time.Millisecond),
+ )
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+
+ resp, err := r.Lookup(ctx, "nonexistent.xyz.", dns.TypeA)
+ if err != nil {
+ t.Fatalf("Lookup failed: %v", err)
+ }
+ if resp.Rcode != dns.RcodeNameError {
+ t.Fatalf("expected NXDOMAIN, got %d", resp.Rcode)
+ }
+}
+
+func TestNextServersWithGlue(t *testing.T) {
+ msg := new(dns.Msg)
+ msg.Authoritative = false
+ msg.Ns = append(msg.Ns, &dns.NS{
+ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Ttl: 300},
+ Ns: "ns1.example.com.",
+ })
+ msg.Extra = append(msg.Extra, &dns.A{
+ Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeA, Ttl: 300},
+ A: net.ParseIP("192.0.2.1").To4(),
+ })
+ msg.Extra = append(msg.Extra, &dns.AAAA{
+ Hdr: dns.RR_Header{Name: "ns1.example.com.", Rrtype: dns.TypeAAAA, Ttl: 300},
+ AAAA: net.ParseIP("2001:db8::1"),
+ })
+
+ r := &Resolver{}
+ addrs, err := r.nextServers(context.Background(), msg)
+ if err != nil {
+ t.Fatalf("nextServers failed: %v", err)
+ }
+ if len(addrs) != 1 || addrs[0] != "192.0.2.1" {
+ t.Fatalf("expected [192.0.2.1], got %v", addrs)
+ }
+}
+
+func TestNextServersNoGlue(t *testing.T) {
+ msg := new(dns.Msg)
+ msg.Authoritative = false
+ msg.Ns = append(msg.Ns, &dns.NS{
+ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeNS, Ttl: 300},
+ Ns: "ns1.example.com.",
+ })
+
+ r := &Resolver{maxDelegations: 30, timeout: time.Second, retries: 1}
+ _, err := r.nextServers(context.Background(), msg)
+ if err == nil {
+ t.Fatal("expected error when no glue and no roots")
+ }
+}
diff --git a/internal/resolver/root.go b/internal/resolver/root.go
new file mode 100644
index 0000000..9dac31c
--- /dev/null
+++ b/internal/resolver/root.go
@@ -0,0 +1,55 @@
+package resolver
+
+import (
+ _ "embed"
+ "github.com/miekg/dns"
+ "strings"
+)
+
+//go:embed named.root
+var rootHintsData []byte
+
+func loadRootServers() []string {
+ var addrs []string
+ seen := make(map[string]bool)
+
+ zp := dns.NewZoneParser(strings.NewReader(string(rootHintsData)), "", "")
+ for {
+ rr, ok := zp.Next()
+ if !ok {
+ break
+ }
+ a, ok := rr.(*dns.A)
+ if !ok {
+ continue
+ }
+ ip := a.A.String()
+ if !seen[ip] {
+ seen[ip] = true
+ addrs = append(addrs, ip)
+ }
+ }
+
+ if err := zp.Err(); err != nil || len(addrs) == 0 {
+ return hardcodedRoots()
+ }
+ return addrs
+}
+
+func hardcodedRoots() []string {
+ return []string{
+ "198.41.0.4",
+ "170.247.170.2",
+ "192.33.4.12",
+ "199.7.91.13",
+ "192.203.230.10",
+ "192.5.5.241",
+ "192.112.36.4",
+ "198.97.190.53",
+ "192.36.148.17",
+ "192.58.128.30",
+ "193.0.14.129",
+ "199.7.83.42",
+ "202.12.27.33",
+ }
+}
diff --git a/internal/server/doh.go b/internal/server/doh.go
index e9cf466..3f5a538 100644
--- a/internal/server/doh.go
+++ b/internal/server/doh.go
@@ -52,7 +52,7 @@ func (s *Server) dohHandler(w http.ResponseWriter, r *http.Request) {
return
}
- resp := buildResponse(msg)
+ resp := s.buildResponse(msg)
packed, err := resp.Pack()
if err != nil {
http.Error(w, "pack response", http.StatusInternalServerError)
diff --git a/internal/server/handler.go b/internal/server/handler.go
index 2e2f08b..bac1c81 100644
--- a/internal/server/handler.go
+++ b/internal/server/handler.go
@@ -1,11 +1,13 @@
package server
import (
+ "context"
"github.com/miekg/dns"
"log/slog"
+ "time"
)
-func handleQuery(w dns.ResponseWriter, req *dns.Msg) {
+func (s *Server) handleQuery(w dns.ResponseWriter, req *dns.Msg) {
if len(req.Question) == 0 {
m := new(dns.Msg)
m.SetRcode(req, dns.RcodeFormatError)
@@ -13,7 +15,7 @@ func handleQuery(w dns.ResponseWriter, req *dns.Msg) {
return
}
- resp := buildResponse(req)
+ resp := s.buildResponse(req)
if err := w.WriteMsg(resp); err != nil {
slog.Error("write response failed",
@@ -31,7 +33,7 @@ func handleQuery(w dns.ResponseWriter, req *dns.Msg) {
)
}
-func buildResponse(req *dns.Msg) *dns.Msg {
+func (s *Server) buildResponse(req *dns.Msg) *dns.Msg {
if len(req.Question) == 0 {
m := new(dns.Msg)
m.SetRcode(req, dns.RcodeFormatError)
@@ -46,20 +48,19 @@ func buildResponse(req *dns.Msg) *dns.Msg {
resp.SetEdns0(4096, false)
}
- if q.Name == "example.com." && q.Qtype == dns.TypeA {
- resp.Answer = []dns.RR{
- &dns.A{
- Hdr: dns.RR_Header{
- Name: q.Name,
- Rrtype: dns.TypeA,
- Class: dns.ClassINET,
- Ttl: 60,
- },
- A: []byte{127, 0, 0, 1},
- },
- }
- } else {
- resp.Rcode = dns.RcodeNameError
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ reply, err := s.resolver.Lookup(ctx, q.Name, q.Qtype)
+ if err != nil {
+ slog.Error("resolution failed",
+ "err", err,
+ "qname", q.Name,
+ "qtype", dns.TypeToString[q.Qtype],
+ )
+ resp.Rcode = dns.RcodeServerFailure
+ return resp
}
- return resp
+
+ return reply
}
diff --git a/internal/server/server.go b/internal/server/server.go
index f40648e..3114073 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -7,20 +7,21 @@ import (
"time"
"github.com/miekg/dns"
+ "sdns/internal/resolver"
)
type Server struct {
- logger *slog.Logger
- udp *dns.Server
- tcp *dns.Server
- doh *http.Server
+ logger *slog.Logger
+ resolver *resolver.Resolver
+ udp *dns.Server
+ tcp *dns.Server
+ doh *http.Server
}
-func New(udpAddr, tcpAddr, dohAddr string, logger *slog.Logger) (*Server, error) {
+func New(udpAddr, tcpAddr, dohAddr string, logger *slog.Logger, r *resolver.Resolver) (*Server, error) {
+ s := &Server{logger: logger, resolver: r}
mux := dns.NewServeMux()
- mux.HandleFunc(".", handleQuery)
-
- s := &Server{logger: logger}
+ mux.HandleFunc(".", s.handleQuery)
if udpAddr != "" {
s.udp = &dns.Server{
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index eaf0190..6fc2092 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -1,12 +1,25 @@
package server
import (
+ "log/slog"
"testing"
+ "time"
"github.com/miekg/dns"
+ "sdns/internal/resolver"
)
+func testServer(t *testing.T) *Server {
+ t.Helper()
+ r := resolver.New(
+ resolver.WithRootAddresses([]string{"127.0.0.1:1"}),
+ resolver.WithTimeout(50*time.Millisecond),
+ )
+ return &Server{logger: slog.Default(), resolver: r}
+}
+
func TestBuildResponse(t *testing.T) {
+ s := testServer(t)
tests := []struct {
name string
req *dns.Msg
@@ -15,39 +28,6 @@ func TestBuildResponse(t *testing.T) {
wantEdns0 bool
}{
{
- name: "example.com A returns 127.0.0.1",
- req: func() *dns.Msg {
- m := new(dns.Msg)
- m.SetQuestion("example.com.", dns.TypeA)
- return m
- }(),
- wantRcode: dns.RcodeSuccess,
- wantAnswers: 1,
- wantEdns0: false,
- },
- {
- name: "google.com A returns NXDOMAIN",
- req: func() *dns.Msg {
- m := new(dns.Msg)
- m.SetQuestion("google.com.", dns.TypeA)
- return m
- }(),
- wantRcode: dns.RcodeNameError,
- wantAnswers: 0,
- wantEdns0: false,
- },
- {
- name: "other.com A returns NXDOMAIN",
- req: func() *dns.Msg {
- m := new(dns.Msg)
- m.SetQuestion("other.com.", dns.TypeA)
- return m
- }(),
- wantRcode: dns.RcodeNameError,
- wantAnswers: 0,
- wantEdns0: false,
- },
- {
name: "no questions returns FORMERR",
req: func() *dns.Msg {
return new(dns.Msg)
@@ -56,23 +36,11 @@ func TestBuildResponse(t *testing.T) {
wantAnswers: 0,
wantEdns0: false,
},
- {
- name: "EDNS0 query preserved with 4096 buffer",
- req: func() *dns.Msg {
- m := new(dns.Msg)
- m.SetQuestion("example.com.", dns.TypeA)
- m.SetEdns0(1232, true)
- return m
- }(),
- wantRcode: dns.RcodeSuccess,
- wantAnswers: 1,
- wantEdns0: true,
- },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- resp := buildResponse(tt.req)
+ resp := s.buildResponse(tt.req)
if resp.Rcode != tt.wantRcode {
t.Errorf("rcode: got %d, want %d", resp.Rcode, tt.wantRcode)
}
@@ -90,7 +58,28 @@ func TestBuildResponse(t *testing.T) {
}
}
+func TestBuildResponseWithQuery(t *testing.T) {
+ s := testServer(t)
+ // Valid query → must not panic, rcode must be valid
+ m := new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeA)
+ resp := s.buildResponse(m)
+ if resp == nil {
+ t.Fatal("buildResponse returned nil")
+ }
+ if resp.Rcode != dns.RcodeSuccess && resp.Rcode != dns.RcodeServerFailure {
+ t.Errorf("expected success or server failure, got %d", resp.Rcode)
+ }
+}
+
func FuzzBuildResponse(f *testing.F) {
+ s := &Server{logger: slog.Default()}
+ // For fuzz, use a resolver that won't make real network calls
+ s.resolver = resolver.New(
+ resolver.WithRootAddresses([]string{"127.0.0.1:1"}),
+ resolver.WithTimeout(10*time.Millisecond),
+ )
+
seed := []byte{
0x00, 0x00, // ID
0x01, 0x00, // flags: RD
@@ -111,7 +100,7 @@ func FuzzBuildResponse(f *testing.F) {
if err := msg.Unpack(data); err != nil {
return
}
- resp := buildResponse(msg)
+ resp := s.buildResponse(msg)
if resp == nil {
t.Fatal("buildResponse returned nil")
}
diff --git a/main.go b/main.go
index de6cfb4..2ddc86f 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ import (
"os/signal"
"syscall"
+ "sdns/internal/resolver"
"sdns/internal/server"
)
@@ -16,6 +17,8 @@ func main() {
}))
slog.SetDefault(logger)
+ r := resolver.New()
+
udp := os.Getenv("SDNS_LISTEN_UDP")
if udp == "" {
udp = ":5353"
@@ -34,7 +37,7 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
- srv, err := server.New(udp, tcp, doh, logger)
+ srv, err := server.New(udp, tcp, doh, logger, r)
if err != nil {
logger.Error("create server failed", "err", err)
os.Exit(1)