Browse code

Update container resolv.conf when host network changes /etc/resolv.conf

Only modifies non-running containers resolv.conf bind mount, and only if
the container has an unmodified resolv.conf compared to its contents at
container start time (so we don't overwrite manual/automated changes
within the container runtime). For containers which are running when
the host resolv.conf changes, the update will only be applied to the
container version of resolv.conf when the container is "bounced" down
and back up (e.g. stop/start or restart)

Docker-DCO-1.1-Signed-off-by: Phil Estes <estesp@linux.vnet.ibm.com> (github: estesp)

Phil Estes authored on 2014/12/10 14:55:09
Showing 8 changed files
... ...
@@ -81,6 +81,7 @@ type Container struct {
81 81
 	MountLabel, ProcessLabel string
82 82
 	AppArmorProfile          string
83 83
 	RestartCount             int
84
+	UpdateDns                bool
84 85
 
85 86
 	// Maps container paths to volume paths.  The key in this is the path to which
86 87
 	// the volume is being mounted inside the container.  Value is the path of the
... ...
@@ -945,6 +946,29 @@ func (container *Container) DisableLink(name string) {
945 945
 
946 946
 func (container *Container) setupContainerDns() error {
947 947
 	if container.ResolvConfPath != "" {
948
+		// check if this is an existing container that needs DNS update:
949
+		if container.UpdateDns {
950
+			// read the host's resolv.conf, get the hash and call updateResolvConf
951
+			log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID)
952
+			latestResolvConf, latestHash := resolvconf.GetLastModified()
953
+
954
+			// because the new host resolv.conf might have localhost nameservers..
955
+			updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf)
956
+			if modified {
957
+				// changes have occurred during resolv.conf localhost cleanup: generate an updated hash
958
+				newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
959
+				if err != nil {
960
+					return err
961
+				}
962
+				latestHash = newHash
963
+			}
964
+
965
+			if err := container.updateResolvConf(updatedResolvConf, latestHash); err != nil {
966
+				return err
967
+			}
968
+			// successful update of the restarting container; set the flag off
969
+			container.UpdateDns = false
970
+		}
948 971
 		return nil
949 972
 	}
950 973
 
... ...
@@ -983,17 +1007,86 @@ func (container *Container) setupContainerDns() error {
983 983
 		}
984 984
 
985 985
 		// replace any localhost/127.* nameservers
986
-		resolvConf = utils.RemoveLocalDns(resolvConf)
987
-		// if the resulting resolvConf is empty, use DefaultDns
988
-		if !bytes.Contains(resolvConf, []byte("nameserver")) {
989
-			log.Infof("No non localhost DNS resolver found in resolv.conf and containers can't use it. Using default external servers : %v", DefaultDns)
990
-			// prefix the default dns options with nameserver
991
-			resolvConf = append(resolvConf, []byte("\nnameserver "+strings.Join(DefaultDns, "\nnameserver "))...)
992
-		}
986
+		resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf)
987
+	}
988
+	//get a sha256 hash of the resolv conf at this point so we can check
989
+	//for changes when the host resolv.conf changes (e.g. network update)
990
+	resolvHash, err := utils.HashData(bytes.NewReader(resolvConf))
991
+	if err != nil {
992
+		return err
993
+	}
994
+	resolvHashFile := container.ResolvConfPath + ".hash"
995
+	if err = ioutil.WriteFile(resolvHashFile, []byte(resolvHash), 0644); err != nil {
996
+		return err
993 997
 	}
994 998
 	return ioutil.WriteFile(container.ResolvConfPath, resolvConf, 0644)
995 999
 }
996 1000
 
1001
+// called when the host's resolv.conf changes to check whether container's resolv.conf
1002
+// is unchanged by the container "user" since container start: if unchanged, the
1003
+// container's resolv.conf will be updated to match the host's new resolv.conf
1004
+func (container *Container) updateResolvConf(updatedResolvConf []byte, newResolvHash string) error {
1005
+
1006
+	if container.ResolvConfPath == "" {
1007
+		return nil
1008
+	}
1009
+	if container.Running {
1010
+		//set a marker in the hostConfig to update on next start/restart
1011
+		container.UpdateDns = true
1012
+		return nil
1013
+	}
1014
+
1015
+	resolvHashFile := container.ResolvConfPath + ".hash"
1016
+
1017
+	//read the container's current resolv.conf and compute the hash
1018
+	resolvBytes, err := ioutil.ReadFile(container.ResolvConfPath)
1019
+	if err != nil {
1020
+		return err
1021
+	}
1022
+	curHash, err := utils.HashData(bytes.NewReader(resolvBytes))
1023
+	if err != nil {
1024
+		return err
1025
+	}
1026
+
1027
+	//read the hash from the last time we wrote resolv.conf in the container
1028
+	hashBytes, err := ioutil.ReadFile(resolvHashFile)
1029
+	if err != nil {
1030
+		return err
1031
+	}
1032
+
1033
+	//if the user has not modified the resolv.conf of the container since we wrote it last
1034
+	//we will replace it with the updated resolv.conf from the host
1035
+	if string(hashBytes) == curHash {
1036
+		log.Debugf("replacing %q with updated host resolv.conf", container.ResolvConfPath)
1037
+
1038
+		// for atomic updates to these files, use temporary files with os.Rename:
1039
+		dir := path.Dir(container.ResolvConfPath)
1040
+		tmpHashFile, err := ioutil.TempFile(dir, "hash")
1041
+		if err != nil {
1042
+			return err
1043
+		}
1044
+		tmpResolvFile, err := ioutil.TempFile(dir, "resolv")
1045
+		if err != nil {
1046
+			return err
1047
+		}
1048
+
1049
+		// write the updates to the temp files
1050
+		if err = ioutil.WriteFile(tmpHashFile.Name(), []byte(newResolvHash), 0644); err != nil {
1051
+			return err
1052
+		}
1053
+		if err = ioutil.WriteFile(tmpResolvFile.Name(), updatedResolvConf, 0644); err != nil {
1054
+			return err
1055
+		}
1056
+
1057
+		// rename the temp files for atomic replace
1058
+		if err = os.Rename(tmpHashFile.Name(), resolvHashFile); err != nil {
1059
+			return err
1060
+		}
1061
+		return os.Rename(tmpResolvFile.Name(), container.ResolvConfPath)
1062
+	}
1063
+	return nil
1064
+}
1065
+
997 1066
 func (container *Container) updateParentsHosts() error {
998 1067
 	parents, err := container.daemon.Parents(container.Name)
999 1068
 	if err != nil {
... ...
@@ -1,6 +1,7 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"bytes"
4 5
 	"fmt"
5 6
 	"io"
6 7
 	"io/ioutil"
... ...
@@ -32,6 +33,7 @@ import (
32 32
 	"github.com/docker/docker/pkg/graphdb"
33 33
 	"github.com/docker/docker/pkg/ioutils"
34 34
 	"github.com/docker/docker/pkg/namesgenerator"
35
+	"github.com/docker/docker/pkg/networkfs/resolvconf"
35 36
 	"github.com/docker/docker/pkg/parsers"
36 37
 	"github.com/docker/docker/pkg/parsers/kernel"
37 38
 	"github.com/docker/docker/pkg/sysinfo"
... ...
@@ -40,10 +42,11 @@ import (
40 40
 	"github.com/docker/docker/trust"
41 41
 	"github.com/docker/docker/utils"
42 42
 	"github.com/docker/docker/volumes"
43
+
44
+	"github.com/go-fsnotify/fsnotify"
43 45
 )
44 46
 
45 47
 var (
46
-	DefaultDns                = []string{"8.8.8.8", "8.8.4.4"}
47 48
 	validContainerNameChars   = `[a-zA-Z0-9][a-zA-Z0-9_.-]`
48 49
 	validContainerNamePattern = regexp.MustCompile(`^/?` + validContainerNameChars + `+$`)
49 50
 )
... ...
@@ -402,6 +405,60 @@ func (daemon *Daemon) restore() error {
402 402
 	return nil
403 403
 }
404 404
 
405
+// set up the watch on the host's /etc/resolv.conf so that we can update container's
406
+// live resolv.conf when the network changes on the host
407
+func (daemon *Daemon) setupResolvconfWatcher() error {
408
+
409
+	watcher, err := fsnotify.NewWatcher()
410
+	if err != nil {
411
+		return err
412
+	}
413
+
414
+	//this goroutine listens for the events on the watch we add
415
+	//on the resolv.conf file on the host
416
+	go func() {
417
+		for {
418
+			select {
419
+			case event := <-watcher.Events:
420
+				if event.Op&fsnotify.Write == fsnotify.Write {
421
+					// verify a real change happened before we go further--a file write may have happened
422
+					// without an actual change to the file
423
+					updatedResolvConf, newResolvConfHash, err := resolvconf.GetIfChanged()
424
+					if err != nil {
425
+						log.Debugf("Error retrieving updated host resolv.conf: %v", err)
426
+					} else if updatedResolvConf != nil {
427
+						// because the new host resolv.conf might have localhost nameservers..
428
+						updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf)
429
+						if modified {
430
+							// changes have occurred during localhost cleanup: generate an updated hash
431
+							newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
432
+							if err != nil {
433
+								log.Debugf("Error generating hash of new resolv.conf: %v", err)
434
+							} else {
435
+								newResolvConfHash = newHash
436
+							}
437
+						}
438
+						log.Debugf("host network resolv.conf changed--walking container list for updates")
439
+						contList := daemon.containers.List()
440
+						for _, container := range contList {
441
+							if err := container.updateResolvConf(updatedResolvConf, newResolvConfHash); err != nil {
442
+								log.Debugf("Error on resolv.conf update check for container ID: %s: %v", container.ID, err)
443
+							}
444
+						}
445
+					}
446
+				}
447
+			case err := <-watcher.Errors:
448
+				log.Debugf("host resolv.conf notify error: %v", err)
449
+			}
450
+		}
451
+	}()
452
+
453
+	if err := watcher.Add("/etc/resolv.conf"); err != nil {
454
+		return err
455
+	}
456
+	return nil
457
+}
458
+
405 459
 func (daemon *Daemon) checkDeprecatedExpose(config *runconfig.Config) bool {
406 460
 	if config != nil {
407 461
 		if config.PortSpecs != nil {
... ...
@@ -924,6 +981,12 @@ func NewDaemonFromDirectory(config *Config, eng *engine.Engine) (*Daemon, error)
924 924
 	if err := daemon.restore(); err != nil {
925 925
 		return nil, err
926 926
 	}
927
+
928
+	// set up filesystem watch on resolv.conf for network changes
929
+	if err := daemon.setupResolvconfWatcher(); err != nil {
930
+		return nil, err
931
+	}
932
+
927 933
 	// Setup shutdown handlers
928 934
 	// FIXME: can these shutdown handlers be registered closer to their source?
929 935
 	eng.OnShutdown(func() {
... ...
@@ -24,34 +24,3 @@ func TestMergeLxcConfig(t *testing.T) {
24 24
 		t.Fatalf("expected %s got %s", expected, cpuset)
25 25
 	}
26 26
 }
27
-
28
-func TestRemoveLocalDns(t *testing.T) {
29
-	ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"
30
-
31
-	if result := utils.RemoveLocalDns([]byte(ns0)); result != nil {
32
-		if ns0 != string(result) {
33
-			t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
34
-		}
35
-	}
36
-
37
-	ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n"
38
-	if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
39
-		if ns0 != string(result) {
40
-			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
41
-		}
42
-	}
43
-
44
-	ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n"
45
-	if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
46
-		if ns0 != string(result) {
47
-			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
48
-		}
49
-	}
50
-
51
-	ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n"
52
-	if result := utils.RemoveLocalDns([]byte(ns1)); result != nil {
53
-		if ns0 != string(result) {
54
-			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
55
-		}
56
-	}
57
-}
... ...
@@ -130,7 +130,7 @@ information.  You can see this by running `mount` inside a container:
130 130
     ...
131 131
     /dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ...
132 132
     /dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ...
133
-    tmpfs on /etc/resolv.conf type tmpfs ...
133
+    /dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ...
134 134
     ...
135 135
 
136 136
 This arrangement allows Docker to do clever things like keep
... ...
@@ -178,7 +178,20 @@ Four different options affect container domain name services.
178 178
 Note that Docker, in the absence of either of the last two options
179 179
 above, will make `/etc/resolv.conf` inside of each container look like
180 180
 the `/etc/resolv.conf` of the host machine where the `docker` daemon is
181
-running.  The options then modify this default configuration.
181
+running.  You might wonder what happens when the host machine's
182
+`/etc/resolv.conf` file changes.  The `docker` daemon has a file change
183
+notifier active which will watch for changes to the host DNS configuration.
184
+When the host file changes, all stopped containers which have a matching
185
+`resolv.conf` to the host will be updated immediately to this newest host
186
+configuration.  Containers which are running when the host configuration
187
+changes will need to stop and start to pick up the host changes due to lack
188
+of a facility to ensure atomic writes of the `resolv.conf` file while the
189
+container is running. If the container's `resolv.conf` has been edited since
190
+it was started with the default configuration, no replacement will be
191
+attempted as it would overwrite the changes performed by the container.
192
+If the options (`--dns` or `--dns-search`) have been used to modify the 
193
+default host configuration, then the replacement with an updated host's
194
+`/etc/resolv.conf` will not happen as well.
182 195
 
183 196
 ## Communication between containers and the wider world
184 197
 
... ...
@@ -1403,6 +1403,157 @@ func TestRunDnsOptionsBasedOnHostResolvConf(t *testing.T) {
1403 1403
 	logDone("run - dns options based on host resolv.conf")
1404 1404
 }
1405 1405
 
1406
+// Test the file watch notifier on docker host's /etc/resolv.conf
1407
+// A go-routine is responsible for auto-updating containers which are
1408
+// stopped and have an unmodified copy of resolv.conf, as well as
1409
+// marking running containers as requiring an update on next restart
1410
+func TestRunResolvconfUpdater(t *testing.T) {
1411
+
1412
+	tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78")
1413
+	tmpLocalhostResolvConf := []byte("nameserver 127.0.0.1")
1414
+
1415
+	//take a copy of resolv.conf for restoring after test completes
1416
+	resolvConfSystem, err := ioutil.ReadFile("/etc/resolv.conf")
1417
+	if err != nil {
1418
+		t.Fatal(err)
1419
+	}
1420
+
1421
+	//cleanup
1422
+	defer func() {
1423
+		deleteAllContainers()
1424
+		if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil {
1425
+			t.Fatal(err)
1426
+		}
1427
+	}()
1428
+
1429
+	//1. test that a non-running container gets an updated resolv.conf
1430
+	cmd := exec.Command(dockerBinary, "run", "--name='first'", "busybox", "true")
1431
+	if _, err := runCommand(cmd); err != nil {
1432
+		t.Fatal(err)
1433
+	}
1434
+	containerID1, err := getIDByName("first")
1435
+	if err != nil {
1436
+		t.Fatal(err)
1437
+	}
1438
+
1439
+	// replace resolv.conf with our temporary copy
1440
+	bytesResolvConf := []byte(tmpResolvConf)
1441
+	if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
1442
+		t.Fatal(err)
1443
+	}
1444
+
1445
+	time.Sleep(time.Second / 2)
1446
+	// check for update in container
1447
+	containerResolv, err := readContainerFile(containerID1, "resolv.conf")
1448
+	if err != nil {
1449
+		t.Fatal(err)
1450
+	}
1451
+	if !bytes.Equal(containerResolv, bytesResolvConf) {
1452
+		t.Fatalf("Stopped container does not have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv))
1453
+	}
1454
+
1455
+	//2. test that a non-running container does not receive resolv.conf updates
1456
+	//   if it modified the container copy of the starting point resolv.conf
1457
+	cmd = exec.Command(dockerBinary, "run", "--name='second'", "busybox", "sh", "-c", "echo 'search mylittlepony.com' >>/etc/resolv.conf")
1458
+	if _, err = runCommand(cmd); err != nil {
1459
+		t.Fatal(err)
1460
+	}
1461
+	containerID2, err := getIDByName("second")
1462
+	if err != nil {
1463
+		t.Fatal(err)
1464
+	}
1465
+	containerResolvHashBefore, err := readContainerFile(containerID2, "resolv.conf.hash")
1466
+	if err != nil {
1467
+		t.Fatal(err)
1468
+	}
1469
+
1470
+	//make a change to resolv.conf (in this case replacing our tmp copy with orig copy)
1471
+	if err := ioutil.WriteFile("/etc/resolv.conf", resolvConfSystem, 0644); err != nil {
1472
+		t.Fatal(err)
1473
+	}
1474
+
1475
+	time.Sleep(time.Second / 2)
1476
+	containerResolvHashAfter, err := readContainerFile(containerID2, "resolv.conf.hash")
1477
+	if err != nil {
1478
+		t.Fatal(err)
1479
+	}
1480
+
1481
+	if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) {
1482
+		t.Fatalf("Stopped container with modified resolv.conf should not have been updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter)
1483
+	}
1484
+
1485
+	//3. test that a running container's resolv.conf is not modified while running
1486
+	cmd = exec.Command(dockerBinary, "run", "-d", "busybox", "top")
1487
+	out, _, err := runCommandWithOutput(cmd)
1488
+	if err != nil {
1489
+		t.Fatal(err)
1490
+	}
1491
+	runningContainerID := strings.TrimSpace(out)
1492
+
1493
+	containerResolvHashBefore, err = readContainerFile(runningContainerID, "resolv.conf.hash")
1494
+	if err != nil {
1495
+		t.Fatal(err)
1496
+	}
1497
+
1498
+	// replace resolv.conf
1499
+	if err := ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
1500
+		t.Fatal(err)
1501
+	}
1502
+
1503
+	// make sure the updater has time to run to validate we really aren't
1504
+	// getting updated
1505
+	time.Sleep(time.Second / 2)
1506
+	containerResolvHashAfter, err = readContainerFile(runningContainerID, "resolv.conf.hash")
1507
+	if err != nil {
1508
+		t.Fatal(err)
1509
+	}
1510
+
1511
+	if !bytes.Equal(containerResolvHashBefore, containerResolvHashAfter) {
1512
+		t.Fatalf("Running container's resolv.conf should not be updated; expected hash: %v, new hash: %v", containerResolvHashBefore, containerResolvHashAfter)
1513
+	}
1514
+
1515
+	//4. test that a running container's resolv.conf is updated upon restart
1516
+	//   (the above container is still running..)
1517
+	cmd = exec.Command(dockerBinary, "restart", runningContainerID)
1518
+	if _, err = runCommand(cmd); err != nil {
1519
+		t.Fatal(err)
1520
+	}
1521
+
1522
+	// check for update in container
1523
+	containerResolv, err = readContainerFile(runningContainerID, "resolv.conf")
1524
+	if err != nil {
1525
+		t.Fatal(err)
1526
+	}
1527
+	if !bytes.Equal(containerResolv, bytesResolvConf) {
1528
+		t.Fatalf("Restarted container should have updated resolv.conf; expected %q, got %q", tmpResolvConf, string(containerResolv))
1529
+	}
1530
+
1531
+	//5. test that additions of a localhost resolver are cleaned from
1532
+	//   host resolv.conf before updating container's resolv.conf copies
1533
+
1534
+	// replace resolv.conf with a localhost-only nameserver copy
1535
+	bytesResolvConf = []byte(tmpLocalhostResolvConf)
1536
+	if err = ioutil.WriteFile("/etc/resolv.conf", bytesResolvConf, 0644); err != nil {
1537
+		t.Fatal(err)
1538
+	}
1539
+
1540
+	time.Sleep(time.Second / 2)
1541
+	// our first exited container ID should have been updated, but with default DNS
1542
+	// after the cleanup of resolv.conf found only a localhost nameserver:
1543
+	containerResolv, err = readContainerFile(containerID1, "resolv.conf")
1544
+	if err != nil {
1545
+		t.Fatal(err)
1546
+	}
1547
+
1548
+	expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
1549
+	if !bytes.Equal(containerResolv, []byte(expected)) {
1550
+		t.Fatalf("Container does not have cleaned/replaced DNS in resolv.conf; expected %q, got %q", expected, string(containerResolv))
1551
+	}
1552
+
1553
+	//cleanup, restore original resolv.conf happens in defer func()
1554
+	logDone("run - resolv.conf updater")
1555
+}
1556
+
1406 1557
 func TestRunAddHost(t *testing.T) {
1407 1558
 	defer deleteAllContainers()
1408 1559
 	cmd := exec.Command(dockerBinary, "run", "--add-host=extra:86.75.30.9", "busybox", "grep", "extra", "/etc/hosts")
... ...
@@ -5,13 +5,25 @@ import (
5 5
 	"io/ioutil"
6 6
 	"regexp"
7 7
 	"strings"
8
+	"sync"
9
+
10
+	log "github.com/Sirupsen/logrus"
11
+	"github.com/docker/docker/utils"
8 12
 )
9 13
 
10 14
 var (
11
-	nsRegexp     = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
12
-	searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
15
+	defaultDns      = []string{"8.8.8.8", "8.8.4.4"}
16
+	localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`)
17
+	nsRegexp        = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
18
+	searchRegexp    = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
13 19
 )
14 20
 
21
+var lastModified struct {
22
+	sync.Mutex
23
+	sha256   string
24
+	contents []byte
25
+}
26
+
15 27
 func Get() ([]byte, error) {
16 28
 	resolv, err := ioutil.ReadFile("/etc/resolv.conf")
17 29
 	if err != nil {
... ...
@@ -20,6 +32,57 @@ func Get() ([]byte, error) {
20 20
 	return resolv, nil
21 21
 }
22 22
 
23
+// Retrieves the host /etc/resolv.conf file, checks against the last hash
24
+// and, if modified since last check, returns the bytes and new hash.
25
+// This feature is used by the resolv.conf updater for containers
26
+func GetIfChanged() ([]byte, string, error) {
27
+	lastModified.Lock()
28
+	defer lastModified.Unlock()
29
+
30
+	resolv, err := ioutil.ReadFile("/etc/resolv.conf")
31
+	if err != nil {
32
+		return nil, "", err
33
+	}
34
+	newHash, err := utils.HashData(bytes.NewReader(resolv))
35
+	if err != nil {
36
+		return nil, "", err
37
+	}
38
+	if lastModified.sha256 != newHash {
39
+		lastModified.sha256 = newHash
40
+		lastModified.contents = resolv
41
+		return resolv, newHash, nil
42
+	}
43
+	// nothing changed, so return no data
44
+	return nil, "", nil
45
+}
46
+
47
+// retrieve the last used contents and hash of the host resolv.conf
48
+// Used by containers updating on restart
49
+func GetLastModified() ([]byte, string) {
50
+	lastModified.Lock()
51
+	defer lastModified.Unlock()
52
+
53
+	return lastModified.contents, lastModified.sha256
54
+}
55
+
56
+// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided
57
+// resolv.conf, removing local nameserver entries, and, if the resulting
58
+// cleaned config has no defined nameservers left, adds default DNS entries
59
+// It also returns a boolean to notify the caller if changes were made at all
60
+func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) {
61
+	changed := false
62
+	cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{})
63
+	// if the resulting resolvConf is empty, use defaultDns
64
+	if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) {
65
+		log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns)
66
+		cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...)
67
+	}
68
+	if !bytes.Equal(resolvConf, cleanedResolvConf) {
69
+		changed = true
70
+	}
71
+	return cleanedResolvConf, changed
72
+}
73
+
23 74
 // getLines parses input into lines and strips away comments.
24 75
 func getLines(input []byte, commentMarker []byte) [][]byte {
25 76
 	lines := bytes.Split(input, []byte("\n"))
... ...
@@ -156,3 +156,34 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) {
156 156
 		t.Fatalf("Expected to not find '%s' got '%s'", notExpected, content)
157 157
 	}
158 158
 }
159
+
160
+func TestRemoveReplaceLocalDns(t *testing.T) {
161
+	ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"
162
+
163
+	if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil {
164
+		if ns0 != string(result) {
165
+			t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
166
+		}
167
+	}
168
+
169
+	ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n"
170
+	if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
171
+		if ns0 != string(result) {
172
+			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
173
+		}
174
+	}
175
+
176
+	ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n"
177
+	if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
178
+		if ns0 != string(result) {
179
+			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
180
+		}
181
+	}
182
+
183
+	ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n"
184
+	if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
185
+		if ns0 != string(result) {
186
+			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
187
+		}
188
+	}
189
+}
... ...
@@ -290,14 +290,6 @@ func NewHTTPRequestError(msg string, res *http.Response) error {
290 290
 	}
291 291
 }
292 292
 
293
-var localHostRx = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`)
294
-
295
-// RemoveLocalDns looks into the /etc/resolv.conf,
296
-// and removes any local nameserver entries.
297
-func RemoveLocalDns(resolvConf []byte) []byte {
298
-	return localHostRx.ReplaceAll(resolvConf, []byte{})
299
-}
300
-
301 293
 // An StatusError reports an unsuccessful exit by a command.
302 294
 type StatusError struct {
303 295
 	Status     string