github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/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.EquateComparable(netip.Addr{}))) 225 assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch)) 226 assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions)) 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))) 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{mke("10.0.0.1", false)}, 358 }, 359 { 360 name: "IPv4 and IPv6, ipv6 disabled", 361 input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", 362 ipv6: false, 363 expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)}, 364 }, 365 { 366 name: "IPv4 localhost", 367 input: "nameserver 127.0.0.53", 368 ipv6: false, 369 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 370 }, 371 { 372 // Overriding the nameserver with a localhost address means use the container's 373 // loopback interface, not the host's. 374 name: "IPv4 localhost override", 375 input: "nameserver 10.0.0.1", 376 ipv6: false, 377 overrideNS: []string{"127.0.0.53"}, 378 expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)}, 379 }, 380 { 381 name: "IPv4 localhost, ipv6 enabled", 382 input: "nameserver 127.0.0.53", 383 ipv6: true, 384 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 385 }, 386 { 387 name: "IPv6 addr, IPv6 enabled", 388 input: "nameserver fd14:6e0e:f855::1", 389 ipv6: true, 390 // Note that there are no ext servers in this case, the internal resolver 391 // will only look up container names. The default nameservers aren't added 392 // because the host's IPv6 nameserver remains in the container's resolv.conf, 393 // (because only IPv4 ext servers are currently allowed). 394 }, 395 { 396 name: "IPv4 and IPv6 localhost, IPv6 disabled", 397 input: "nameserver 127.0.0.53\nnameserver ::1", 398 ipv6: false, 399 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 400 }, 401 { 402 name: "IPv4 and IPv6 localhost, ipv6 enabled", 403 input: "nameserver 127.0.0.53\nnameserver ::1", 404 ipv6: true, 405 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 406 }, 407 { 408 name: "IPv4 localhost, IPv6 private, IPv6 enabled", 409 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 410 ipv6: true, 411 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 412 }, 413 { 414 name: "IPv4 localhost, IPv6 private, IPv6 disabled", 415 input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", 416 ipv6: false, 417 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 418 }, 419 { 420 name: "No host nameserver, no iv6", 421 input: "", 422 ipv6: false, 423 expExtServers: []ExtDNSEntry{ 424 mke("8.8.8.8", false), 425 mke("8.8.4.4", false), 426 }, 427 }, 428 { 429 name: "No host nameserver, iv6", 430 input: "", 431 ipv6: true, 432 expExtServers: []ExtDNSEntry{ 433 mke("8.8.8.8", false), 434 mke("8.8.4.4", false), 435 mke("2001:4860:4860::8888", false), 436 mke("2001:4860:4860::8844", false), 437 }, 438 }, 439 { 440 name: "ndots present and required", 441 input: "nameserver 127.0.0.53\noptions ndots:1", 442 reqdOptions: []string{"ndots:0"}, 443 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 444 }, 445 { 446 name: "ndots missing but required", 447 input: "nameserver 127.0.0.53", 448 reqdOptions: []string{"ndots:0"}, 449 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 450 }, 451 { 452 name: "ndots host, override and required", 453 input: "nameserver 127.0.0.53", 454 reqdOptions: []string{"ndots:0"}, 455 overrideOptions: []string{"ndots:2"}, 456 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 457 }, 458 { 459 name: "Extra required options", 460 input: "nameserver 127.0.0.53\noptions trust-ad", 461 reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"}, 462 expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)}, 463 }, 464 } 465 466 for _, tc := range testcases { 467 t.Run(tc.name, func(t *testing.T) { 468 tc := tc 469 rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf") 470 assert.NilError(t, err) 471 472 if tc.intNameServer == "" { 473 tc.intNameServer = "127.0.0.11" 474 } 475 if len(tc.overrideNS) > 0 { 476 rc.OverrideNameServers(s2a(tc.overrideNS)) 477 } 478 if len(tc.overrideOptions) > 0 { 479 rc.OverrideOptions(tc.overrideOptions) 480 } 481 intNS := netip.MustParseAddr(tc.intNameServer) 482 extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions) 483 if tc.expErr != "" { 484 assert.Check(t, is.ErrorContains(err, tc.expErr)) 485 return 486 } 487 assert.NilError(t, err) 488 489 d := t.TempDir() 490 path := filepath.Join(d, "resolv.conf") 491 err = rc.WriteFile(path, "", 0644) 492 assert.NilError(t, err) 493 494 content, err := os.ReadFile(path) 495 assert.NilError(t, err) 496 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 497 assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers, 498 cmpopts.EquateComparable(netip.Addr{}))) 499 }) 500 } 501 } 502 503 func TestRCRead(t *testing.T) { 504 d := t.TempDir() 505 path := filepath.Join(d, "resolv.conf") 506 507 // Try to read a nonexistent file, equivalent to an empty file. 508 _, err := Load(path) 509 assert.Check(t, is.ErrorIs(err, fs.ErrNotExist)) 510 511 err = os.WriteFile(path, []byte("options edns0"), 0644) 512 assert.NilError(t, err) 513 514 // Read that file in the constructor. 515 rc, err := Load(path) 516 assert.NilError(t, err) 517 assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"})) 518 519 // Pass in an os.File, check the path is extracted. 520 file, err := os.Open(path) 521 assert.NilError(t, err) 522 defer file.Close() 523 rc, err = Parse(file, "") 524 assert.NilError(t, err) 525 assert.Check(t, is.Equal(rc.md.SourcePath, path)) 526 } 527 528 func TestRCInvalidNS(t *testing.T) { 529 d := t.TempDir() 530 531 // A resolv.conf with an invalid nameserver address. 532 rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "") 533 assert.NilError(t, err) 534 535 path := filepath.Join(d, "resolv.conf") 536 err = rc.WriteFile(path, "", 0644) 537 assert.NilError(t, err) 538 539 content, err := os.ReadFile(path) 540 assert.NilError(t, err) 541 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 542 } 543 544 func TestRCSetHeader(t *testing.T) { 545 rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf") 546 assert.NilError(t, err) 547 548 rc.SetHeader("# This is a comment.") 549 d := t.TempDir() 550 path := filepath.Join(d, "resolv.conf") 551 err = rc.WriteFile(path, "", 0644) 552 assert.NilError(t, err) 553 554 content, err := os.ReadFile(path) 555 assert.NilError(t, err) 556 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 557 } 558 559 func TestRCUnknownDirectives(t *testing.T) { 560 const input = ` 561 something unexpected 562 nameserver 127.0.0.53 563 options ndots:1 564 unrecognised thing 565 ` 566 rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf") 567 assert.NilError(t, err) 568 569 d := t.TempDir() 570 path := filepath.Join(d, "resolv.conf") 571 err = rc.WriteFile(path, "", 0644) 572 assert.NilError(t, err) 573 574 content, err := os.ReadFile(path) 575 assert.NilError(t, err) 576 assert.Check(t, golden.String(string(content), t.Name()+".golden")) 577 }