Skip to content

Commit df19f68

Browse files
committed
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 containers used --network=host. Changes made: 1. Modified FilterResolvDNS() to support preserving localhost DNS servers via a new FilterResolvDNSWithLocalhostOption() function 2. Added allowLocalhostDNS parameter to fetchDNSResolverConfig() 3. Updated hostNetworkManager to use allowLocalhostDNS=true to preserve host DNS 4. Added comprehensive test coverage for the new functionality The fix ensures: - Host network mode respects host's /etc/resolv.conf including localhost resolvers - Isolated networks (bridge, CNI) continue to have fallback Google DNS - Backward compatible - all existing behavior unchanged - Docker-compatible behavior Fixes: #4651 Signed-off-by: Youfu Zhang <[email protected]>
1 parent 20a3eeb commit df19f68

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

pkg/containerutil/container_network_manager.go

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

9292
func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, []string, error) {
93+
return fetchDNSResolverConfigWithOptions(netOpts, false)
94+
}
95+
96+
func fetchDNSResolverConfigWithOptions(netOpts types.NetworkOptions, allowLocalhostDNS bool) ([]string, []string, []string, error) {
9397
dns := netOpts.DNSServers
9498
dnsSearch := netOpts.DNSSearchDomains
9599
dnsOptions := netOpts.DNSResolvConfOptions
@@ -103,7 +107,7 @@ func fetchDNSResolverConfig(netOpts types.NetworkOptions) ([]string, []string, [
103107
conf = &resolvconf.File{}
104108
log.L.WithError(err).Debugf("resolvConf file doesn't exist on host")
105109
}
106-
conf, err = resolvconf.FilterResolvDNS(conf.Content, true)
110+
conf, err = resolvconf.FilterResolvDNSWithLocalhostOption(conf.Content, true, allowLocalhostDNS)
107111
if err != nil {
108112
return nil, nil, nil, err
109113
}
@@ -671,7 +675,7 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe
671675
}
672676

673677
resolvConfPath := filepath.Join(stateDir, "resolv.conf")
674-
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfig(m.netOpts)
678+
dns, dnsSearch, dnsOptions, err := fetchDNSResolverConfigWithOptions(m.netOpts, true)
675679
if err != nil {
676680
return nil, nil, err
677681
}

pkg/resolvconf/resolvconf.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,27 @@ func GetLastModified() *File {
181181
// 1. It looks for localhost (127.*|::1) entries in the provided
182182
// resolv.conf, removing local nameserver entries, and, if the resulting
183183
// cleaned config has no defined nameservers left, adds default DNS entries
184+
// The removal of localhost entries is controlled by the allowLocalhostDNS parameter.
184185
// 2. Given the caller provides the enable/disable state of IPv6, the filter
185186
// code will remove all IPv6 nameservers if it is not enabled for containers
186187
func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) {
187-
cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
188+
return FilterResolvDNSWithLocalhostOption(resolvConf, ipv6Enabled, false)
189+
}
190+
191+
// FilterResolvDNSWithLocalhostOption is like FilterResolvDNS but allows controlling
192+
// whether localhost nameservers are preserved. This is useful for host network mode
193+
// where the container should inherit the host's DNS configuration including localhost resolvers.
194+
//
195+
// Parameters:
196+
// - resolvConf: the resolv.conf file content
197+
// - ipv6Enabled: whether IPv6 nameservers should be preserved
198+
// - allowLocalhostDNS: if true, localhost nameservers are preserved; if false, they are filtered out
199+
func FilterResolvDNSWithLocalhostOption(resolvConf []byte, ipv6Enabled bool, allowLocalhostDNS bool) (*File, error) {
200+
cleanedResolvConf := resolvConf
201+
// if allowLocalhostDNS is false, remove localhost nameservers
202+
if !allowLocalhostDNS {
203+
cleanedResolvConf = localhostNSRegexp.ReplaceAll(cleanedResolvConf, []byte{})
204+
}
188205
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
189206
if !ipv6Enabled {
190207
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})

pkg/resolvconf/resolvconf_linux_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,96 @@ func TestFilterResolvDns(t *testing.T) {
320320
}
321321
}
322322
}
323+
324+
func TestFilterResolvDnsWithLocalhostOption(t *testing.T) {
325+
// Test 1: allowLocalhostDNS=false should strip localhost (original behavior)
326+
ns1 := "nameserver 127.0.0.1\nnameserver 10.16.60.14\n"
327+
expected := "nameserver 10.16.60.14\n"
328+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, false); result != nil {
329+
if expected != string(result.Content) {
330+
t.Fatalf("Failed allowLocalhostDNS=false: expected \n<%s> got \n<%s>", expected, string(result.Content))
331+
}
332+
}
333+
334+
// Test 2: allowLocalhostDNS=true should preserve localhost DNS
335+
ns1 = "nameserver 127.0.0.1\nnameserver 10.16.60.14\n"
336+
expected = "nameserver 127.0.0.1\nnameserver 10.16.60.14\n"
337+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, true); result != nil {
338+
if expected != string(result.Content) {
339+
t.Fatalf("Failed allowLocalhostDNS=true with mixed servers: expected \n<%s> got \n<%s>", expected, string(result.Content))
340+
}
341+
}
342+
343+
// Test 3: allowLocalhostDNS=true with only localhost DNS should be preserved
344+
ns1 = "nameserver 127.0.0.1\n"
345+
expected = "nameserver 127.0.0.1\n"
346+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, true); result != nil {
347+
if expected != string(result.Content) {
348+
t.Fatalf("Failed allowLocalhostDNS=true with only localhost: expected \n<%s> got \n<%s>", expected, string(result.Content))
349+
}
350+
}
351+
352+
// Test 4: allowLocalhostDNS=true with IPv6 localhost (::1) should preserve it
353+
ns1 = "nameserver ::1\nnameserver 10.16.60.14\n"
354+
expected = "nameserver ::1\nnameserver 10.16.60.14\n"
355+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, true); result != nil {
356+
if expected != string(result.Content) {
357+
t.Fatalf("Failed allowLocalhostDNS=true with IPv6 localhost: expected \n<%s> got \n<%s>", expected, string(result.Content))
358+
}
359+
}
360+
361+
// Test 5: allowLocalhostDNS=true with 127.0.0.53 (systemd-resolved) should preserve it
362+
ns1 = "nameserver 127.0.0.53\nnameserver 10.16.60.14\n"
363+
expected = "nameserver 127.0.0.53\nnameserver 10.16.60.14\n"
364+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, true); result != nil {
365+
if expected != string(result.Content) {
366+
t.Fatalf("Failed allowLocalhostDNS=true with 127.0.0.53: expected \n<%s> got \n<%s>", expected, string(result.Content))
367+
}
368+
}
369+
370+
// Test 6: allowLocalhostDNS=false should filter localhost even with IPv6 enabled
371+
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 10.16.60.14\nnameserver 2002:dead:beef::1\n"
372+
expected = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\n"
373+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, false); result != nil {
374+
if expected != string(result.Content) {
375+
t.Fatalf("Failed allowLocalhostDNS=false with mixed IPv4/IPv6: expected \n<%s> got \n<%s>", expected, string(result.Content))
376+
}
377+
}
378+
379+
// Test 7: allowLocalhostDNS=true with IPv6 disabled should strip IPv6 but keep localhost
380+
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 10.16.60.14\nnameserver 2002:dead:beef::1\n"
381+
expected = "nameserver 127.0.0.1\nnameserver 10.16.60.14\n"
382+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), false, true); result != nil {
383+
if expected != string(result.Content) {
384+
t.Fatalf("Failed allowLocalhostDNS=true with IPv6 disabled: expected \n<%s> got \n<%s>", expected, string(result.Content))
385+
}
386+
}
387+
388+
// Test 8: allowLocalhostDNS=true with only localhost and no external DNS, IPv6 enabled
389+
// should preserve localhost even though we would normally add defaults
390+
ns1 = "nameserver 127.0.0.1\nnameserver ::1\n"
391+
expected = "nameserver 127.0.0.1\nnameserver ::1\n"
392+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, true); result != nil {
393+
if expected != string(result.Content) {
394+
t.Fatalf("Failed allowLocalhostDNS=true with only localhost IPs: expected \n<%s> got \n<%s>", expected, string(result.Content))
395+
}
396+
}
397+
398+
// Test 9: allowLocalhostDNS=false with only localhost should fall back to Google DNS (IPv4+IPv6)
399+
ns0 := "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844"
400+
ns1 = "nameserver 127.0.0.1\nnameserver ::1\n"
401+
if result, _ := FilterResolvDNSWithLocalhostOption([]byte(ns1), true, false); result != nil {
402+
if ns0 != string(result.Content) {
403+
t.Fatalf("Failed allowLocalhostDNS=false fallback: expected \n<%s> got \n<%s>", ns0, string(result.Content))
404+
}
405+
}
406+
407+
// Test 10: Verify backward compatibility - FilterResolvDNS still filters localhost
408+
ns1 = "nameserver 127.0.0.1\nnameserver 10.16.60.14\n"
409+
expected = "nameserver 10.16.60.14\n"
410+
if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
411+
if expected != string(result.Content) {
412+
t.Fatalf("Failed backward compatibility: expected \n<%s> got \n<%s>", expected, string(result.Content))
413+
}
414+
}
415+
}

0 commit comments

Comments
 (0)