blob: 76bd32d138d7655f80d37ec3737e1667b02cf795 [file] [log] [blame]
package kubernetes
import (
"context"
"fmt"
"net/netip"
"slices"
"strings"
"testing"
"time"
"github.com/miekg/dns"
api "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"
netDNS "source.monogon.dev/osbase/net/dns"
)
const testdataClusterDomain = "cluster.local"
var testdataIPRanges = []string{
// service IP
"10.0.0.1/16",
"1234:abcd::/64",
// pod IP
"172.32.0.0/11",
"170::/14",
}
var testdataNamespaces = []string{"testns"}
var testdataServices = []*api.Service{
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-clusterip",
Namespace: "testns",
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeClusterIP,
ClusterIPs: []string{"10.0.0.10"},
Ports: []api.ServicePort{
{Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(82)},
},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-dualstack",
Namespace: "testns",
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeClusterIP,
ClusterIPs: []string{"10.0.0.11", "1234:abcd::11"},
Ports: []api.ServicePort{
{Name: "http", Protocol: api.ProtocolTCP, Port: 80},
{Name: "dns", Protocol: api.ProtocolUDP, Port: 53},
},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-headless",
Namespace: "testns",
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeClusterIP,
ClusterIP: api.ClusterIPNone,
ClusterIPs: []string{api.ClusterIPNone},
Ports: []api.ServicePort{
{Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromString("http")},
},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-headless-notready",
Namespace: "testns",
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeClusterIP,
ClusterIP: api.ClusterIPNone,
ClusterIPs: []string{api.ClusterIPNone},
Ports: []api.ServicePort{
{Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromString("http")},
},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-external",
Namespace: "testns",
},
Spec: api.ServiceSpec{
Type: api.ServiceTypeExternalName,
ExternalName: "external.example.com",
},
},
}
var testdataEndpointSlices = []*discovery.EndpointSlice{
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-clusterip-slice",
Namespace: "testns",
Labels: map[string]string{discovery.LabelServiceName: "svc-clusterip"},
},
Endpoints: []discovery.Endpoint{
{Addresses: []string{"172.45.0.1"}},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-headless-slice1",
Namespace: "testns",
Labels: map[string]string{
discovery.LabelServiceName: "svc-headless",
api.IsHeadlessService: "",
},
},
Endpoints: []discovery.Endpoint{
{
Addresses: []string{"172.45.0.2"},
},
{
Addresses: []string{"172.45.0.2"},
},
{
Hostname: ptr.To("pod3"),
Addresses: []string{"172.45.0.3"},
Conditions: discovery.EndpointConditions{Ready: ptr.To(true)},
},
{
Addresses: []string{"172.45.0.4"},
Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
},
{
Hostname: ptr.To("pod5"),
Addresses: []string{"172.45.0.5", "172.45.0.2"},
},
},
Ports: []discovery.EndpointPort{
{Name: ptr.To("http"), Port: ptr.To(int32(8000)), Protocol: ptr.To(api.ProtocolTCP)},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-headless-slice2",
Namespace: "testns",
Labels: map[string]string{
discovery.LabelServiceName: "svc-headless",
api.IsHeadlessService: "",
},
},
Endpoints: []discovery.Endpoint{
{
Hostname: ptr.To("pod3"),
Addresses: []string{"172::3"},
Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
},
{
Hostname: ptr.To("pod5"),
Addresses: []string{"172::5"},
},
{
Addresses: []string{"172::7"},
},
},
Ports: []discovery.EndpointPort{
{Name: ptr.To("http"), Port: ptr.To(int32(8001)), Protocol: ptr.To(api.ProtocolTCP)},
},
},
{
ObjectMeta: meta.ObjectMeta{
Name: "svc-headless-notready-slice1",
Namespace: "testns",
Labels: map[string]string{
discovery.LabelServiceName: "svc-headless-notready",
api.IsHeadlessService: "",
},
},
Endpoints: []discovery.Endpoint{
{
Addresses: []string{"172.45.0.20"},
Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
},
{
Hostname: ptr.To("pod21"),
Addresses: []string{"172.45.0.21"},
Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
},
},
Ports: []discovery.EndpointPort{
{Name: ptr.To("http"), Port: ptr.To(int32(8000)), Protocol: ptr.To(api.ProtocolTCP)},
},
},
}
// handlerTestcase contains a query name, and the expected records
// under that name given the above test data.
type handlerTestcase struct {
// Query name
qname string
// Expected reply
rcode int
answer, extra []string
notHandled bool
// zone is the zone that is expected in the NS SOA if the answer is empty.
// If zone is empty, defaults to "cluster.local."
zone string
}
// nameErrorIfSynced means name error if synced, else server failure.
const nameErrorIfSynced = -1
var handlerTestcases = []handlerTestcase{
// cluster domain root
{
qname: "cluster.local.",
answer: []string{
"cluster.local. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
"cluster.local. 5 IN NS ns.dns.cluster.local.",
},
},
{
qname: "example.cluster.local.",
rcode: dns.RcodeNameError,
},
// dns-version
{
qname: "dns-version.cluster.local.",
answer: []string{
`dns-version.cluster.local. 5 IN TXT "1.1.0"`,
},
},
{
qname: "example.dns-version.cluster.local.",
rcode: dns.RcodeNameError,
},
// ns.dns
{
qname: "dns.cluster.local.",
},
{
qname: "ns.dns.cluster.local.",
},
{
qname: "example.dns.cluster.local.",
rcode: dns.RcodeNameError,
},
// svc
{
qname: "svc.cluster.local.",
},
// namespace
{
qname: "testns.svc.cluster.local.",
},
{
qname: "inexistent-ns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
// cluster IP service
{
qname: "svc-clusterip.testns.svc.cluster.local.",
answer: []string{
"svc-clusterip.testns.svc.cluster.local. 5 IN A 10.0.0.10",
},
},
{
qname: "_http._tcp.svc-clusterip.testns.svc.cluster.local.",
answer: []string{
"_http._tcp.svc-clusterip.testns.svc.cluster.local. 5 IN SRV 0 0 80 svc-clusterip.testns.svc.cluster.local.",
},
extra: []string{
"svc-clusterip.testns.svc.cluster.local. 5 IN A 10.0.0.10",
},
},
{
qname: "_udp.svc-clusterip.testns.svc.cluster.local.",
},
{
qname: "_http._udp.svc-clusterip.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "http._tcp.svc-clusterip.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "example._http._tcp.svc-clusterip.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "10.0.0.10.in-addr.arpa.",
answer: []string{
"10.0.0.10.in-addr.arpa. 5 IN PTR svc-clusterip.testns.svc.cluster.local.",
},
zone: "0.10.in-addr.arpa.",
},
{
qname: "172-45-0-1.svc-clusterip.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "1.0.45.172.in-addr.arpa.",
rcode: nameErrorIfSynced,
zone: "45.172.in-addr.arpa.",
},
// dual stack cluster IP service
{
qname: "svc-dualstack.testns.svc.cluster.local.",
answer: []string{
"svc-dualstack.testns.svc.cluster.local. 5 IN A 10.0.0.11",
"svc-dualstack.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::11",
},
},
{
qname: "_http._tcp.svc-dualstack.testns.svc.cluster.local.",
answer: []string{
"_http._tcp.svc-dualstack.testns.svc.cluster.local. 5 IN SRV 0 0 80 svc-dualstack.testns.svc.cluster.local.",
},
extra: []string{
"svc-dualstack.testns.svc.cluster.local. 5 IN A 10.0.0.11",
"svc-dualstack.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::11",
},
},
{
qname: "11.0.0.10.in-addr.arpa.",
answer: []string{
"11.0.0.10.in-addr.arpa. 5 IN PTR svc-dualstack.testns.svc.cluster.local.",
},
zone: "0.10.in-addr.arpa.",
},
{
qname: "1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa.",
answer: []string{
"1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa. 5 IN PTR svc-dualstack.testns.svc.cluster.local.",
},
zone: "0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa.",
},
// headless service
{
qname: "svc-headless.testns.svc.cluster.local.",
answer: []string{
"svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
"svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.3",
"svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
"svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
"svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::7",
},
},
{
qname: "_http._tcp.svc-headless.testns.svc.cluster.local.",
answer: []string{
"_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 172-45-0-2.svc-headless.testns.svc.cluster.local.",
"_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 pod3.svc-headless.testns.svc.cluster.local.",
"_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 pod5.svc-headless.testns.svc.cluster.local.",
"_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8001 pod5.svc-headless.testns.svc.cluster.local.",
"_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8001 172--7.svc-headless.testns.svc.cluster.local.",
},
extra: []string{
"172-45-0-2.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
"pod3.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.3",
"pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
"pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
"pod5.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
"172--7.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::7",
},
},
{
qname: "_udp.svc-headless.testns.svc.cluster.local.",
},
{
qname: "_http._udp.svc-headless.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "http._tcp.svc-headless.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "_._udp.svc-headless.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "example._http._tcp.svc-headless.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "172-45-0-2.svc-headless.testns.svc.cluster.local.",
answer: []string{
"172-45-0-2.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
},
},
{
qname: "pod5.svc-headless.testns.svc.cluster.local.",
answer: []string{
"pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
"pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
"pod5.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
},
},
{
qname: "example.pod5.svc-headless.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "172-45-0-5.svc-headless.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "2.0.45.172.in-addr.arpa.",
answer: []string{
"2.0.45.172.in-addr.arpa. 5 IN PTR 172-45-0-2.svc-headless.testns.svc.cluster.local.",
"2.0.45.172.in-addr.arpa. 5 IN PTR pod5.svc-headless.testns.svc.cluster.local.",
},
zone: "45.172.in-addr.arpa.",
},
{
qname: "5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.7.1.0.ip6.arpa.",
answer: []string{
"5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.7.1.0.ip6.arpa. 5 IN PTR pod5.svc-headless.testns.svc.cluster.local.",
},
zone: "2.7.1.0.ip6.arpa.",
},
// not ready headless service
{
qname: "svc-headless-notready.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "_tcp.svc-headless-notready.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "_http._tcp.svc-headless-notready.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "pod21.svc-headless-notready.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "21.0.45.172.in-addr.arpa.",
rcode: nameErrorIfSynced,
zone: "45.172.in-addr.arpa.",
},
// external service
{
qname: "svc-external.testns.svc.cluster.local.",
answer: []string{
"svc-external.testns.svc.cluster.local. 5 IN CNAME external.example.com.",
},
},
{
qname: "_tcp.svc-external.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "_http._tcp.svc-external.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "pod.svc-external.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
// service does not exist
{
qname: "inexistent-svc.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "_tcp.inexistent-svc.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "_http._tcp.inexistent-svc.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "example._tcp.inexistent-svc.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "example._http._tcp.inexistent-svc.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "pod.inexistent-svc.testns.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "example.pod.inexistent-svc.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
// names which do not exist but will get queried because of ndots=5
{
qname: "www.example.com.cluster.local.",
rcode: dns.RcodeNameError,
},
{
qname: "www.example.com.svc.cluster.local.",
rcode: nameErrorIfSynced,
},
{
qname: "www.example.com.testns.svc.cluster.local.",
rcode: dns.RcodeNameError,
},
// names which are not handled
{
qname: "www.example.com.",
notHandled: true,
},
{
qname: "12.0.31.172.in-addr.arpa.",
notHandled: true,
},
{
qname: "5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.4.7.1.0.ip6.arpa.",
notHandled: true,
},
{
qname: "10.in-addr.arpa.",
notHandled: true,
},
{
qname: "7.1.0.ip6.arpa.",
notHandled: true,
},
// reverse lookup zone
{
qname: "45.172.in-addr.arpa.",
answer: []string{
"45.172.in-addr.arpa. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
"45.172.in-addr.arpa. 5 IN NS ns.dns.cluster.local.",
},
zone: "45.172.in-addr.arpa.",
},
{
qname: "255.45.172.in-addr.arpa.",
zone: "45.172.in-addr.arpa.",
},
{
qname: "02.0.45.172.in-addr.arpa.",
rcode: dns.RcodeNameError,
zone: "45.172.in-addr.arpa.",
},
{
qname: "1.2.0.45.172.in-addr.arpa.",
rcode: dns.RcodeNameError,
zone: "45.172.in-addr.arpa.",
},
{
qname: "2.7.1.0.ip6.arpa.",
answer: []string{
"2.7.1.0.ip6.arpa. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
"2.7.1.0.ip6.arpa. 5 IN NS ns.dns.cluster.local.",
},
zone: "2.7.1.0.ip6.arpa.",
},
{
qname: "a.2.7.1.0.ip6.arpa.",
zone: "2.7.1.0.ip6.arpa.",
},
{
qname: "x.a.2.7.1.0.ip6.arpa.",
rcode: dns.RcodeNameError,
zone: "2.7.1.0.ip6.arpa.",
},
// mixed case
{
qname: "SvC-cLUSteRIp.TesTNS.sVC.ClUSTer.locAL.",
answer: []string{
"SvC-cLUSteRIp.TesTNS.sVC.ClUSTer.locAL. 5 IN A 10.0.0.10",
},
zone: "ClUSTer.locAL.",
},
{
qname: "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
answer: []string{
"_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 172-45-0-2.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
"_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 pod3.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
"_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
"_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8001 pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
"_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8001 172--7.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
},
extra: []string{
"172-45-0-2.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.2",
"pod3.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.3",
"pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.5",
"pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.2",
"pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN AAAA 172::5",
"172--7.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN AAAA 172::7",
},
zone: "ClUSTer.locAL.",
},
{
qname: "1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.C.b.a.4.3.2.1.iP6.ARpa.",
answer: []string{
"1.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.d.C.b.a.4.3.2.1.iP6.ARpa. 5 IN PTR svc-dualstack.testns.svc.cluster.local.",
},
zone: "0.0.0.0.0.0.0.0.d.C.b.a.4.3.2.1.iP6.ARpa.",
},
}
// TestHandler constructs a fake Kubernetes clientset containing the above
// testdata, and then evaluates each test case in handlerTestcases.
func TestHandler(t *testing.T) {
ctx := context.Background()
client := fake.NewSimpleClientset()
// Add resources
for _, name := range testdataNamespaces {
namespace := &api.Namespace{
ObjectMeta: meta.ObjectMeta{Name: name},
}
_, err := client.CoreV1().Namespaces().Create(ctx, namespace, meta.CreateOptions{})
if err != nil {
t.Fatal(err)
}
}
for _, service := range testdataServices {
_, err := client.CoreV1().Services(service.Namespace).Create(ctx, service, meta.CreateOptions{})
if err != nil {
t.Fatal(err)
}
}
for _, slice := range testdataEndpointSlices {
_, err := client.DiscoveryV1().EndpointSlices(slice.Namespace).Create(ctx, slice, meta.CreateOptions{})
if err != nil {
t.Fatal(err)
}
}
// Create handler
var ipRanges []netip.Prefix
for _, ipRange := range testdataIPRanges {
ipRanges = append(ipRanges, netip.MustParsePrefix(ipRange))
}
handler := New(testdataClusterDomain, ipRanges)
handler.ClientSet = client
wrapper := &dnsControllerWrapper{dnsController: newdnsController(ctx, handler.ClientSet)}
handler.apiConn = wrapper
stopCh := make(chan struct{})
defer close(stopCh)
handler.apiConn.Start(stopCh)
for !wrapper.dnsController.HasSynced() {
time.Sleep(time.Millisecond)
}
for _, hasSynced := range []bool{true, false} {
wrapper.hasSynced = hasSynced
for _, testcase := range handlerTestcases {
if testcase.zone == "" {
testcase.zone = "cluster.local."
}
if testcase.rcode == nameErrorIfSynced {
if hasSynced {
testcase.rcode = dns.RcodeNameError
} else {
testcase.rcode = dns.RcodeServerFailure
}
}
qtypes := []uint16{
dns.TypeANY, dns.TypeA, dns.TypeAAAA, dns.TypeSRV, dns.TypeTXT,
dns.TypeNS, dns.TypeSOA, dns.TypePTR, dns.TypeMX, dns.TypeCNAME,
}
for _, qtype := range qtypes {
doHandlerTestcase(t, handler, testcase, qtype)
}
}
}
wrapper.hasSynced = false
testNotSyncedOpt(t, handler)
}
func doHandlerTestcase(t *testing.T, handler *Kubernetes, testcase handlerTestcase, qtype uint16) {
// Create request
req := netDNS.CreateTestRequest(testcase.qname, qtype, "udp")
req.Reply.RecursionDesired = false
req.Qopt = nil
req.Ropt = nil
handler.HandleDNS(req)
caseName := fmt.Sprintf("Query %s %s", testcase.qname, dns.TypeToString[qtype])
if !handler.apiConn.HasSynced() {
caseName += " not_synced"
}
if req.Handled != !testcase.notHandled {
t.Errorf("%s: Expected handled %v, got %v", caseName,
!testcase.notHandled, req.Handled,
)
return
}
if !req.Handled {
return
}
if req.Reply.Rcode != testcase.rcode {
t.Errorf("%s: Expected rcode %s, got %s", caseName,
dns.RcodeToString[testcase.rcode], dns.RcodeToString[req.Reply.Rcode],
)
return
}
// Create expected answer
var answer []string
for _, rr := range testcase.answer {
rrParsed, err := dns.NewRR(rr)
if err != nil {
t.Fatalf("Failed to parse DNS RR %q: %v", rr, err)
}
if qtype == dns.TypeANY || qtype == rrParsed.Header().Rrtype || rrParsed.Header().Rrtype == dns.TypeCNAME {
answer = append(answer, rr)
}
}
var extra []string
var ns []string
if len(answer) != 0 {
extra = testcase.extra
} else {
ns = []string{
testcase.zone + " 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
}
}
checkReplySection(t, caseName, "answer", answer, req.Reply.Answer)
checkReplySection(t, caseName, "ns", ns, req.Reply.Ns)
checkReplySection(t, caseName, "extra", extra, req.Reply.Extra)
}
func checkReplySection(t *testing.T, caseName string, sectionName string, expected []string, got []dns.RR) {
slices.Sort(expected)
var gotStr []string
for _, rr := range got {
gotStr = append(gotStr, rr.String())
}
slices.Sort(gotStr)
if !slices.Equal(expected, gotStr) {
t.Errorf("%s: Expected %s:\n%s\nGot:\n%v", caseName, sectionName,
strings.Join(expected, "\n"), strings.Join(gotStr, "\n"))
}
}
// testNotSyncedOpt tests that we get the Not Ready extended error
// when not synced and an OPT is present and no result was found.
func testNotSyncedOpt(t *testing.T, handler *Kubernetes) {
req := netDNS.CreateTestRequest("inexistent-ns.svc.cluster.local.", dns.TypeA, "udp")
handler.HandleDNS(req)
extra := []string{
"\n" +
";; OPT PSEUDOSECTION:\n" +
"; EDNS: version 0; flags:; udp: 1232\n" +
"; EDE: 14 (Not Ready): (Kubernetes objects not yet synced)",
}
checkReplySection(t, "testNotSyncedOpt", "extra", extra, req.Reply.Extra)
}
type dnsControllerWrapper struct {
dnsController
hasSynced bool
}
func (dns *dnsControllerWrapper) HasSynced() bool {
return dns.hasSynced
}
func (dns *dnsControllerWrapper) Modified() int64 {
return 12345
}