mirror of
https://github.com/coredns/coredns.git
synced 2026-10-26 12:15:50 -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:
@@ -232,6 +232,15 @@ Kubernetes API.
|
||||
This plugin creates PTR records for every Pod selected by a Service. If a given Pod is selected by more than
|
||||
one Service a separate PTR record will exist for each Service selecting it.
|
||||
|
||||
## EndpointSlice Address Types
|
||||
|
||||
Endpoint records are served from EndpointSlices of address type `IPv4` and `IPv6` only. The
|
||||
[Kubernetes EndpointSlice API reference](https://kubernetes.io/docs/reference/kubernetes-api/discovery/endpoint-slice-v1/)
|
||||
documents `FQDN` as deprecated, defines no semantics for it, and states that kube-proxy only processes
|
||||
`IPv4` and `IPv6` slices. An FQDN slice carries names chosen by whoever wrote it rather than addresses,
|
||||
and the plugin publishes no records for it. A `CNAME` target for a Service is expressed with a Service
|
||||
of type `ExternalName`.
|
||||
|
||||
## Examples
|
||||
|
||||
Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster. Also handle all
|
||||
|
||||
@@ -145,7 +145,12 @@ func generateEndpointSlices(cidr string, svcType string, client kubernetes.Inter
|
||||
port := int32(80)
|
||||
protocol := api.Protocol("tcp")
|
||||
name := "http"
|
||||
addressType := discovery.AddressTypeIPv4
|
||||
if ip.To4() == nil {
|
||||
addressType = discovery.AddressTypeIPv6
|
||||
}
|
||||
eps := &discovery.EndpointSlice{
|
||||
AddressType: addressType,
|
||||
Ports: []discovery.EndpointPort{
|
||||
{
|
||||
Port: &port,
|
||||
|
||||
@@ -3,6 +3,7 @@ package object
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1"
|
||||
@@ -103,11 +104,35 @@ func endpointSliceToEndpoints(obj meta.Object, withZones bool) (meta.Object, err
|
||||
}
|
||||
}
|
||||
|
||||
// The Kubernetes DNS specification defines endpoint records for IP
|
||||
// addresses only. Other address types carry data that must not be treated
|
||||
// as an address record. In particular, treating an FQDN address as record
|
||||
// data turns it into a CNAME that CoreDNS chases through its plugin chain.
|
||||
// Keep the object so the Service still exists, but publish no addresses.
|
||||
switch ends.AddressType {
|
||||
case discovery.AddressTypeIPv4, discovery.AddressTypeIPv6:
|
||||
default:
|
||||
*ends = discovery.EndpointSlice{}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
for _, end := range ends.Endpoints {
|
||||
if !endpointsliceReady(end.Conditions.Ready) {
|
||||
continue
|
||||
}
|
||||
for _, a := range end.Addresses {
|
||||
// The API server validates each address against the slice's address
|
||||
// type. Retain the check here for non-conforming API implementations.
|
||||
ip, err := netip.ParseAddr(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ends.AddressType == discovery.AddressTypeIPv4 && !ip.Is4() {
|
||||
continue
|
||||
}
|
||||
if ends.AddressType == discovery.AddressTypeIPv6 && !ip.Is6() {
|
||||
continue
|
||||
}
|
||||
ea := EndpointAddress{IP: a}
|
||||
if end.Hostname != nil {
|
||||
ea.Hostname = *end.Hostname
|
||||
|
||||
65
plugin/kubernetes/object/endpoint_test.go
Normal file
65
plugin/kubernetes/object/endpoint_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package object
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
discovery "k8s.io/api/discovery/v1"
|
||||
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestEndpointSliceToEndpointsAddressTypes(t *testing.T) {
|
||||
ready := true
|
||||
tests := []struct {
|
||||
name string
|
||||
addrType discovery.AddressType
|
||||
addresses []string
|
||||
want []string
|
||||
}{
|
||||
{"ipv4", discovery.AddressTypeIPv4, []string{"172.0.0.1", "172.0.0.2"}, []string{"172.0.0.1", "172.0.0.2"}},
|
||||
{"ipv4 slices drop ipv6 addresses", discovery.AddressTypeIPv4, []string{"172.0.0.1", "fd00::1"}, []string{"172.0.0.1"}},
|
||||
{"ipv6", discovery.AddressTypeIPv6, []string{"fd00::1"}, []string{"fd00::1"}},
|
||||
{"ipv6 slices drop ipv4 addresses", discovery.AddressTypeIPv6, []string{"fd00::1", "172.0.0.1"}, []string{"fd00::1"}},
|
||||
{"fqdn slices publish no addresses", discovery.AddressTypeFQDN, []string{"other.ns.svc.cluster.local", "db.example.com"}, nil},
|
||||
{"unknown address types publish no addresses", discovery.AddressType("Future"), []string{"172.0.0.1"}, nil},
|
||||
{"non-ip address in an ip slice is dropped", discovery.AddressTypeIPv4, []string{"172.0.0.1", "not-an-ip"}, []string{"172.0.0.1"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
slice := &discovery.EndpointSlice{
|
||||
ObjectMeta: meta.ObjectMeta{Name: "svc1-abcde", Namespace: "testns", Labels: map[string]string{discovery.LabelServiceName: "svc1"}},
|
||||
AddressType: tc.addrType,
|
||||
Ports: []discovery.EndpointPort{{}},
|
||||
}
|
||||
for _, a := range tc.addresses {
|
||||
slice.Endpoints = append(slice.Endpoints, discovery.Endpoint{Addresses: []string{a}, Conditions: discovery.EndpointConditions{Ready: &ready}})
|
||||
}
|
||||
for _, convert := range []ToFunc{EndpointSliceToEndpoints, EndpointSliceToEndpointsWithZones} {
|
||||
obj, err := convert(slice.DeepCopy())
|
||||
if err != nil {
|
||||
t.Fatalf("conversion failed: %v", err)
|
||||
}
|
||||
e := obj.(*Endpoints)
|
||||
if e.Index != EndpointsKey("svc1", "testns") {
|
||||
t.Errorf("Index = %q, want %q", e.Index, EndpointsKey("svc1", "testns"))
|
||||
}
|
||||
var got []string
|
||||
for _, s := range e.Subsets {
|
||||
for _, a := range s.Addresses {
|
||||
got = append(got, a.IP)
|
||||
}
|
||||
}
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("addresses = %v, want %v", got, tc.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Errorf("address %d = %q, want %q", i, got[i], tc.want[i])
|
||||
}
|
||||
}
|
||||
if len(e.IndexIP) != len(tc.want) {
|
||||
t.Errorf("IndexIP = %v, want %v", e.IndexIP, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user