Repository: coredns/coredns (go)

Issue:
coredns does not add NS records to zone file (transfer plugin)
**What happened**:

I try to import coredns running in k8s into an external bind using:

```
zone "k8s.place7.ungleich.ch"  {
    type slave;
     masters { 2a0a:e5c0:13:e2::a; };
};
```

The result is:

```
Jun 13 22:35:26 router1 daemon.info named[11365]: zone k8s.place7.ungleich.ch/IN: Transfer started.
Jun 13 22:35:26 router1 daemon.info named[11365]: transfer of 'k8s.place7.ungleich.ch/IN' from 2a0a:e5c0:13:e2::a#53: connected using 2a0a:e5c0:1:8::3#52129
Jun 13 22:35:26 router1 daemon.err named[11365]: zone k8s.place7.ungleich.ch/IN: has no NS records
Jun 13 22:35:26 router1 daemon.err named[11365]: transfer of 'k8s.place7.ungleich.ch/IN' from 2a0a:e5c0:13:e2::a#53: failed while receiving responses: bad zone
Jun 13 22:35:26 router1 daemon.info named[11365]: transfer of 'k8s.place7.ungleich.ch/IN' from 2a0a:e5c0:13:e2::a#53: Transfer status: bad zone
Jun 13 22:35:26 router1 daemon.info named[11365]: transfer of 'k8s.place7.ungleich.ch/IN' from 2a0a:e5c0:13:e2::a#53: Transfer completed: 1 messages, 115 records, 14649 bytes, 0.023 secs (636913 bytes/sec)
```

**What you expected to happen**:

The AXFR/IXFR to happen, bind serving the zone contents.

**How to reproduce it (as minimally and precisely as possible)**:

Setup k8s with the following CM:

```
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        transfer {
          to *
        }
        ready
        kubernetes k8s.place7.ungleich.ch in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }

        prometheus :9153
        forward . /etc/resolv.conf {
           max_concurrent 1000
        }
        cache 30
        loop
        reload
        loadbalance
    }
```

**Anything else we need to know?**:

AXFR in general works:

```
[22:43] nb3:~% dig k8s.place7.ungleich.ch axfr @2a0a:e5c0:13:e2::a | head -n 10

; <<>> DiG 9.16.16 <<>> k8s.place7.ungleich.ch axfr @2a0a:e5c0:13:e2::a
;; global options: +cmd
k8s.place7.ungleich.ch.	5	IN	SOA	ns.dns.k8s.place7.ungleich.ch. hostmaster.k8s.place7.ungleich.ch. 1623616348 7200 1800 86400 30
argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch. 5 IN AAAA 2a0a:e5c0:13:e2::56c2
argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch. 5 IN SRV 0 100 5556 argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.
_http._tcp.argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.	5 IN SRV 0 100 5556 argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.
argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch. 5 IN SRV 0 100 5557 argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.
_grpc._tcp.argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.	5 IN SRV 0 100 5557 argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.
argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch. 5 IN SRV 0 100 5558 argocd-dex-server.argocd.svc.k8s.place7.ungleich.ch.
```

But I can confirm that in the axfr there are no NS entries.

However when I *query* for the NS entry, I do get a result from CoreDNS:

```
[22:43] nb3:~% dig k8s.place7.ungleich.ch ns @2a0a:e5c0:13:e2::a             

; <<>> DiG 9.16.16 <<>> k8s.place7.ungleich.ch ns @2a0a:e5c0:13:e2::a
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 40946
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: eb4959f3a65015f3 (echoed)
;; QUESTION SECTION:
;k8s.place7.ungleich.ch.		IN	NS

;; ANSWER SECTION:
k8s.place7.ungleich.ch.	30	IN	NS	kube-dns.kube-system.svc.k8s.place7.ungleich.ch.

;; ADDITIONAL SECTION:
kube-dns.kube-system.svc.k8s.place7.ungleich.ch. 30 IN AAAA 2a0a:e5c0:13:e2::a

;; Query time: 19 msec
;; SERVER: 2a0a:e5c0:13:e2::a#53(2a0a:e5c0:13:e2::a)
;; WHEN: Sun Jun 13 22:44:01 CEST 2021
;; MSG SIZE  rcvd: 221
```

**Environment**:

- the version of CoreDNS: k8s.gcr.io/coredns/coredns:v1.8.0
- Corefile: see above
- logs, if applicable: see above
- OS (e.g: `cat /etc/os-release`): docker container
- Others: IPv6 only environment


Pull request description:
plugin/kubernetes: Add NS+hosts records to XFR response
Signed-off-by: Chris O'Haver <cohaver@infoblox.com>

### 1. Why is this pull request needed and what does it do?

Adds NS and its host record to XFR response.
Adjust test data to include Service and Endpoint records for CoreDNS service as it would be in a Kubernetes cluster.

### 2. Which issues (if any) are related?

Fixes #4689

### 3. Which documentation changes (if any) need to be made?

### 4. Does this introduce a backward incompatible change or deprecation?


Patch:
diff --git a/plugin/kubernetes/xfr.go b/plugin/kubernetes/xfr.go
index 812604966..b82c4d144 100644
--- a/plugin/kubernetes/xfr.go
+++ b/plugin/kubernetes/xfr.go
@@ -38,6 +38,17 @@ func (k *Kubernetes) Transfer(zone string, serial uint32) (<-chan []dns.RR, erro
 		}
 		ch <- soa
 
+		nsAddrs := k.nsAddrs(false, zone)
+		nsHosts := make(map[string]struct{})
+		for _, nsAddr := range nsAddrs {
+			nsHost := nsAddr.Header().Name
+			if _, ok := nsHosts[nsHost]; !ok {
+				nsHosts[nsHost] = struct{}{}
+				ch <- []dns.RR{&dns.NS{Hdr: dns.RR_Header{Name: zone, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: k.ttl}, Ns: nsHost}}
+			}
+			ch <- nsAddrs
+		}
+
 		sort.Slice(serviceList, func(i, j int) bool {
 			return serviceList[i].Name < serviceList[j].Name
 		})


Test patch:
diff --git a/plugin/kubernetes/handler_test.go b/plugin/kubernetes/handler_test.go
index 7904e67a6..19eed2d50 100644
--- a/plugin/kubernetes/handler_test.go
+++ b/plugin/kubernetes/handler_test.go
@@ -568,6 +568,17 @@ func (APIConnServeTest) PodIndex(ip string) []*object.Pod {
 }
 
 var svcIndex = map[string][]*object.Service{
+	"kubedns.kube-system": {
+		{
+			Name:       "kubedns",
+			Namespace:  "kube-system",
+			Type:       api.ServiceTypeClusterIP,
+			ClusterIPs: []string{"10.0.0.10"},
+			Ports: []api.ServicePort{
+				{Name: "dns", Protocol: "udp", Port: 53},
+			},
+		},
+	},
 	"svc1.testns": {
 		{
 			Name:       "svc1",
@@ -673,6 +684,21 @@ func (APIConnServeTest) ServiceList() []*object.Service {
 }
 
 var epsIndex = map[string][]*object.Endpoints{
+	"kubedns.kube-system": {{
+		Subsets: []object.EndpointSubset{
+			{
+				Addresses: []object.EndpointAddress{
+					{IP: "172.0.0.100"},
+				},
+				Ports: []object.EndpointPort{
+					{Port: 53, Protocol: "udp", Name: "dns"},
+				},
+			},
+		},
+		Name:      "kubedns",
+		Namespace: "kube-system",
+		Index:     object.EndpointsKey("kubedns", "kube-system"),
+	}},
 	"svc1.testns": {{
 		Subsets: []object.EndpointSubset{
 			{
diff --git a/plugin/kubernetes/xfr_test.go b/plugin/kubernetes/xfr_test.go
index 39c4ed226..45044463b 100644
--- a/plugin/kubernetes/xfr_test.go
+++ b/plugin/kubernetes/xfr_test.go
@@ -1,6 +1,7 @@
 package kubernetes
 
 import (
+	"net"
 	"strings"
 	"testing"
 
@@ -10,7 +11,8 @@ import (
 func TestKubernetesAXFR(t *testing.T) {
 	k := New([]string{"cluster.local."})
 	k.APIConn = &APIConnServeTest{}
-	k.Namespaces = map[string]struct{}{"testns": {}}
+	k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}}
+	k.localIPs = []net.IP{net.ParseIP("10.0.0.10")}
 
 	dnsmsg := &dns.Msg{}
 	dnsmsg.SetAxfr(k.Zones[0])
@@ -25,7 +27,8 @@ func TestKubernetesAXFR(t *testing.T) {
 func TestKubernetesIXFRFallback(t *testing.T) {
 	k := New([]string{"cluster.local."})
 	k.APIConn = &APIConnServeTest{}
-	k.Namespaces = map[string]struct{}{"testns": {}}
+	k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}}
+	k.localIPs = []net.IP{net.ParseIP("10.0.0.10")}
 
 	dnsmsg := &dns.Msg{}
 	dnsmsg.SetAxfr(k.Zones[0])
@@ -40,7 +43,8 @@ func TestKubernetesIXFRFallback(t *testing.T) {
 func TestKubernetesIXFRCurrent(t *testing.T) {
 	k := New([]string{"cluster.local."})
 	k.APIConn = &APIConnServeTest{}
-	k.Namespaces = map[string]struct{}{"testns": {}}
+	k.Namespaces = map[string]struct{}{"testns": {}, "kube-system": {}}
+	k.localIPs = []net.IP{net.ParseIP("10.0.0.10")}
 
 	dnsmsg := &dns.Msg{}
 	dnsmsg.SetAxfr(k.Zones[0])
@@ -91,6 +95,8 @@ func validateAXFR(t *testing.T, ch <-chan []dns.RR) {
 
 const expectedZone = `
 cluster.local.	5	IN	SOA	ns.dns.cluster.local. hostmaster.cluster.local. 3 7200 1800 86400 5
+cluster.local.	5	IN	NS	ns.dns.cluster.local.
+ns.dns.cluster.local.	5	IN	A	10.0.0.10
 external.testns.svc.cluster.local.	5	IN	CNAME	ext.interwebs.test.
 external-to-service.testns.svc.cluster.local.	5	IN	CNAME	svc1.testns.svc.cluster.local.
 hdls1.testns.svc.cluster.local.	5	IN	A	172.0.0.2
@@ -113,6 +119,9 @@ hdls1.testns.svc.cluster.local.	5	IN	AAAA	5678:abcd::2
 _http._tcp.hdls1.testns.svc.cluster.local.	5	IN	SRV	0 16 80 5678-abcd--2.hdls1.testns.svc.cluster.local.
 hdlsprtls.testns.svc.cluster.local.	5	IN	A	172.0.0.20
 172-0-0-20.hdlsprtls.testns.svc.cluster.local.	5	IN	A	172.0.0.20
+kubedns.kube-system.svc.cluster.local.	5	IN	A	10.0.0.10
+kubedns.kube-system.svc.cluster.local.	5	IN	SRV	0 100 53 kubedns.kube-system.svc.cluster.local.
+_dns._udp.kubedns.kube-system.svc.cluster.local.	5	IN	SRV	0 100 53 kubedns.kube-system.svc.cluster.local.
 svc-dual-stack.testns.svc.cluster.local.	5	IN	A	10.0.0.3
 svc-dual-stack.testns.svc.cluster.local.	5	IN	AAAA	10::3
 svc-dual-stack.testns.svc.cluster.local.	5	IN	SRV	0 100 80 svc-dual-stack.testns.svc.cluster.local.