Skip to content

Commit

Permalink
Clean up localhost resolv logic and add IPv6 support to regexp
Browse files Browse the repository at this point in the history
Addresses moby#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)
  • Loading branch information
estesp committed Jan 20, 2015
1 parent ef2d6dd commit 93d51e5
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 38 deletions.
8 changes: 4 additions & 4 deletions daemon/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,8 +958,8 @@ func (container *Container) setupContainerDns() error {
log.Debugf("Check container (%s) for update to resolv.conf - UpdateDns flag was set", container.ID)
latestResolvConf, latestHash := resolvconf.GetLastModified()

// because the new host resolv.conf might have localhost nameservers..
updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(latestResolvConf)
// clean container resolv.conf re: localhost nameservers and IPv6 NS (if IPv6 disabled)
updatedResolvConf, modified := resolvconf.FilterResolvDns(latestResolvConf, container.daemon.config.EnableIPv6)
if modified {
// changes have occurred during resolv.conf localhost cleanup: generate an updated hash
newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
Expand Down Expand Up @@ -1012,8 +1012,8 @@ func (container *Container) setupContainerDns() error {
return resolvconf.Build(container.ResolvConfPath, dns, dnsSearch)
}

// replace any localhost/127.* nameservers
resolvConf, _ = resolvconf.RemoveReplaceLocalDns(resolvConf)
// replace any localhost/127.*, and remove IPv6 nameservers if IPv6 disabled in daemon
resolvConf, _ = resolvconf.FilterResolvDns(resolvConf, daemon.config.EnableIPv6)
}
//get a sha256 hash of the resolv conf at this point so we can check
//for changes when the host resolv.conf changes (e.g. network update)
Expand Down
2 changes: 1 addition & 1 deletion daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ func (daemon *Daemon) setupResolvconfWatcher() error {
log.Debugf("Error retrieving updated host resolv.conf: %v", err)
} else if updatedResolvConf != nil {
// because the new host resolv.conf might have localhost nameservers..
updatedResolvConf, modified := resolvconf.RemoveReplaceLocalDns(updatedResolvConf)
updatedResolvConf, modified := resolvconf.FilterResolvDns(updatedResolvConf, daemon.config.EnableIPv6)
if modified {
// changes have occurred during localhost cleanup: generate an updated hash
newHash, err := utils.HashData(bytes.NewReader(updatedResolvConf))
Expand Down
34 changes: 18 additions & 16 deletions integration-cli/docker_cli_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1238,40 +1238,42 @@ func TestRunDisallowBindMountingRootToRoot(t *testing.T) {
logDone("run - bind mount /:/ as volume should fail")
}

// Verify that a container gets default DNS when only localhost resolvers exist
func TestRunDnsDefaultOptions(t *testing.T) {
// ci server has default resolv.conf
// so rewrite it for the test

// preserve original resolv.conf for restoring after test
origResolvConf, err := ioutil.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
t.Fatalf("/etc/resolv.conf does not exist")
}

// test with file
tmpResolvConf := []byte("nameserver 127.0.0.1")
if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil {
t.Fatal(err)
}
// put the old resolvconf back
// defer restored original conf
defer func() {
if err := ioutil.WriteFile("/etc/resolv.conf", origResolvConf, 0644); err != nil {
t.Fatal(err)
}
}()

// test 3 cases: standard IPv4 localhost, commented out localhost, and IPv6 localhost
// 2 are removed from the file at container start, and the 3rd (commented out) one is ignored by
// GetNameservers(), leading to a replacement of nameservers with the default set
tmpResolvConf := []byte("nameserver 127.0.0.1\n#nameserver 127.0.2.1\nnameserver ::1")
if err := ioutil.WriteFile("/etc/resolv.conf", tmpResolvConf, 0644); err != nil {
t.Fatal(err)
}

cmd := exec.Command(dockerBinary, "run", "busybox", "cat", "/etc/resolv.conf")

actual, _, err := runCommandWithOutput(cmd)
if err != nil {
t.Error(err, actual)
return
t.Fatal(err, actual)
}

// check that the actual defaults are there
// if we ever change the defaults from google dns, this will break
expected := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
// check that the actual defaults are appended to the commented out
// localhost resolver (which should be preserved)
// NOTE: if we ever change the defaults from google dns, this will break
expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
if actual != expected {
t.Errorf("expected resolv.conf be: %q, but was: %q", expected, actual)
return
t.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual)
}

deleteAllContainers()
Expand Down
50 changes: 38 additions & 12 deletions pkg/networkfs/resolvconf/resolvconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ import (
)

var (
defaultDns = []string{"8.8.8.8", "8.8.4.4"}
localHostRegexp = regexp.MustCompile(`(?m)^nameserver 127[^\n]+\n*`)
nsRegexp = regexp.MustCompile(`^\s*nameserver\s*(([0-9]+\.){3}([0-9]+))\s*$`)
// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS
defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
// For readability and sufficiency for Docker purposes this seemed more reasonable than a
// 1000+ character regexp with exact and complete IPv6 validation
ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})`

localhostRegexp = regexp.MustCompile(`(?m)^nameserver\s+((127\.([0-9]{1,3}.){2}[0-9]{1,3})|(::1))\s*\n*`)
nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
)

Expand Down Expand Up @@ -65,17 +77,31 @@ func GetLastModified() ([]byte, string) {
return lastModified.contents, lastModified.sha256
}

// RemoveReplaceLocalDns looks for localhost (127.*) entries in the provided
// resolv.conf, removing local nameserver entries, and, if the resulting
// cleaned config has no defined nameservers left, adds default DNS entries
// FilterResolvDns has two main jobs:
// 1. It looks for localhost (127.*|::1) entries in the provided
// resolv.conf, removing local nameserver entries, and, if the resulting
// cleaned config has no defined nameservers left, adds default DNS entries
// 2. Given the caller provides the enable/disable state of IPv6, the filter
// code will remove all IPv6 nameservers if it is not enabled for containers
//
// It also returns a boolean to notify the caller if changes were made at all
func RemoveReplaceLocalDns(resolvConf []byte) ([]byte, bool) {
func FilterResolvDns(resolvConf []byte, ipv6Enabled bool) ([]byte, bool) {
changed := false
cleanedResolvConf := localHostRegexp.ReplaceAll(resolvConf, []byte{})
// if the resulting resolvConf is empty, use defaultDns
if !bytes.Contains(cleanedResolvConf, []byte("nameserver")) {
log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultDns)
cleanedResolvConf = append(cleanedResolvConf, []byte("\nnameserver "+strings.Join(defaultDns, "\nnameserver "))...)
cleanedResolvConf := localhostRegexp.ReplaceAll(resolvConf, []byte{})
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
if !ipv6Enabled {
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
}
// if the resulting resolvConf has no more nameservers defined, add appropriate
// default DNS servers for IPv4 and (optionally) IPv6
if len(GetNameservers(cleanedResolvConf)) == 0 {
log.Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers : %v", defaultIPv4Dns)
dns := defaultIPv4Dns
if ipv6Enabled {
log.Infof("IPv6 enabled; Adding default IPv6 external servers : %v", defaultIPv6Dns)
dns = append(dns, defaultIPv6Dns...)
}
cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
}
if !bytes.Equal(resolvConf, cleanedResolvConf) {
changed = true
Expand Down
59 changes: 54 additions & 5 deletions pkg/networkfs/resolvconf/resolvconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,33 +157,82 @@ func TestBuildWithZeroLengthDomainSearch(t *testing.T) {
}
}

func TestRemoveReplaceLocalDns(t *testing.T) {
func TestFilterResolvDns(t *testing.T) {
ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"

if result, _ := RemoveReplaceLocalDns([]byte(ns0)); result != nil {
if result, _ := FilterResolvDns([]byte(ns0), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n"
if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n"
if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n"
if result, _ := RemoveReplaceLocalDns([]byte(ns1)); result != nil {
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

// with IPv6 disabled (false param), the IPv6 nameserver should be removed
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

// with IPv6 enabled, the IPv6 nameserver should be preserved
ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n"
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDns([]byte(ns1), true); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844"
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
if result, _ := FilterResolvDns([]byte(ns1), true); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result))
}
}

// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
if result, _ := FilterResolvDns([]byte(ns1), false); result != nil {
if ns0 != string(result) {
t.Fatalf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result))
}
}
}

0 comments on commit 93d51e5

Please sign in to comment.