diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index 6d7b353cdea..e63fae9c7a6 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -41,6 +41,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/resolvconf" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -957,6 +958,50 @@ func TestHostNetworkHostName(t *testing.T) { testCase.Run(t) } +func TestHostNetworkDnsPreserved(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Custom("grep", "-E", "^nameserver\\s+", resolvconf.Path()).Run(&test.Expected{ + Output: func(stdout string, t tig.T) { + data.Labels().Set("nameservers", stdout) + }, + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--network", "host", + testutil.AlpineImage, "grep", "-E", "^nameserver\\s+", "/etc/resolv.conf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // container with --network=host should have same nameserver as host + nameservers := data.Labels().Get("nameservers") + return &test.Expected{ + Output: expect.Equals(nameservers), + } + }, + } + testCase.Run(t) +} + +func TestDefaultNetworkDnsNoLocalhost(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.Not(require.Windows), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + testutil.AlpineImage, "grep", "-E", "^nameserver\\s+(127\\.|::1)", "/etc/resolv.conf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, // no match + } + }, + } + testCase.Run(t) +} + func TestNoneNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go index d41e3c7e17a..3638b450917 100644 --- a/pkg/containerutil/container_network_manager.go +++ b/pkg/containerutil/container_network_manager.go @@ -89,7 +89,7 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C } } -func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, []string, error) { +func fetchDNSResolverConfig(netOpts types.NetworkOptions, allowLocalhostDNS bool) ([]string, []string, []string, error) { dns := netOpts.DNSServers dnsSearch := netOpts.DNSSearchDomains dnsOptions := netOpts.DNSResolvConfOptions @@ -103,7 +103,7 @@ func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, [ conf = &resolvconf.File{} log.L.WithError(err).Debugf("resolvConf file doesn't exist on host") } - conf, err = resolvconf.FilterResolvDNS(conf.Content, true) + conf, err = resolvconf.FilterResolvDNSWithLocalhostOption(conf.Content, true, allowLocalhostDNS) if err != nil { return nil, nil, nil, err } @@ -291,7 +291,7 @@ func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, containe } resolvConfPath := filepath.Join(stateDir, "resolv.conf") - dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts) + dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, false) if err != nil { return nil, nil, err } @@ -671,7 +671,7 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe } resolvConfPath := filepath.Join(stateDir, "resolv.conf") - dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts) + dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, true) if err != nil { return nil, nil, err } diff --git a/pkg/resolvconf/resolvconf.go b/pkg/resolvconf/resolvconf.go index 16a809d390d..30aa53fe21c 100644 --- a/pkg/resolvconf/resolvconf.go +++ b/pkg/resolvconf/resolvconf.go @@ -184,7 +184,23 @@ func GetLastModified() *File { // 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 func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) { - cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{}) + return FilterResolvDNSWithLocalhostOption(resolvConf, ipv6Enabled, false) +} + +// FilterResolvDNSWithLocalhostOption is like FilterResolvDNS but allows controlling +// whether localhost nameservers are preserved. This is useful for host network mode +// where the container should inherit the host's DNS configuration including localhost resolvers. +// +// Parameters: +// - resolvConf: the resolv.conf file content +// - ipv6Enabled: whether IPv6 nameservers should be preserved +// - allowLocalhostDNS: if true, localhost nameservers are preserved; if false, they are filtered out +func FilterResolvDNSWithLocalhostOption(resolvConf []byte, ipv6Enabled bool, allowLocalhostDNS bool) (*File, error) { + cleanedResolvConf := resolvConf + // if allowLocalhostDNS is false, remove localhost nameservers + if !allowLocalhostDNS { + cleanedResolvConf = localhostNSRegexp.ReplaceAll(cleanedResolvConf, []byte{}) + } // if IPv6 is not enabled, also clean out any IPv6 address nameserver if !ipv6Enabled { cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{}) diff --git a/pkg/resolvconf/resolvconf_linux_test.go b/pkg/resolvconf/resolvconf_linux_test.go index 2b7790712ac..cf67bfd1bcd 100644 --- a/pkg/resolvconf/resolvconf_linux_test.go +++ b/pkg/resolvconf/resolvconf_linux_test.go @@ -320,3 +320,100 @@ func TestFilterResolvDns(t *testing.T) { } } } + +func TestFilterResolvDnsWithLocalhostOption(t *testing.T) { + testCases := []struct { + name string + input string + allowLocalhostDNS bool + ipv6Enabled bool + expected string + }{ + { + name: "filter_disallow_noIPv6", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "nameserver 192.88.99.1\n", + }, + { + name: "filter_allow_noIPv6", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: true, + ipv6Enabled: false, + expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\n", + }, + { + name: "filter_disallow_IPv6", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "nameserver 192.88.99.1\nnameserver 2001:db8::1\n", + }, + { + name: "filter_allow_IPv6", + input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + allowLocalhostDNS: true, + ipv6Enabled: true, + expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n", + }, + { + name: "fallback_no_server_noIPv6", + input: "", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_no_server_IPv6", + input: "", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + { + name: "fallback_localhost4_noIPv6", + input: "nameserver 127.0.0.53", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_localhost4_IPv6", + input: "nameserver 127.0.0.53", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + { + name: "fallback_localhost6_noIPv6", // insane but test it anyway + input: "nameserver ::1", + allowLocalhostDNS: false, + ipv6Enabled: false, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4", + }, + { + name: "fallback_localhost6_IPv6", + input: "nameserver ::1", + allowLocalhostDNS: false, + ipv6Enabled: true, + expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + result, err := FilterResolvDNSWithLocalhostOption([]byte(tc.input), tc.ipv6Enabled, tc.allowLocalhostDNS) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("result is nil") + } + if tc.expected != string(result.Content) { + t.Fatalf("expected \n<%s> got \n<%s>", tc.expected, string(result.Content)) + } + }) + } +}