From e80d696502863f2e35099e6e366bb00418c28d80 Mon Sep 17 00:00:00 2001 From: Chris O'Haver Date: Wed, 6 Jul 2022 13:55:15 -0400 Subject: [PATCH] plugin/k8s_external: Add support for PTR requests (#5435) * Exclude External IP addresses from being added to the existing kubernetes' plugin IP->Service index * Add support for PTR requests on External IPs of Services to the k8s_external plugin Signed-off-by: Chris O'Haver --- plugin/k8s_external/README.md | 5 +-- plugin/k8s_external/external.go | 2 ++ plugin/k8s_external/external_test.go | 47 ++++++++++++++++++++++++---- plugin/k8s_external/msg_to_dns.go | 14 +++++++++ plugin/k8s_external/transfer_test.go | 5 +++ plugin/kubernetes/controller.go | 34 +++++++++++++++++--- plugin/kubernetes/external.go | 37 ++++++++++++++++++++++ plugin/kubernetes/external_test.go | 1 + plugin/kubernetes/handler_test.go | 13 ++++---- plugin/kubernetes/kubernetes_test.go | 15 ++++----- plugin/kubernetes/ns_test.go | 17 +++++----- plugin/kubernetes/reverse_test.go | 17 +++++----- 12 files changed, 163 insertions(+), 44 deletions(-) diff --git a/plugin/k8s_external/README.md b/plugin/k8s_external/README.md index c7df91abc..7d75c360d 100644 --- a/plugin/k8s_external/README.md +++ b/plugin/k8s_external/README.md @@ -10,7 +10,7 @@ This plugin allows an additional zone to resolve the external IP address(es) of service. This plugin is only useful if the *kubernetes* plugin is also loaded. The plugin uses an external zone to resolve in-cluster IP addresses. It only handles queries for A, -AAAA and SRV records; all others result in NODATA responses. To make it a proper DNS zone, it handles +AAAA, SRV, and PTR records; all others result in NODATA responses. To make it a proper DNS zone, it handles SOA and NS queries for the apex of the zone. By default the apex of the zone will look like the following (assuming the zone used is `example.org`): @@ -101,6 +101,3 @@ zone transfers. Notifies are not supported. For some background see [resolve external IP address](https://github.com/kubernetes/dns/issues/242). And [A records for services with Load Balancer IP](https://github.com/coredns/coredns/issues/1851). -# Bugs - -PTR queries for the reverse zone is not supported. diff --git a/plugin/k8s_external/external.go b/plugin/k8s_external/external.go index 1096c2ea7..bbb844be7 100644 --- a/plugin/k8s_external/external.go +++ b/plugin/k8s_external/external.go @@ -105,6 +105,8 @@ func (e *External) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms m.Answer, m.Truncated = e.aaaa(ctx, svc, state) case dns.TypeSRV: m.Answer, m.Extra = e.srv(ctx, svc, state) + case dns.TypePTR: + m.Answer = e.ptr(svc, state) default: m.Ns = []dns.RR{e.soa(state)} } diff --git a/plugin/k8s_external/external_test.go b/plugin/k8s_external/external_test.go index 558c894f6..22ef08cc1 100644 --- a/plugin/k8s_external/external_test.go +++ b/plugin/k8s_external/external_test.go @@ -20,7 +20,7 @@ func TestExternal(t *testing.T) { k.APIConn = &external{} e := New() - e.Zones = []string{"example.com."} + e.Zones = []string{"example.com.", "in-addr.arpa."} e.Next = test.NextHandler(dns.RcodeSuccess, nil) e.externalFunc = k.External e.externalAddrFunc = externalAddress // internal test function @@ -49,12 +49,33 @@ func TestExternal(t *testing.T) { t.Error("Expected authoritative answer") } if err = test.SortAndCheck(resp, tc); err != nil { - t.Error(err) + t.Errorf("Test %d: %v", i, err) } } } var tests = []test.Case{ + // PTR reverse lookup + { + Qname: "4.3.2.1.in-addr.arpa.", Qtype: dns.TypePTR, Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.PTR("4.3.2.1.in-addr.arpa. 5 IN PTR svc1.testns.example.com."), + }, + }, + // Bad PTR reverse lookup using existing service name + { + Qname: "svc1.testns.example.com.", Qtype: dns.TypePTR, Rcode: dns.RcodeSuccess, + Ns: []dns.RR{ + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + }, + }, + // Bad PTR reverse lookup using non-existing service name + { + Qname: "not-existing.testns.example.com.", Qtype: dns.TypePTR, Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("example.com. 5 IN SOA ns1.dns.example.com. hostmaster.example.com. 1499347823 7200 1800 86400 5"), + }, + }, // A Service { Qname: "svc1.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, @@ -155,7 +176,7 @@ var tests = []test.Case{ { Qname: "svc11.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"), + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), }, }, { @@ -164,7 +185,7 @@ var tests = []test.Case{ test.SRV("_http._tcp.svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."), }, Extra: []dns.RR{ - test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"), + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), }, }, { @@ -173,7 +194,7 @@ var tests = []test.Case{ test.SRV("svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."), }, Extra: []dns.RR{ - test.A("svc11.testns.example.com. 5 IN A 1.2.3.4"), + test.A("svc11.testns.example.com. 5 IN A 2.3.4.5"), }, }, // svc12 @@ -211,6 +232,20 @@ func (external) GetNodeByName(ctx context.Context, name string) (*api.Node, erro func (external) SvcIndex(s string) []*object.Service { return svcIndexExternal[s] } func (external) PodIndex(string) []*object.Pod { return nil } +func (external) SvcExtIndexReverse(ip string) (result []*object.Service) { + for _, svcs := range svcIndexExternal { + for _, svc := range svcs { + for _, exIp := range svc.ExternalIPs { + if exIp != ip { + continue + } + result = append(result, svc) + } + } + } + return result +} + func (external) GetNamespaceByName(name string) (*object.Namespace, error) { return &object.Namespace{ Name: name, @@ -243,7 +278,7 @@ var svcIndexExternal = map[string][]*object.Service{ Name: "svc11", Namespace: "testns", Type: api.ServiceTypeLoadBalancer, - ExternalIPs: []string{"1.2.3.4"}, + ExternalIPs: []string{"2.3.4.5"}, Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, }, }, diff --git a/plugin/k8s_external/msg_to_dns.go b/plugin/k8s_external/msg_to_dns.go index 540c8f442..db859cf70 100644 --- a/plugin/k8s_external/msg_to_dns.go +++ b/plugin/k8s_external/msg_to_dns.go @@ -5,6 +5,7 @@ import ( "math" "github.com/coredns/coredns/plugin/etcd/msg" + "github.com/coredns/coredns/plugin/pkg/dnsutil" "github.com/coredns/coredns/request" "github.com/miekg/dns" @@ -76,6 +77,19 @@ func (e *External) aaaa(ctx context.Context, services []msg.Service, state reque return records, truncated } +func (e *External) ptr(services []msg.Service, state request.Request) (records []dns.RR) { + dup := make(map[string]struct{}) + for _, s := range services { + if _, ok := dup[s.Host]; !ok { + dup[s.Host] = struct{}{} + rr := s.NewPTR(state.QName(), dnsutil.Join(s.Host, e.Zones[0])) + rr.Hdr.Ttl = e.ttl + records = append(records, rr) + } + } + return records +} + func (e *External) srv(ctx context.Context, services []msg.Service, state request.Request) (records, extra []dns.RR) { dup := make(map[item]struct{}) diff --git a/plugin/k8s_external/transfer_test.go b/plugin/k8s_external/transfer_test.go index c55f14c1f..62d48be8a 100644 --- a/plugin/k8s_external/transfer_test.go +++ b/plugin/k8s_external/transfer_test.go @@ -59,6 +59,11 @@ func TestTransferAXFR(t *testing.T) { if ans.Header().Rrtype == dns.TypeTXT { continue } + + // Exclude PTR records + if ans.Header().Rrtype == dns.TypePTR { + continue + } expect = append(expect, ans) } } diff --git a/plugin/kubernetes/controller.go b/plugin/kubernetes/controller.go index f2c349dba..f8c2ee144 100644 --- a/plugin/kubernetes/controller.go +++ b/plugin/kubernetes/controller.go @@ -25,6 +25,7 @@ const ( podIPIndex = "PodIP" svcNameNamespaceIndex = "ServiceNameNamespace" svcIPIndex = "ServiceIP" + svcExtIPIndex = "ServiceExternalIP" epNameNamespaceIndex = "EndpointNameNamespace" epIPIndex = "EndpointsIP" ) @@ -34,6 +35,7 @@ type dnsController interface { EndpointsList() []*object.Endpoints SvcIndex(string) []*object.Service SvcIndexReverse(string) []*object.Service + SvcExtIndexReverse(string) []*object.Service PodIndex(string) []*object.Pod EpIndex(string) []*object.Endpoints EpIndexReverse(string) []*object.Endpoints @@ -122,7 +124,7 @@ func newdnsController(ctx context.Context, kubeClient kubernetes.Interface, opts }, &api.Service{}, cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, - cache.Indexers{svcNameNamespaceIndex: svcNameNamespaceIndexFunc, svcIPIndex: svcIPIndexFunc}, + cache.Indexers{svcNameNamespaceIndex: svcNameNamespaceIndexFunc, svcIPIndex: svcIPIndexFunc, svcExtIPIndex: svcExtIPIndexFunc}, object.DefaultProcessor(object.ToService, nil), ) @@ -232,12 +234,18 @@ func svcIPIndexFunc(obj interface{}) ([]string, error) { if !ok { return nil, errObj } - idx := make([]string, len(svc.ClusterIPs)+len(svc.ExternalIPs)) + idx := make([]string, len(svc.ClusterIPs)) copy(idx, svc.ClusterIPs) - if len(svc.ExternalIPs) == 0 { - return idx, nil + return idx, nil +} + +func svcExtIPIndexFunc(obj interface{}) ([]string, error) { + svc, ok := obj.(*object.Service) + if !ok { + return nil, errObj } - copy(idx[len(svc.ClusterIPs):], svc.ExternalIPs) + idx := make([]string, len(svc.ExternalIPs)) + copy(idx, svc.ExternalIPs) return idx, nil } @@ -502,6 +510,22 @@ func (dns *dnsControl) SvcIndexReverse(ip string) (svcs []*object.Service) { return svcs } +func (dns *dnsControl) SvcExtIndexReverse(ip string) (svcs []*object.Service) { + os, err := dns.svcLister.ByIndex(svcExtIPIndex, ip) + if err != nil { + return nil + } + + for _, o := range os { + s, ok := o.(*object.Service) + if !ok { + continue + } + svcs = append(svcs, s) + } + return svcs +} + func (dns *dnsControl) EpIndex(idx string) (ep []*object.Endpoints) { dns.epLock.RLock() defer dns.epLock.RUnlock() diff --git a/plugin/kubernetes/external.go b/plugin/kubernetes/external.go index 74e7151fc..702bdc30c 100644 --- a/plugin/kubernetes/external.go +++ b/plugin/kubernetes/external.go @@ -14,6 +14,18 @@ import ( // External implements the ExternalFunc call from the external plugin. // It returns any services matching in the services' ExternalIPs. func (k *Kubernetes) External(state request.Request) ([]msg.Service, int) { + if state.QType() == dns.TypePTR { + ip := dnsutil.ExtractAddressFromReverse(state.Name()) + if ip != "" { + svcs, err := k.ExternalReverse(ip) + if err != nil { + return nil, dns.RcodeNameError + } + return svcs, dns.RcodeSuccess + } + // for invalid reverse names, fall through to determine proper nxdomain/nodata response + } + base, _ := dnsutil.TrimZone(state.Name(), state.Zone) segs := dns.SplitDomainName(base) @@ -76,6 +88,10 @@ func (k *Kubernetes) External(state request.Request) ([]msg.Service, int) { } } } + if state.QType() == dns.TypePTR { + // if this was a PTR request, return empty service list, but retain rcode for proper nxdomain/nodata response + return nil, rcode + } return services, rcode } @@ -111,3 +127,24 @@ func (k *Kubernetes) ExternalServices(zone string) (services []msg.Service) { func (k *Kubernetes) ExternalSerial(string) uint32 { return uint32(k.APIConn.Modified(true)) } + +// ExternalReverse does a reverse lookup for the external IPs +func (k *Kubernetes) ExternalReverse(ip string) ([]msg.Service, error) { + records := k.serviceRecordForExternalIP(ip) + if len(records) == 0 { + return records, errNoItems + } + return records, nil +} + +func (k *Kubernetes) serviceRecordForExternalIP(ip string) []msg.Service { + var svcs []msg.Service + for _, service := range k.APIConn.SvcExtIndexReverse(ip) { + if len(k.Namespaces) > 0 && !k.namespaceExposed(service.Namespace) { + continue + } + domain := strings.Join([]string{service.Name, service.Namespace}, ".") + svcs = append(svcs, msg.Service{Host: domain, TTL: k.ttl}) + } + return svcs +} diff --git a/plugin/kubernetes/external_test.go b/plugin/kubernetes/external_test.go index 28ccc608e..b0e89e000 100644 --- a/plugin/kubernetes/external_test.go +++ b/plugin/kubernetes/external_test.go @@ -80,6 +80,7 @@ func (external) Run() func (external) Stop() error { return nil } func (external) EpIndexReverse(string) []*object.Endpoints { return nil } func (external) SvcIndexReverse(string) []*object.Service { return nil } +func (external) SvcExtIndexReverse(string) []*object.Service { return nil } func (external) Modified(bool) int64 { return 0 } func (external) EpIndex(s string) []*object.Endpoints { return nil } func (external) EndpointsList() []*object.Endpoints { return nil } diff --git a/plugin/kubernetes/handler_test.go b/plugin/kubernetes/handler_test.go index d867f727a..d991f79d6 100644 --- a/plugin/kubernetes/handler_test.go +++ b/plugin/kubernetes/handler_test.go @@ -536,12 +536,13 @@ type APIConnServeTest struct { notSynced bool } -func (a APIConnServeTest) HasSynced() bool { return !a.notSynced } -func (APIConnServeTest) Run() {} -func (APIConnServeTest) Stop() error { return nil } -func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil } -func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil } -func (APIConnServeTest) Modified(bool) int64 { return int64(3) } +func (a APIConnServeTest) HasSynced() bool { return !a.notSynced } +func (APIConnServeTest) Run() {} +func (APIConnServeTest) Stop() error { return nil } +func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil } +func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnServeTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnServeTest) Modified(bool) int64 { return int64(3) } func (APIConnServeTest) PodIndex(ip string) []*object.Pod { if ip != "10.240.0.1" { diff --git a/plugin/kubernetes/kubernetes_test.go b/plugin/kubernetes/kubernetes_test.go index 9832fbd92..acdfd4c64 100644 --- a/plugin/kubernetes/kubernetes_test.go +++ b/plugin/kubernetes/kubernetes_test.go @@ -39,13 +39,14 @@ func TestEndpointHostname(t *testing.T) { type APIConnServiceTest struct{} -func (APIConnServiceTest) HasSynced() bool { return true } -func (APIConnServiceTest) Run() {} -func (APIConnServiceTest) Stop() error { return nil } -func (APIConnServiceTest) PodIndex(string) []*object.Pod { return nil } -func (APIConnServiceTest) SvcIndexReverse(string) []*object.Service { return nil } -func (APIConnServiceTest) EpIndexReverse(string) []*object.Endpoints { return nil } -func (APIConnServiceTest) Modified(bool) int64 { return 0 } +func (APIConnServiceTest) HasSynced() bool { return true } +func (APIConnServiceTest) Run() {} +func (APIConnServiceTest) Stop() error { return nil } +func (APIConnServiceTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnServiceTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnServiceTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnServiceTest) EpIndexReverse(string) []*object.Endpoints { return nil } +func (APIConnServiceTest) Modified(bool) int64 { return 0 } func (APIConnServiceTest) SvcIndex(string) []*object.Service { svcs := []*object.Service{ diff --git a/plugin/kubernetes/ns_test.go b/plugin/kubernetes/ns_test.go index ca69c7c3b..abc5fa935 100644 --- a/plugin/kubernetes/ns_test.go +++ b/plugin/kubernetes/ns_test.go @@ -14,14 +14,15 @@ import ( type APIConnTest struct{} -func (APIConnTest) HasSynced() bool { return true } -func (APIConnTest) Run() {} -func (APIConnTest) Stop() error { return nil } -func (APIConnTest) PodIndex(string) []*object.Pod { return nil } -func (APIConnTest) SvcIndexReverse(string) []*object.Service { return nil } -func (APIConnTest) EpIndex(string) []*object.Endpoints { return nil } -func (APIConnTest) EndpointsList() []*object.Endpoints { return nil } -func (APIConnTest) Modified(bool) int64 { return 0 } +func (APIConnTest) HasSynced() bool { return true } +func (APIConnTest) Run() {} +func (APIConnTest) Stop() error { return nil } +func (APIConnTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnTest) SvcIndexReverse(string) []*object.Service { return nil } +func (APIConnTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnTest) EpIndex(string) []*object.Endpoints { return nil } +func (APIConnTest) EndpointsList() []*object.Endpoints { return nil } +func (APIConnTest) Modified(bool) int64 { return 0 } func (a APIConnTest) SvcIndex(s string) []*object.Service { switch s { diff --git a/plugin/kubernetes/reverse_test.go b/plugin/kubernetes/reverse_test.go index aa21a9f86..b1835b3c8 100644 --- a/plugin/kubernetes/reverse_test.go +++ b/plugin/kubernetes/reverse_test.go @@ -15,14 +15,15 @@ import ( type APIConnReverseTest struct{} -func (APIConnReverseTest) HasSynced() bool { return true } -func (APIConnReverseTest) Run() {} -func (APIConnReverseTest) Stop() error { return nil } -func (APIConnReverseTest) PodIndex(string) []*object.Pod { return nil } -func (APIConnReverseTest) EpIndex(string) []*object.Endpoints { return nil } -func (APIConnReverseTest) EndpointsList() []*object.Endpoints { return nil } -func (APIConnReverseTest) ServiceList() []*object.Service { return nil } -func (APIConnReverseTest) Modified(bool) int64 { return 0 } +func (APIConnReverseTest) HasSynced() bool { return true } +func (APIConnReverseTest) Run() {} +func (APIConnReverseTest) Stop() error { return nil } +func (APIConnReverseTest) PodIndex(string) []*object.Pod { return nil } +func (APIConnReverseTest) EpIndex(string) []*object.Endpoints { return nil } +func (APIConnReverseTest) EndpointsList() []*object.Endpoints { return nil } +func (APIConnReverseTest) ServiceList() []*object.Service { return nil } +func (APIConnReverseTest) SvcExtIndexReverse(string) []*object.Service { return nil } +func (APIConnReverseTest) Modified(bool) int64 { return 0 } func (APIConnReverseTest) SvcIndex(svc string) []*object.Service { if svc != "svc1.testns" {