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 <cohaver@infoblox.com>
This commit is contained in:
Chris O'Haver 2022-07-06 13:55:15 -04:00 committed by GitHub
parent d903a963ee
commit e80d696502
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 163 additions and 44 deletions

View file

@ -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. 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, 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. 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`): 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). 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). 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.

View file

@ -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) m.Answer, m.Truncated = e.aaaa(ctx, svc, state)
case dns.TypeSRV: case dns.TypeSRV:
m.Answer, m.Extra = e.srv(ctx, svc, state) m.Answer, m.Extra = e.srv(ctx, svc, state)
case dns.TypePTR:
m.Answer = e.ptr(svc, state)
default: default:
m.Ns = []dns.RR{e.soa(state)} m.Ns = []dns.RR{e.soa(state)}
} }

View file

@ -20,7 +20,7 @@ func TestExternal(t *testing.T) {
k.APIConn = &external{} k.APIConn = &external{}
e := New() e := New()
e.Zones = []string{"example.com."} e.Zones = []string{"example.com.", "in-addr.arpa."}
e.Next = test.NextHandler(dns.RcodeSuccess, nil) e.Next = test.NextHandler(dns.RcodeSuccess, nil)
e.externalFunc = k.External e.externalFunc = k.External
e.externalAddrFunc = externalAddress // internal test function e.externalAddrFunc = externalAddress // internal test function
@ -49,12 +49,33 @@ func TestExternal(t *testing.T) {
t.Error("Expected authoritative answer") t.Error("Expected authoritative answer")
} }
if err = test.SortAndCheck(resp, tc); err != nil { if err = test.SortAndCheck(resp, tc); err != nil {
t.Error(err) t.Errorf("Test %d: %v", i, err)
} }
} }
} }
var tests = []test.Case{ 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 // A Service
{ {
Qname: "svc1.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, 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, Qname: "svc11.testns.example.com.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{ 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."), test.SRV("_http._tcp.svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."),
}, },
Extra: []dns.RR{ 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."), test.SRV("svc11.testns.example.com. 5 IN SRV 0 100 80 svc11.testns.example.com."),
}, },
Extra: []dns.RR{ 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 // 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) SvcIndex(s string) []*object.Service { return svcIndexExternal[s] }
func (external) PodIndex(string) []*object.Pod { return nil } 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) { func (external) GetNamespaceByName(name string) (*object.Namespace, error) {
return &object.Namespace{ return &object.Namespace{
Name: name, Name: name,
@ -243,7 +278,7 @@ var svcIndexExternal = map[string][]*object.Service{
Name: "svc11", Name: "svc11",
Namespace: "testns", Namespace: "testns",
Type: api.ServiceTypeLoadBalancer, Type: api.ServiceTypeLoadBalancer,
ExternalIPs: []string{"1.2.3.4"}, ExternalIPs: []string{"2.3.4.5"},
Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}}, Ports: []api.ServicePort{{Name: "http", Protocol: "tcp", Port: 80}},
}, },
}, },

View file

@ -5,6 +5,7 @@ import (
"math" "math"
"github.com/coredns/coredns/plugin/etcd/msg" "github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/pkg/dnsutil"
"github.com/coredns/coredns/request" "github.com/coredns/coredns/request"
"github.com/miekg/dns" "github.com/miekg/dns"
@ -76,6 +77,19 @@ func (e *External) aaaa(ctx context.Context, services []msg.Service, state reque
return records, truncated 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) { func (e *External) srv(ctx context.Context, services []msg.Service, state request.Request) (records, extra []dns.RR) {
dup := make(map[item]struct{}) dup := make(map[item]struct{})

View file

@ -59,6 +59,11 @@ func TestTransferAXFR(t *testing.T) {
if ans.Header().Rrtype == dns.TypeTXT { if ans.Header().Rrtype == dns.TypeTXT {
continue continue
} }
// Exclude PTR records
if ans.Header().Rrtype == dns.TypePTR {
continue
}
expect = append(expect, ans) expect = append(expect, ans)
} }
} }

View file

@ -25,6 +25,7 @@ const (
podIPIndex = "PodIP" podIPIndex = "PodIP"
svcNameNamespaceIndex = "ServiceNameNamespace" svcNameNamespaceIndex = "ServiceNameNamespace"
svcIPIndex = "ServiceIP" svcIPIndex = "ServiceIP"
svcExtIPIndex = "ServiceExternalIP"
epNameNamespaceIndex = "EndpointNameNamespace" epNameNamespaceIndex = "EndpointNameNamespace"
epIPIndex = "EndpointsIP" epIPIndex = "EndpointsIP"
) )
@ -34,6 +35,7 @@ type dnsController interface {
EndpointsList() []*object.Endpoints EndpointsList() []*object.Endpoints
SvcIndex(string) []*object.Service SvcIndex(string) []*object.Service
SvcIndexReverse(string) []*object.Service SvcIndexReverse(string) []*object.Service
SvcExtIndexReverse(string) []*object.Service
PodIndex(string) []*object.Pod PodIndex(string) []*object.Pod
EpIndex(string) []*object.Endpoints EpIndex(string) []*object.Endpoints
EpIndexReverse(string) []*object.Endpoints EpIndexReverse(string) []*object.Endpoints
@ -122,7 +124,7 @@ func newdnsController(ctx context.Context, kubeClient kubernetes.Interface, opts
}, },
&api.Service{}, &api.Service{},
cache.ResourceEventHandlerFuncs{AddFunc: dns.Add, UpdateFunc: dns.Update, DeleteFunc: dns.Delete}, 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), object.DefaultProcessor(object.ToService, nil),
) )
@ -232,12 +234,18 @@ func svcIPIndexFunc(obj interface{}) ([]string, error) {
if !ok { if !ok {
return nil, errObj return nil, errObj
} }
idx := make([]string, len(svc.ClusterIPs)+len(svc.ExternalIPs)) idx := make([]string, len(svc.ClusterIPs))
copy(idx, 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 return idx, nil
} }
@ -502,6 +510,22 @@ func (dns *dnsControl) SvcIndexReverse(ip string) (svcs []*object.Service) {
return svcs 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) { func (dns *dnsControl) EpIndex(idx string) (ep []*object.Endpoints) {
dns.epLock.RLock() dns.epLock.RLock()
defer dns.epLock.RUnlock() defer dns.epLock.RUnlock()

View file

@ -14,6 +14,18 @@ import (
// External implements the ExternalFunc call from the external plugin. // External implements the ExternalFunc call from the external plugin.
// It returns any services matching in the services' ExternalIPs. // It returns any services matching in the services' ExternalIPs.
func (k *Kubernetes) External(state request.Request) ([]msg.Service, int) { 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) base, _ := dnsutil.TrimZone(state.Name(), state.Zone)
segs := dns.SplitDomainName(base) 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 return services, rcode
} }
@ -111,3 +127,24 @@ func (k *Kubernetes) ExternalServices(zone string) (services []msg.Service) {
func (k *Kubernetes) ExternalSerial(string) uint32 { func (k *Kubernetes) ExternalSerial(string) uint32 {
return uint32(k.APIConn.Modified(true)) 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
}

View file

@ -80,6 +80,7 @@ func (external) Run()
func (external) Stop() error { return nil } func (external) Stop() error { return nil }
func (external) EpIndexReverse(string) []*object.Endpoints { return nil } func (external) EpIndexReverse(string) []*object.Endpoints { return nil }
func (external) SvcIndexReverse(string) []*object.Service { 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) Modified(bool) int64 { return 0 }
func (external) EpIndex(s string) []*object.Endpoints { return nil } func (external) EpIndex(s string) []*object.Endpoints { return nil }
func (external) EndpointsList() []*object.Endpoints { return nil } func (external) EndpointsList() []*object.Endpoints { return nil }

View file

@ -541,6 +541,7 @@ func (APIConnServeTest) Run() {}
func (APIConnServeTest) Stop() error { return nil } func (APIConnServeTest) Stop() error { return nil }
func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil } func (APIConnServeTest) EpIndexReverse(string) []*object.Endpoints { return nil }
func (APIConnServeTest) SvcIndexReverse(string) []*object.Service { 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) Modified(bool) int64 { return int64(3) }
func (APIConnServeTest) PodIndex(ip string) []*object.Pod { func (APIConnServeTest) PodIndex(ip string) []*object.Pod {

View file

@ -44,6 +44,7 @@ func (APIConnServiceTest) Run() {}
func (APIConnServiceTest) Stop() error { return nil } func (APIConnServiceTest) Stop() error { return nil }
func (APIConnServiceTest) PodIndex(string) []*object.Pod { return nil } func (APIConnServiceTest) PodIndex(string) []*object.Pod { return nil }
func (APIConnServiceTest) SvcIndexReverse(string) []*object.Service { 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) EpIndexReverse(string) []*object.Endpoints { return nil }
func (APIConnServiceTest) Modified(bool) int64 { return 0 } func (APIConnServiceTest) Modified(bool) int64 { return 0 }

View file

@ -19,6 +19,7 @@ func (APIConnTest) Run() {}
func (APIConnTest) Stop() error { return nil } func (APIConnTest) Stop() error { return nil }
func (APIConnTest) PodIndex(string) []*object.Pod { return nil } func (APIConnTest) PodIndex(string) []*object.Pod { return nil }
func (APIConnTest) SvcIndexReverse(string) []*object.Service { 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) EpIndex(string) []*object.Endpoints { return nil }
func (APIConnTest) EndpointsList() []*object.Endpoints { return nil } func (APIConnTest) EndpointsList() []*object.Endpoints { return nil }
func (APIConnTest) Modified(bool) int64 { return 0 } func (APIConnTest) Modified(bool) int64 { return 0 }

View file

@ -22,6 +22,7 @@ func (APIConnReverseTest) PodIndex(string) []*object.Pod { return nil }
func (APIConnReverseTest) EpIndex(string) []*object.Endpoints { return nil } func (APIConnReverseTest) EpIndex(string) []*object.Endpoints { return nil }
func (APIConnReverseTest) EndpointsList() []*object.Endpoints { return nil } func (APIConnReverseTest) EndpointsList() []*object.Endpoints { return nil }
func (APIConnReverseTest) ServiceList() []*object.Service { 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) Modified(bool) int64 { return 0 }
func (APIConnReverseTest) SvcIndex(svc string) []*object.Service { func (APIConnReverseTest) SvcIndex(svc string) []*object.Service {