blob: 9b628fdedddf2d76589320c8306fcdcb56034df7 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Jan Schära48bd3c2024-07-29 17:22:18 +02004package kubernetes
5
6import (
7 "context"
8 "fmt"
9 "net/netip"
10 "slices"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/miekg/dns"
16 api "k8s.io/api/core/v1"
17 discovery "k8s.io/api/discovery/v1"
18 meta "k8s.io/apimachinery/pkg/apis/meta/v1"
19 "k8s.io/apimachinery/pkg/util/intstr"
20 "k8s.io/client-go/kubernetes/fake"
21 "k8s.io/utils/ptr"
22
23 netDNS "source.monogon.dev/osbase/net/dns"
24)
25
26const testdataClusterDomain = "cluster.local"
27
28var testdataIPRanges = []string{
29 // service IP
30 "10.0.0.1/16",
31 "1234:abcd::/64",
32 // pod IP
33 "172.32.0.0/11",
34 "170::/14",
35}
36
37var testdataNamespaces = []string{"testns"}
38
39var testdataServices = []*api.Service{
40 {
41 ObjectMeta: meta.ObjectMeta{
42 Name: "svc-clusterip",
43 Namespace: "testns",
44 },
45 Spec: api.ServiceSpec{
46 Type: api.ServiceTypeClusterIP,
47 ClusterIPs: []string{"10.0.0.10"},
48 Ports: []api.ServicePort{
49 {Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromInt32(82)},
50 },
51 },
52 },
53 {
54 ObjectMeta: meta.ObjectMeta{
55 Name: "svc-dualstack",
56 Namespace: "testns",
57 },
58 Spec: api.ServiceSpec{
59 Type: api.ServiceTypeClusterIP,
60 ClusterIPs: []string{"10.0.0.11", "1234:abcd::11"},
61 Ports: []api.ServicePort{
62 {Name: "http", Protocol: api.ProtocolTCP, Port: 80},
63 {Name: "dns", Protocol: api.ProtocolUDP, Port: 53},
64 },
65 },
66 },
67 {
68 ObjectMeta: meta.ObjectMeta{
69 Name: "svc-headless",
70 Namespace: "testns",
71 },
72 Spec: api.ServiceSpec{
73 Type: api.ServiceTypeClusterIP,
74 ClusterIP: api.ClusterIPNone,
75 ClusterIPs: []string{api.ClusterIPNone},
76 Ports: []api.ServicePort{
77 {Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromString("http")},
78 },
79 },
80 },
81 {
82 ObjectMeta: meta.ObjectMeta{
83 Name: "svc-headless-notready",
84 Namespace: "testns",
85 },
86 Spec: api.ServiceSpec{
87 Type: api.ServiceTypeClusterIP,
88 ClusterIP: api.ClusterIPNone,
89 ClusterIPs: []string{api.ClusterIPNone},
90 Ports: []api.ServicePort{
91 {Name: "http", Protocol: api.ProtocolTCP, Port: 80, TargetPort: intstr.FromString("http")},
92 },
93 },
94 },
95 {
96 ObjectMeta: meta.ObjectMeta{
97 Name: "svc-external",
98 Namespace: "testns",
99 },
100 Spec: api.ServiceSpec{
101 Type: api.ServiceTypeExternalName,
102 ExternalName: "external.example.com",
103 },
104 },
105}
106
107var testdataEndpointSlices = []*discovery.EndpointSlice{
108 {
109 ObjectMeta: meta.ObjectMeta{
110 Name: "svc-clusterip-slice",
111 Namespace: "testns",
112 Labels: map[string]string{discovery.LabelServiceName: "svc-clusterip"},
113 },
114 Endpoints: []discovery.Endpoint{
115 {Addresses: []string{"172.45.0.1"}},
116 },
117 },
118 {
119 ObjectMeta: meta.ObjectMeta{
120 Name: "svc-headless-slice1",
121 Namespace: "testns",
122 Labels: map[string]string{
123 discovery.LabelServiceName: "svc-headless",
124 api.IsHeadlessService: "",
125 },
126 },
127 Endpoints: []discovery.Endpoint{
128 {
129 Addresses: []string{"172.45.0.2"},
130 },
131 {
132 Addresses: []string{"172.45.0.2"},
133 },
134 {
135 Hostname: ptr.To("pod3"),
136 Addresses: []string{"172.45.0.3"},
137 Conditions: discovery.EndpointConditions{Ready: ptr.To(true)},
138 },
139 {
140 Addresses: []string{"172.45.0.4"},
141 Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
142 },
143 {
144 Hostname: ptr.To("pod5"),
145 Addresses: []string{"172.45.0.5", "172.45.0.2"},
146 },
147 },
148 Ports: []discovery.EndpointPort{
149 {Name: ptr.To("http"), Port: ptr.To(int32(8000)), Protocol: ptr.To(api.ProtocolTCP)},
150 },
151 },
152 {
153 ObjectMeta: meta.ObjectMeta{
154 Name: "svc-headless-slice2",
155 Namespace: "testns",
156 Labels: map[string]string{
157 discovery.LabelServiceName: "svc-headless",
158 api.IsHeadlessService: "",
159 },
160 },
161 Endpoints: []discovery.Endpoint{
162 {
163 Hostname: ptr.To("pod3"),
164 Addresses: []string{"172::3"},
165 Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
166 },
167 {
168 Hostname: ptr.To("pod5"),
169 Addresses: []string{"172::5"},
170 },
171 {
172 Addresses: []string{"172::7"},
173 },
174 },
175 Ports: []discovery.EndpointPort{
176 {Name: ptr.To("http"), Port: ptr.To(int32(8001)), Protocol: ptr.To(api.ProtocolTCP)},
177 },
178 },
179 {
180 ObjectMeta: meta.ObjectMeta{
181 Name: "svc-headless-notready-slice1",
182 Namespace: "testns",
183 Labels: map[string]string{
184 discovery.LabelServiceName: "svc-headless-notready",
185 api.IsHeadlessService: "",
186 },
187 },
188 Endpoints: []discovery.Endpoint{
189 {
190 Addresses: []string{"172.45.0.20"},
191 Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
192 },
193 {
194 Hostname: ptr.To("pod21"),
195 Addresses: []string{"172.45.0.21"},
196 Conditions: discovery.EndpointConditions{Ready: ptr.To(false)},
197 },
198 },
199 Ports: []discovery.EndpointPort{
200 {Name: ptr.To("http"), Port: ptr.To(int32(8000)), Protocol: ptr.To(api.ProtocolTCP)},
201 },
202 },
203}
204
205// handlerTestcase contains a query name, and the expected records
206// under that name given the above test data.
207type handlerTestcase struct {
208 // Query name
209 qname string
210
211 // Expected reply
212
213 rcode int
214 answer, extra []string
215 notHandled bool
216 // zone is the zone that is expected in the NS SOA if the answer is empty.
217 // If zone is empty, defaults to "cluster.local."
218 zone string
219}
220
221// nameErrorIfSynced means name error if synced, else server failure.
222const nameErrorIfSynced = -1
223
224var handlerTestcases = []handlerTestcase{
225 // cluster domain root
226 {
227 qname: "cluster.local.",
228 answer: []string{
229 "cluster.local. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
230 "cluster.local. 5 IN NS ns.dns.cluster.local.",
231 },
232 },
233 {
234 qname: "example.cluster.local.",
235 rcode: dns.RcodeNameError,
236 },
237 // dns-version
238 {
239 qname: "dns-version.cluster.local.",
240 answer: []string{
241 `dns-version.cluster.local. 5 IN TXT "1.1.0"`,
242 },
243 },
244 {
245 qname: "example.dns-version.cluster.local.",
246 rcode: dns.RcodeNameError,
247 },
248 // ns.dns
249 {
250 qname: "dns.cluster.local.",
251 },
252 {
253 qname: "ns.dns.cluster.local.",
254 },
255 {
256 qname: "example.dns.cluster.local.",
257 rcode: dns.RcodeNameError,
258 },
259 // svc
260 {
261 qname: "svc.cluster.local.",
262 },
263 // namespace
264 {
265 qname: "testns.svc.cluster.local.",
266 },
267 {
268 qname: "inexistent-ns.svc.cluster.local.",
269 rcode: nameErrorIfSynced,
270 },
271 // cluster IP service
272 {
273 qname: "svc-clusterip.testns.svc.cluster.local.",
274 answer: []string{
275 "svc-clusterip.testns.svc.cluster.local. 5 IN A 10.0.0.10",
276 },
277 },
278 {
279 qname: "_http._tcp.svc-clusterip.testns.svc.cluster.local.",
280 answer: []string{
281 "_http._tcp.svc-clusterip.testns.svc.cluster.local. 5 IN SRV 0 0 80 svc-clusterip.testns.svc.cluster.local.",
282 },
283 extra: []string{
284 "svc-clusterip.testns.svc.cluster.local. 5 IN A 10.0.0.10",
285 },
286 },
287 {
288 qname: "_udp.svc-clusterip.testns.svc.cluster.local.",
289 },
290 {
291 qname: "_http._udp.svc-clusterip.testns.svc.cluster.local.",
292 rcode: dns.RcodeNameError,
293 },
294 {
295 qname: "http._tcp.svc-clusterip.testns.svc.cluster.local.",
296 rcode: dns.RcodeNameError,
297 },
298 {
299 qname: "example._http._tcp.svc-clusterip.testns.svc.cluster.local.",
300 rcode: dns.RcodeNameError,
301 },
302 {
303 qname: "10.0.0.10.in-addr.arpa.",
304 answer: []string{
305 "10.0.0.10.in-addr.arpa. 5 IN PTR svc-clusterip.testns.svc.cluster.local.",
306 },
307 zone: "0.10.in-addr.arpa.",
308 },
309 {
310 qname: "172-45-0-1.svc-clusterip.testns.svc.cluster.local.",
311 rcode: dns.RcodeNameError,
312 },
313 {
314 qname: "1.0.45.172.in-addr.arpa.",
315 rcode: nameErrorIfSynced,
316 zone: "45.172.in-addr.arpa.",
317 },
318 // dual stack cluster IP service
319 {
320 qname: "svc-dualstack.testns.svc.cluster.local.",
321 answer: []string{
322 "svc-dualstack.testns.svc.cluster.local. 5 IN A 10.0.0.11",
323 "svc-dualstack.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::11",
324 },
325 },
326 {
327 qname: "_http._tcp.svc-dualstack.testns.svc.cluster.local.",
328 answer: []string{
329 "_http._tcp.svc-dualstack.testns.svc.cluster.local. 5 IN SRV 0 0 80 svc-dualstack.testns.svc.cluster.local.",
330 },
331 extra: []string{
332 "svc-dualstack.testns.svc.cluster.local. 5 IN A 10.0.0.11",
333 "svc-dualstack.testns.svc.cluster.local. 5 IN AAAA 1234:abcd::11",
334 },
335 },
336 {
337 qname: "11.0.0.10.in-addr.arpa.",
338 answer: []string{
339 "11.0.0.10.in-addr.arpa. 5 IN PTR svc-dualstack.testns.svc.cluster.local.",
340 },
341 zone: "0.10.in-addr.arpa.",
342 },
343 {
344 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.",
345 answer: []string{
346 "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.",
347 },
348 zone: "0.0.0.0.0.0.0.0.d.c.b.a.4.3.2.1.ip6.arpa.",
349 },
350 // headless service
351 {
352 qname: "svc-headless.testns.svc.cluster.local.",
353 answer: []string{
354 "svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
355 "svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.3",
356 "svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
357 "svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
358 "svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::7",
359 },
360 },
361 {
362 qname: "_http._tcp.svc-headless.testns.svc.cluster.local.",
363 answer: []string{
364 "_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 172-45-0-2.svc-headless.testns.svc.cluster.local.",
365 "_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 pod3.svc-headless.testns.svc.cluster.local.",
366 "_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8000 pod5.svc-headless.testns.svc.cluster.local.",
367 "_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8001 pod5.svc-headless.testns.svc.cluster.local.",
368 "_http._tcp.svc-headless.testns.svc.cluster.local. 5 IN SRV 0 0 8001 172--7.svc-headless.testns.svc.cluster.local.",
369 },
370 extra: []string{
371 "172-45-0-2.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
372 "pod3.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.3",
373 "pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
374 "pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
375 "pod5.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
376 "172--7.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::7",
377 },
378 },
379 {
380 qname: "_udp.svc-headless.testns.svc.cluster.local.",
381 },
382 {
383 qname: "_http._udp.svc-headless.testns.svc.cluster.local.",
384 rcode: nameErrorIfSynced,
385 },
386 {
387 qname: "http._tcp.svc-headless.testns.svc.cluster.local.",
388 rcode: dns.RcodeNameError,
389 },
390 {
391 qname: "_._udp.svc-headless.testns.svc.cluster.local.",
392 rcode: dns.RcodeNameError,
393 },
394 {
395 qname: "example._http._tcp.svc-headless.testns.svc.cluster.local.",
396 rcode: dns.RcodeNameError,
397 },
398 {
399 qname: "172-45-0-2.svc-headless.testns.svc.cluster.local.",
400 answer: []string{
401 "172-45-0-2.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
402 },
403 },
404 {
405 qname: "pod5.svc-headless.testns.svc.cluster.local.",
406 answer: []string{
407 "pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.5",
408 "pod5.svc-headless.testns.svc.cluster.local. 5 IN A 172.45.0.2",
409 "pod5.svc-headless.testns.svc.cluster.local. 5 IN AAAA 172::5",
410 },
411 },
412 {
413 qname: "example.pod5.svc-headless.testns.svc.cluster.local.",
414 rcode: dns.RcodeNameError,
415 },
416 {
417 qname: "172-45-0-5.svc-headless.testns.svc.cluster.local.",
418 rcode: nameErrorIfSynced,
419 },
420 {
421 qname: "2.0.45.172.in-addr.arpa.",
422 answer: []string{
423 "2.0.45.172.in-addr.arpa. 5 IN PTR 172-45-0-2.svc-headless.testns.svc.cluster.local.",
424 "2.0.45.172.in-addr.arpa. 5 IN PTR pod5.svc-headless.testns.svc.cluster.local.",
425 },
426 zone: "45.172.in-addr.arpa.",
427 },
428 {
429 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.",
430 answer: []string{
431 "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.",
432 },
433 zone: "2.7.1.0.ip6.arpa.",
434 },
435 // not ready headless service
436 {
437 qname: "svc-headless-notready.testns.svc.cluster.local.",
438 rcode: nameErrorIfSynced,
439 },
440 {
441 qname: "_tcp.svc-headless-notready.testns.svc.cluster.local.",
442 rcode: nameErrorIfSynced,
443 },
444 {
445 qname: "_http._tcp.svc-headless-notready.testns.svc.cluster.local.",
446 rcode: nameErrorIfSynced,
447 },
448 {
449 qname: "pod21.svc-headless-notready.testns.svc.cluster.local.",
450 rcode: nameErrorIfSynced,
451 },
452 {
453 qname: "21.0.45.172.in-addr.arpa.",
454 rcode: nameErrorIfSynced,
455 zone: "45.172.in-addr.arpa.",
456 },
457 // external service
458 {
459 qname: "svc-external.testns.svc.cluster.local.",
460 answer: []string{
461 "svc-external.testns.svc.cluster.local. 5 IN CNAME external.example.com.",
462 },
463 },
464 {
465 qname: "_tcp.svc-external.testns.svc.cluster.local.",
466 rcode: dns.RcodeNameError,
467 },
468 {
469 qname: "_http._tcp.svc-external.testns.svc.cluster.local.",
470 rcode: dns.RcodeNameError,
471 },
472 {
473 qname: "pod.svc-external.testns.svc.cluster.local.",
474 rcode: dns.RcodeNameError,
475 },
476 // service does not exist
477 {
478 qname: "inexistent-svc.testns.svc.cluster.local.",
479 rcode: nameErrorIfSynced,
480 },
481 {
482 qname: "_tcp.inexistent-svc.testns.svc.cluster.local.",
483 rcode: nameErrorIfSynced,
484 },
485 {
486 qname: "_http._tcp.inexistent-svc.testns.svc.cluster.local.",
487 rcode: nameErrorIfSynced,
488 },
489 {
490 qname: "example._tcp.inexistent-svc.testns.svc.cluster.local.",
491 rcode: dns.RcodeNameError,
492 },
493 {
494 qname: "example._http._tcp.inexistent-svc.testns.svc.cluster.local.",
495 rcode: dns.RcodeNameError,
496 },
497 {
498 qname: "pod.inexistent-svc.testns.svc.cluster.local.",
499 rcode: nameErrorIfSynced,
500 },
501 {
502 qname: "example.pod.inexistent-svc.testns.svc.cluster.local.",
503 rcode: dns.RcodeNameError,
504 },
505 // names which do not exist but will get queried because of ndots=5
506 {
507 qname: "www.example.com.cluster.local.",
508 rcode: dns.RcodeNameError,
509 },
510 {
511 qname: "www.example.com.svc.cluster.local.",
512 rcode: nameErrorIfSynced,
513 },
514 {
515 qname: "www.example.com.testns.svc.cluster.local.",
516 rcode: dns.RcodeNameError,
517 },
518 // names which are not handled
519 {
520 qname: "www.example.com.",
521 notHandled: true,
522 },
523 {
524 qname: "12.0.31.172.in-addr.arpa.",
525 notHandled: true,
526 },
527 {
528 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.",
529 notHandled: true,
530 },
531 {
532 qname: "10.in-addr.arpa.",
533 notHandled: true,
534 },
535 {
536 qname: "7.1.0.ip6.arpa.",
537 notHandled: true,
538 },
539 // reverse lookup zone
540 {
541 qname: "45.172.in-addr.arpa.",
542 answer: []string{
543 "45.172.in-addr.arpa. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
544 "45.172.in-addr.arpa. 5 IN NS ns.dns.cluster.local.",
545 },
546 zone: "45.172.in-addr.arpa.",
547 },
548 {
549 qname: "255.45.172.in-addr.arpa.",
550 zone: "45.172.in-addr.arpa.",
551 },
552 {
553 qname: "02.0.45.172.in-addr.arpa.",
554 rcode: dns.RcodeNameError,
555 zone: "45.172.in-addr.arpa.",
556 },
557 {
558 qname: "1.2.0.45.172.in-addr.arpa.",
559 rcode: dns.RcodeNameError,
560 zone: "45.172.in-addr.arpa.",
561 },
562 {
563 qname: "2.7.1.0.ip6.arpa.",
564 answer: []string{
565 "2.7.1.0.ip6.arpa. 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
566 "2.7.1.0.ip6.arpa. 5 IN NS ns.dns.cluster.local.",
567 },
568 zone: "2.7.1.0.ip6.arpa.",
569 },
570 {
571 qname: "a.2.7.1.0.ip6.arpa.",
572 zone: "2.7.1.0.ip6.arpa.",
573 },
574 {
575 qname: "x.a.2.7.1.0.ip6.arpa.",
576 rcode: dns.RcodeNameError,
577 zone: "2.7.1.0.ip6.arpa.",
578 },
579 // mixed case
580 {
581 qname: "SvC-cLUSteRIp.TesTNS.sVC.ClUSTer.locAL.",
582 answer: []string{
583 "SvC-cLUSteRIp.TesTNS.sVC.ClUSTer.locAL. 5 IN A 10.0.0.10",
584 },
585 zone: "ClUSTer.locAL.",
586 },
587 {
588 qname: "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
589 answer: []string{
590 "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 172-45-0-2.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
591 "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 pod3.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
592 "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8000 pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
593 "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8001 pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
594 "_hTTp._tCp.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN SRV 0 0 8001 172--7.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL.",
595 },
596 extra: []string{
597 "172-45-0-2.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.2",
598 "pod3.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.3",
599 "pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.5",
600 "pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN A 172.45.0.2",
601 "pod5.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN AAAA 172::5",
602 "172--7.SVc-hEADlEsS.teSTNs.SVC.ClUSTer.locAL. 5 IN AAAA 172::7",
603 },
604 zone: "ClUSTer.locAL.",
605 },
606 {
607 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.",
608 answer: []string{
609 "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.",
610 },
611 zone: "0.0.0.0.0.0.0.0.d.C.b.a.4.3.2.1.iP6.ARpa.",
612 },
613}
614
615// TestHandler constructs a fake Kubernetes clientset containing the above
616// testdata, and then evaluates each test case in handlerTestcases.
617func TestHandler(t *testing.T) {
618 ctx := context.Background()
619 client := fake.NewSimpleClientset()
620
621 // Add resources
622 for _, name := range testdataNamespaces {
623 namespace := &api.Namespace{
624 ObjectMeta: meta.ObjectMeta{Name: name},
625 }
626 _, err := client.CoreV1().Namespaces().Create(ctx, namespace, meta.CreateOptions{})
627 if err != nil {
628 t.Fatal(err)
629 }
630 }
631 for _, service := range testdataServices {
632 _, err := client.CoreV1().Services(service.Namespace).Create(ctx, service, meta.CreateOptions{})
633 if err != nil {
634 t.Fatal(err)
635 }
636 }
637 for _, slice := range testdataEndpointSlices {
638 _, err := client.DiscoveryV1().EndpointSlices(slice.Namespace).Create(ctx, slice, meta.CreateOptions{})
639 if err != nil {
640 t.Fatal(err)
641 }
642 }
643
644 // Create handler
645 var ipRanges []netip.Prefix
646 for _, ipRange := range testdataIPRanges {
647 ipRanges = append(ipRanges, netip.MustParsePrefix(ipRange))
648 }
649 handler := New(testdataClusterDomain, ipRanges)
650 handler.ClientSet = client
651
652 wrapper := &dnsControllerWrapper{dnsController: newdnsController(ctx, handler.ClientSet)}
653 handler.apiConn = wrapper
654
655 stopCh := make(chan struct{})
656 defer close(stopCh)
657 handler.apiConn.Start(stopCh)
658 for !wrapper.dnsController.HasSynced() {
659 time.Sleep(time.Millisecond)
660 }
661
662 for _, hasSynced := range []bool{true, false} {
663 wrapper.hasSynced = hasSynced
664 for _, testcase := range handlerTestcases {
665 if testcase.zone == "" {
666 testcase.zone = "cluster.local."
667 }
668 if testcase.rcode == nameErrorIfSynced {
669 if hasSynced {
670 testcase.rcode = dns.RcodeNameError
671 } else {
672 testcase.rcode = dns.RcodeServerFailure
673 }
674 }
675
676 qtypes := []uint16{
677 dns.TypeANY, dns.TypeA, dns.TypeAAAA, dns.TypeSRV, dns.TypeTXT,
678 dns.TypeNS, dns.TypeSOA, dns.TypePTR, dns.TypeMX, dns.TypeCNAME,
679 }
680 for _, qtype := range qtypes {
681 doHandlerTestcase(t, handler, testcase, qtype)
682 }
683 }
684 }
685
686 wrapper.hasSynced = false
687 testNotSyncedOpt(t, handler)
688}
689
690func doHandlerTestcase(t *testing.T, handler *Kubernetes, testcase handlerTestcase, qtype uint16) {
691 // Create request
692 req := netDNS.CreateTestRequest(testcase.qname, qtype, "udp")
693 req.Reply.RecursionDesired = false
694 req.Qopt = nil
695 req.Ropt = nil
696
697 handler.HandleDNS(req)
698
699 caseName := fmt.Sprintf("Query %s %s", testcase.qname, dns.TypeToString[qtype])
700 if !handler.apiConn.HasSynced() {
701 caseName += " not_synced"
702 }
703
704 if req.Handled != !testcase.notHandled {
705 t.Errorf("%s: Expected handled %v, got %v", caseName,
706 !testcase.notHandled, req.Handled,
707 )
708 return
709 }
710 if !req.Handled {
711 return
712 }
713
714 if req.Reply.Rcode != testcase.rcode {
715 t.Errorf("%s: Expected rcode %s, got %s", caseName,
716 dns.RcodeToString[testcase.rcode], dns.RcodeToString[req.Reply.Rcode],
717 )
718 return
719 }
720
721 // Create expected answer
722 var answer []string
723 for _, rr := range testcase.answer {
724 rrParsed, err := dns.NewRR(rr)
725 if err != nil {
726 t.Fatalf("Failed to parse DNS RR %q: %v", rr, err)
727 }
728 if qtype == dns.TypeANY || qtype == rrParsed.Header().Rrtype || rrParsed.Header().Rrtype == dns.TypeCNAME {
729 answer = append(answer, rr)
730 }
731 }
732 var extra []string
733 var ns []string
734 if len(answer) != 0 {
735 extra = testcase.extra
736 } else {
737 ns = []string{
738 testcase.zone + " 5 IN SOA ns.dns.cluster.local. nobody.invalid. 12345 7200 1800 86400 5",
739 }
740 }
741
742 checkReplySection(t, caseName, "answer", answer, req.Reply.Answer)
743 checkReplySection(t, caseName, "ns", ns, req.Reply.Ns)
744 checkReplySection(t, caseName, "extra", extra, req.Reply.Extra)
745}
746
747func checkReplySection(t *testing.T, caseName string, sectionName string, expected []string, got []dns.RR) {
748 slices.Sort(expected)
749 var gotStr []string
750 for _, rr := range got {
751 gotStr = append(gotStr, rr.String())
752 }
753 slices.Sort(gotStr)
754 if !slices.Equal(expected, gotStr) {
755 t.Errorf("%s: Expected %s:\n%s\nGot:\n%v", caseName, sectionName,
756 strings.Join(expected, "\n"), strings.Join(gotStr, "\n"))
757 }
758}
759
760// testNotSyncedOpt tests that we get the Not Ready extended error
761// when not synced and an OPT is present and no result was found.
762func testNotSyncedOpt(t *testing.T, handler *Kubernetes) {
763 req := netDNS.CreateTestRequest("inexistent-ns.svc.cluster.local.", dns.TypeA, "udp")
764
765 handler.HandleDNS(req)
766 extra := []string{
767 "\n" +
768 ";; OPT PSEUDOSECTION:\n" +
769 "; EDNS: version 0; flags:; udp: 1232\n" +
770 "; EDE: 14 (Not Ready): (Kubernetes objects not yet synced)",
771 }
772 checkReplySection(t, "testNotSyncedOpt", "extra", extra, req.Reply.Extra)
773}
774
775type dnsControllerWrapper struct {
776 dnsController
777 hasSynced bool
778}
779
780func (dns *dnsControllerWrapper) HasSynced() bool {
781 return dns.hasSynced
782}
783
784func (dns *dnsControllerWrapper) Modified() int64 {
785 return 12345
786}