Repository: coredns/coredns (go)

Issue:
DNSSEC cached signatures prematurely deleted
**What happened**:

Cached DNSSEC sigs are deleted after two days.

**What you expected to happen**:

Cached sigs should live for six days per #1378.

**How to reproduce it (as minimally and precisely as possible)**:

Rebuild with cache timings in seconds instead of days.

**Anything else we need to know?**:

PR incoming

Pull request:
DNSSEC check for two days of remaining validity
Signed-off-by: Keith C <keith@fraudmarc.com>

### 1. Why is this pull request needed and what does it do?
This should allow cached signatures to be cached for the intended six day duration.
### 2. Which issues (if any) are related?
fix #4605 
### 3. Which documentation changes (if any) need to be made?

### 4. Does this introduce a backward incompatible change or deprecation?
No

Patch:
diff --git a/core/dnsserver/config.go b/core/dnsserver/config.go
index 4ff2ecda1..d0a432a85 100644
--- a/core/dnsserver/config.go
+++ b/core/dnsserver/config.go
@@ -37,11 +37,6 @@ type Config struct {
 	// may depend on it.
 	HTTPRequestValidateFunc func(*http.Request) bool
 
-	// If this function is not nil it will be used to further filter access
-	// to this handler. The primary use is to limit access to a reverse zone
-	// on a non-octet boundary, i.e. /17
-	FilterFunc func(string) bool
-
 	// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
 	TLSConfig *tls.Config
 
diff --git a/core/dnsserver/onstartup.go b/core/dnsserver/onstartup.go
index 7aa519e98..113bf15e5 100644
--- a/core/dnsserver/onstartup.go
+++ b/core/dnsserver/onstartup.go
@@ -1,6 +1,9 @@
 package dnsserver
 
-import "fmt"
+import (
+	"fmt"
+	"sort"
+)
 
 // startUpZones creates the text that we show when starting up:
 // grpc://example.com.:1055
@@ -8,7 +11,15 @@ import "fmt"
 func startUpZones(protocol, addr string, zones map[string]*Config) string {
 	s := ""
 
-	for zone := range zones {
+	keys := make([]string, len(zones))
+	i := 0
+	for k := range zones {
+		keys[i] = k
+		i++
+	}
+	sort.Strings(keys)
+
+	for _, zone := range keys {
 		// split addr into protocol, IP and Port
 		_, ip, port, err := SplitProtocolHostPort(addr)
 
diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go
index b4e9f4fde..505aa8580 100644
--- a/core/dnsserver/register.go
+++ b/core/dnsserver/register.go
@@ -4,13 +4,11 @@ import (
 	"flag"
 	"fmt"
 	"net"
-	"strings"
 	"time"
 
 	"github.com/coredns/caddy"
 	"github.com/coredns/caddy/caddyfile"
 	"github.com/coredns/coredns/plugin"
-	"github.com/coredns/coredns/plugin/pkg/dnsutil"
 	"github.com/coredns/coredns/plugin/pkg/parse"
 	"github.com/coredns/coredns/plugin/pkg/transport"
 )
@@ -60,6 +58,28 @@ var _ caddy.Context = &dnsContext{}
 func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
 	// Normalize and check all the zone names and check for duplicates
 	for ib, s := range serverBlocks {
+		// Walk the s.Keys and expand any reverse address in their proper DNS in-addr zones. If the expansions leads for
+		// more than one reverse zone, replace the current value and add the rest to s.Keys.
+		for ik, k := range s.Keys {
+			_, k1 := parse.Transport(k) // get rid of any dns:// or other scheme.
+			_, port, ipnet, err := plugin.SplitHostPort(k1)
+			if ipnet == nil || err != nil { // err will be caught below
+				continue
+			}
+			if port != "" {
+				port = ":" + port
+			}
+			nets := classFromCIDR(ipnet)
+			if len(nets) > 1 {
+				s.Keys[ik] = nets[0] + port  // replace for the first
+				for _, n := range nets[1:] { // add  the rest
+					s.Keys = append(s.Keys, n+port)
+				}
+			}
+		}
+
+		serverBlocks[ib].Keys = s.Keys // important to save back the new keys that are potentially created here.
+
 		for ik, k := range s.Keys {
 			za, err := normalizeZone(k)
 			if err != nil {
@@ -74,22 +94,6 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
 				Transport:   za.Transport,
 			}
 			keyConfig := keyForConfig(ib, ik)
-			if za.IPNet == nil {
-				h.saveConfig(keyConfig, cfg)
-				continue
-			}
-
-			ones, bits := za.IPNet.Mask.Size()
-			if (bits-ones)%8 != 0 { // only do this for non-octet boundaries
-				cfg.FilterFunc = func(s string) bool {
-					// TODO(miek): strings.ToLower! Slow and allocates new string.
-					addr := dnsutil.ExtractAddressFromReverse(strings.ToLower(s))
-					if addr == "" {
-						return true
-					}
-					return za.IPNet.Contains(net.ParseIP(addr))
-				}
-			}
 			h.saveConfig(keyConfig, cfg)
 		}
 	}
@@ -222,7 +226,6 @@ func (h *dnsContext) validateZonesAndListeningAddresses() error {
 // address (what you pass into net.Listen) to the list of site configs.
 // This function does NOT vet the configs to ensure they are compatible.
 func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
-
 	groups := make(map[string][]*Config)
 	for _, conf := range configs {
 		for _, h := range conf.ListenHosts {
diff --git a/core/dnsserver/reverse.go b/core/dnsserver/reverse.go
new file mode 100644
index 000000000..2d0a7c740
--- /dev/null
+++ b/core/dnsserver/reverse.go
@@ -0,0 +1,38 @@
+package dnsserver
+
+import (
+	"math"
+	"net"
+
+	"github.com/apparentlymart/go-cidr/cidr"
+)
+
+// classFromCIDR return slice of "classful" (/8, /16, /24 or /32 only) CIDR's from the CIDR in net.
+func classFromCIDR(n *net.IPNet) []string {
+	ones, _ := n.Mask.Size()
+	if ones%8 == 0 {
+		return []string{n.String()}
+	}
+
+	mask := int(math.Ceil(float64(ones)/8)) * 8
+	networks := subnets(n, mask)
+	cidrs := make([]string, len(networks))
+	for i := range networks {
+		cidrs[i] = networks[i].String()
+	}
+	return cidrs
+}
+
+// subnets return a slice of prefixes with the desired mask subnetted from original network.
+func subnets(network *net.IPNet, newPrefixLen int) []*net.IPNet {
+	prefixLen, _ := network.Mask.Size()
+	maxSubnets := int(math.Exp2(float64(newPrefixLen)) / math.Exp2(float64(prefixLen)))
+	nets := []*net.IPNet{{network.IP, net.CIDRMask(newPrefixLen, 8*len(network.IP))}}
+
+	for i := 1; i < maxSubnets; i++ {
+		next, _ := cidr.NextSubnet(nets[len(nets)-1], newPrefixLen)
+		nets = append(nets, next)
+	}
+
+	return nets
+}
diff --git a/core/dnsserver/server.go b/core/dnsserver/server.go
index c7304d763..638cbb8fe 100644
--- a/core/dnsserver/server.go
+++ b/core/dnsserver/server.go
@@ -247,22 +247,11 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
 				return
 			}
 			if r.Question[0].Qtype != dns.TypeDS {
-				if h.FilterFunc == nil {
-					rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
-					if !plugin.ClientWrite(rcode) {
-						errorFunc(s.Addr, w, r, rcode)
-					}
-					return
-				}
-				// FilterFunc is set, call it to see if we should use this handler.
-				// This is given to full query name.
-				if h.FilterFunc(q) {
-					rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
-					if !plugin.ClientWrite(rcode) {
-						errorFunc(s.Addr, w, r, rcode)
-					}
-					return
+				rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
+				if !plugin.ClientWrite(rcode) {
+					errorFunc(s.Addr, w, r, rcode)
 				}
+				return
 			}
 			// The type is DS, keep the handler, but keep on searching as maybe we are serving
 			// the parent as well and the DS should be routed to it - this will probably *misroute* DS
diff --git a/go.mod b/go.mod
index 3c3cff389..88ece3ae4 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,8 @@ require (
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.7
 	github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect
 	github.com/DataDog/datadog-go v3.5.0+incompatible // indirect
-	github.com/aws/aws-sdk-go v1.38.30
+	github.com/apparentlymart/go-cidr v1.1.0
+	github.com/aws/aws-sdk-go v1.38.37
 	github.com/coredns/caddy v1.1.0
 	github.com/dnstap/golang-dnstap v0.4.0
 	github.com/dustin/go-humanize v1.0.0 // indirect
@@ -19,7 +20,7 @@ require (
 	github.com/imdario/mergo v0.3.12 // indirect
 	github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9
 	github.com/matttproud/golang_protobuf_extensions v1.0.1
-	github.com/miekg/dns v1.1.41
+	github.com/miekg/dns v1.1.42
 	github.com/opentracing/opentracing-go v1.2.0
 	github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5
 	github.com/openzipkin/zipkin-go v0.2.2
diff --git a/go.sum b/go.sum
index 0cf7da696..76f648a7d 100644
--- a/go.sum
+++ b/go.sum
@@ -70,6 +70,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
+github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -77,8 +79,8 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
 github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.38.30 h1:X+JDSwkpSQfoLqH4fBLmS0rou8W/cdCCCD5lntTk9Vs=
-github.com/aws/aws-sdk-go v1.38.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.38.37 h1:Eh3/jog9t2NhGcOi086NRfRqVKduRXkaH6svI8yF1Jg=
+github.com/aws/aws-sdk-go v1.38.37/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -335,8 +337,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
-github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
-github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
+github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
diff --git a/notes/coredns-1.8.1.md b/notes/coredns-1.8.1.md
index d02423bb2..4d80c0dbc 100644
--- a/notes/coredns-1.8.1.md
+++ b/notes/coredns-1.8.1.md
@@ -13,7 +13,10 @@ The CoreDNS team has released
 This release fixes a bunch of bugs, and adds a (very) simple new plugin
 called [local](https://coredns.io/plugins/local/) to answer "local" queries. Bunch of work in
 the [kubernetes](https://coredns.io/plugins/kubernetes) plugin to add support for new upstream
-feautres.
+features.
+
+If using the [kubernetes](https://coredns.io/plugins/kubernetes) plugin for a Kubernetes
+cluster >= 1.19, CoreDNS must be granted `list` and `watch` access to `endpointslices`.
 
 ## Brought to You By
 
diff --git a/notes/coredns-1.8.4.md b/notes/coredns-1.8.4.md
new file mode 100644
index 000000000..4b9e30c20
--- /dev/null
+++ b/notes/coredns-1.8.4.md
@@ -0,0 +1,43 @@
++++
+title = "CoreDNS-1.8.4 Release"
+description = "CoreDNS-1.8.4 Release Notes."
+tags = ["Release", "1.8.4", "Notes"]
+release = "1.8.4"
+date = 2021-05-12T07:00:00+00:00
+author = "coredns"
++++
+
+The CoreDNS team has released
+[CoreDNS-1.8.4](https://github.com/coredns/coredns/releases/tag/v1.8.4). This release includes a
+bunch of bugfixes and a few enhancements, see below.
+
+## Brought to You By
+
+Chris O'Haver,
+Frank Riley,
+Keith Coleman,
+Miek Gieben,
+Mohammad Yosefpor,
+Paco Xu,
+Soumya Ghosh Dastidar,
+Steve Greene,
+Théotime Lévêque,
+Uwe Krueger,
+Yong Tang,
+Yury Tsarev,
+cuirunxing-hub,
+milgradesec,
+ntoofu,
+wangchenglong01.
+
+## Noteworthy Changes
+
+* plugin/metrics: remove RR type (https://github.com/coredns/coredns/pull/4534)
+* plugin/health: add logging for local health request (https://github.com/coredns/coredns/pull/4533)
+* plugin/bind: Bind by interface name (https://github.com/coredns/coredns/pull/4522)
+* plugin/bind: Exclude interface or ip address  (https://github.com/coredns/coredns/pull/4543)
+* plugin/forward: Add upstream metadata (https://github.com/coredns/coredns/pull/4521)
+* plugin/minimal: Add minimal-responses plugin (https://github.com/coredns/coredns/pull/4417)
+* plugin/transfer: reply with refused (https://github.com/coredns/coredns/pull/4510)
+* plugin/kubernetes: Exclude unready endpoints from endpointslices (https://github.com/coredns/coredns/pull/4580)
+* plugin/kubernetes: do endpoint/slice check in retry loop (https://github.com/coredns/coredns/pull/4492)
diff --git a/plugin/dnssec/cache.go b/plugin/dnssec/cache.go
index 4dfe48d0c..2acfe72a5 100644
--- a/plugin/dnssec/cache.go
+++ b/plugin/dnssec/cache.go
@@ -31,11 +31,12 @@ func periodicClean(c *cache.Cache, stop <-chan struct{}) {
 		case <-tick.C:
 			// we sign for 8 days, check if a signature in the cache reached 75% of that (i.e. 6), if found delete
 			// the signature
-			is75 := time.Now().UTC().Add(sixDays)
+			is75 := time.Now().UTC().Add(twoDays)
 			c.Walk(func(items map[uint64]interface{}, key uint64) bool {
-				sig := items[key].(*dns.RRSIG)
-				if !sig.ValidityPeriod(is75) {
-					delete(items, key)
+				for _, rr := range items[key].([]dns.RR) {
+					if !rr.(*dns.RRSIG).ValidityPeriod(is75) {
+						delete(items, key)
+					}
 				}
 				return true
 			})
diff --git a/plugin/dnssec/dnssec.go b/plugin/dnssec/dnssec.go
index 6f943ec88..9e050f590 100644
--- a/plugin/dnssec/dnssec.go
+++ b/plugin/dnssec/dnssec.go
@@ -131,7 +131,7 @@ func (d Dnssec) set(key uint64, sigs []dns.RR) { d.cache.Add(key, sigs) }
 func (d Dnssec) get(key uint64, server string) ([]dns.RR, bool) {
 	if s, ok := d.cache.Get(key); ok {
 		// we sign for 8 days, check if a signature in the cache reached 3/4 of that
-		is75 := time.Now().UTC().Add(sixDays)
+		is75 := time.Now().UTC().Add(twoDays)
 		for _, rr := range s.([]dns.RR) {
 			if !rr.(*dns.RRSIG).ValidityPeriod(is75) {
 				cacheMisses.WithLabelValues(server).Inc()
@@ -154,6 +154,6 @@ func incepExpir(now time.Time) (uint32, uint32) {
 
 const (
 	eightDays  = 8 * 24 * time.Hour
-	sixDays    = 6 * 24 * time.Hour
+	twoDays    = 2 * 24 * time.Hour
 	defaultCap = 10000 // default capacity of the cache.
 )
diff --git a/plugin/kubernetes/controller.go b/plugin/kubernetes/controller.go
index 890785d71..f17576eb4 100644
--- a/plugin/kubernetes/controller.go
+++ b/plugin/kubernetes/controller.go
@@ -11,7 +11,8 @@ import (
 	"github.com/coredns/coredns/plugin/kubernetes/object"
 
 	api "k8s.io/api/core/v1"
-	discovery "k8s.io/api/discovery/v1beta1"
+	discovery "k8s.io/api/discovery/v1"
+	discoveryV1beta1 "k8s.io/api/discovery/v1beta1"
 	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -180,6 +181,23 @@ func (dns *dnsControl) WatchEndpoints(ctx context.Context) {
 	dns.epLock.Unlock()
 }
 
+// WatchEndpointSliceV1beta1 will set the endpoint Lister and Controller to watch v1beta1
+// instead of the default v1.
+func (dns *dnsControl) WatchEndpointSliceV1beta1(ctx context.Context) {
+	dns.epLock.Lock()
+	dns.epLister, dns.epController = object.NewIndexerInformer(
+		&cache.ListWatch{
+			ListFunc:  endpointSliceListFuncV1beta1(ctx, dns.client, api.NamespaceAll, dns.selector),
+			WatchFunc: endpointSliceWatchFuncV1beta1(ctx, dns.client, api.NamespaceAll, dns.selector),
+		},
+		&discoveryV1beta1.EndpointSlice{},
+		cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete},
+		cache.Indexers{epNameNamespaceIndex: epNameNamespaceIndexFunc, epIPIndex: epIPIndexFunc},
+		object.DefaultProcessor(object.EndpointSliceV1beta1ToEndpoints, dns.EndpointSliceLatencyRecorder()),
+	)
+	dns.epLock.Unlock()
+}
+
 func (dns *dnsControl) EndpointsLatencyRecorder() *object.EndpointLatencyRecorder {
 	return &object.EndpointLatencyRecorder{
 		ServiceFunc: func(o meta.Object) []*object.Service {
@@ -262,13 +280,21 @@ func podListFunc(ctx context.Context, c kubernetes.Interface, ns string, s label
 		return c.CoreV1().Pods(ns).List(ctx, opts)
 	}
 }
+func endpointSliceListFuncV1beta1(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) {
+	return func(opts meta.ListOptions) (runtime.Object, error) {
+		if s != nil {
+			opts.LabelSelector = s.String()
+		}
+		return c.DiscoveryV1beta1().EndpointSlices(ns).List(ctx, opts)
+	}
+}
 
 func endpointSliceListFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(meta.ListOptions) (runtime.Object, error) {
 	return func(opts meta.ListOptions) (runtime.Object, error) {
 		if s != nil {
 			opts.LabelSelector = s.String()
 		}
-		return c.DiscoveryV1beta1().EndpointSlices(ns).List(ctx, opts)
+		return c.DiscoveryV1().EndpointSlices(ns).List(ctx, opts)
 	}
 }
 
@@ -312,7 +338,7 @@ func podWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labe
 	}
 }
 
-func endpointSliceWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) {
+func endpointSliceWatchFuncV1beta1(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) {
 	return func(options meta.ListOptions) (watch.Interface, error) {
 		if s != nil {
 			options.LabelSelector = s.String()
@@ -321,6 +347,15 @@ func endpointSliceWatchFunc(ctx context.Context, c kubernetes.Interface, ns stri
 	}
 }
 
+func endpointSliceWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) {
+	return func(options meta.ListOptions) (watch.Interface, error) {
+		if s != nil {
+			options.LabelSelector = s.String()
+		}
+		return c.DiscoveryV1().EndpointSlices(ns).Watch(ctx, options)
+	}
+}
+
 func endpointsWatchFunc(ctx context.Context, c kubernetes.Interface, ns string, s labels.Selector) func(options meta.ListOptions) (watch.Interface, error) {
 	return func(options meta.ListOptions) (watch.Interface, error) {
 		if s != nil {
diff --git a/plugin/kubernetes/kubernetes.go b/plugin/kubernetes/kubernetes.go
index bf1f01664..ea99a81ce 100644
--- a/plugin/kubernetes/kubernetes.go
+++ b/plugin/kubernetes/kubernetes.go
@@ -20,7 +20,8 @@ import (
 
 	"github.com/miekg/dns"
 	api "k8s.io/api/core/v1"
-	discovery "k8s.io/api/discovery/v1beta1"
+	discovery "k8s.io/api/discovery/v1"
+	discoveryV1beta1 "k8s.io/api/discovery/v1beta1"
 	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/client-go/kubernetes"
@@ -256,10 +257,15 @@ func (k *Kubernetes) InitKubeCache(ctx context.Context) (onStart func() error, o
 		go func() {
 			if initEndpointWatch {
 				// Revert to watching Endpoints for incompatible K8s.
-				// This can be remove when all supported k8s versions support endpointslices.
-				if ok := k.endpointSliceSupported(kubeClient); !ok {
+				// This can be removed when all supported k8s versions support endpointslices.
+				ok, v := k.endpointSliceSupported(kubeClient)
+				if !ok {
 					k.APIConn.(*dnsControl).WatchEndpoints(ctx)
 				}
+				// Revert to EndpointSlice v1beta1 if v1 is not supported
+				if ok && v == discoveryV1beta1.SchemeGroupVersion.String() {
+					k.APIConn.(*dnsControl).WatchEndpointSliceV1beta1(ctx)
+				}
 			}
 			k.APIConn.Run()
 		}()
@@ -290,9 +296,12 @@ func (k *Kubernetes) InitKubeCache(ctx context.Context) (onStart func() error, o
 // endpointSliceSupported will determine which endpoint object type to watch (endpointslices or endpoints)
 // based on the supportability of endpointslices in the API and server version. It will return true when endpointslices
 // should be watched, and false when endpoints should be watched.
-// If the API supports discovery v1 beta1, and the server versions >= 1.19, endpointslices are watched.
-// This function should be removed, along with non-slice endpoint watch code, when support for k8s < 1.19 is dropped.
-func (k *Kubernetes) endpointSliceSupported(kubeClient *kubernetes.Clientset) bool {
+// If the API supports discovery, and the server versions >= 1.19, true is returned.
+// Also returned is the discovery version supported: "v1" if v1 is supported, and v1beta1 if v1beta1 is supported and
+// v1 is not supported.
+// This function should be removed, when all supported versions of k8s support v1.
+func (k *Kubernetes) endpointSliceSupported(kubeClient *kubernetes.Clientset) (bool, string) {
+	var sliceVer string
 	useEndpointSlices := false
 	ticker := time.NewTicker(100 * time.Millisecond)
 	defer ticker.Stop()
@@ -303,9 +312,13 @@ func (k *Kubernetes) endpointSliceSupported(kubeClient *kubernetes.Clientset) bo
 			if err != nil {
 				continue
 			}
-			// Enable use of endpoint slices if the API supports the discovery v1 beta1 api
+			// Enable use of endpoint slices if the API supports the discovery api
 			if _, err := kubeClient.Discovery().ServerResourcesForGroupVersion(discovery.SchemeGroupVersion.String()); err == nil {
 				useEndpointSlices = true
+				sliceVer = discovery.SchemeGroupVersion.String()
+			} else if _, err := kubeClient.Discovery().ServerResourcesForGroupVersion(discoveryV1beta1.SchemeGroupVersion.String()); err == nil {
+				useEndpointSlices = true
+				sliceVer = discoveryV1beta1.SchemeGroupVersion.String()
 			}
 			// Disable use of endpoint slices for k8s versions 1.18 and earlier. The Endpointslices API was enabled
 			// by default in 1.17 but Service -> Pod proxy continued to use Endpoints by default until 1.19.
@@ -317,7 +330,7 @@ func (k *Kubernetes) endpointSliceSupported(kubeClient *kubernetes.Clientset) bo
 				log.Info("Watching Endpoints instead of EndpointSlices in k8s versions < 1.19")
 				useEndpointSlices = false
 			}
-			return useEndpointSlices
+			return useEndpointSlices, sliceVer
 		}
 	}
 }
diff --git a/plugin/kubernetes/object/endpoint.go b/plugin/kubernetes/object/endpoint.go
index fc25aa4fb..f504904a8 100644
--- a/plugin/kubernetes/object/endpoint.go
+++ b/plugin/kubernetes/object/endpoint.go
@@ -4,7 +4,8 @@ import (
 	"fmt"
 
 	api "k8s.io/api/core/v1"
-	discovery "k8s.io/api/discovery/v1beta1"
+	discovery "k8s.io/api/discovery/v1"
+	discoveryV1beta1 "k8s.io/api/discovery/v1beta1"
 	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
@@ -151,6 +152,54 @@ func EndpointSliceToEndpoints(obj meta.Object) (meta.Object, error) {
 	return e, nil
 }
 
+// EndpointSliceV1beta1ToEndpoints converts a v1beta1 *discovery.EndpointSlice to a *Endpoints.
+func EndpointSliceV1beta1ToEndpoints(obj meta.Object) (meta.Object, error) {
+	ends, ok := obj.(*discoveryV1beta1.EndpointSlice)
+	if !ok {
+		return nil, fmt.Errorf("unexpected object %v", obj)
+	}
+	e := &Endpoints{
+		Version:   ends.GetResourceVersion(),
+		Name:      ends.GetName(),
+		Namespace: ends.GetNamespace(),
+		Index:     EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()),
+		Subsets:   make([]EndpointSubset, 1),
+	}
+
+	if len(ends.Ports) == 0 {
+		// Add sentinel if there are no ports.
+		e.Subsets[0].Ports = []EndpointPort{{Port: -1}}
+	} else {
+		e.Subsets[0].Ports = make([]EndpointPort, len(ends.Ports))
+		for k, p := range ends.Ports {
+			ep := EndpointPort{Port: *p.Port, Name: *p.Name, Protocol: string(*p.Protocol)}
+			e.Subsets[0].Ports[k] = ep
+		}
+	}
+
+	for _, end := range ends.Endpoints {
+		if end.Conditions.Ready == nil || !*end.Conditions.Ready {
+			continue
+		}
+		for _, a := range end.Addresses {
+			ea := EndpointAddress{IP: a}
+			if end.Hostname != nil {
+				ea.Hostname = *end.Hostname
+			}
+			if end.TargetRef != nil {
+				ea.TargetRefName = end.TargetRef.Name
+			}
+			// EndpointSlice does not contain NodeName, leave blank
+			e.Subsets[0].Addresses = append(e.Subsets[0].Addresses, ea)
+			e.IndexIP = append(e.IndexIP, a)
+		}
+	}
+
+	*ends = discoveryV1beta1.EndpointSlice{}
+
+	return e, nil
+}
+
 // CopyWithoutSubsets copies e, without the subsets.
 func (e *Endpoints) CopyWithoutSubsets() *Endpoints {
 	e1 := &Endpoints{


Test patch:
diff --git a/core/dnsserver/reverse_test.go b/core/dnsserver/reverse_test.go
new file mode 100644
index 000000000..bc6909b8a
--- /dev/null
+++ b/core/dnsserver/reverse_test.go
@@ -0,0 +1,31 @@
+package dnsserver
+
+import (
+	"net"
+	"testing"
+)
+
+func TestClassFromCIDR(t *testing.T) {
+	tests := []struct {
+		in       string
+		expected []string
+	}{
+		{"10.0.0.0/15", []string{"10.0.0.0/16", "10.1.0.0/16"}},
+		{"10.0.0.0/16", []string{"10.0.0.0/16"}},
+		{"192.168.1.1/23", []string{"192.168.0.0/24", "192.168.1.0/24"}},
+		{"10.129.60.0/22", []string{"10.129.60.0/24", "10.129.61.0/24", "10.129.62.0/24", "10.129.63.0/24"}},
+	}
+	for i, tc := range tests {
+		_, n, _ := net.ParseCIDR(tc.in)
+		nets := classFromCIDR(n)
+		if len(nets) != len(tc.expected) {
+			t.Errorf("Test %d, expected %d subnets, got %d", i, len(tc.expected), len(nets))
+			continue
+		}
+		for j := range nets {
+			if nets[j] != tc.expected[j] {
+				t.Errorf("Test %d, expected %s, got %s", i, tc.expected[j], nets[j])
+			}
+		}
+	}
+}
diff --git a/test/file_reload_test.go b/test/file_reload_test.go
index 95cc860bb..69b9af5f6 100644
--- a/test/file_reload_test.go
+++ b/test/file_reload_test.go
@@ -47,7 +47,7 @@ func TestZoneReload(t *testing.T) {
 	// Remove RR from the Apex
 	ioutil.WriteFile(name, []byte(exampleOrgUpdated), 0644)
 
-	time.Sleep(10 * time.Millisecond) // reload time
+	time.Sleep(20 * time.Millisecond) // reload time, with some race insurance
 
 	resp, err = dns.Exchange(m, udp)
 	if err != nil {
@@ -55,7 +55,7 @@ func TestZoneReload(t *testing.T) {
 	}
 
 	if len(resp.Answer) != 1 {
-		t.Fatalf("Expected two RR in answer section got %d", len(resp.Answer))
+		t.Fatalf("Expected one RR in answer section got %d", len(resp.Answer))
 	}
 }
 
diff --git a/test/server_test.go b/test/server_test.go
index f03a27749..c58915ee7 100644
--- a/test/server_test.go
+++ b/test/server_test.go
@@ -53,3 +53,57 @@ func chaosTest(t *testing.T, server string) {
 		t.Fatalf("Expected version.bind. reply, got %s", r.Answer[0].String())
 	}
 }
+
+func TestReverseExpansion(t *testing.T) {
+	// this test needs a fixed port, because with :0 the expanded reverse zone will listen on different
+	// addresses and we can't check which ones...
+	corefile := `10.0.0.0/15:5053 {
+		whoami
+	}`
+
+	server, udp, _, err := CoreDNSServerAndPorts(corefile)
+	if err != nil {
+		t.Fatalf("Could not get CoreDNS serving instance: %s", err)
+	}
+
+	defer server.Stop()
+
+	m := new(dns.Msg)
+	m.SetQuestion("whoami.0.10.in-addr.arpa.", dns.TypeA)
+
+	r, err := dns.Exchange(m, udp)
+	if err != nil {
+		t.Fatalf("Could not send message: %s", err)
+	}
+	if r.Rcode != dns.RcodeSuccess {
+		t.Errorf("Expected NOERROR, got %d", r.Rcode)
+	}
+	if len(r.Extra) != 2 {
+		t.Errorf("Expected 2 RRs in additional section, got %d", len(r.Extra))
+	}
+
+	m.SetQuestion("whoami.1.10.in-addr.arpa.", dns.TypeA)
+	r, err = dns.Exchange(m, udp)
+	if err != nil {
+		t.Fatalf("Could not send message: %s", err)
+	}
+	if r.Rcode != dns.RcodeSuccess {
+		t.Errorf("Expected NOERROR, got %d", r.Rcode)
+	}
+	if len(r.Extra) != 2 {
+		t.Errorf("Expected 2 RRs in additional section, got %d", len(r.Extra))
+	}
+
+	// should be refused
+	m.SetQuestion("whoami.2.10.in-addr.arpa.", dns.TypeA)
+	r, err = dns.Exchange(m, udp)
+	if err != nil {
+		t.Fatalf("Could not send message: %s", err)
+	}
+	if r.Rcode != dns.RcodeRefused {
+		t.Errorf("Expected REFUSED, got %d", r.Rcode)
+	}
+	if len(r.Extra) != 0 {
+		t.Errorf("Expected 0 RRs in additional section, got %d", len(r.Extra))
+	}
+}