mirror of
https://github.com/coredns/coredns.git
synced 2026-10-26 20:25:43 -04:00
Merge commit from fork
EndpointSlice FQDN addresses were copied into endpoint records as if they were IP addresses. CoreDNS then treated each value as a CNAME target. A tenant could build a branching chain of headless Services that made one query perform exponential work and exhaust CoreDNS memory. Publish endpoints only from IPv4 and IPv6 slices and validate every address against its declared family. Deduplicate CNAME targets for the whole query so repeated or converging paths are chased once. Protect internal self-lookups with an independent depth limit. This stops direct and indirect k8s_external CNAME loops without consuming the loop counter already maintained by the file and rewrite plugins. Add regression coverage for address types, recursive convergence, repeated k8s_external targets, and direct and indirect self-reference. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/etcd/msg"
|
||||
"github.com/coredns/coredns/plugin/kubernetes"
|
||||
"github.com/coredns/coredns/plugin/kubernetes/object"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
@@ -101,6 +102,130 @@ func TestExternalCNAMENilUpstreamResponse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalCNAMETargetsChasedOnce(t *testing.T) {
|
||||
lookups := 0
|
||||
cfg := &dnsserver.Config{
|
||||
Zone: ".",
|
||||
Plugin: []plugin.Plugin{
|
||||
func(plugin.Handler) plugin.Handler {
|
||||
return plugin.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
lookups++
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
w.WriteMsg(m)
|
||||
return dns.RcodeSuccess, nil
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
server, err := dnsserver.NewServer("dns://:53", []*dnsserver.Config{cfg})
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), dnsserver.Key{}, server)
|
||||
|
||||
e := New()
|
||||
e.upstream = upstream.New()
|
||||
services := make([]msg.Service, 50)
|
||||
for i := range services {
|
||||
services[i] = msg.Service{Host: "target.example.org"}
|
||||
}
|
||||
|
||||
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
|
||||
lookups = 0
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("service.example.com.", qtype)
|
||||
state := request.Request{W: &test.ResponseWriter{}, Req: m}
|
||||
if qtype == dns.TypeA {
|
||||
e.a(ctx, services, state)
|
||||
} else {
|
||||
e.aaaa(ctx, services, state)
|
||||
}
|
||||
if lookups != 1 {
|
||||
t.Errorf("%s: expected one lookup for a repeated target, got %d", dns.TypeToString[qtype], lookups)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalCNAMESelfLookupIsBounded(t *testing.T) {
|
||||
e := New()
|
||||
e.Zones = []string{"example.com."}
|
||||
e.headless = true
|
||||
e.upstream = upstream.New()
|
||||
e.externalSerialFunc = externalSerial
|
||||
|
||||
calls := 0
|
||||
e.externalFunc = func(state request.Request, _ bool) ([]msg.Service, int) {
|
||||
calls++
|
||||
return []msg.Service{{Host: state.QName(), TTL: 5}}, dns.RcodeSuccess
|
||||
}
|
||||
|
||||
cfg := &dnsserver.Config{
|
||||
Zone: ".",
|
||||
Plugin: []plugin.Plugin{
|
||||
func(plugin.Handler) plugin.Handler { return e },
|
||||
},
|
||||
}
|
||||
server, err := dnsserver.NewServer("dns://:53", []*dnsserver.Config{cfg})
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), dnsserver.Key{}, server)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("service.example.com.", dns.TypeA)
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
server.ServeDNS(ctx, w, m)
|
||||
|
||||
if calls != 1 {
|
||||
t.Fatalf("expected a direct self-reference to stop immediately, got %d calls", calls)
|
||||
}
|
||||
if w.Msg == nil {
|
||||
t.Fatal("expected a response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalCNAMEIndirectLoopIsBounded(t *testing.T) {
|
||||
e := New()
|
||||
e.Zones = []string{"example.com."}
|
||||
e.headless = true
|
||||
e.upstream = upstream.New()
|
||||
|
||||
calls := 0
|
||||
e.externalFunc = func(state request.Request, _ bool) ([]msg.Service, int) {
|
||||
calls++
|
||||
target := "service.example.com."
|
||||
if state.Name() == target {
|
||||
target = "other.example.com."
|
||||
}
|
||||
return []msg.Service{{Host: target, TTL: 5}}, dns.RcodeSuccess
|
||||
}
|
||||
|
||||
cfg := &dnsserver.Config{
|
||||
Zone: ".",
|
||||
Plugin: []plugin.Plugin{
|
||||
func(plugin.Handler) plugin.Handler { return e },
|
||||
},
|
||||
}
|
||||
server, err := dnsserver.NewServer("dns://:53", []*dnsserver.Config{cfg})
|
||||
if err != nil {
|
||||
t.Fatalf("NewServer: %v", err)
|
||||
}
|
||||
ctx := context.WithValue(context.Background(), dnsserver.Key{}, server)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("service.example.com.", dns.TypeA)
|
||||
w := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
server.ServeDNS(ctx, w, m)
|
||||
|
||||
if calls != 11 {
|
||||
t.Fatalf("expected the initial query plus ten bounded self-lookups, got %d calls", calls)
|
||||
}
|
||||
if w.Msg == nil {
|
||||
t.Fatal("expected a response")
|
||||
}
|
||||
}
|
||||
|
||||
var tests = []test.Case{
|
||||
// PTR reverse lookup
|
||||
{
|
||||
|
||||
@@ -13,12 +13,26 @@ import (
|
||||
|
||||
func (e *External) a(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR, truncated bool) {
|
||||
dup := make(map[string]struct{})
|
||||
qname := ""
|
||||
|
||||
for _, s := range services {
|
||||
what, ip := s.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
// One chase per distinct target: every lookup re-enters the plugin
|
||||
// chain, and a headless Service can list the same name many times.
|
||||
target := dns.CanonicalName(s.Host)
|
||||
if qname == "" {
|
||||
qname = dns.CanonicalName(state.Name())
|
||||
}
|
||||
if target == qname {
|
||||
continue
|
||||
}
|
||||
if _, ok := dup[target]; ok {
|
||||
continue
|
||||
}
|
||||
dup[target] = struct{}{}
|
||||
rr := s.NewCNAME(state.QName(), s.Host)
|
||||
records = append(records, rr)
|
||||
if resp, err := e.upstream.Lookup(ctx, state, dns.Fqdn(s.Host), dns.TypeA); err == nil && resp != nil {
|
||||
@@ -45,12 +59,24 @@ func (e *External) a(ctx context.Context, services []msg.Service, state request.
|
||||
|
||||
func (e *External) aaaa(ctx context.Context, services []msg.Service, state request.Request) (records []dns.RR, truncated bool) {
|
||||
dup := make(map[string]struct{})
|
||||
qname := ""
|
||||
|
||||
for _, s := range services {
|
||||
what, ip := s.HostType()
|
||||
|
||||
switch what {
|
||||
case dns.TypeCNAME:
|
||||
target := dns.CanonicalName(s.Host)
|
||||
if qname == "" {
|
||||
qname = dns.CanonicalName(state.Name())
|
||||
}
|
||||
if target == qname {
|
||||
continue
|
||||
}
|
||||
if _, ok := dup[target]; ok {
|
||||
continue
|
||||
}
|
||||
dup[target] = struct{}{}
|
||||
rr := s.NewCNAME(state.QName(), s.Host)
|
||||
records = append(records, rr)
|
||||
if resp, err := e.upstream.Lookup(ctx, state, dns.Fqdn(s.Host), dns.TypeAAAA); err == nil && resp != nil {
|
||||
|
||||
Reference in New Issue
Block a user