Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions internal/dns/server/axfr.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
// handleAXFR processes a DNS zone transfer (AXFR) request over TCP.
func (s *Server) handleAXFR(ctx context.Context, conn net.Conn, request *packet.DNSPacket, rawData []byte, buffer *packet.BytePacketBuffer) {
defer packet.PutBuffer(buffer)

// Rate limit check — AXFR/IXFR bypass handlePacket where limiter lives
clientIP := extractClientIP(conn.RemoteAddr())
if !s.limiter.Allow(clientIP) {
s.sendTCPError(conn, request.Header.ID, 2) // SERVFAIL
return
}
q := request.Questions[0]
if !strings.HasSuffix(q.Name, ".") {
q.Name += "."
Expand Down Expand Up @@ -182,6 +189,13 @@ func (s *Server) sendTCPError(conn net.Conn, id uint16, rcode uint8) {
// handleIXFR processes an incremental zone transfer (IXFR) request over TCP.
func (s *Server) handleIXFR(ctx context.Context, conn net.Conn, request *packet.DNSPacket, rawData []byte, buffer *packet.BytePacketBuffer) {
defer packet.PutBuffer(buffer)

// Rate limit check — AXFR/IXFR bypass handlePacket where limiter lives
clientIP := extractClientIP(conn.RemoteAddr())
if !s.limiter.Allow(clientIP) {
s.sendTCPError(conn, request.Header.ID, 2) // SERVFAIL
return
}
q := request.Questions[0]
if !strings.HasSuffix(q.Name, ".") {
q.Name += "."
Expand Down Expand Up @@ -323,6 +337,22 @@ func (s *Server) handleIXFR(ctx context.Context, conn net.Conn, request *packet.
}
}

// If original IXFR had TSIG, require TSIG on fallback AXFR too
if request.TSIGStart != -1 {
tsig := request.Resources[len(request.Resources)-1]
key, ok := s.TsigKeys[tsig.Name]
if !ok {
s.Logger.Debug("AXFR fallback failed: unknown TSIG key", "key", tsig.Name, "zone", q.Name)
s.sendTCPError(conn, request.Header.ID, 5) // NotAuth
return
}
if errVerify := request.VerifyTSIG(rawData, request.TSIGStart, key.Secret); errVerify != nil {
s.Logger.Warn("AXFR fallback failed: TSIG verification failed", "error", errVerify, "zone", q.Name)
s.sendTCPError(conn, request.Header.ID, 5) // NotAuth
return
}
}

if err != nil {
s.Logger.Warn("IXFR chain query failed, falling back to AXFR", "zone", zone.Name, "error", err)
} else if !historyValid {
Expand Down
80 changes: 78 additions & 2 deletions internal/dns/server/edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,24 @@ func TestHandleIXFR_NoAuthority(t *testing.T) {

type mockConn struct {
net.Conn
localAddr net.Addr
remoteAddr net.Addr
}

func (m *mockConn) Write(b []byte) (int, error) { return len(b), nil }
func (m *mockConn) Close() error { return nil }
func (m *mockConn) Write(b []byte) (int, error) { return len(b), nil }
func (m *mockConn) Close() error { return nil }
func (m *mockConn) RemoteAddr() net.Addr {
if m.remoteAddr != nil {
return m.remoteAddr
}
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
}
func (m *mockConn) LocalAddr() net.Addr {
if m.localAddr != nil {
return m.localAddr
}
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 54321}
}

// TestHandlePacket_NoQuestions verifies that a DNS packet with no questions
// returns a FORMERR response as per RFC standards.
Expand All @@ -100,3 +114,65 @@ func TestHandlePacket_NoQuestions(t *testing.T) {
t.Fatalf("handlePacket failed: %v", err)
}
}

// TestHandleAXFR_RateLimited verifies that AXFR returns SERVFAIL when rate limited.
func TestHandleAXFR_RateLimited(t *testing.T) {
repo := &mockServerRepo{
zones: []domain.Zone{{ID: "z1", Name: "test."}},
records: []domain.Record{
{ZoneID: "z1", Name: "test.", Type: domain.TypeSOA, Content: "ns1. ns2. 1 2 3 4 5"},
},
}
srv := NewServer(":0", repo, nil)
// Replace limiter with one that denies all
srv.limiter = newRateLimiter(0, 0, 1000)

req := packet.NewDNSPacket()
req.Header.ID = 1234
req.Questions = append(req.Questions, packet.DNSQuestion{Name: "test.", QType: packet.AXFR})

conn := &mockTCPConn{}
srv.handleAXFR(context.Background(), conn, req, nil, nil)

if len(conn.captured) != 1 {
t.Fatalf("Expected 1 response, got %d", len(conn.captured))
}
res := packet.NewDNSPacket()
pb := packet.NewBytePacketBuffer()
pb.Load(conn.captured[0])
_ = res.FromBuffer(pb)
if res.Header.ResCode != 2 { // SERVFAIL
t.Errorf("Expected SERVFAIL (2), got %d", res.Header.ResCode)
}
}

// TestHandleIXFR_RateLimited verifies that IXFR returns SERVFAIL when rate limited.
func TestHandleIXFR_RateLimited(t *testing.T) {
repo := &mockServerRepo{
zones: []domain.Zone{{ID: "z1", Name: "test."}},
records: []domain.Record{
{ZoneID: "z1", Name: "test.", Type: domain.TypeSOA, Content: "ns1. ns2. 1 2 3 4 5"},
},
}
srv := NewServer(":0", repo, nil)
// Replace limiter with one that denies all
srv.limiter = newRateLimiter(0, 0, 1000)

req := packet.NewDNSPacket()
req.Header.ID = 1234
req.Questions = append(req.Questions, packet.DNSQuestion{Name: "test.", QType: packet.IXFR})

conn := &mockTCPConn{}
srv.handleIXFR(context.Background(), conn, req, nil, nil)

if len(conn.captured) != 1 {
t.Fatalf("Expected 1 response, got %d", len(conn.captured))
}
res := packet.NewDNSPacket()
pb := packet.NewBytePacketBuffer()
pb.Load(conn.captured[0])
_ = res.FromBuffer(pb)
if res.Header.ResCode != 2 { // SERVFAIL
t.Errorf("Expected SERVFAIL (2), got %d", res.Header.ResCode)
}
}
Loading