mirror of
				https://github.com/coredns/coredns.git
				synced 2025-11-03 18:53:13 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			217 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			217 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
//go:build gofuzz
 | 
						||
 | 
						||
package grpc
 | 
						||
 | 
						||
import (
 | 
						||
	"context"
 | 
						||
 | 
						||
	"github.com/coredns/coredns/pb"
 | 
						||
	"github.com/coredns/coredns/plugin/pkg/fuzz"
 | 
						||
	"github.com/coredns/coredns/plugin/test"
 | 
						||
 | 
						||
	"github.com/miekg/dns"
 | 
						||
	grpcgo "google.golang.org/grpc"
 | 
						||
	"google.golang.org/grpc/codes"
 | 
						||
	"google.golang.org/grpc/status"
 | 
						||
)
 | 
						||
 | 
						||
// fakeClient implements pb.DnsServiceClient without doing any network I/O.
 | 
						||
// Its behavior is controlled by the mode field.
 | 
						||
type fakeClient struct {
 | 
						||
	mode byte
 | 
						||
	idx  int
 | 
						||
}
 | 
						||
 | 
						||
func (f *fakeClient) Query(_ context.Context, in *pb.DnsPacket, _ ...grpcgo.CallOption) (*pb.DnsPacket, error) {
 | 
						||
	// Derive mode deterministically from request bytes to vary behavior per call.
 | 
						||
	m := f.mode
 | 
						||
	if len(in.GetMsg()) > 0 {
 | 
						||
		b := in.GetMsg()[f.idx%len(in.GetMsg())]
 | 
						||
		f.idx++
 | 
						||
		m = b
 | 
						||
	}
 | 
						||
 | 
						||
	switch m % 12 {
 | 
						||
	case 0:
 | 
						||
		// Success echo: return the same bytes.
 | 
						||
		return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
	case 1:
 | 
						||
		// Return NotFound to exercise NXDOMAIN conversion and optional fallthrough.
 | 
						||
		return nil, status.Error(codes.NotFound, "not found")
 | 
						||
	case 2:
 | 
						||
		// Return a transient error to trigger retry/rotation.
 | 
						||
		return nil, status.Error(codes.Unavailable, "unavailable")
 | 
						||
	case 3:
 | 
						||
		// Corrupt response that fails dns.Msg Unpack.
 | 
						||
		return &pb.DnsPacket{Msg: []byte{0x00, 0x01, 0x02}}, nil
 | 
						||
	case 4:
 | 
						||
		// Valid DNS message with mismatched ID/qname to trigger formerr path in ServeDNS.
 | 
						||
		var req dns.Msg
 | 
						||
		if err := req.Unpack(in.GetMsg()); err != nil {
 | 
						||
			// If input isn't a DNS message, just echo to avoid blocking fuzzing.
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		resp := new(dns.Msg)
 | 
						||
		resp.SetReply(&req)
 | 
						||
		resp.Id = req.Id + 1
 | 
						||
		// Alter question name if present.
 | 
						||
		if len(req.Question) > 0 {
 | 
						||
			resp.Question[0].Name = "example.net."
 | 
						||
		}
 | 
						||
		packed, err := resp.Pack()
 | 
						||
		if err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		return &pb.DnsPacket{Msg: packed}, nil
 | 
						||
	case 5:
 | 
						||
		// Success with EDNS and larger answer to stress flags and sizes.
 | 
						||
		var req dns.Msg
 | 
						||
		if err := req.Unpack(in.GetMsg()); err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		resp := new(dns.Msg)
 | 
						||
		resp.SetReply(&req)
 | 
						||
		// Set EDNS0 with varying UDP size and DO bit based on m.
 | 
						||
		size := uint16(512)
 | 
						||
		if (m>>1)&1 == 1 {
 | 
						||
			size = 1232
 | 
						||
		}
 | 
						||
		if (m>>2)&1 == 1 {
 | 
						||
			size = 4096
 | 
						||
		}
 | 
						||
		do := ((m>>3)&1 == 1)
 | 
						||
		resp.SetEdns0(size, do)
 | 
						||
		// Optionally set TC bit to exercise truncation handling.
 | 
						||
		if (m>>4)&1 == 1 {
 | 
						||
			resp.Truncated = true
 | 
						||
		}
 | 
						||
		// Add a few TXT records to grow the payload.
 | 
						||
		name := "."
 | 
						||
		if len(req.Question) > 0 {
 | 
						||
			name = req.Question[0].Name
 | 
						||
		}
 | 
						||
		n := int(1 + (m % 16))
 | 
						||
		for range n {
 | 
						||
			resp.Answer = append(resp.Answer, &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, Txt: []string{"aaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbb"}})
 | 
						||
		}
 | 
						||
		packed, err := resp.Pack()
 | 
						||
		if err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		return &pb.DnsPacket{Msg: packed}, nil
 | 
						||
	case 6:
 | 
						||
		return nil, status.Error(codes.DeadlineExceeded, "timeout")
 | 
						||
	case 7:
 | 
						||
		return nil, status.Error(codes.Internal, "internal")
 | 
						||
	case 8:
 | 
						||
		return nil, status.Error(codes.ResourceExhausted, "quota")
 | 
						||
	case 9:
 | 
						||
		return nil, status.Error(codes.PermissionDenied, "denied")
 | 
						||
	case 10:
 | 
						||
		// NODATA: NOERROR with empty Answer and SOA in Authority.
 | 
						||
		var req dns.Msg
 | 
						||
		if err := req.Unpack(in.GetMsg()); err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		resp := new(dns.Msg)
 | 
						||
		resp.SetRcode(&req, dns.RcodeSuccess)
 | 
						||
		name := "."
 | 
						||
		if len(req.Question) > 0 {
 | 
						||
			name = req.Question[0].Name
 | 
						||
		}
 | 
						||
		resp.Ns = append(resp.Ns, &dns.SOA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 60}, Ns: "ns.example.", Mbox: "hostmaster.example.", Serial: 1, Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 60})
 | 
						||
		packed, err := resp.Pack()
 | 
						||
		if err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		return &pb.DnsPacket{Msg: packed}, nil
 | 
						||
	case 11:
 | 
						||
		// TC-only: truncated response without answers.
 | 
						||
		var req dns.Msg
 | 
						||
		if err := req.Unpack(in.GetMsg()); err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		resp := new(dns.Msg)
 | 
						||
		resp.SetReply(&req)
 | 
						||
		resp.Truncated = true
 | 
						||
		packed, err := resp.Pack()
 | 
						||
		if err != nil {
 | 
						||
			return &pb.DnsPacket{Msg: in.GetMsg()}, nil
 | 
						||
		}
 | 
						||
		return &pb.DnsPacket{Msg: packed}, nil
 | 
						||
	default:
 | 
						||
		// Empty/zero-length response to exercise unpack error path.
 | 
						||
		return &pb.DnsPacket{Msg: nil}, nil
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
// Fuzz exercises the grpc plugin using a fake client and the shared fuzz harness.
 | 
						||
func Fuzz(data []byte) int {
 | 
						||
	if len(data) == 0 {
 | 
						||
		return 0
 | 
						||
	}
 | 
						||
 | 
						||
	cfg := data[0]
 | 
						||
	rest := data[1:]
 | 
						||
 | 
						||
	g := &GRPC{
 | 
						||
		from: ".",
 | 
						||
		Next: test.ErrorHandler(),
 | 
						||
	}
 | 
						||
 | 
						||
	// Select policy based on cfg bits to vary list() ordering.
 | 
						||
	switch cfg % 3 {
 | 
						||
	case 0:
 | 
						||
		g.p = &random{}
 | 
						||
	case 1:
 | 
						||
		g.p = &roundRobin{}
 | 
						||
	default:
 | 
						||
		g.p = &sequential{}
 | 
						||
	}
 | 
						||
 | 
						||
	// Optionally enable fallthrough; choose scope based on input bit.
 | 
						||
	if cfg&0x80 != 0 {
 | 
						||
		if cfg&0x01 != 0 {
 | 
						||
			g.Fall.SetZonesFromArgs([]string{"."})
 | 
						||
		} else {
 | 
						||
			g.Fall.SetZonesFromArgs([]string{g.from})
 | 
						||
		}
 | 
						||
	}
 | 
						||
 | 
						||
	// Create 0–3 fake proxies with varied behaviors.
 | 
						||
	numProxies := int((cfg >> 4) & 0x03)
 | 
						||
	if numProxies == 0 {
 | 
						||
		if _, is := g.p.(*roundRobin); is {
 | 
						||
			// Avoid divide-by-zero in roundRobin policy when pool is empty.
 | 
						||
			g.p = &sequential{}
 | 
						||
		}
 | 
						||
	}
 | 
						||
	for i := range numProxies {
 | 
						||
		mode := byte(i)
 | 
						||
		if len(rest) > 0 {
 | 
						||
			mode = rest[i%len(rest)]
 | 
						||
		}
 | 
						||
		p := &Proxy{addr: "fake"}
 | 
						||
		p.client = &fakeClient{mode: mode}
 | 
						||
		g.proxies = append(g.proxies, p)
 | 
						||
	}
 | 
						||
 | 
						||
	// Deterministically set a narrow from to miss match and hit Next/SERVFAIL paths.
 | 
						||
	if cfg&0x20 != 0 {
 | 
						||
		g.from = "_not_matching_."
 | 
						||
	}
 | 
						||
 | 
						||
	// Optionally construct a tiny deterministic query to vary RD/CD flags.
 | 
						||
	if cfg&0x08 != 0 {
 | 
						||
		var rq dns.Msg
 | 
						||
		rq.SetQuestion("example.org.", dns.TypeA)
 | 
						||
		rq.RecursionDesired = (cfg&0x04 != 0)
 | 
						||
		rq.CheckingDisabled = (cfg&0x02 != 0)
 | 
						||
		if packed, err := rq.Pack(); err == nil {
 | 
						||
			rest = packed
 | 
						||
		}
 | 
						||
	}
 | 
						||
 | 
						||
	return fuzz.Do(g, rest)
 | 
						||
}
 |