mirror of
https://github.com/coredns/coredns.git
synced 2026-10-26 20:25:43 -04:00
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>
239 lines
7.0 KiB
Go
239 lines
7.0 KiB
Go
package object
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/netip"
|
|
"strings"
|
|
|
|
discovery "k8s.io/api/discovery/v1"
|
|
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
// Endpoints is a stripped down api.Endpoints with only the items we need for CoreDNS.
|
|
type Endpoints struct {
|
|
// Don't add new fields to this struct without talking to the CoreDNS maintainers.
|
|
Version string
|
|
Name string
|
|
Namespace string
|
|
Index string
|
|
IndexIP []string
|
|
Subsets []EndpointSubset
|
|
// Zones maps address IPs to their topology.kubernetes.io/zone,
|
|
// lowercased. Nil unless the kubernetes plugin's `zonal` option
|
|
// selected the zone-retaining transform, so default configurations
|
|
// carry one nil pointer per slice and their addresses stay exactly
|
|
// as slim as before.
|
|
Zones map[string]string
|
|
|
|
*Empty
|
|
}
|
|
|
|
// EndpointSubset is a group of addresses with a common set of ports. The
|
|
// expanded set of endpoints is the Cartesian product of Addresses x Ports.
|
|
type EndpointSubset struct {
|
|
Addresses []EndpointAddress
|
|
Ports []EndpointPort
|
|
}
|
|
|
|
// EndpointAddress is a tuple that describes single IP address.
|
|
type EndpointAddress struct {
|
|
IP string
|
|
Hostname string
|
|
NodeName string
|
|
TargetRefName string
|
|
}
|
|
|
|
// EndpointPort is a tuple that describes a single port.
|
|
type EndpointPort struct {
|
|
Port int32
|
|
Name string
|
|
Protocol string
|
|
}
|
|
|
|
// EndpointsKey returns a string using for the index.
|
|
func EndpointsKey(name, namespace string) string { return name + "." + namespace }
|
|
|
|
// EndpointSliceToEndpoints converts a *discovery.EndpointSlice to a *Endpoints.
|
|
func EndpointSliceToEndpoints(obj meta.Object) (meta.Object, error) {
|
|
return endpointSliceToEndpoints(obj, false /* withZones */)
|
|
}
|
|
|
|
// EndpointSliceToEndpointsWithZones is EndpointSliceToEndpoints, but also
|
|
// retains each endpoint's topology zone. Used only when the kubernetes
|
|
// plugin's zonal option is enabled, so the default configuration's cache
|
|
// stays exactly as slim as before.
|
|
func EndpointSliceToEndpointsWithZones(obj meta.Object) (meta.Object, error) {
|
|
return endpointSliceToEndpoints(obj, true /* withZones */)
|
|
}
|
|
|
|
func endpointSliceToEndpoints(obj meta.Object, withZones bool) (meta.Object, error) {
|
|
ends, ok := obj.(*discovery.EndpointSlice)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected object %v", obj)
|
|
}
|
|
e := &Endpoints{
|
|
Version: ends.GetResourceVersion(),
|
|
Name: ends.GetName(),
|
|
Namespace: ends.GetNamespace(),
|
|
Index: EndpointsKey(ends.Labels[discovery.LabelServiceName], ends.GetNamespace()),
|
|
Subsets: make([]EndpointSubset, 1),
|
|
}
|
|
|
|
if len(ends.Ports) == 0 {
|
|
// Add sentinel if there are no ports.
|
|
e.Subsets[0].Ports = []EndpointPort{{Port: -1}}
|
|
} else {
|
|
e.Subsets[0].Ports = make([]EndpointPort, len(ends.Ports))
|
|
for k, p := range ends.Ports {
|
|
port := int32(-1)
|
|
name := ""
|
|
protocol := ""
|
|
if p.Port != nil {
|
|
port = *p.Port
|
|
}
|
|
if p.Name != nil {
|
|
name = *p.Name
|
|
}
|
|
if p.Protocol != nil {
|
|
protocol = string(*p.Protocol)
|
|
}
|
|
ep := EndpointPort{Port: port, Name: name, Protocol: protocol}
|
|
e.Subsets[0].Ports[k] = ep
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if withZones && end.Zone != nil {
|
|
if e.Zones == nil {
|
|
e.Zones = make(map[string]string)
|
|
}
|
|
// Lowercased once here: qnames arrive case-folded, so
|
|
// lookups compare without folding per query.
|
|
e.Zones[a] = strings.ToLower(*end.Zone)
|
|
}
|
|
// ignore pod names that are too long to be a valid label
|
|
if end.TargetRef != nil && len(end.TargetRef.Name) < 64 {
|
|
ea.TargetRefName = end.TargetRef.Name
|
|
}
|
|
if end.NodeName != nil {
|
|
ea.NodeName = *end.NodeName
|
|
}
|
|
e.Subsets[0].Addresses = append(e.Subsets[0].Addresses, ea)
|
|
e.IndexIP = append(e.IndexIP, a)
|
|
}
|
|
}
|
|
|
|
*ends = discovery.EndpointSlice{}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
func endpointsliceReady(ready *bool) bool {
|
|
// Per API docs: a nil value indicates an unknown state. In most cases consumers
|
|
// should interpret this unknown state as ready.
|
|
if ready == nil {
|
|
return true
|
|
}
|
|
return *ready
|
|
}
|
|
|
|
// CopyWithoutSubsets copies e, without the subsets.
|
|
func (e *Endpoints) CopyWithoutSubsets() *Endpoints {
|
|
e1 := &Endpoints{
|
|
Version: e.Version,
|
|
Name: e.Name,
|
|
Namespace: e.Namespace,
|
|
Index: e.Index,
|
|
IndexIP: make([]string, len(e.IndexIP)),
|
|
}
|
|
copy(e1.IndexIP, e.IndexIP)
|
|
return e1
|
|
}
|
|
|
|
var _ runtime.Object = &Endpoints{}
|
|
|
|
// DeepCopyObject implements the ObjectKind interface.
|
|
func (e *Endpoints) DeepCopyObject() runtime.Object {
|
|
e1 := &Endpoints{
|
|
Version: e.Version,
|
|
Name: e.Name,
|
|
Namespace: e.Namespace,
|
|
Index: e.Index,
|
|
IndexIP: make([]string, len(e.IndexIP)),
|
|
Subsets: make([]EndpointSubset, len(e.Subsets)),
|
|
}
|
|
copy(e1.IndexIP, e.IndexIP)
|
|
if e.Zones != nil {
|
|
e1.Zones = maps.Clone(e.Zones)
|
|
}
|
|
|
|
for i, eps := range e.Subsets {
|
|
sub := EndpointSubset{
|
|
Addresses: make([]EndpointAddress, len(eps.Addresses)),
|
|
Ports: make([]EndpointPort, len(eps.Ports)),
|
|
}
|
|
for j, a := range eps.Addresses {
|
|
ea := EndpointAddress{IP: a.IP, Hostname: a.Hostname, NodeName: a.NodeName, TargetRefName: a.TargetRefName}
|
|
sub.Addresses[j] = ea
|
|
}
|
|
for k, p := range eps.Ports {
|
|
ep := EndpointPort{Port: p.Port, Name: p.Name, Protocol: p.Protocol}
|
|
sub.Ports[k] = ep
|
|
}
|
|
e1.Subsets[i] = sub
|
|
}
|
|
return e1
|
|
}
|
|
|
|
// GetNamespace implements the metav1.Object interface.
|
|
func (e *Endpoints) GetNamespace() string { return e.Namespace }
|
|
|
|
// SetNamespace implements the metav1.Object interface.
|
|
func (e *Endpoints) SetNamespace(_namespace string) {}
|
|
|
|
// GetName implements the metav1.Object interface.
|
|
func (e *Endpoints) GetName() string { return e.Name }
|
|
|
|
// SetName implements the metav1.Object interface.
|
|
func (e *Endpoints) SetName(_name string) {}
|
|
|
|
// GetResourceVersion implements the metav1.Object interface.
|
|
func (e *Endpoints) GetResourceVersion() string { return e.Version }
|
|
|
|
// SetResourceVersion implements the metav1.Object interface.
|
|
func (e *Endpoints) SetResourceVersion(_version string) {}
|