github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+incompatible/libnetwork/internal/resolvconf/resolvconf_test.go (about) 1 package resolvconf 2 3 import ( 4 "bytes" 5 "io/fs" 6 "net/netip" 7 "os" 8 "path/filepath" 9 "runtime" 10 "strings" 11 "testing" 12 13 "github.com/docker/docker/internal/sliceutil" 14 "github.com/google/go-cmp/cmp/cmpopts" 15 "gotest.tools/v3/assert" 16 is "gotest.tools/v3/assert/cmp" 17 "gotest.tools/v3/golden" 18 ) 19 20 func TestRCOption(t *testing.T) { 21 testcases := []struct { 22 name string 23 options string 24 search string 25 expFound bool 26 expValue string 27 }{ 28 { 29 name: "Empty options", 30 options: "", 31 search: "ndots", 32 }, 33 { 34 name: "Not found", 35 options: "ndots:0 edns0", 36 search: "trust-ad", 37 }, 38 { 39 name: "Found with value", 40 options: "ndots:0 edns0", 41 search: "ndots", 42 expFound: true, 43 expValue: "0", 44 }, 45 { 46 name: "Found without value", 47 options: "ndots:0 edns0", 48 search: "edns0", 49 expFound: true, 50 expValue: "", 51 }, 52 { 53 name: "Found last value", 54 options: "ndots:0 edns0 ndots:1", 55 search: "ndots", 56 expFound: true, 57 expValue: "1", 58 }, 59 } 60 61 for _, tc := range testcases { 62 t.Run(tc.name, func(t *testing.T) { 63 rc, err := Parse(bytes.NewBuffer([]byte("options "+tc.options)), "") 64 assert.NilError(t, err) 65 value, found := rc.Option(tc.search) 66 assert.Check(t, is.Equal(found, tc.expFound)) 67 assert.Check(t, is.Equal(value, tc.expValue)) 68 }) 69 } 70 } 71 72 func TestRCWrite(t *testing.T) { 73 testcases := []struct { 74 name string 75 fileName string 76 perm os.FileMode 77 hashFileName string 78 modify bool 79 expUserModified bool 80 }{ 81 { 82 name: "Write with hash", 83 fileName: "testfile", 84 hashFileName: "testfile.hash", 85 }, 86 { 87 name: "Write with hash and modify", 88 fileName: "testfile", 89 hashFileName: "testfile.hash", 90 modify: true, 91 expUserModified: true, 92 }, 93 { 94 name: "Write without hash and modify", 95 fileName: "testfile", 96 modify: true, 97 expUserModified: false, 98 }, 99 { 100 name: "Write perm", 101 fileName: "testfile", 102 perm: 0640, 103 }, 104 } 105 106 rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4")), "") 107 assert.NilError(t, err) 108 109 for _, tc := range testcases { 110 t.Run(tc.name, func(t *testing.T) { 111 tc := tc 112 d := t.TempDir() 113 path := filepath.Join(d, tc.fileName) 114 var hashPath string 115 if tc.hashFileName != "" { 116 hashPath = filepath.Join(d, tc.hashFileName) 117 } 118 if tc.perm == 0 { 119 tc.perm = 0644 120 } 121 err := rc.WriteFile(path, hashPath, tc.perm) 122 assert.NilError(t, err) 123 124 fi, err := os.Stat(path) 125 assert.NilError(t, err) 126 // Windows files won't have the expected perms. 127 if runtime.GOOS != "windows" { 128 assert.Check(t, is.Equal(fi.Mode(), tc.perm)) 129 } 130 131 if tc.modify { 132 err := os.WriteFile(path, []byte("modified"), 0644) 133 assert.NilError(t, err) 134 } 135 136 um, err := UserModified(path, hashPath) 137 assert.NilError(t, err) 138 assert.Check(t, is.Equal(um, tc.expUserModified)) 139 }) 140 } 141 } 142 143 var a2s = sliceutil.Mapper(netip.Addr.String) 144 var s2a = sliceutil.Mapper(netip.MustParseAddr) 145 146 // Test that a resolv.conf file can be modified using OverrideXXX() methods 147 // to modify nameservers/search/options directives, and tha options can be 148 // added via AddOption(). 149 func TestRCModify(t *testing.T) { 150 testcases := []struct { 151 name string 152 inputNS []string 153 inputSearch []string 154 inputOptions []string 155 noOverrides bool // Whether to apply overrides (empty lists are valid overrides). 156 overrideNS []string 157 overrideSearch []string 158 overrideOptions []string 159 addOption string 160 }{ 161 { 162 name: "No content no overrides", 163 inputNS: []string{}, 164 }, 165 { 166 name: "No overrides", 167 noOverrides: true, 168 inputNS: []string{"1.2.3.4"}, 169 inputSearch: []string{"invalid"}, 170 inputOptions: []string{"ndots:0"}, 171 }, 172 { 173 name: "Empty overrides", 174 inputNS: []string{"1.2.3.4"}, 175 inputSearch: []string{"invalid"}, 176 inputOptions: []string{"ndots:0"}, 177 }, 178 { 179 name: "Overrides", 180 inputNS: []string{"1.2.3.4"}, 181 inputSearch: []string{"invalid"}, 182 inputOptions: []string{"ndots:0"}, 183 overrideNS: []string{"2.3.4.5", "fdba:acdd:587c::53"}, 184 overrideSearch: []string{"com", "invalid", "example"}, 185 overrideOptions: []string{"ndots:1", "edns0", "trust-ad"}, 186 }, 187 { 188 name: "Add option no overrides", 189 noOverrides: true, 190 inputNS: []string{"1.2.3.4"}, 191 inputSearch: []string{"invalid"}, 192 inputOptions: []string{"ndots:0"}, 193 addOption: "attempts:3", 194 }, 195 } 196 197 for _, tc := range testcases { 198 t.Run(tc.name, func(t *testing.T) { 199 tc := tc 200 var input string 201 if len(tc.inputNS) != 0 { 202 for _, ns := range tc.inputNS { 203 input += "nameserver " + ns + "\n" 204 } 205 } 206 if len(tc.inputSearch) != 0 { 207 input += "search " + strings.Join(tc.inputSearch, " ") + "\n" 208 } 209 if len(tc.inputOptions) != 0 { 210 input += "options " + strings.Join(tc.inputOptions, " ") + "\n" 211 } 212 rc, err := Parse(bytes.NewBuffer([]byte(input)), "") 213 assert.NilError(t, err) 214 assert.Check(t, is.DeepEqual(a2s(rc.NameServers()), tc.inputNS)) 215 assert.Check(t, is.DeepEqual(rc.Search(), tc.inputSearch)) 216 assert.Check(t, is.DeepEqual(rc.Options(), tc.inputOptions)) 217 218 if !tc.noOverrides { 219 overrideNS := s2a(tc.overrideNS) 220 rc.OverrideNameServers(overrideNS) 221 rc.OverrideSearch(tc.overrideSearch) 222 rc.OverrideOptions(tc.overrideOptions) 223 224 assert.Check(t, is.DeepEqual(rc.NameServers(), overrideNS, cmpopts.EquateEmpty(), cmpopts.EquateComparable(netip.Addr{}))) 225 assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch, cmpopts.EquateEmpty())) 226 assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions, cmpopts.EquateEmpty())) 227 } 228 229 if tc.addOption != "" { 230 options := rc.Options() 231 rc.AddOption(tc.addOption) 232 assert.Check(t, is.DeepEqual(rc.Options(), append(options, tc.addOption), cmpopts.EquateEmpty())) 233 } 234 235 d := t.TempDir() 236 path := filepath.Join(d, "resolv.conf") 237 err = rc.WriteFile(path, "", 0644) 238 assert.NilError(t, err) 239 240 content, err := os.ReadFile(path) 241 assert.NilError(t, err) 242 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 243 }) 244 } 245 } 246 247 func TestRCTransformForLegacyNw(t *testing.T) { 248 testcases := []struct { 249 name string 250 input string 251 ipv6 bool 252 overrideNS []string 253 }{ 254 { 255 name: "Routable IPv4 only", 256 input: "nameserver 10.0.0.1", 257 }, 258 { 259 name: "Routable IPv4 and IPv6, ipv6 enabled", 260 input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", 261 ipv6: true, 262 }, 263 { 264 name: "Routable IPv4 and IPv6, ipv6 disabled", 265 input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", 266 ipv6: false, 267 }, 268 { 269 name: "IPv4 localhost, ipv6 disabled", 270 input: "nameserver 127.0.0.53", 271 ipv6: false, 272 }, 273 { 274 name: "IPv4 localhost, ipv6 enabled", 275 input: "nameserver 127.0.0.53", 276 ipv6: true, 277 }, 278 { 279 name: "IPv4 and IPv6 localhost, ipv6 disabled", 280 input: "nameserver 127.0.0.53\nnameserver ::1", 281 ipv6: false, 282 }, 283 { 284 name: "IPv4 and IPv6 localhost, ipv6 enabled", 285 input: "nameserver 127.0.0.53\nnameserver ::1", 286 ipv6: true, 287 }, 288 { 289 name: "IPv4 localhost, IPv6 routeable, ipv6 enabled", 290 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 291 ipv6: true, 292 }, 293 { 294 name: "IPv4 localhost, IPv6 routeable, ipv6 disabled", 295 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 296 ipv6: false, 297 }, 298 { 299 name: "Override nameservers", 300 input: "nameserver 127.0.0.53", 301 overrideNS: []string{"127.0.0.1", "::1"}, 302 ipv6: false, 303 }, 304 } 305 306 for _, tc := range testcases { 307 t.Run(tc.name, func(t *testing.T) { 308 tc := tc 309 rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf") 310 assert.NilError(t, err) 311 if tc.overrideNS != nil { 312 rc.OverrideNameServers(s2a(tc.overrideNS)) 313 } 314 315 rc.TransformForLegacyNw(tc.ipv6) 316 317 d := t.TempDir() 318 path := filepath.Join(d, "resolv.conf") 319 err = rc.WriteFile(path, "", 0644) 320 assert.NilError(t, err) 321 322 content, err := os.ReadFile(path) 323 assert.NilError(t, err) 324 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 325 }) 326 } 327 } 328 329 func TestRCTransformForIntNS(t *testing.T) { 330 mke := func(addr string, hostLoopback bool) ExtDNSEntry { 331 return ExtDNSEntry{ 332 Addr: netip.MustParseAddr(addr), 333 HostLoopback: hostLoopback, 334 } 335 } 336 337 testcases := []struct { 338 name string 339 input string 340 intNameServer string 341 ipv6 bool 342 overrideNS []string 343 overrideOptions []string 344 reqdOptions []string 345 expExtServers []ExtDNSEntry 346 expErr string 347 }{ 348 { 349 name: "IPv4 only", 350 input: "nameserver 10.0.0.1", 351 expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)}, 352 }, 353 { 354 name: "IPv4 and IPv6, ipv6 enabled", 355 input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", 356 ipv6: true, 357 expExtServers: []ExtDNSEntry{ 358 mke("10.0.0.1", false), 359 mke("fdb6:b8fe:b528::1", false), 360 }, 361 }, 362 { 363 name: "IPv4 and IPv6, ipv6 disabled", 364 input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", 365 ipv6: false, 366 expExtServers: []ExtDNSEntry{ 367 mke("10.0.0.1", false), 368 mke("fdb6:b8fe:b528::1", true), 369 }, 370 }, 371 { 372 name: "IPv4 localhost", 373 input: "nameserver 127.0.0.53", 374 ipv6: false, 375 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 376 }, 377 { 378 // Overriding the nameserver with a localhost address means use the container's 379 // loopback interface, not the host's. 380 name: "IPv4 localhost override", 381 input: "nameserver 10.0.0.1", 382 ipv6: false, 383 overrideNS: []string{"127.0.0.53"}, 384 expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)}, 385 }, 386 { 387 name: "IPv4 localhost, ipv6 enabled", 388 input: "nameserver 127.0.0.53", 389 ipv6: true, 390 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 391 }, 392 { 393 name: "IPv6 addr, IPv6 enabled", 394 input: "nameserver fd14:6e0e:f855::1", 395 ipv6: true, 396 expExtServers: []ExtDNSEntry{mke("fd14:6e0e:f855::1", false)}, 397 }, 398 { 399 name: "IPv4 and IPv6 localhost, IPv6 disabled", 400 input: "nameserver 127.0.0.53\nnameserver ::1", 401 ipv6: false, 402 expExtServers: []ExtDNSEntry{ 403 mke("127.0.0.53", true), 404 mke("::1", true), 405 }, 406 }, 407 { 408 name: "IPv4 and IPv6 localhost, ipv6 enabled", 409 input: "nameserver 127.0.0.53\nnameserver ::1", 410 ipv6: true, 411 expExtServers: []ExtDNSEntry{ 412 mke("127.0.0.53", true), 413 mke("::1", true), 414 }, 415 }, 416 { 417 name: "IPv4 localhost, IPv6 private, IPv6 enabled", 418 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 419 ipv6: true, 420 expExtServers: []ExtDNSEntry{ 421 mke("127.0.0.53", true), 422 mke("fd3e:2d1a:1f5a::1", false), 423 }, 424 }, 425 { 426 name: "IPv4 localhost, IPv6 private, IPv6 disabled", 427 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 428 ipv6: false, 429 expExtServers: []ExtDNSEntry{ 430 mke("127.0.0.53", true), 431 mke("fd3e:2d1a:1f5a::1", true), 432 }, 433 }, 434 { 435 name: "No host nameserver, no iv6", 436 input: "", 437 ipv6: false, 438 expExtServers: []ExtDNSEntry{ 439 mke("8.8.8.8", false), 440 mke("8.8.4.4", false), 441 }, 442 }, 443 { 444 name: "No host nameserver, iv6", 445 input: "", 446 ipv6: true, 447 expExtServers: []ExtDNSEntry{ 448 mke("8.8.8.8", false), 449 mke("8.8.4.4", false), 450 mke("2001:4860:4860::8888", false), 451 mke("2001:4860:4860::8844", false), 452 }, 453 }, 454 { 455 name: "ndots present and required", 456 input: "nameserver 127.0.0.53\noptions ndots:1", 457 reqdOptions: []string{"ndots:0"}, 458 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 459 }, 460 { 461 name: "ndots missing but required", 462 input: "nameserver 127.0.0.53", 463 reqdOptions: []string{"ndots:0"}, 464 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 465 }, 466 { 467 name: "ndots host, override and required", 468 input: "nameserver 127.0.0.53", 469 reqdOptions: []string{"ndots:0"}, 470 overrideOptions: []string{"ndots:2"}, 471 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 472 }, 473 { 474 name: "Extra required options", 475 input: "nameserver 127.0.0.53\noptions trust-ad", 476 reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"}, 477 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 478 }, 479 } 480 481 for _, tc := range testcases { 482 t.Run(tc.name, func(t *testing.T) { 483 tc := tc 484 rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf") 485 assert.NilError(t, err) 486 487 if tc.intNameServer == "" { 488 tc.intNameServer = "127.0.0.11" 489 } 490 if len(tc.overrideNS) > 0 { 491 rc.OverrideNameServers(s2a(tc.overrideNS)) 492 } 493 if len(tc.overrideOptions) > 0 { 494 rc.OverrideOptions(tc.overrideOptions) 495 } 496 intNS := netip.MustParseAddr(tc.intNameServer) 497 extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions) 498 if tc.expErr != "" { 499 assert.Check(t, is.ErrorContains(err, tc.expErr)) 500 return 501 } 502 assert.NilError(t, err) 503 504 d := t.TempDir() 505 path := filepath.Join(d, "resolv.conf") 506 err = rc.WriteFile(path, "", 0644) 507 assert.NilError(t, err) 508 509 content, err := os.ReadFile(path) 510 assert.NilError(t, err) 511 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 512 assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers, 513 cmpopts.EquateComparable(netip.Addr{}))) 514 }) 515 } 516 } 517 518 // Check that invalid ndots options in the host's file are ignored, unless 519 // starting the internal resolver (which requires an ndots option), in which 520 // case invalid ndots should be replaced. 521 func TestRCTransformForIntNSInvalidNdots(t *testing.T) { 522 testcases := []struct { 523 name string 524 options string 525 reqdOptions []string 526 expVal string 527 expOptions []string 528 expNDotsFrom string 529 }{ 530 { 531 name: "Negative value", 532 options: "options ndots:-1", 533 expOptions: []string{"ndots:-1"}, 534 expVal: "-1", 535 expNDotsFrom: "host", 536 }, 537 { 538 name: "Invalid values with reqd ndots", 539 options: "options ndots:-1 foo:bar ndots ndots:", 540 reqdOptions: []string{"ndots:2"}, 541 expVal: "2", 542 expNDotsFrom: "internal", 543 expOptions: []string{"foo:bar", "ndots:2"}, 544 }, 545 { 546 name: "Valid value with reqd ndots", 547 options: "options ndots:1 foo:bar ndots ndots:", 548 reqdOptions: []string{"ndots:2"}, 549 expVal: "1", 550 expNDotsFrom: "host", 551 expOptions: []string{"ndots:1", "foo:bar"}, 552 }, 553 } 554 555 for _, tc := range testcases { 556 t.Run(tc.name, func(t *testing.T) { 557 content := "nameserver 8.8.8.8\n" + tc.options 558 rc, err := Parse(bytes.NewBuffer([]byte(content)), "/etc/resolv.conf") 559 assert.NilError(t, err) 560 _, err = rc.TransformForIntNS(false, netip.MustParseAddr("127.0.0.11"), tc.reqdOptions) 561 assert.NilError(t, err) 562 563 val, found := rc.Option("ndots") 564 assert.Check(t, is.Equal(found, true)) 565 assert.Check(t, is.Equal(val, tc.expVal)) 566 assert.Check(t, is.Equal(rc.md.NDotsFrom, tc.expNDotsFrom)) 567 assert.Check(t, is.DeepEqual(rc.options, tc.expOptions)) 568 }) 569 } 570 } 571 572 func TestRCRead(t *testing.T) { 573 d := t.TempDir() 574 path := filepath.Join(d, "resolv.conf") 575 576 // Try to read a nonexistent file, equivalent to an empty file. 577 _, err := Load(path) 578 assert.Check(t, is.ErrorIs(err, fs.ErrNotExist)) 579 580 err = os.WriteFile(path, []byte("options edns0"), 0644) 581 assert.NilError(t, err) 582 583 // Read that file in the constructor. 584 rc, err := Load(path) 585 assert.NilError(t, err) 586 assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"})) 587 588 // Pass in an os.File, check the path is extracted. 589 file, err := os.Open(path) 590 assert.NilError(t, err) 591 defer file.Close() 592 rc, err = Parse(file, "") 593 assert.NilError(t, err) 594 assert.Check(t, is.Equal(rc.md.SourcePath, path)) 595 } 596 597 func TestRCInvalidNS(t *testing.T) { 598 d := t.TempDir() 599 600 // A resolv.conf with an invalid nameserver address. 601 rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "") 602 assert.NilError(t, err) 603 604 path := filepath.Join(d, "resolv.conf") 605 err = rc.WriteFile(path, "", 0644) 606 assert.NilError(t, err) 607 608 content, err := os.ReadFile(path) 609 assert.NilError(t, err) 610 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 611 } 612 613 func TestRCSetHeader(t *testing.T) { 614 rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf") 615 assert.NilError(t, err) 616 617 rc.SetHeader("# This is a comment.") 618 d := t.TempDir() 619 path := filepath.Join(d, "resolv.conf") 620 err = rc.WriteFile(path, "", 0644) 621 assert.NilError(t, err) 622 623 content, err := os.ReadFile(path) 624 assert.NilError(t, err) 625 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 626 } 627 628 func TestRCUnknownDirectives(t *testing.T) { 629 const input = ` 630 something unexpected 631 nameserver 127.0.0.53 632 options ndots:1 633 unrecognised thing 634 ` 635 rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf") 636 assert.NilError(t, err) 637 638 d := t.TempDir() 639 path := filepath.Join(d, "resolv.conf") 640 err = rc.WriteFile(path, "", 0644) 641 assert.NilError(t, err) 642 643 content, err := os.ReadFile(path) 644 assert.NilError(t, err) 645 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 646 }