Browse code

Add IPv6 nameserver to the internal DNS's upstreams.

When configuring the internal DNS resolver - rather than keep IPv6
nameservers read from the host's resolv.conf in the container's
resolv.conf, treat them like IPv4 addresses and use them as upstream
resolvers.

For IPv6 nameservers, if there's a zone identifier in the address or
the container itself doesn't have IPv6 support, mark the upstream
addresses for use in the host's network namespace.

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2024/03/06 19:47:18
Showing 11 changed files
... ...
@@ -237,7 +237,7 @@ func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
237 237
 // use in a network sandbox that has an internal DNS resolver.
238 238
 //   - Add internalNS as a nameserver.
239 239
 //   - Remove other nameservers, stashing them as ExtNameServers for the
240
-//     internal resolver to use. (Apart from IPv6 nameservers, if keepIPv6.)
240
+//     internal resolver to use.
241 241
 //   - Mark ExtNameServers that must be used in the host namespace.
242 242
 //   - If no ExtNameServer addresses are found, use the defaults.
243 243
 //   - Return an error if an "ndots" option inherited from the host's config, or
... ...
@@ -246,7 +246,7 @@ func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
246 246
 //     option includes a ':', and an option with a matching prefix exists, it
247 247
 //     is not modified.
248 248
 func (rc *ResolvConf) TransformForIntNS(
249
-	keepIPv6 bool,
249
+	ipv6 bool,
250 250
 	internalNS netip.Addr,
251 251
 	reqdOptions []string,
252 252
 ) ([]ExtDNSEntry, error) {
... ...
@@ -256,30 +256,16 @@ func (rc *ResolvConf) TransformForIntNS(
256 256
 	// internal nameserver.
257 257
 	rc.md.ExtNameServers = nil
258 258
 	for _, addr := range rc.nameServers {
259
-		// The internal resolver only uses IPv4 addresses so, keep IPv6 nameservers in
260
-		// the container's file if keepIPv6, else drop them.
261
-		if addr.Is6() {
262
-			if keepIPv6 {
263
-				newNSs = append(newNSs, addr)
264
-			}
265
-		} else {
266
-			// Extract this NS. Mark loopback addresses that did not come from an override as
267
-			// 'HostLoopback'. Upstream requests for these servers will be made in the host's
268
-			// network namespace. (So, '--dns 127.0.0.53' means use a nameserver listening on
269
-			// the container's loopback interface. But, if the host's resolv.conf contains
270
-			// 'nameserver 127.0.0.53', the host's resolver will be used.)
271
-			//
272
-			//  TODO(robmry) - why only loopback addresses?
273
-			//   Addresses from the host's resolv.conf must be usable in the host's namespace,
274
-			//   and a lookup from the container's namespace is more expensive? And, for
275
-			//   example, if the host has a nameserver with an IPv6 LL address with a zone-id,
276
-			//   it won't work from the container's namespace (now, while the address is left in
277
-			//   the container's resolv.conf, or in future for the internal resolver).
278
-			rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
279
-				Addr:         addr,
280
-				HostLoopback: addr.IsLoopback() && !rc.md.NSOverride,
281
-			})
282
-		}
259
+		// Extract this NS. Mark addresses that did not come from an override, but will
260
+		// definitely not work in the container's namespace as 'HostLoopback'. Upstream
261
+		// requests for these servers will be made in the host's network namespace. (So,
262
+		// '--dns 127.0.0.53' means use a nameserver listening on the container's
263
+		// loopback interface. But, if the host's resolv.conf contains 'nameserver
264
+		// 127.0.0.53', the host's resolver will be used.)
265
+		rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
266
+			Addr:         addr,
267
+			HostLoopback: !rc.md.NSOverride && (addr.IsLoopback() || (addr.Is6() && !ipv6) || addr.Zone() != ""),
268
+		})
283 269
 	}
284 270
 	rc.nameServers = newNSs
285 271
 
... ...
@@ -287,7 +273,7 @@ func (rc *ResolvConf) TransformForIntNS(
287 287
 	// internal resolver, use the defaults as ext nameservers.
288 288
 	if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
289 289
 		log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
290
-		for _, addr := range defaultNSAddrs(keepIPv6) {
290
+		for _, addr := range defaultNSAddrs(ipv6) {
291 291
 			rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
292 292
 		}
293 293
 		rc.md.UsedDefaultNS = true
... ...
@@ -351,16 +351,22 @@ func TestRCTransformForIntNS(t *testing.T) {
351 351
 			expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
352 352
 		},
353 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)},
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
+			},
358 361
 		},
359 362
 		{
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)},
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
+			},
364 370
 		},
365 371
 		{
366 372
 			name:          "IPv4 localhost",
... ...
@@ -384,37 +390,46 @@ func TestRCTransformForIntNS(t *testing.T) {
384 384
 			expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
385 385
 		},
386 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).
387
+			name:          "IPv6 addr, IPv6 enabled",
388
+			input:         "nameserver fd14:6e0e:f855::1",
389
+			ipv6:          true,
390
+			expExtServers: []ExtDNSEntry{mke("fd14:6e0e:f855::1", false)},
394 391
 		},
395 392
 		{
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)},
393
+			name:  "IPv4 and IPv6 localhost, IPv6 disabled",
394
+			input: "nameserver 127.0.0.53\nnameserver ::1",
395
+			ipv6:  false,
396
+			expExtServers: []ExtDNSEntry{
397
+				mke("127.0.0.53", true),
398
+				mke("::1", true),
399
+			},
400 400
 		},
401 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)},
402
+			name:  "IPv4 and IPv6 localhost, ipv6 enabled",
403
+			input: "nameserver 127.0.0.53\nnameserver ::1",
404
+			ipv6:  true,
405
+			expExtServers: []ExtDNSEntry{
406
+				mke("127.0.0.53", true),
407
+				mke("::1", true),
408
+			},
406 409
 		},
407 410
 		{
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)},
411
+			name:  "IPv4 localhost, IPv6 private, IPv6 enabled",
412
+			input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
413
+			ipv6:  true,
414
+			expExtServers: []ExtDNSEntry{
415
+				mke("127.0.0.53", true),
416
+				mke("fd3e:2d1a:1f5a::1", false),
417
+			},
412 418
 		},
413 419
 		{
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)},
420
+			name:  "IPv4 localhost, IPv6 private, IPv6 disabled",
421
+			input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
422
+			ipv6:  false,
423
+			expExtServers: []ExtDNSEntry{
424
+				mke("127.0.0.53", true),
425
+				mke("fd3e:2d1a:1f5a::1", true),
426
+			},
418 427
 		},
419 428
 		{
420 429
 			name:  "No host nameserver, no iv6",
... ...
@@ -1,5 +1,5 @@
1 1
 nameserver 127.0.0.11
2 2
 
3 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
4
-# ExtServers: [10.0.0.1]
4
+# ExtServers: [10.0.0.1 host(fdb6:b8fe:b528::1)]
5 5
 # Overrides: []
... ...
@@ -1,6 +1,5 @@
1 1
 nameserver 127.0.0.11
2
-nameserver fdb6:b8fe:b528::1
3 2
 
4 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
5
-# ExtServers: [10.0.0.1]
4
+# ExtServers: [10.0.0.1 fdb6:b8fe:b528::1]
6 5
 # Overrides: []
... ...
@@ -1,5 +1,5 @@
1 1
 nameserver 127.0.0.11
2 2
 
3 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
4
-# ExtServers: [host(127.0.0.53)]
4
+# ExtServers: [host(127.0.0.53) host(::1)]
5 5
 # Overrides: []
... ...
@@ -1,6 +1,5 @@
1 1
 nameserver 127.0.0.11
2
-nameserver ::1
3 2
 
4 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
5
-# ExtServers: [host(127.0.0.53)]
4
+# ExtServers: [host(127.0.0.53) host(::1)]
6 5
 # Overrides: []
... ...
@@ -1,5 +1,5 @@
1 1
 nameserver 127.0.0.11
2 2
 
3 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
4
-# ExtServers: [host(127.0.0.53)]
4
+# ExtServers: [host(127.0.0.53) host(fd3e:2d1a:1f5a::1)]
5 5
 # Overrides: []
... ...
@@ -1,6 +1,5 @@
1 1
 nameserver 127.0.0.11
2
-nameserver fd3e:2d1a:1f5a::1
3 2
 
4 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
5
-# ExtServers: [host(127.0.0.53)]
4
+# ExtServers: [host(127.0.0.53) fd3e:2d1a:1f5a::1]
6 5
 # Overrides: []
... ...
@@ -1,5 +1,5 @@
1 1
 nameserver 127.0.0.11
2
-nameserver fd14:6e0e:f855::1
3 2
 
4 3
 # Based on host file: '/etc/resolv.conf' (internal resolver)
4
+# ExtServers: [fd14:6e0e:f855::1]
5 5
 # Overrides: []
... ...
@@ -1838,7 +1838,7 @@ func TestResolvConf(t *testing.T) {
1838 1838
 			makeNet:          makeTestIPv6Network,
1839 1839
 			delNet:           true,
1840 1840
 			originResolvConf: "search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n",
1841
-			expResolvConf:    "nameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\nsearch pommesfrites.fr\noptions ndots:0",
1841
+			expResolvConf:    "nameserver 127.0.0.11\nsearch pommesfrites.fr\noptions ndots:0",
1842 1842
 		},
1843 1843
 		{
1844 1844
 			name:             "host network",
... ...
@@ -327,20 +327,17 @@ func (sb *Sandbox) rebuildDNS() error {
327 327
 		return err
328 328
 	}
329 329
 
330
-	// Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers
331
-	// will be left in the container's 'resolv.conf'.
332
-	// TODO(robmry) - preserving old behaviour, but ...
333
-	//   IPv6 nameservers should be treated like IPv4 ones, and used as upstream
334
-	//   servers for the internal resolver (if it has IPv6 connectivity). This
335
-	//   doesn't need to depend on whether there are currently any IPv6 endpoints.
336
-	//   Removing IPv6 nameservers from the container's resolv.conf will avoid the
337
-	//   problem that musl-libc's resolver tries all nameservers in parallel, so an
338
-	//   external IPv6 resolver can return NXDOMAIN before the internal resolver
339
-	//   returns the address of a container.
330
+	// Check for IPv6 endpoints in this sandbox. If there are any, and the container has
331
+	// IPv6 enabled, upstream requests from the internal DNS resolver can be made from
332
+	// the container's namespace.
333
+	// TODO(robmry) - this can only check networks connected when the resolver is set up,
334
+	//  the configuration won't be updated if the container gets an IPv6 address later.
340 335
 	ipv6 := false
341 336
 	for _, ep := range sb.endpoints {
342 337
 		if ep.network.enableIPv6 {
343
-			ipv6 = true
338
+			if en, ok := sb.ipv6Enabled(); ok {
339
+				ipv6 = en
340
+			}
344 341
 			break
345 342
 		}
346 343
 	}