diff --git a/ecs-agent/netlib/platform/common_linux.go b/ecs-agent/netlib/platform/common_linux.go index 2b8bb0a4829..98a80d01860 100644 --- a/ecs-agent/netlib/platform/common_linux.go +++ b/ecs-agent/netlib/platform/common_linux.go @@ -566,10 +566,54 @@ func (c *common) createResolvConf(netNSDir string, return err } - // Copy Host's resolv.conf file. - return c.copyFile(filepath.Join(netNSDir, ResolveConfFileName), - filepath.Join(c.resolvConfPath, ResolveConfFileName), - taskDNSConfigFileMode) + // Copy Host's resolv.conf file and backfill DNS fields on the interface + // so downstream consumers can read DNS config from the model. + src := filepath.Join(c.resolvConfPath, ResolveConfFileName) + contents, err := c.ioutil.ReadFile(src) + if err != nil { + return errors.Wrapf(err, "unable to read %s", src) + } + dst := filepath.Join(netNSDir, ResolveConfFileName) + if err := c.ioutil.WriteFile(dst, contents, taskDNSConfigFileMode); err != nil { + return errors.Wrapf(err, "unable to write to %s", dst) + } + servers, searches := parseResolvConf(contents) + iface.DomainNameServers = servers + iface.DomainNameSearchList = searches + return nil +} + +// parseResolvConf extracts nameserver addresses and search domains from content +// in the resolv.conf(5) format. +// +// Parsing rules (matching the behavior of Go's net.dnsReadConfig in +// src/net/dnsconfig_unix.go and glibc's res_init): +// - Lines starting with ';' or '#' are treated as comments and skipped. +// - Fields are separated by any combination of spaces and tabs. +// - A "nameserver" line contributes one IP address (the second field). +// - A "search" line contributes all subsequent fields as search domains; +// if multiple search lines appear, only the last is used. +// - All other directives are ignored. +func parseResolvConf(content []byte) (servers, searches []string) { + for _, line := range bytes.Split(content, []byte("\n")) { + if len(line) == 0 || line[0] == ';' || line[0] == '#' { + continue + } + fields := bytes.Fields(line) + if len(fields) < 2 { + continue + } + switch string(fields[0]) { + case "nameserver": + servers = append(servers, string(fields[1])) + case "search": + searches = nil + for _, f := range fields[1:] { + searches = append(searches, string(f)) + } + } + } + return servers, searches } func (c *common) copyFile(dst, src string, fileMode os.FileMode) error { diff --git a/ecs-agent/netlib/platform/common_linux_test.go b/ecs-agent/netlib/platform/common_linux_test.go index 5866f5dcbd3..f4d2916cb6b 100644 --- a/ecs-agent/netlib/platform/common_linux_test.go +++ b/ecs-agent/netlib/platform/common_linux_test.go @@ -606,3 +606,78 @@ func TestParseHosts(t *testing.T) { }) } } + +func TestParseResolvConf(t *testing.T) { + tests := []struct { + name string + input string + expectedServers []string + expectedSearch []string + }{ + { + // Test fixture from Go's net package resolv.conf parser tests: + // https://github.com/golang/go/blob/master/src/net/testdata/resolv.conf + name: "Go stdlib test fixture", + input: "# /etc/resolv.conf\n" + + "domain localdomain\n" + + "nameserver 8.8.8.8\n" + + "nameserver 2001:4860:4860::8888\n" + + "nameserver fe80::1%lo0\n" + + "options ndots:5 timeout:10 attempts:3 rotate\n" + + "options attempts 3\n", + expectedServers: []string{"8.8.8.8", "2001:4860:4860::8888", "fe80::1%lo0"}, + expectedSearch: nil, + }, + { + name: "semicolon comments", + input: "; this is a comment\nnameserver 8.8.8.8\n", + expectedServers: []string{"8.8.8.8"}, + expectedSearch: nil, + }, + { + name: "tabs as separators", + input: "nameserver\t10.0.0.2\nsearch\tlocal\texample.com\n", + expectedServers: []string{"10.0.0.2"}, + expectedSearch: []string{"local", "example.com"}, + }, + { + name: "multiple spaces between keyword and value", + input: "nameserver 8.8.8.8\nsearch example.com\n", + expectedServers: []string{"8.8.8.8"}, + expectedSearch: []string{"example.com"}, + }, + { + name: "last search directive wins", + input: "search first.com\n" + + "search second.com third.com\n", + expectedServers: nil, + expectedSearch: []string{"second.com", "third.com"}, + }, + { + name: "empty input", + input: "", + expectedServers: nil, + expectedSearch: nil, + }, + { + name: "only comments and blanks", + input: "# comment\n\n; another\n", + expectedServers: nil, + expectedSearch: nil, + }, + { + name: "unknown directives ignored", + input: "options ndots:5\nnameserver 10.0.0.2\ndomain example.com\nsortlist 130.155.160.0\n", + expectedServers: []string{"10.0.0.2"}, + expectedSearch: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + servers, searches := parseResolvConf([]byte(tc.input)) + assert.Equal(t, tc.expectedServers, servers) + assert.Equal(t, tc.expectedSearch, searches) + }) + } +}