Skip to content

Commit 5e3e478

Browse files
committed
fix: allow localhost DNS servers when using host network
This commit addresses the issue where nerdctl was unconditionally stripping localhost DNS servers from /etc/resolv.conf when container is using host network. Fixes: #4651 Signed-off-by: Youfu Zhang <[email protected]>
1 parent 20a3eeb commit 5e3e478

File tree

4 files changed

+164
-5
lines changed

4 files changed

+164
-5
lines changed

cmd/nerdctl/container/container_run_network_linux_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/containerd/nerdctl/mod/tigron/test"
4242
"github.com/containerd/nerdctl/mod/tigron/tig"
4343

44+
"github.com/containerd/nerdctl/v2/pkg/resolvconf"
4445
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
4546
"github.com/containerd/nerdctl/v2/pkg/testutil"
4647
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
@@ -957,6 +958,51 @@ func TestHostNetworkHostName(t *testing.T) {
957958
testCase.Run(t)
958959
}
959960

961+
func TestHostNetworkDnsPreserved(t *testing.T) {
962+
nerdtest.Setup()
963+
testCase := &test.Case{
964+
Require: require.Not(require.Windows),
965+
Setup: func(data test.Data, helpers test.Helpers) {
966+
// Capture the entire host resolv.conf for comparison
967+
helpers.Custom("cat", resolvconf.Path()).Run(&test.Expected{
968+
Output: func(stdout string, t tig.T) {
969+
data.Labels().Set("hostResolvConf", stdout)
970+
},
971+
})
972+
},
973+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
974+
return helpers.Command("run", "--rm",
975+
"--network", "host",
976+
testutil.AlpineImage, "cat", "/etc/resolv.conf")
977+
},
978+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
979+
// Container with --network=host should have same resolv.conf as host
980+
hostResolvConf := data.Labels().Get("hostResolvConf")
981+
return &test.Expected{
982+
Output: expect.Equals(hostResolvConf),
983+
}
984+
},
985+
}
986+
testCase.Run(t)
987+
}
988+
989+
func TestDefaultNetworkDnsNoLocalhost(t *testing.T) {
990+
nerdtest.Setup()
991+
testCase := &test.Case{
992+
Require: require.Not(require.Windows),
993+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
994+
return helpers.Command("run", "--rm",
995+
testutil.AlpineImage, "grep", "-E", "nameserver\\s+(127\\.|::1)", "/etc/resolv.conf")
996+
},
997+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
998+
return &test.Expected{
999+
ExitCode: 1, // no match
1000+
}
1001+
},
1002+
}
1003+
testCase.Run(t)
1004+
}
1005+
9601006
func TestNoneNetworkDnsConfigs(t *testing.T) {
9611007
nerdtest.Setup()
9621008
testCase := &test.Case{

pkg/containerutil/container_network_manager.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C
8989
}
9090
}
9191

92-
func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, []string, error) {
92+
func fetchDNSResolverConfig(netOpts types.NetworkOptions, allowLocalhostDNS bool) ([]string, []string, []string, error) {
9393
dns := netOpts.DNSServers
9494
dnsSearch := netOpts.DNSSearchDomains
9595
dnsOptions := netOpts.DNSResolvConfOptions
@@ -103,7 +103,7 @@ func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, [
103103
conf = &resolvconf.File{}
104104
log.L.WithError(err).Debugf("resolvConf file doesn't exist on host")
105105
}
106-
conf, err = resolvconf.FilterResolvDNS(conf.Content, true)
106+
conf, err = resolvconf.FilterResolvDNSWithLocalhostOption(conf.Content, true, allowLocalhostDNS)
107107
if err != nil {
108108
return nil, nil, nil, err
109109
}
@@ -291,7 +291,7 @@ func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, containe
291291
}
292292

293293
resolvConfPath := filepath.Join(stateDir, "resolv.conf")
294-
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts)
294+
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, false)
295295
if err != nil {
296296
return nil, nil, err
297297
}
@@ -671,7 +671,7 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe
671671
}
672672

673673
resolvConfPath := filepath.Join(stateDir, "resolv.conf")
674-
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts)
674+
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts, true)
675675
if err != nil {
676676
return nil, nil, err
677677
}

pkg/resolvconf/resolvconf.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,23 @@ func GetLastModified() *File {
184184
// 2. Given the caller provides the enable/disable state of IPv6, the filter
185185
// code will remove all IPv6 nameservers if it is not enabled for containers
186186
func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) {
187-
cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
187+
return FilterResolvDNSWithLocalhostOption(resolvConf, ipv6Enabled, false)
188+
}
189+
190+
// FilterResolvDNSWithLocalhostOption is like FilterResolvDNS but allows controlling
191+
// whether localhost nameservers are preserved. This is useful for host network mode
192+
// where the container should inherit the host's DNS configuration including localhost resolvers.
193+
//
194+
// Parameters:
195+
// - resolvConf: the resolv.conf file content
196+
// - ipv6Enabled: whether IPv6 nameservers should be preserved
197+
// - allowLocalhostDNS: if true, localhost nameservers are preserved; if false, they are filtered out
198+
func FilterResolvDNSWithLocalhostOption(resolvConf []byte, ipv6Enabled bool, allowLocalhostDNS bool) (*File, error) {
199+
cleanedResolvConf := resolvConf
200+
// if allowLocalhostDNS is false, remove localhost nameservers
201+
if !allowLocalhostDNS {
202+
cleanedResolvConf = localhostNSRegexp.ReplaceAll(cleanedResolvConf, []byte{})
203+
}
188204
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
189205
if !ipv6Enabled {
190206
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})

pkg/resolvconf/resolvconf_linux_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,100 @@ func TestFilterResolvDns(t *testing.T) {
320320
}
321321
}
322322
}
323+
324+
func TestFilterResolvDnsWithLocalhostOption(t *testing.T) {
325+
testCases := []struct {
326+
name string
327+
input string
328+
allowLocalhostDNS bool
329+
ipv6Enabled bool
330+
expected string
331+
}{
332+
{
333+
name: "filter_disallow_noIPv6",
334+
input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n",
335+
allowLocalhostDNS: false,
336+
ipv6Enabled: false,
337+
expected: "nameserver 192.88.99.1\n",
338+
},
339+
{
340+
name: "filter_allow_noIPv6",
341+
input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n",
342+
allowLocalhostDNS: true,
343+
ipv6Enabled: false,
344+
expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\n",
345+
},
346+
{
347+
name: "filter_disallow_IPv6",
348+
input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n",
349+
allowLocalhostDNS: false,
350+
ipv6Enabled: true,
351+
expected: "nameserver 192.88.99.1\nnameserver 2001:db8::1\n",
352+
},
353+
{
354+
name: "filter_allow_IPv6",
355+
input: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n",
356+
allowLocalhostDNS: true,
357+
ipv6Enabled: true,
358+
expected: "nameserver 127.0.0.53\nnameserver 192.88.99.1\nnameserver ::1\nnameserver 2001:db8::1\n",
359+
},
360+
{
361+
name: "fallback_no_server_noIPv6",
362+
input: "",
363+
allowLocalhostDNS: false,
364+
ipv6Enabled: false,
365+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4",
366+
},
367+
{
368+
name: "fallback_no_server_IPv6",
369+
input: "",
370+
allowLocalhostDNS: false,
371+
ipv6Enabled: true,
372+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844",
373+
},
374+
{
375+
name: "fallback_localhost4_noIPv6",
376+
input: "nameserver 127.0.0.53",
377+
allowLocalhostDNS: false,
378+
ipv6Enabled: false,
379+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4",
380+
},
381+
{
382+
name: "fallback_localhost4_IPv6",
383+
input: "nameserver 127.0.0.53",
384+
allowLocalhostDNS: false,
385+
ipv6Enabled: true,
386+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844",
387+
},
388+
{
389+
name: "fallback_localhost6_noIPv6", // insane but test it anyway
390+
input: "nameserver ::1",
391+
allowLocalhostDNS: false,
392+
ipv6Enabled: false,
393+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4",
394+
},
395+
{
396+
name: "fallback_localhost6_IPv6",
397+
input: "nameserver ::1",
398+
allowLocalhostDNS: false,
399+
ipv6Enabled: true,
400+
expected: "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844",
401+
},
402+
}
403+
404+
for _, tc := range testCases {
405+
tc := tc
406+
t.Run(tc.name, func(t *testing.T) {
407+
result, err := FilterResolvDNSWithLocalhostOption([]byte(tc.input), tc.ipv6Enabled, tc.allowLocalhostDNS)
408+
if err != nil {
409+
t.Fatalf("unexpected error: %v", err)
410+
}
411+
if result == nil {
412+
t.Fatal("result is nil")
413+
}
414+
if tc.expected != string(result.Content) {
415+
t.Fatalf("expected \n<%s> got \n<%s>", tc.expected, string(result.Content))
416+
}
417+
})
418+
}
419+
}

0 commit comments

Comments
 (0)