mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	test(grpc): add fuzzer (#7513)
This commit is contained in:
		
							
								
								
									
										216
									
								
								plugin/grpc/fuzz.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								plugin/grpc/fuzz.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| //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) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user