Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions cmd/nerdctl/container/container_run_network_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down
8 changes: 4 additions & 4 deletions pkg/containerutil/container_network_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
18 changes: 17 additions & 1 deletion pkg/resolvconf/resolvconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
97 changes: 97 additions & 0 deletions pkg/resolvconf/resolvconf_linux_test.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have some integration tests too?

func TestHostNetworkHostName(t *testing.T) {
nerdtest.Setup()
testCase := &test.Case{
Require: require.Not(require.Windows),
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{
Output: func(stdout string, t tig.T) {
data.Labels().Set("hostHostname", stdout)
},
})
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("run", "--rm",
"--network", "host",
testutil.AlpineImage, "cat", "/etc/hostname")
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
Output: expect.Equals(data.Labels().Get("hostHostname")),
}
},
}
testCase.Run(t)
}
func TestNoneNetworkDnsConfigs(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",
"--network", "none",
"--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5",
testutil.CommonImage, "cat", "/etc/resolv.conf")
},
Expected: test.Expects(0, nil, expect.Contains(
"0.1.2.3",
"example.com",
"attempts:5",
"timeout:3",
)),
}
testCase.Run(t)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented some simple integration tests. I don't think we should modify host /etc/resolv.conf content on-the-fly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm investigating CI failure in rootless setup.

Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
})
}
}
Loading