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