Browse code

Clean up localhost resolv logic and add IPv6 support to regexp

Addresses #5811

This cleans up an error in the logic which removes localhost resolvers
from the host resolv.conf at container creation start time. Specifically
when the determination is made if any nameservers are left after
removing localhost resolvers, it was using a string match on the word
"nameserver", which could have been anywhere (including commented out)
leading to incorrect situations where no nameservers were left but the
default ones were not added.

This also adds some complexity to the regular expressions for finding
nameservers in general, as well as matching on localhost resolvers due
to the recent addition of IPv6 support. Because of IPv6 support now
available in the Docker daemon, the resolvconf code is now aware of
IPv6 enable/disable state and uses that for both filter/cleaning of
nameservers as well as adding default Google DNS (IPv4 only vs. IPv4
and IPv6 if IPv6 enabled). For all these changes, tests have been
added/strengthened to test these additional capabilities.

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

Phil Estes authored on 2015/01/10 02:06:48
Showing 5 changed files
... ...
@@ -958,8 +958,8 @@ func (container *Container) setupContainerDns() error {
958 958
 			log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID)
959 959
 			latestResolvConf, latestHash := resolvconf.GetLastModified()
960 960
 
961
-			// because the new host resolv.conf might have localhost nameservers..
962
-			updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf)
961
+			// clean container resolv.conf re: localhost nameservers and IPv6 NS (if IPv6 disabled)
962
+			updatedResolvConf, modified := resolvconf.FilterResolvDns(latestResolvConf, container.daemon.config.EnableIPv6)
963 963
 			if modified {
964 964
 				// changes have occurred during resolv.conf localhost cleanup: generate an updated hash
965 965
 				newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
... ...
@@ -1012,8 +1012,8 @@ func (container *Container) setupContainerDns() error {
1012 1012
 			return resolvconf.Build(container.ResolvConfPath, dns, dnsSearch)
1013 1013
 		}
1014 1014
 
1015
-		// replace any localhost/127.* nameservers
1016
-		resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf)
1015
+		// replace any localhost/127.*, and remove IPv6 nameservers if IPv6 disabled in daemon
1016
+		resolvConf, _ = resolvconf.FilterResolvDns(resolvConf, daemon.config.EnableIPv6)
1017 1017
 	}
1018 1018
 	//get a sha256 hash of the resolv conf at this point so we can check
1019 1019
 	//for changes when the host resolv.conf changes (e.g. network update)
... ...
@@ -434,7 +434,7 @@ func (daemon *Daemon) setupResolvconfWatcher() error {
434 434
 						log.Debugf("Error retrieving updated host resolv.conf: %v", err)
435 435
 					} else if updatedResolvConf != nil {
436 436
 						// because the new host resolv.conf might have localhost nameservers..
437
-						updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf)
437
+						updatedResolvConf, modified := resolvconf.FilterResolvDns(updatedResolvConf, daemon.config.EnableIPv6)
438 438
 						if modified {
439 439
 							// changes have occurred during localhost cleanup: generate an updated hash
440 440
 							newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
... ...
@@ -1238,40 +1238,42 @@ func TestRunDisallowBindMountingRootToRoot(t *testing.T) {
1238 1238
 	logDone("run - bind mount /:/ as volume should fail")
1239 1239
 }
1240 1240
 
1241
+// Verify that a container gets default DNS when only localhost resolvers exist
1241 1242
 func TestRunDnsDefaultOptions(t *testing.T) {
1242
-	// ci server has default resolv.conf
1243
-	// so rewrite it for the test
1243
+
1244
+	// preserve original resolv.conf for restoring after test
1244 1245
 	origResolvConf, err := ioutil.ReadFile("/etc/resolv.conf")
1245 1246
 	if os.IsNotExist(err) {
1246 1247
 		t.Fatalf("/etc/resolv.conf does not exist")
1247 1248
 	}
1248
-
1249
-	// test with file
1250
-	tmpResolvConf := []byte("nameserver 127.0.0.1")
1251
-	if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil {
1252
-		t.Fatal(err)
1253
-	}
1254
-	// put the old resolvconf back
1249
+	// defer restored original conf
1255 1250
 	defer func() {
1256 1251
 		if err := ioutil.WriteFile("/etc/resolv.conf", origResolvConf, 0644); err != nil {
1257 1252
 			t.Fatal(err)
1258 1253
 		}
1259 1254
 	}()
1260 1255
 
1256
+	// test 3 cases: standard IPv4 localhost, commented out localhost, and IPv6 localhost
1257
+	// 2 are removed from the file at container start, and the 3rd (commented out) one is ignored by
1258
+	// GetNameservers(), leading to a replacement of nameservers with the default set
1259
+	tmpResolvConf := []byte("nameserver 127.0.0.1\n#nameserver 127.0.2.1\nnameserver ::1")
1260
+	if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil {
1261
+		t.Fatal(err)
1262
+	}
1263
+
1261 1264
 	cmd := exec.Command(dockerBinary, "run", "busybox", "cat", "/etc/resolv.conf")
1262 1265
 
1263 1266
 	actual, _, err := runCommandWithOutput(cmd)
1264 1267
 	if err != nil {
1265
-		t.Error(err, actual)
1266
-		return
1268
+		t.Fatal(err, actual)
1267 1269
 	}
1268 1270
 
1269
-	// check that the actual defaults are there
1270
-	// if we ever change the defaults from google dns, this will break
1271
-	expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
1271
+	// check that the actual defaults are appended to the commented out
1272
+	// localhost resolver (which should be preserved)
1273
+	// NOTE: if we ever change the defaults from google dns, this will break
1274
+	expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
1272 1275
 	if actual != expected {
1273
-		t.Errorf("expected resolv.conf be: %q, but was: %q", expected, actual)
1274
-		return
1276
+		t.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual)
1275 1277
 	}
1276 1278
 
1277 1279
 	deleteAllContainers()
... ...
@@ -12,9 +12,21 @@ import (
12 12
 )
13 13
 
14 14
 var (
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*$`)
15
+	// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS
16
+	defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
17
+	defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
18
+	ipv4NumBlock   = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
19
+	ipv4Address    = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
20
+	// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
21
+	// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
22
+	// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
23
+	// For readability and sufficiency for Docker purposes this seemed more reasonable than a
24
+	// 1000+ character regexp with exact and complete IPv6 validation
25
+	ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})`
26
+
27
+	localhostRegexp = regexp.MustCompile(`(?m)^nameserver\s+((127\.([0-9]{1,3}.){2}[0-9]{1,3})|(::1))\s*\n*`)
28
+	nsIPv6Regexp    = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
29
+	nsRegexp        = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
18 30
 	searchRegexp    = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
19 31
 )
20 32
 
... ...
@@ -65,17 +77,31 @@ func GetLastModified() ([]byte, string) {
65 65
 	return lastModified.contents, lastModified.sha256
66 66
 }
67 67
 
68
-// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided
69
-// resolv.conf, removing local nameserver entries, and, if the resulting
70
-// cleaned config has no defined nameservers left, adds default DNS entries
68
+// FilterResolvDns has two main jobs:
69
+// 1. It looks for localhost (127.*|::1) entries in the provided
70
+//    resolv.conf, removing local nameserver entries, and, if the resulting
71
+//    cleaned config has no defined nameservers left, adds default DNS entries
72
+// 2. Given the caller provides the enable/disable state of IPv6, the filter
73
+//    code will remove all IPv6 nameservers if it is not enabled for containers
74
+//
71 75
 // It also returns a boolean to notify the caller if changes were made at all
72
-func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) {
76
+func FilterResolvDns(resolvConf []byte, ipv6Enabled bool) ([]byte, bool) {
73 77
 	changed := false
74
-	cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{})
75
-	// if the resulting resolvConf is empty, use defaultDns
76
-	if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) {
77
-		log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns)
78
-		cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...)
78
+	cleanedResolvConf := localhostRegexp.ReplaceAll(resolvConf, []byte{})
79
+	// if IPv6 is not enabled, also clean out any IPv6 address nameserver
80
+	if !ipv6Enabled {
81
+		cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
82
+	}
83
+	// if the resulting resolvConf has no more nameservers defined, add appropriate
84
+	// default DNS servers for IPv4 and (optionally) IPv6
85
+	if len(GetNameservers(cleanedResolvConf)) == 0 {
86
+		log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultIPv4Dns)
87
+		dns := defaultIPv4Dns
88
+		if ipv6Enabled {
89
+			log.Infof("IPv6 enabled; Adding default IPv6 external servers : %v", defaultIPv6Dns)
90
+			dns = append(dns, defaultIPv6Dns...)
91
+		}
92
+		cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
79 93
 	}
80 94
 	if !bytes.Equal(resolvConf, cleanedResolvConf) {
81 95
 		changed = true
... ...
@@ -157,33 +157,82 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) {
157 157
 	}
158 158
 }
159 159
 
160
-func TestRemoveReplaceLocalDns(t *testing.T) {
160
+func TestFilterResolvDns(t *testing.T) {
161 161
 	ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"
162 162
 
163
-	if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil {
163
+	if result, _ := FilterResolvDns([]byte(ns0), false); result != nil {
164 164
 		if ns0 != string(result) {
165 165
 			t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
166 166
 		}
167 167
 	}
168 168
 
169 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 {
170
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
171 171
 		if ns0 != string(result) {
172 172
 			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
173 173
 		}
174 174
 	}
175 175
 
176 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 {
177
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
178 178
 		if ns0 != string(result) {
179 179
 			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
180 180
 		}
181 181
 	}
182 182
 
183 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 {
184
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
185 185
 		if ns0 != string(result) {
186 186
 			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
187 187
 		}
188 188
 	}
189
+
190
+	ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n"
191
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
192
+		if ns0 != string(result) {
193
+			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
194
+		}
195
+	}
196
+
197
+	ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1"
198
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
199
+		if ns0 != string(result) {
200
+			t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
201
+		}
202
+	}
203
+
204
+	// with IPv6 disabled (false param), the IPv6 nameserver should be removed
205
+	ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
206
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
207
+		if ns0 != string(result) {
208
+			t.Fatalf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result))
209
+		}
210
+	}
211
+
212
+	// with IPv6 enabled, the IPv6 nameserver should be preserved
213
+	ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n"
214
+	ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
215
+	if result, _ := FilterResolvDns([]byte(ns1), true); result != nil {
216
+		if ns0 != string(result) {
217
+			t.Fatalf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result))
218
+		}
219
+	}
220
+
221
+	// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
222
+	ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844"
223
+	ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
224
+	if result, _ := FilterResolvDns([]byte(ns1), true); result != nil {
225
+		if ns0 != string(result) {
226
+			t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result))
227
+		}
228
+	}
229
+
230
+	// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
231
+	ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
232
+	ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
233
+	if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
234
+		if ns0 != string(result) {
235
+			t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result))
236
+		}
237
+	}
189 238
 }