Refactor 'resolv.conf' generation.

Replace regex matching/replacement and re-reading of generated files
with a simple parser, and struct to remember and manipulate the file
content.

Annotate the generated file with a header comment saying the file is
generated, but can be modified, and a trailing comment describing how
the file was generated and listing external nameservers.

Always start with the host's resolv.conf file, whether generating config
for host networking, or with/without an internal resolver - rather than
editing a file previously generated for a different use-case.

Resolves an issue where rewrites of the generated file resulted in
default IPv6 nameservers being unnecessarily added to the config.

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray 2024-01-03 09:10:51 +00:00
parent f472dda2e9
commit beb97f7fdf
50 changed files with 1805 additions and 775 deletions

View file

@ -419,6 +419,7 @@ func serviceDiscoveryOnDefaultNetwork() bool {
func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Config, sboxOptions *[]libnetwork.SandboxOption) error {
var err error
var originResolvConfPath string
// Set the correct paths for /etc/hosts and /etc/resolv.conf, based on the
// networking-mode of the container. Note that containers with "container"
@ -432,8 +433,8 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con
*sboxOptions = append(
*sboxOptions,
libnetwork.OptionOriginHostsPath("/etc/hosts"),
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
)
originResolvConfPath = "/etc/resolv.conf"
case container.HostConfig.NetworkMode.IsUserDefined():
// The container uses a user-defined network. We use the embedded DNS
// server for container name resolution and to act as a DNS forwarder
@ -446,10 +447,7 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con
// If systemd-resolvd is used, the "upstream" DNS servers can be found in
// /run/systemd/resolve/resolv.conf. We do not query those DNS servers
// directly, as they can be dynamically reconfigured.
*sboxOptions = append(
*sboxOptions,
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
)
originResolvConfPath = "/etc/resolv.conf"
default:
// For other situations, such as the default bridge network, container
// discovery / name resolution is handled through /etc/hosts, and no
@ -462,12 +460,16 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con
// DNS servers on the host can be dynamically updated.
//
// Copy the host's resolv.conf for the container (/run/systemd/resolve/resolv.conf or /etc/resolv.conf)
*sboxOptions = append(
*sboxOptions,
libnetwork.OptionOriginResolvConfPath(cfg.GetResolvConf()),
)
originResolvConfPath = cfg.GetResolvConf()
}
// Allow tests to point at their own resolv.conf file.
if envPath := os.Getenv("DOCKER_TEST_RESOLV_CONF_PATH"); envPath != "" {
log.G(context.TODO()).Infof("Using OriginResolvConfPath from env: %s", envPath)
originResolvConfPath = envPath
}
*sboxOptions = append(*sboxOptions, libnetwork.OptionOriginResolvConfPath(originResolvConfPath))
container.HostsPath, err = container.GetRootResourcePath("hosts")
if err != nil {
return err

View file

@ -1275,10 +1275,11 @@ func (s *DockerCLIRunSuite) TestRunDNSDefaultOptions(c *testing.T) {
}
actual := cli.DockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf").Combined()
// check that the actual defaults are appended to the commented out
// localhost resolver (which should be preserved)
actual = regexp.MustCompile("(?m)^#.*$").ReplaceAllString(actual, "")
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
// NOTE: if we ever change the defaults from google dns, this will break
expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4\n"
expected := "nameserver 8.8.8.8 nameserver 8.8.4.4"
if actual != expected {
c.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual)
}
@ -1295,14 +1296,16 @@ func (s *DockerCLIRunSuite) TestRunDNSOptions(c *testing.T) {
c.Fatalf("Expected warning on stderr about localhost resolver, but got %q", result.Stderr())
}
actual := strings.ReplaceAll(strings.Trim(result.Stdout(), "\r\n"), "\n", " ")
if actual != "search mydomain nameserver 127.0.0.1 options ndots:9" {
c.Fatalf("expected 'search mydomain nameserver 127.0.0.1 options ndots:9', but says: %q", actual)
actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(result.Stdout(), "")
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
if actual != "nameserver 127.0.0.1 search mydomain options ndots:9" {
c.Fatalf("nameserver 127.0.0.1 expected 'search mydomain options ndots:9', but says: %q", actual)
}
out := cli.DockerCmd(c, "run", "--dns=1.1.1.1", "--dns-search=.", "--dns-opt=ndots:3", "busybox", "cat", "/etc/resolv.conf").Combined()
actual = strings.ReplaceAll(strings.Trim(strings.Trim(out, "\r\n"), " "), "\n", " ")
actual = regexp.MustCompile("(?m)^#.*$").ReplaceAllString(out, "")
actual = strings.ReplaceAll(strings.Trim(strings.Trim(actual, "\r\n"), " "), "\n", " ")
if actual != "nameserver 1.1.1.1 options ndots:3" {
c.Fatalf("expected 'nameserver 1.1.1.1 options ndots:3', but says: %q", actual)
}
@ -1312,9 +1315,10 @@ func (s *DockerCLIRunSuite) TestRunDNSRepeatOptions(c *testing.T) {
testRequires(c, DaemonIsLinux)
out := cli.DockerCmd(c, "run", "--dns=1.1.1.1", "--dns=2.2.2.2", "--dns-search=mydomain", "--dns-search=mydomain2", "--dns-opt=ndots:9", "--dns-opt=timeout:3", "busybox", "cat", "/etc/resolv.conf").Stdout()
actual := strings.ReplaceAll(strings.Trim(out, "\r\n"), "\n", " ")
if actual != "search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3" {
c.Fatalf("expected 'search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3', but says: %q", actual)
actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(out, "")
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
if actual != "nameserver 1.1.1.1 nameserver 2.2.2.2 search mydomain mydomain2 options ndots:9 timeout:3" {
c.Fatalf("expected 'nameserver 1.1.1.1 nameserver 2.2.2.2 search mydomain mydomain2 options ndots:9 timeout:3', but says: %q", actual)
}
}

View file

@ -0,0 +1,72 @@
package networking
import (
"os"
"strings"
"testing"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/integration/internal/network"
"github.com/docker/docker/testutil/daemon"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
// Regression test for https://github.com/moby/moby/issues/46968
func TestResolvConfLocalhostIPv6(t *testing.T) {
// No "/etc/resolv.conf" on Windows.
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
ctx := setupTest(t)
// Write a resolv.conf that only contains a loopback address.
// Not using t.TempDir() here because in rootless mode, while the temporary
// directory gets mode 0777, it's a subdir of an 0700 directory owned by root.
// So, it's not accessible by the daemon.
f, err := os.CreateTemp("", "resolv.conf")
assert.NilError(t, err)
defer os.Remove(f.Name())
err = f.Chmod(0644)
assert.NilError(t, err)
f.Write([]byte("nameserver 127.0.0.53\n"))
d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_RESOLV_CONF_PATH="+f.Name()))
d.StartWithBusybox(ctx, t, "--experimental", "--ip6tables")
defer d.Stop(t)
c := d.NewClientT(t)
defer c.Close()
netName := "nnn"
network.CreateNoError(ctx, t, c, netName,
network.WithDriver("bridge"),
network.WithIPv6(),
network.WithIPAM("fd49:b5ef:36d9::/64", "fd49:b5ef:36d9::1"),
)
defer network.RemoveNoError(ctx, t, c, netName)
result := container.RunAttach(ctx, t, c,
container.WithImage("busybox:latest"),
container.WithNetworkMode(netName),
container.WithCmd("cat", "/etc/resolv.conf"),
)
defer c.ContainerRemove(ctx, result.ContainerID, containertypes.RemoveOptions{
Force: true,
})
output := strings.ReplaceAll(result.Stdout.String(), f.Name(), "RESOLV.CONF")
assert.Check(t, is.Equal(output, `# Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it
# has been modified.
nameserver 127.0.0.11
options ndots:0
# Based on host file: 'RESOLV.CONF' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []
# Option ndots from: internal
`))
}

View file

@ -0,0 +1,510 @@
// Package resolvconf is used to generate a container's /etc/resolv.conf file.
//
// Constructor Load and Parse read a resolv.conf file from the filesystem or
// a reader respectively, and return a ResolvConf object.
//
// The ResolvConf object can then be updated with overrides for nameserver,
// search domains, and DNS options.
//
// ResolvConf can then be transformed to make it suitable for legacy networking,
// a network with an internal nameserver, or used as-is for host networking.
//
// This package includes methods to write the file for the container, along with
// a hash that can be used to detect modifications made by the user to avoid
// overwriting those updates.
package resolvconf
import (
"bufio"
"bytes"
"context"
_ "embed"
"fmt"
"io"
"io/fs"
"net/netip"
"os"
"slices"
"strconv"
"strings"
"text/template"
"github.com/containerd/log"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/ioutils"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// Fallback nameservers, to use if none can be obtained from the host or command
// line options.
var (
defaultIPv4NSs = []netip.Addr{
netip.MustParseAddr("8.8.8.8"),
netip.MustParseAddr("8.8.4.4"),
}
defaultIPv6NSs = []netip.Addr{
netip.MustParseAddr("2001:4860:4860::8888"),
netip.MustParseAddr("2001:4860:4860::8844"),
}
)
// ResolvConf represents a resolv.conf file. It can be constructed by
// reading a resolv.conf file, using method Parse().
type ResolvConf struct {
nameServers []netip.Addr
search []string
options []string
other []string // Unrecognised directives from the host's file, if any.
md metadata
}
// ExtDNSEntry represents a nameserver address that was removed from the
// container's resolv.conf when it was transformed by TransformForIntNS(). These
// are addresses read from the host's file, or applied via an override ('--dns').
type ExtDNSEntry struct {
Addr netip.Addr
HostLoopback bool // The address is loopback, in the host's namespace.
}
func (ed ExtDNSEntry) String() string {
if ed.HostLoopback {
return fmt.Sprintf("host(%s)", ed.Addr)
}
return ed.Addr.String()
}
// metadata is used to track where components of the generated file have come
// from, in order to generate comments in the file for debug/info. Struct members
// are exported for use by 'text/template'.
type metadata struct {
SourcePath string
Header string
NSOverride bool
SearchOverride bool
OptionsOverride bool
NDotsFrom string
UsedDefaultNS bool
Transform string
InvalidNSs []string
ExtNameServers []ExtDNSEntry
}
// Load opens a file at path and parses it as a resolv.conf file.
// On error, the returned ResolvConf will be zero-valued.
func Load(path string) (ResolvConf, error) {
f, err := os.Open(path)
if err != nil {
return ResolvConf{}, err
}
defer f.Close()
return Parse(f, path)
}
// Parse parses a resolv.conf file from reader.
// path is optional if reader is an *os.File.
// On error, the returned ResolvConf will be zero-valued.
func Parse(reader io.Reader, path string) (ResolvConf, error) {
var rc ResolvConf
rc.md.SourcePath = path
if path == "" {
if namer, ok := reader.(interface{ Name() string }); ok {
rc.md.SourcePath = namer.Name()
}
}
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
rc.processLine(scanner.Text())
}
if err := scanner.Err(); err != nil {
return ResolvConf{}, errdefs.System(err)
}
if _, ok := rc.Option("ndots"); ok {
rc.md.NDotsFrom = "host"
}
return rc, nil
}
// SetHeader sets the content to be included verbatim at the top of the
// generated resolv.conf file. No formatting or checking is done on the
// string. It must be valid resolv.conf syntax. (Comments must have '#'
// or ';' in the first column of each line).
//
// For example:
//
// SetHeader("# My resolv.conf\n# This file was generated.")
func (rc *ResolvConf) SetHeader(c string) {
rc.md.Header = c
}
// NameServers returns addresses used in nameserver directives.
func (rc *ResolvConf) NameServers() []netip.Addr {
return slices.Clone(rc.nameServers)
}
// OverrideNameServers replaces the current set of nameservers.
func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) {
rc.nameServers = nameServers
rc.md.NSOverride = true
}
// Search returns the current DNS search domains.
func (rc *ResolvConf) Search() []string {
return slices.Clone(rc.search)
}
// OverrideSearch replaces the current DNS search domains.
func (rc *ResolvConf) OverrideSearch(search []string) {
var filtered []string
for _, s := range search {
if s != "." {
filtered = append(filtered, s)
}
}
rc.search = filtered
rc.md.SearchOverride = true
}
// Options returns the current options.
func (rc *ResolvConf) Options() []string {
return slices.Clone(rc.options)
}
// Option finds the last option named search, and returns (value, true) if
// found, else ("", false). Options are treated as "name:value", where the
// ":value" may be omitted.
//
// For example, for "ndots:1 edns0":
//
// Option("ndots") -> ("1", true)
// Option("edns0") -> ("", true)
func (rc *ResolvConf) Option(search string) (string, bool) {
for i := len(rc.options) - 1; i >= 0; i -= 1 {
k, v, _ := strings.Cut(rc.options[i], ":")
if k == search {
return v, true
}
}
return "", false
}
// OverrideOptions replaces the current DNS options.
func (rc *ResolvConf) OverrideOptions(options []string) {
rc.options = slices.Clone(options)
rc.md.NDotsFrom = ""
if _, exists := rc.Option("ndots"); exists {
rc.md.NDotsFrom = "override"
}
rc.md.OptionsOverride = true
}
// AddOption adds a single DNS option.
func (rc *ResolvConf) AddOption(option string) {
if len(option) > 6 && option[:6] == "ndots:" {
rc.md.NDotsFrom = "internal"
}
rc.options = append(rc.options, option)
}
// TransformForLegacyNw makes sure the resolv.conf file will be suitable for
// use in a legacy network (one that has no internal resolver).
// - Remove loopback addresses inherited from the host's resolv.conf, because
// they'll only work in the host's namespace.
// - Remove IPv6 addresses if !ipv6.
// - Add default nameservers if there are no addresses left.
func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
rc.md.Transform = "legacy"
if rc.md.NSOverride {
return
}
var filtered []netip.Addr
for _, addr := range rc.nameServers {
if !addr.IsLoopback() && (!addr.Is6() || ipv6) {
filtered = append(filtered, addr)
}
}
rc.nameServers = filtered
if len(rc.nameServers) == 0 {
log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
rc.nameServers = defaultNSAddrs(ipv6)
rc.md.UsedDefaultNS = true
}
}
// TransformForIntNS makes sure the resolv.conf file will be suitable for
// use in a network sandbox that has an internal DNS resolver.
// - Add internalNS as a nameserver.
// - Remove other nameservers, stashing them as ExtNameServers for the
// internal resolver to use. (Apart from IPv6 nameservers, if keepIPv6.)
// - Mark ExtNameServers that must be used in the host namespace.
// - If no ExtNameServer addresses are found, use the defaults.
// - Return an error if an "ndots" option inherited from the host's config, or
// supplied in an override is not valid.
// - Ensure there's an 'options' value for each entry in reqdOptions. If the
// option includes a ':', and an option with a matching prefix exists, it
// is not modified.
func (rc *ResolvConf) TransformForIntNS(
keepIPv6 bool,
internalNS netip.Addr,
reqdOptions []string,
) ([]ExtDNSEntry, error) {
// The transformed config must list the internal nameserver.
newNSs := []netip.Addr{internalNS}
// Filter out other nameservers, keeping them for use as upstream nameservers by the
// internal nameserver.
rc.md.ExtNameServers = nil
for _, addr := range rc.nameServers {
// The internal resolver only uses IPv4 addresses so, keep IPv6 nameservers in
// the container's file if keepIPv6, else drop them.
if addr.Is6() {
if keepIPv6 {
newNSs = append(newNSs, addr)
}
} else {
// Extract this NS. Mark loopback addresses that did not come from an override as
// 'HostLoopback'. Upstream requests for these servers will be made in the host's
// network namespace. (So, '--dns 127.0.0.53' means use a nameserver listening on
// the container's loopback interface. But, if the host's resolv.conf contains
// 'nameserver 127.0.0.53', the host's resolver will be used.)
//
// TODO(robmry) - why only loopback addresses?
// Addresses from the host's resolv.conf must be usable in the host's namespace,
// and a lookup from the container's namespace is more expensive? And, for
// example, if the host has a nameserver with an IPv6 LL address with a zone-id,
// it won't work from the container's namespace (now, while the address is left in
// the container's resolv.conf, or in future for the internal resolver).
rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
Addr: addr,
HostLoopback: addr.IsLoopback() && !rc.md.NSOverride,
})
}
}
rc.nameServers = newNSs
// If there are no external nameservers, and the only nameserver left is the
// internal resolver, use the defaults as ext nameservers.
if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
for _, addr := range defaultNSAddrs(keepIPv6) {
rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
}
rc.md.UsedDefaultNS = true
}
// Validate the ndots option from host config or overrides, if present.
// TODO(robmry) - pre-existing behaviour, but ...
// Validating ndots from an override is good, but not-liking something in the
// host's resolv.conf isn't a reason to fail - just remove? (And it'll be
// replaced by the value in reqdOptions, if given.)
if ndots, exists := rc.Option("ndots"); exists {
if n, err := strconv.Atoi(ndots); err != nil || n < 0 {
return nil, errdefs.InvalidParameter(
fmt.Errorf("invalid number for ndots option: %v", ndots))
}
}
// For each option required by the nameserver, add it if not already
// present (if the option already has a value don't change it).
for _, opt := range reqdOptions {
optName, _, _ := strings.Cut(opt, ":")
if _, exists := rc.Option(optName); !exists {
rc.AddOption(opt)
}
}
rc.md.Transform = "internal resolver"
return slices.Clone(rc.md.ExtNameServers), nil
}
// Generate returns content suitable for writing to a resolv.conf file. If comments
// is true, the file will include header information if supplied, and a trailing
// comment that describes how the file was constructed and lists external resolvers.
func (rc *ResolvConf) Generate(comments bool) ([]byte, error) {
s := struct {
Md *metadata
NameServers []netip.Addr
Search []string
Options []string
Other []string
Overrides []string
Comments bool
}{
Md: &rc.md,
NameServers: rc.nameServers,
Search: rc.search,
Options: rc.options,
Other: rc.other,
Comments: comments,
}
if rc.md.NSOverride {
s.Overrides = append(s.Overrides, "nameservers")
}
if rc.md.SearchOverride {
s.Overrides = append(s.Overrides, "search")
}
if rc.md.OptionsOverride {
s.Overrides = append(s.Overrides, "options")
}
const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}}
{{end}}{{end}}{{range .NameServers -}}
nameserver {{.}}
{{end}}{{with .Search -}}
search {{join . " "}}
{{end}}{{with .Options -}}
options {{join . " "}}
{{end}}{{with .Other -}}
{{join . "\n"}}
{{end}}{{if .Comments}}
# Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}}
{{if .Md.UsedDefaultNS -}}
# Used default nameservers.
{{end -}}
{{with .Md.ExtNameServers -}}
# ExtServers: {{.}}
{{end -}}
{{with .Md.InvalidNSs -}}
# Invalid nameservers: {{.}}
{{end -}}
# Overrides: {{.Overrides}}
{{with .Md.NDotsFrom -}}
# Option ndots from: {{.}}
{{end -}}
{{end -}}
`
funcs := template.FuncMap{"join": strings.Join}
var buf bytes.Buffer
templ, err := template.New("summary").Funcs(funcs).Parse(templateText)
if err != nil {
return nil, errdefs.System(err)
}
if err := templ.Execute(&buf, s); err != nil {
return nil, errdefs.System(err)
}
return buf.Bytes(), nil
}
// WriteFile generates content and writes it to path. If hashPath is non-zero, it
// also writes a file containing a hash of the content, to enable UserModified()
// to determine whether the file has been modified.
func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error {
content, err := rc.Generate(true)
if err != nil {
return err
}
// Write the resolv.conf file - it's bind-mounted into the container, so can't
// move a temp file into place, just have to truncate and write it.
if err := os.WriteFile(path, content, perm); err != nil {
return errdefs.System(err)
}
// Write the hash file.
if hashPath != "" {
hashFile, err := ioutils.NewAtomicFileWriter(hashPath, perm)
if err != nil {
return errdefs.System(err)
}
defer hashFile.Close()
digest := digest.FromBytes(content)
if _, err = hashFile.Write([]byte(digest)); err != nil {
return err
}
}
return nil
}
// UserModified can be used to determine whether the resolv.conf file has been
// modified since it was generated. It returns false with no error if the file
// matches the hash, true with no error if the file no longer matches the hash,
// and false with an error if the result cannot be determined.
func UserModified(rcPath, rcHashPath string) (bool, error) {
currRCHash, err := os.ReadFile(rcHashPath)
if err != nil {
// If the hash file doesn't exist, can only assume it hasn't been written
// yet (so, the user hasn't modified the file it hashes).
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath)
}
expected, err := digest.Parse(string(currRCHash))
if err != nil {
return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath)
}
v := expected.Verifier()
currRC, err := os.Open(rcPath)
if err != nil {
return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath)
}
defer currRC.Close()
if _, err := io.Copy(v, currRC); err != nil {
return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath)
}
return !v.Verified(), nil
}
func (rc *ResolvConf) processLine(line string) {
fields := strings.Fields(line)
// Strip comments.
// TODO(robmry) - ignore comment chars except in column 0.
// This preserves old behaviour, but it's wrong. For example, resolvers
// will honour the option in line "options # ndots:0" (and ignore the
// "#" as an unknown option).
for i, s := range fields {
if s[0] == '#' || s[0] == ';' {
fields = fields[:i]
break
}
}
if len(fields) == 0 {
return
}
switch fields[0] {
case "nameserver":
if len(fields) < 2 {
return
}
if addr, err := netip.ParseAddr(fields[1]); err != nil {
rc.md.InvalidNSs = append(rc.md.InvalidNSs, fields[1])
} else {
rc.nameServers = append(rc.nameServers, addr)
}
case "domain":
// 'domain' is an obsolete name for 'search'.
fallthrough
case "search":
if len(fields) < 2 {
return
}
// Only the last 'search' directive is used.
rc.search = fields[1:]
case "options":
if len(fields) < 2 {
return
}
// Replace options from earlier directives.
// TODO(robmry) - preserving incorrect behaviour, options should accumulate.
// rc.options = append(rc.options, fields[1:]...)
rc.options = fields[1:]
default:
// Copy anything that's not a recognised directive.
rc.other = append(rc.other, line)
}
}
func defaultNSAddrs(ipv6 bool) []netip.Addr {
var addrs []netip.Addr
addrs = append(addrs, defaultIPv4NSs...)
if ipv6 {
addrs = append(addrs, defaultIPv6NSs...)
}
return addrs
}

View file

@ -0,0 +1,56 @@
package resolvconf
import (
"context"
"net/netip"
"sync"
"github.com/containerd/log"
)
const (
// defaultPath is the default path to the resolv.conf that contains information to resolve DNS. See Path().
defaultPath = "/etc/resolv.conf"
// alternatePath is a path different from defaultPath, that may be used to resolve DNS. See Path().
alternatePath = "/run/systemd/resolve/resolv.conf"
)
// For Path to detect systemd (only needed for legacy networking).
var (
detectSystemdResolvConfOnce sync.Once
pathAfterSystemdDetection = defaultPath
)
// Path returns the path to the resolv.conf file that libnetwork should use.
//
// When /etc/resolv.conf contains 127.0.0.53 as the only nameserver, then
// it is assumed systemd-resolved manages DNS. Because inside the container 127.0.0.53
// is not a valid DNS server, Path() returns /run/systemd/resolve/resolv.conf
// which is the resolv.conf that systemd-resolved generates and manages.
// Otherwise Path() returns /etc/resolv.conf.
//
// Errors are silenced as they will inevitably resurface at future open/read calls.
//
// More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf
//
// TODO(robmry) - alternatePath is only needed for legacy networking ...
//
// Host networking can use the host's resolv.conf as-is, and with an internal
// resolver it's also possible to use nameservers on the host's loopback
// interface. Once legacy networking is removed, this can always return
// defaultPath.
func Path() string {
detectSystemdResolvConfOnce.Do(func() {
rc, err := Load(defaultPath)
if err != nil {
// silencing error as it will resurface at next calls trying to read defaultPath
return
}
ns := rc.nameServers
if len(ns) == 1 && ns[0] == netip.MustParseAddr("127.0.0.53") {
pathAfterSystemdDetection = alternatePath
log.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath)
}
})
return pathAfterSystemdDetection
}

View file

@ -0,0 +1,577 @@
package resolvconf
import (
"bytes"
"io/fs"
"net/netip"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/docker/docker/internal/sliceutil"
"github.com/google/go-cmp/cmp/cmpopts"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden"
)
func TestRCOption(t *testing.T) {
testcases := []struct {
name string
options string
search string
expFound bool
expValue string
}{
{
name: "Empty options",
options: "",
search: "ndots",
},
{
name: "Not found",
options: "ndots:0 edns0",
search: "trust-ad",
},
{
name: "Found with value",
options: "ndots:0 edns0",
search: "ndots",
expFound: true,
expValue: "0",
},
{
name: "Found without value",
options: "ndots:0 edns0",
search: "edns0",
expFound: true,
expValue: "",
},
{
name: "Found last value",
options: "ndots:0 edns0 ndots:1",
search: "ndots",
expFound: true,
expValue: "1",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
rc, err := Parse(bytes.NewBuffer([]byte("options "+tc.options)), "")
assert.NilError(t, err)
value, found := rc.Option(tc.search)
assert.Check(t, is.Equal(found, tc.expFound))
assert.Check(t, is.Equal(value, tc.expValue))
})
}
}
func TestRCWrite(t *testing.T) {
testcases := []struct {
name string
fileName string
perm os.FileMode
hashFileName string
modify bool
expUserModified bool
}{
{
name: "Write with hash",
fileName: "testfile",
hashFileName: "testfile.hash",
},
{
name: "Write with hash and modify",
fileName: "testfile",
hashFileName: "testfile.hash",
modify: true,
expUserModified: true,
},
{
name: "Write without hash and modify",
fileName: "testfile",
modify: true,
expUserModified: false,
},
{
name: "Write perm",
fileName: "testfile",
perm: 0640,
},
}
rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4")), "")
assert.NilError(t, err)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
d := t.TempDir()
path := filepath.Join(d, tc.fileName)
var hashPath string
if tc.hashFileName != "" {
hashPath = filepath.Join(d, tc.hashFileName)
}
if tc.perm == 0 {
tc.perm = 0644
}
err := rc.WriteFile(path, hashPath, tc.perm)
assert.NilError(t, err)
fi, err := os.Stat(path)
assert.NilError(t, err)
// Windows files won't have the expected perms.
if runtime.GOOS != "windows" {
assert.Check(t, is.Equal(fi.Mode(), tc.perm))
}
if tc.modify {
err := os.WriteFile(path, []byte("modified"), 0644)
assert.NilError(t, err)
}
um, err := UserModified(path, hashPath)
assert.NilError(t, err)
assert.Check(t, is.Equal(um, tc.expUserModified))
})
}
}
var a2s = sliceutil.Mapper(netip.Addr.String)
var s2a = sliceutil.Mapper(netip.MustParseAddr)
// Test that a resolv.conf file can be modified using OverrideXXX() methods
// to modify nameservers/search/options directives, and tha options can be
// added via AddOption().
func TestRCModify(t *testing.T) {
testcases := []struct {
name string
inputNS []string
inputSearch []string
inputOptions []string
noOverrides bool // Whether to apply overrides (empty lists are valid overrides).
overrideNS []string
overrideSearch []string
overrideOptions []string
addOption string
}{
{
name: "No content no overrides",
inputNS: []string{},
},
{
name: "No overrides",
noOverrides: true,
inputNS: []string{"1.2.3.4"},
inputSearch: []string{"invalid"},
inputOptions: []string{"ndots:0"},
},
{
name: "Empty overrides",
inputNS: []string{"1.2.3.4"},
inputSearch: []string{"invalid"},
inputOptions: []string{"ndots:0"},
},
{
name: "Overrides",
inputNS: []string{"1.2.3.4"},
inputSearch: []string{"invalid"},
inputOptions: []string{"ndots:0"},
overrideNS: []string{"2.3.4.5", "fdba:acdd:587c::53"},
overrideSearch: []string{"com", "invalid", "example"},
overrideOptions: []string{"ndots:1", "edns0", "trust-ad"},
},
{
name: "Add option no overrides",
noOverrides: true,
inputNS: []string{"1.2.3.4"},
inputSearch: []string{"invalid"},
inputOptions: []string{"ndots:0"},
addOption: "attempts:3",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
var input string
if len(tc.inputNS) != 0 {
for _, ns := range tc.inputNS {
input += "nameserver " + ns + "\n"
}
}
if len(tc.inputSearch) != 0 {
input += "search " + strings.Join(tc.inputSearch, " ") + "\n"
}
if len(tc.inputOptions) != 0 {
input += "options " + strings.Join(tc.inputOptions, " ") + "\n"
}
rc, err := Parse(bytes.NewBuffer([]byte(input)), "")
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(a2s(rc.NameServers()), tc.inputNS))
assert.Check(t, is.DeepEqual(rc.Search(), tc.inputSearch))
assert.Check(t, is.DeepEqual(rc.Options(), tc.inputOptions))
if !tc.noOverrides {
overrideNS := s2a(tc.overrideNS)
rc.OverrideNameServers(overrideNS)
rc.OverrideSearch(tc.overrideSearch)
rc.OverrideOptions(tc.overrideOptions)
assert.Check(t, is.DeepEqual(rc.NameServers(), overrideNS, cmpopts.EquateComparable(netip.Addr{})))
assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch))
assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions))
}
if tc.addOption != "" {
options := rc.Options()
rc.AddOption(tc.addOption)
assert.Check(t, is.DeepEqual(rc.Options(), append(options, tc.addOption)))
}
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
})
}
}
func TestRCTransformForLegacyNw(t *testing.T) {
testcases := []struct {
name string
input string
ipv6 bool
overrideNS []string
}{
{
name: "Routable IPv4 only",
input: "nameserver 10.0.0.1",
},
{
name: "Routable IPv4 and IPv6, ipv6 enabled",
input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
ipv6: true,
},
{
name: "Routable IPv4 and IPv6, ipv6 disabled",
input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
ipv6: false,
},
{
name: "IPv4 localhost, ipv6 disabled",
input: "nameserver 127.0.0.53",
ipv6: false,
},
{
name: "IPv4 localhost, ipv6 enabled",
input: "nameserver 127.0.0.53",
ipv6: true,
},
{
name: "IPv4 and IPv6 localhost, ipv6 disabled",
input: "nameserver 127.0.0.53\nnameserver ::1",
ipv6: false,
},
{
name: "IPv4 and IPv6 localhost, ipv6 enabled",
input: "nameserver 127.0.0.53\nnameserver ::1",
ipv6: true,
},
{
name: "IPv4 localhost, IPv6 routeable, ipv6 enabled",
input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
ipv6: true,
},
{
name: "IPv4 localhost, IPv6 routeable, ipv6 disabled",
input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
ipv6: false,
},
{
name: "Override nameservers",
input: "nameserver 127.0.0.53",
overrideNS: []string{"127.0.0.1", "::1"},
ipv6: false,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
assert.NilError(t, err)
if tc.overrideNS != nil {
rc.OverrideNameServers(s2a(tc.overrideNS))
}
rc.TransformForLegacyNw(tc.ipv6)
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
})
}
}
func TestRCTransformForIntNS(t *testing.T) {
mke := func(addr string, hostLoopback bool) ExtDNSEntry {
return ExtDNSEntry{
Addr: netip.MustParseAddr(addr),
HostLoopback: hostLoopback,
}
}
testcases := []struct {
name string
input string
intNameServer string
ipv6 bool
overrideNS []string
overrideOptions []string
reqdOptions []string
expExtServers []ExtDNSEntry
expErr string
}{
{
name: "IPv4 only",
input: "nameserver 10.0.0.1",
expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
},
{
name: "IPv4 and IPv6, ipv6 enabled",
input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
ipv6: true,
expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
},
{
name: "IPv4 and IPv6, ipv6 disabled",
input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1",
ipv6: false,
expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
},
{
name: "IPv4 localhost",
input: "nameserver 127.0.0.53",
ipv6: false,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
// Overriding the nameserver with a localhost address means use the container's
// loopback interface, not the host's.
name: "IPv4 localhost override",
input: "nameserver 10.0.0.1",
ipv6: false,
overrideNS: []string{"127.0.0.53"},
expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)},
},
{
name: "IPv4 localhost, ipv6 enabled",
input: "nameserver 127.0.0.53",
ipv6: true,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "IPv6 addr, IPv6 enabled",
input: "nameserver fd14:6e0e:f855::1",
ipv6: true,
// Note that there are no ext servers in this case, the internal resolver
// will only look up container names. The default nameservers aren't added
// because the host's IPv6 nameserver remains in the container's resolv.conf,
// (because only IPv4 ext servers are currently allowed).
},
{
name: "IPv4 and IPv6 localhost, IPv6 disabled",
input: "nameserver 127.0.0.53\nnameserver ::1",
ipv6: false,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "IPv4 and IPv6 localhost, ipv6 enabled",
input: "nameserver 127.0.0.53\nnameserver ::1",
ipv6: true,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "IPv4 localhost, IPv6 private, IPv6 enabled",
input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
ipv6: true,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "IPv4 localhost, IPv6 private, IPv6 disabled",
input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1",
ipv6: false,
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "No host nameserver, no iv6",
input: "",
ipv6: false,
expExtServers: []ExtDNSEntry{
mke("8.8.8.8", false),
mke("8.8.4.4", false),
},
},
{
name: "No host nameserver, iv6",
input: "",
ipv6: true,
expExtServers: []ExtDNSEntry{
mke("8.8.8.8", false),
mke("8.8.4.4", false),
mke("2001:4860:4860::8888", false),
mke("2001:4860:4860::8844", false),
},
},
{
name: "ndots present and required",
input: "nameserver 127.0.0.53\noptions ndots:1",
reqdOptions: []string{"ndots:0"},
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "ndots missing but required",
input: "nameserver 127.0.0.53",
reqdOptions: []string{"ndots:0"},
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "ndots host, override and required",
input: "nameserver 127.0.0.53",
reqdOptions: []string{"ndots:0"},
overrideOptions: []string{"ndots:2"},
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
{
name: "Extra required options",
input: "nameserver 127.0.0.53\noptions trust-ad",
reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"},
expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tc := tc
rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf")
assert.NilError(t, err)
if tc.intNameServer == "" {
tc.intNameServer = "127.0.0.11"
}
if len(tc.overrideNS) > 0 {
rc.OverrideNameServers(s2a(tc.overrideNS))
}
if len(tc.overrideOptions) > 0 {
rc.OverrideOptions(tc.overrideOptions)
}
intNS := netip.MustParseAddr(tc.intNameServer)
extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions)
if tc.expErr != "" {
assert.Check(t, is.ErrorContains(err, tc.expErr))
return
}
assert.NilError(t, err)
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers,
cmpopts.EquateComparable(netip.Addr{})))
})
}
}
func TestRCRead(t *testing.T) {
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
// Try to read a nonexistent file, equivalent to an empty file.
_, err := Load(path)
assert.Check(t, is.ErrorIs(err, fs.ErrNotExist))
err = os.WriteFile(path, []byte("options edns0"), 0644)
assert.NilError(t, err)
// Read that file in the constructor.
rc, err := Load(path)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"}))
// Pass in an os.File, check the path is extracted.
file, err := os.Open(path)
assert.NilError(t, err)
defer file.Close()
rc, err = Parse(file, "")
assert.NilError(t, err)
assert.Check(t, is.Equal(rc.md.SourcePath, path))
}
func TestRCInvalidNS(t *testing.T) {
d := t.TempDir()
// A resolv.conf with an invalid nameserver address.
rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "")
assert.NilError(t, err)
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
}
func TestRCSetHeader(t *testing.T) {
rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf")
assert.NilError(t, err)
rc.SetHeader("# This is a comment.")
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
}
func TestRCUnknownDirectives(t *testing.T) {
const input = `
something unexpected
nameserver 127.0.0.53
options ndots:1
unrecognised thing
`
rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf")
assert.NilError(t, err)
d := t.TempDir()
path := filepath.Join(d, "resolv.conf")
err = rc.WriteFile(path, "", 0644)
assert.NilError(t, err)
content, err := os.ReadFile(path)
assert.NilError(t, err)
assert.Check(t, golden.String(string(content), t.Name()+".golden"))
}

View file

@ -0,0 +1 @@
* text=auto eol=lf

View file

@ -0,0 +1,4 @@
# Based on host file: ''
# Invalid nameservers: [1.2.3.4.5]
# Overrides: []

View file

@ -0,0 +1,7 @@
nameserver 1.2.3.4
search invalid
options ndots:0 attempts:3
# Based on host file: ''
# Overrides: []
# Option ndots from: host

View file

@ -0,0 +1,3 @@
# Based on host file: ''
# Overrides: [nameservers search options]

View file

@ -0,0 +1,3 @@
# Based on host file: ''
# Overrides: [nameservers search options]

View file

@ -0,0 +1,7 @@
nameserver 1.2.3.4
search invalid
options ndots:0
# Based on host file: ''
# Overrides: []
# Option ndots from: host

View file

@ -0,0 +1,8 @@
nameserver 2.3.4.5
nameserver fdba:acdd:587c::53
search com invalid example
options ndots:1 edns0 trust-ad
# Based on host file: ''
# Overrides: [nameservers search options]
# Option ndots from: override

View file

@ -0,0 +1,6 @@
# This is a comment.
nameserver 127.0.0.53
# Based on host file: '/etc/resolv.conf'
# Overrides: []

View file

@ -0,0 +1,7 @@
nameserver 127.0.0.11
options trust-ad ndots:0 attempts:3 edns0
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []
# Option ndots from: internal

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [10.0.0.1]
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 127.0.0.11
nameserver fdb6:b8fe:b528::1
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [10.0.0.1]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 127.0.0.11
nameserver ::1
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 127.0.0.11
nameserver fd3e:2d1a:1f5a::1
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [127.0.0.53]
# Overrides: [nameservers]

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [10.0.0.1]
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.11
nameserver fd14:6e0e:f855::1
# Based on host file: '/etc/resolv.conf' (internal resolver)
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# Used default nameservers.
# ExtServers: [8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844]
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 127.0.0.11
# Based on host file: '/etc/resolv.conf' (internal resolver)
# Used default nameservers.
# ExtServers: [8.8.8.8 8.8.4.4]
# Overrides: []

View file

@ -0,0 +1,7 @@
nameserver 127.0.0.11
options ndots:2
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: [options]
# Option ndots from: override

View file

@ -0,0 +1,7 @@
nameserver 127.0.0.11
options ndots:0
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []
# Option ndots from: internal

View file

@ -0,0 +1,7 @@
nameserver 127.0.0.11
options ndots:1
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []
# Option ndots from: host

View file

@ -0,0 +1,6 @@
nameserver 8.8.8.8
nameserver 8.8.4.4
# Based on host file: '/etc/resolv.conf' (legacy)
# Used default nameservers.
# Overrides: []

View file

@ -0,0 +1,8 @@
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 2001:4860:4860::8888
nameserver 2001:4860:4860::8844
# Based on host file: '/etc/resolv.conf' (legacy)
# Used default nameservers.
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 8.8.8.8
nameserver 8.8.4.4
# Based on host file: '/etc/resolv.conf' (legacy)
# Used default nameservers.
# Overrides: []

View file

@ -0,0 +1,4 @@
nameserver fd3e:2d1a:1f5a::1
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: []

View file

@ -0,0 +1,6 @@
nameserver 8.8.8.8
nameserver 8.8.4.4
# Based on host file: '/etc/resolv.conf' (legacy)
# Used default nameservers.
# Overrides: []

View file

@ -0,0 +1,8 @@
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 2001:4860:4860::8888
nameserver 2001:4860:4860::8844
# Based on host file: '/etc/resolv.conf' (legacy)
# Used default nameservers.
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 127.0.0.1
nameserver ::1
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: [nameservers]

View file

@ -0,0 +1,4 @@
nameserver 10.0.0.1
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: []

View file

@ -0,0 +1,5 @@
nameserver 10.0.0.1
nameserver fdb6:b8fe:b528::1
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: []

View file

@ -0,0 +1,4 @@
nameserver 10.0.0.1
# Based on host file: '/etc/resolv.conf' (legacy)
# Overrides: []

View file

@ -0,0 +1,8 @@
nameserver 127.0.0.53
options ndots:1
something unexpected
unrecognised thing
# Based on host file: '/etc/resolv.conf'
# Overrides: []
# Option ndots from: host

View file

@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
@ -32,6 +33,8 @@ import (
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"golang.org/x/sync/errgroup"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
const (
@ -1278,6 +1281,28 @@ func makeTesthostNetwork(t *testing.T, c *libnetwork.Controller) *libnetwork.Net
return n
}
func makeTestIPv6Network(t *testing.T, c *libnetwork.Controller) *libnetwork.Network {
t.Helper()
netOptions := options.Generic{
netlabel.EnableIPv6: true,
netlabel.GenericData: options.Generic{
"BridgeName": "testnetwork",
},
}
ipamV6ConfList := []*libnetwork.IpamConf{
{PreferredPool: "fd81:fb6e:38ba:abcd::/64", Gateway: "fd81:fb6e:38ba:abcd::9"},
}
n, err := createTestNetwork(c,
"bridge",
"testnetwork",
netOptions,
nil,
ipamV6ConfList,
)
assert.NilError(t, err)
return n
}
func TestHost(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
controller := newController(t)
@ -1790,295 +1815,92 @@ func reexecSetKey(key string, containerID string, controllerID string) error {
return cmd.Run()
}
func TestEnableIPv6(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
controller := newController(t)
tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n")
expectedResolvConf := []byte("search pommesfrites.fr\nnameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\noptions ndots:0\n")
// take a copy of resolv.conf for restoring after test completes
resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
if err != nil {
t.Fatal(err)
}
// cleanup
defer func() {
if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
t.Fatal(err)
}
}()
netOption := options.Generic{
netlabel.EnableIPv6: true,
netlabel.GenericData: options.Generic{
"BridgeName": "testnetwork",
},
}
ipamV6ConfList := []*libnetwork.IpamConf{{PreferredPool: "fe99::/64", Gateway: "fe99::9"}}
n, err := createTestNetwork(controller, "bridge", "testnetwork", netOption, nil, ipamV6ConfList)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := n.Delete(); err != nil {
t.Fatal(err)
}
}()
ep1, err := n.CreateEndpoint("ep1")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf, 0o644); err != nil {
t.Fatal(err)
}
resolvConfPath := "/tmp/libnetwork_test/resolv.conf"
defer os.Remove(resolvConfPath)
sb, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath))
if err != nil {
t.Fatal(err)
}
defer func() {
if err := sb.Delete(); err != nil {
t.Fatal(err)
}
}()
err = ep1.Join(sb)
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(resolvConfPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, expectedResolvConf) {
t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf), string(content))
}
if err != nil {
t.Fatal(err)
}
}
func TestResolvConfHost(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
controller := newController(t)
tmpResolvConf := []byte("search localhost.net\nnameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\n")
// take a copy of resolv.conf for restoring after test completes
resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
if err != nil {
t.Fatal(err)
}
// cleanup
defer func() {
if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
t.Fatal(err)
}
}()
n := makeTesthostNetwork(t, controller)
ep1, err := n.CreateEndpoint("ep1", libnetwork.CreateOptionDisableResolution())
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf, 0o644); err != nil {
t.Fatal(err)
}
resolvConfPath := "/tmp/libnetwork_test/resolv.conf"
defer os.Remove(resolvConfPath)
sb, err := controller.NewSandbox(containerID,
libnetwork.OptionUseDefaultSandbox(),
libnetwork.OptionResolvConfPath(resolvConfPath),
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"))
if err != nil {
t.Fatal(err)
}
defer func() {
if err := sb.Delete(); err != nil {
t.Fatal(err)
}
}()
err = ep1.Join(sb)
if err != nil {
t.Fatal(err)
}
defer func() {
err = ep1.Leave(sb)
if err != nil {
t.Fatal(err)
}
}()
finfo, err := os.Stat(resolvConfPath)
if err != nil {
t.Fatal(err)
}
fmode := (os.FileMode)(0o644)
if finfo.Mode() != fmode {
t.Fatalf("Expected file mode %s, got %s", fmode.String(), finfo.Mode().String())
}
content, err := os.ReadFile(resolvConfPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, tmpResolvConf) {
t.Fatalf("Expected:\n%s\nGot:\n%s", string(tmpResolvConf), string(content))
}
}
func TestResolvConf(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
controller := newController(t)
tmpDir := t.TempDir()
originResolvConfPath := filepath.Join(tmpDir, "origin_resolv.conf")
resolvConfPath := filepath.Join(tmpDir, "resolv.conf")
tmpResolvConf1 := []byte("search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n")
tmpResolvConf2 := []byte("search pommesfrites.fr\nnameserver 112.34.56.78\nnameserver 2001:4860:4860::8888\n")
expectedResolvConf1 := []byte("search pommesfrites.fr\nnameserver 127.0.0.11\noptions ndots:0\n")
tmpResolvConf3 := []byte("search pommesfrites.fr\nnameserver 113.34.56.78\n")
// Strip comments that end in a newline (a comment with no newline at the end
// of the file will not be stripped).
stripCommentsRE := regexp.MustCompile(`(?m)^#.*\n`)
// take a copy of resolv.conf for restoring after test completes
resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
if err != nil {
t.Fatal(err)
}
// cleanup
defer func() {
if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
t.Fatal(err)
}
}()
netOption := options.Generic{
netlabel.GenericData: options.Generic{
"BridgeName": "testnetwork",
testcases := []struct {
name string
makeNet func(t *testing.T, c *libnetwork.Controller) *libnetwork.Network
delNet bool
epOpts []libnetwork.EndpointOption
sbOpts []libnetwork.SandboxOption
originResolvConf string
expResolvConf string
}{
{
name: "IPv6 network",
makeNet: makeTestIPv6Network,
delNet: true,
originResolvConf: "search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n",
expResolvConf: "nameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\nsearch pommesfrites.fr\noptions ndots:0",
},
{
name: "host network",
makeNet: makeTesthostNetwork,
epOpts: []libnetwork.EndpointOption{libnetwork.CreateOptionDisableResolution()},
sbOpts: []libnetwork.SandboxOption{libnetwork.OptionUseDefaultSandbox()},
originResolvConf: "search localhost.net\nnameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\n",
expResolvConf: "nameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\nsearch localhost.net",
},
}
n, err := createTestNetwork(controller, "bridge", "testnetwork", netOption, nil, nil)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := n.Delete(); err != nil {
t.Fatal(err)
}
}()
ep, err := n.CreateEndpoint("ep")
if err != nil {
t.Fatal(err)
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
defer netnsutils.SetupTestOSContext(t)()
c := newController(t)
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf1, 0o644); err != nil {
t.Fatal(err)
}
err := os.WriteFile(originResolvConfPath, []byte(tc.originResolvConf), 0o644)
assert.NilError(t, err)
resolvConfPath := "/tmp/libnetwork_test/resolv.conf"
defer os.Remove(resolvConfPath)
n := tc.makeNet(t, c)
if tc.delNet {
defer func() {
err := n.Delete()
assert.Check(t, err)
}()
}
sb1, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath))
if err != nil {
t.Fatal(err)
}
defer func() {
if err := sb1.Delete(); err != nil {
t.Fatal(err)
}
}()
sbOpts := append(tc.sbOpts,
libnetwork.OptionResolvConfPath(resolvConfPath),
libnetwork.OptionOriginResolvConfPath(originResolvConfPath),
)
sb, err := c.NewSandbox(containerID, sbOpts...)
assert.NilError(t, err)
defer func() {
err := sb.Delete()
assert.Check(t, err)
}()
err = ep.Join(sb1)
if err != nil {
t.Fatal(err)
}
ep, err := n.CreateEndpoint("ep", tc.epOpts...)
assert.NilError(t, err)
defer func() {
err := ep.Delete(false)
assert.Check(t, err)
}()
finfo, err := os.Stat(resolvConfPath)
if err != nil {
t.Fatal(err)
}
err = ep.Join(sb)
assert.NilError(t, err)
defer func() {
err := ep.Leave(sb)
assert.Check(t, err)
}()
fmode := (os.FileMode)(0o644)
if finfo.Mode() != fmode {
t.Fatalf("Expected file mode %s, got %s", fmode.String(), finfo.Mode().String())
}
content, err := os.ReadFile(resolvConfPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, expectedResolvConf1) {
fmt.Printf("\n%v\n%v\n", expectedResolvConf1, content)
t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf1), string(content))
}
err = ep.Leave(sb1)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf2, 0o644); err != nil {
t.Fatal(err)
}
sb2, err := controller.NewSandbox(containerID+"_2", libnetwork.OptionResolvConfPath(resolvConfPath))
if err != nil {
t.Fatal(err)
}
defer func() {
if err := sb2.Delete(); err != nil {
t.Fatal(err)
}
}()
err = ep.Join(sb2)
if err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(resolvConfPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, expectedResolvConf1) {
t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf1), string(content))
}
if err := os.WriteFile(resolvConfPath, tmpResolvConf3, 0o644); err != nil {
t.Fatal(err)
}
err = ep.Leave(sb2)
if err != nil {
t.Fatal(err)
}
err = ep.Join(sb2)
if err != nil {
t.Fatal(err)
}
content, err = os.ReadFile(resolvConfPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, tmpResolvConf3) {
t.Fatalf("Expected:\n%s\nGot:\n%s", string(tmpResolvConf3), string(content))
finfo, err := os.Stat(resolvConfPath)
assert.NilError(t, err)
expFMode := (os.FileMode)(0o644)
assert.Check(t, is.Equal(finfo.Mode().String(), expFMode.String()))
content, err := os.ReadFile(resolvConfPath)
assert.NilError(t, err)
actual := stripCommentsRE.ReplaceAllString(string(content), "")
actual = strings.TrimSpace(actual)
assert.Check(t, is.Equal(actual, tc.expResolvConf))
})
}
}

View file

@ -3,20 +3,12 @@ package resolvconf
import (
"bytes"
"context"
"fmt"
"os"
"regexp"
"strings"
"sync"
"github.com/containerd/log"
)
const (
// defaultPath is the default path to the resolv.conf that contains information to resolve DNS. See Path().
defaultPath = "/etc/resolv.conf"
// alternatePath is a path different from defaultPath, that may be used to resolve DNS. See Path().
alternatePath = "/run/systemd/resolve/resolv.conf"
"github.com/docker/docker/libnetwork/internal/resolvconf"
"github.com/opencontainers/go-digest"
)
// constants for the IP address type
@ -26,72 +18,16 @@ const (
IPv6
)
var (
detectSystemdResolvConfOnce sync.Once
pathAfterSystemdDetection = defaultPath
)
// Path returns the path to the resolv.conf file that libnetwork should use.
//
// When /etc/resolv.conf contains 127.0.0.53 as the only nameserver, then
// it is assumed systemd-resolved manages DNS. Because inside the container 127.0.0.53
// is not a valid DNS server, Path() returns /run/systemd/resolve/resolv.conf
// which is the resolv.conf that systemd-resolved generates and manages.
// Otherwise Path() returns /etc/resolv.conf.
//
// Errors are silenced as they will inevitably resurface at future open/read calls.
//
// More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf
func Path() string {
detectSystemdResolvConfOnce.Do(func() {
candidateResolvConf, err := os.ReadFile(defaultPath)
if err != nil {
// silencing error as it will resurface at next calls trying to read defaultPath
return
}
ns := GetNameservers(candidateResolvConf, IP)
if len(ns) == 1 && ns[0] == "127.0.0.53" {
pathAfterSystemdDetection = alternatePath
log.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath)
}
})
return pathAfterSystemdDetection
}
const (
// ipLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)`
ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
// This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also
// will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants
// -- e.g. other link-local types -- either won't work in containers or are unnecessary.
// For readability and sufficiency for Docker purposes this seemed more reasonable than a
// 1000+ character regexp with exact and complete IPv6 validation
ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?`
)
var (
// Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS
defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipLocalhost + `\s*\n*`)
nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`)
nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`)
nsIPv6Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv6Address + `))\s*$`)
nsIPv4Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `))\s*$`)
searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`)
optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`)
)
// File contains the resolv.conf content and its hash
type File struct {
Content []byte
Hash []byte
}
func Path() string {
return resolvconf.Path()
}
// Get returns the contents of /etc/resolv.conf and its hash
func Get() (*File, error) {
return GetSpecific(Path())
@ -103,7 +39,8 @@ func GetSpecific(path string) (*File, error) {
if err != nil {
return nil, err
}
return &File{Content: resolv, Hash: hashData(resolv)}, nil
hash := digest.FromBytes(resolv)
return &File{Content: resolv, Hash: []byte(hash)}, nil
}
// FilterResolvDNS cleans up the config in resolvConf. It has two main jobs:
@ -113,54 +50,34 @@ func GetSpecific(path string) (*File, error) {
// 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{})
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
if !ipv6Enabled {
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
if err != nil {
return nil, err
}
// if the resulting resolvConf has no more nameservers defined, add appropriate
// default DNS servers for IPv4 and (optionally) IPv6
if len(GetNameservers(cleanedResolvConf, IP)) == 0 {
log.G(context.TODO()).Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns)
dns := defaultIPv4Dns
if ipv6Enabled {
log.G(context.TODO()).Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns)
dns = append(dns, defaultIPv6Dns...)
}
cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
rc.TransformForLegacyNw(ipv6Enabled)
content, err := rc.Generate(false)
if err != nil {
return nil, err
}
return &File{Content: cleanedResolvConf, Hash: hashData(cleanedResolvConf)}, nil
}
// getLines parses input into lines and strips away comments.
func getLines(input []byte, commentMarker []byte) [][]byte {
lines := bytes.Split(input, []byte("\n"))
var output [][]byte
for _, currentLine := range lines {
commentIndex := bytes.Index(currentLine, commentMarker)
if commentIndex == -1 {
output = append(output, currentLine)
} else {
output = append(output, currentLine[:commentIndex])
}
}
return output
hash := digest.FromBytes(content)
return &File{Content: content, Hash: []byte(hash)}, nil
}
// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
func GetNameservers(resolvConf []byte, kind int) []string {
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
if err != nil {
return nil
}
nsAddrs := rc.NameServers()
var nameservers []string
for _, line := range getLines(resolvConf, []byte("#")) {
var ns [][]byte
for _, addr := range nsAddrs {
if kind == IP {
ns = nsRegexp.FindSubmatch(line)
} else if kind == IPv4 {
ns = nsIPv4Regexpmatch.FindSubmatch(line)
} else if kind == IPv6 {
ns = nsIPv6Regexpmatch.FindSubmatch(line)
}
if len(ns) > 0 {
nameservers = append(nameservers, string(ns[1]))
nameservers = append(nameservers, addr.String())
} else if kind == IPv4 && addr.Is4() {
nameservers = append(nameservers, addr.String())
} else if kind == IPv6 && addr.Is6() {
nameservers = append(nameservers, addr.String())
}
}
return nameservers
@ -170,16 +87,15 @@ func GetNameservers(resolvConf []byte, kind int) []string {
// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
// This function's output is intended for net.ParseCIDR
func GetNameserversAsCIDR(resolvConf []byte) []string {
var nameservers []string
for _, nameserver := range GetNameservers(resolvConf, IP) {
var address string
// If IPv6, strip zone if present
if strings.Contains(nameserver, ":") {
address = strings.Split(nameserver, "%")[0] + "/128"
} else {
address = nameserver + "/32"
}
nameservers = append(nameservers, address)
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
if err != nil {
return nil
}
nsAddrs := rc.NameServers()
nameservers := make([]string, 0, len(nsAddrs))
for _, addr := range nsAddrs {
str := fmt.Sprintf("%s/%d", addr.WithZone("").String(), addr.BitLen())
nameservers = append(nameservers, str)
}
return nameservers
}
@ -188,36 +104,30 @@ func GetNameserversAsCIDR(resolvConf []byte) []string {
// If more than one search line is encountered, only the contents of the last
// one is returned.
func GetSearchDomains(resolvConf []byte) []string {
var domains []string
for _, line := range getLines(resolvConf, []byte("#")) {
match := searchRegexp.FindSubmatch(line)
if match == nil {
continue
}
domains = strings.Fields(string(match[1]))
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
if err != nil {
return nil
}
return domains
return rc.Search()
}
// GetOptions returns options (if any) listed in /etc/resolv.conf
// If more than one options line is encountered, only the contents of the last
// one is returned.
func GetOptions(resolvConf []byte) []string {
var options []string
for _, line := range getLines(resolvConf, []byte("#")) {
match := optionsRegexp.FindSubmatch(line)
if match == nil {
continue
}
options = strings.Fields(string(match[1]))
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
if err != nil {
return nil
}
return options
return rc.Options()
}
// Build generates and writes a configuration file to path containing a nameserver
// entry for every element in nameservers, a "search" entry for every element in
// dnsSearch, and an "options" entry for every element in dnsOptions. It returns
// a File containing the generated content and its (sha256) hash.
//
// Note that the resolv.conf file is written, but the hash file is not.
func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, error) {
content := bytes.NewBuffer(nil)
if len(dnsSearch) > 0 {
@ -244,5 +154,6 @@ func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, err
return nil, err
}
return &File{Content: content.Bytes(), Hash: hashData(content.Bytes())}, nil
hash := digest.FromBytes(content.Bytes())
return &File{Content: content.Bytes(), Hash: []byte(hash)}, nil
}

View file

@ -5,7 +5,12 @@ package resolvconf
import (
"bytes"
"os"
"strings"
"testing"
"github.com/opencontainers/go-digest"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGet(t *testing.T) {
@ -20,7 +25,8 @@ func TestGet(t *testing.T) {
if !bytes.Equal(actual.Content, expected) {
t.Errorf("%s and GetResolvConf have different content.", Path())
}
if !bytes.Equal(actual.Hash, hashData(expected)) {
hash := digest.FromBytes(expected)
if !bytes.Equal(actual.Hash, []byte(hash)) {
t.Errorf("%s and GetResolvConf have different hashes.", Path())
}
}
@ -111,6 +117,14 @@ nameserver 1.2.3.4
nameserver 1.2.3.4 # not 4.3.2.1`,
result: []string{"1.2.3.4/32"},
},
{
input: `nameserver fd6f:c490:ec68::1`,
result: []string{"fd6f:c490:ec68::1/128"},
},
{
input: `nameserver fe80::1234%eth0`,
result: []string{"fe80::1234/128"},
},
} {
test := GetNameserversAsCIDR([]byte(tc.input))
if !strSlicesEqual(test, tc.result) {
@ -175,6 +189,10 @@ search foo.example.com example.com
nameserver 4.30.20.100`,
result: []string{"foo.example.com", "example.com"},
},
{
input: `domain an.example`,
result: []string{"an.example"},
},
} {
test := GetSearchDomains([]byte(tc.input))
if !strSlicesEqual(test, tc.result) {
@ -338,89 +356,79 @@ func TestBuildWithNoOptions(t *testing.T) {
}
func TestFilterResolvDNS(t *testing.T) {
ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDNS([]byte(ns0), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
testcases := []struct {
name string
input string
ipv6Enabled bool
expOut string
}{
{
name: "No localhost",
input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\n",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "Localhost last",
input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "Localhost middle",
input: "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "Localhost first",
input: "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "IPv6 Localhost",
input: "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "Two IPv6 Localhosts",
input: "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "IPv6 disabled",
input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "IPv6 link-local disabled",
input: "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21",
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
},
{
name: "IPv6 enabled",
input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1\n",
ipv6Enabled: true,
expOut: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21",
},
{
// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
name: "localhost only IPv6",
input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1",
ipv6Enabled: true,
expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844",
},
{
// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
name: "localhost only no IPv6",
input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1",
expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4",
},
}
ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
// with IPv6 disabled (false param), the IPv6 nameserver should be removed
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
// with IPv6 disabled (false param), the IPv6 link-local nameserver with zone ID should be removed
ns1 = "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21\n"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
// with IPv6 enabled, the IPv6 nameserver should be preserved
ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n"
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844"
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
}
// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
if ns0 != string(result.Content) {
t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
f, err := FilterResolvDNS([]byte(tc.input), tc.ipv6Enabled)
assert.Check(t, is.Nil(err))
out := strings.TrimSpace(string(f.Content))
assert.Check(t, is.Equal(out, tc.expOut))
})
}
}

View file

@ -1,14 +0,0 @@
package resolvconf
import (
"crypto/sha256"
"encoding/hex"
)
// hashData returns the sha256 sum of data.
func hashData(data []byte) []byte {
f := sha256.Sum256(data)
out := make([]byte, 2*sha256.Size)
hex.Encode(out, f[:])
return append([]byte("sha256:"), out...)
}

View file

@ -1,21 +0,0 @@
package resolvconf
import (
"bytes"
"testing"
)
func TestHashData(t *testing.T) {
const expected = "sha256:4d11186aed035cc624d553e10db358492c84a7cd6b9670d92123c144930450aa"
if actual := hashData([]byte("hash-me")); !bytes.Equal(actual, []byte(expected)) {
t.Fatalf("Expecting %s, got %s", expected, string(actual))
}
}
func BenchmarkHashData(b *testing.B) {
b.ReportAllocs()
data := []byte("hash-me")
for i := 0; i < b.N; i++ {
_ = hashData(data)
}
}

View file

@ -3,22 +3,19 @@
package libnetwork
import (
"bytes"
"context"
"fmt"
"net"
"io/fs"
"net/netip"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/containerd/log"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/libnetwork/etchosts"
"github.com/docker/docker/libnetwork/resolvconf"
"github.com/docker/docker/libnetwork/internal/resolvconf"
"github.com/docker/docker/libnetwork/types"
"github.com/pkg/errors"
)
const (
@ -100,6 +97,13 @@ func (sb *Sandbox) setupResolutionFiles() error {
}
func (sb *Sandbox) buildHostsFile() error {
sb.restoreHostsPath()
dir, _ := filepath.Split(sb.config.hostsPath)
if err := createBasePath(dir); err != nil {
return err
}
// This is for the host mode networking
if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
// We are working under the assumption that the origin file option had been properly expressed by the upper layer
@ -208,276 +212,171 @@ func (sb *Sandbox) updateParentHosts() error {
return nil
}
func (sb *Sandbox) restorePath() {
func (sb *Sandbox) restoreResolvConfPath() {
if sb.config.resolvConfPath == "" {
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
}
sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
}
func (sb *Sandbox) restoreHostsPath() {
if sb.config.hostsPath == "" {
sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
}
}
func (sb *Sandbox) setExternalResolvers(content []byte, addrType int, checkLoopback bool) {
servers := resolvconf.GetNameservers(content, addrType)
for _, ip := range servers {
hostLoopback := false
if checkLoopback && isIPv4Loopback(ip) {
hostLoopback = true
}
func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) {
sb.extDNS = make([]extDNSEntry, 0, len(entries))
for _, entry := range entries {
sb.extDNS = append(sb.extDNS, extDNSEntry{
IPStr: ip,
HostLoopback: hostLoopback,
IPStr: entry.Addr.String(),
HostLoopback: entry.HostLoopback,
})
}
}
// isIPv4Loopback checks if the given IP address is an IPv4 loopback address.
// It's based on the logic in Go's net.IP.IsLoopback(), but only the IPv4 part:
// https://github.com/golang/go/blob/go1.16.6/src/net/ip.go#L120-L126
func isIPv4Loopback(ipAddress string) bool {
if ip := net.ParseIP(ipAddress); ip != nil {
if ip4 := ip.To4(); ip4 != nil {
return ip4[0] == 127
}
func (c *containerConfig) getOriginResolvConfPath() string {
if c.originResolvConfPath != "" {
return c.originResolvConfPath
}
return false
// Fallback if not specified.
return resolvconf.Path()
}
func (sb *Sandbox) setupDNS() error {
if sb.config.resolvConfPath == "" {
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
// loadResolvConf reads the resolv.conf file at path, and merges in overrides for
// nameservers, options, and search domains.
func (sb *Sandbox) loadResolvConf(path string) (*resolvconf.ResolvConf, error) {
rc, err := resolvconf.Load(path)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
// Proceed with rc, which might be zero-valued if path does not exist.
sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
rc.SetHeader(`# Generated by Docker Engine.
# This file can be edited; Docker Engine will not make further changes once it
# has been modified.`)
if len(sb.config.dnsList) > 0 {
var dnsAddrs []netip.Addr
for _, ns := range sb.config.dnsList {
addr, err := netip.ParseAddr(ns)
if err != nil {
return nil, errors.Wrapf(err, "bad nameserver address %s", ns)
}
dnsAddrs = append(dnsAddrs, addr)
}
rc.OverrideNameServers(dnsAddrs)
}
if len(sb.config.dnsSearchList) > 0 {
rc.OverrideSearch(sb.config.dnsSearchList)
}
if len(sb.config.dnsOptionsList) > 0 {
rc.OverrideOptions(sb.config.dnsOptionsList)
}
return &rc, nil
}
// For a new sandbox, write an initial version of the container's resolv.conf. It'll
// be a copy of the host's file, with overrides for nameservers, options and search
// domains applied.
func (sb *Sandbox) setupDNS() error {
// Make sure the directory exists.
sb.restoreResolvConfPath()
dir, _ := filepath.Split(sb.config.resolvConfPath)
if err := createBasePath(dir); err != nil {
return err
}
// When the user specify a conainter in the host namespace and do no have any dns option specified
// we just copy the host resolv.conf from the host itself
if sb.config.useDefaultSandBox && len(sb.config.dnsList) == 0 && len(sb.config.dnsSearchList) == 0 && len(sb.config.dnsOptionsList) == 0 {
// We are working under the assumption that the origin file option had been properly expressed by the upper layer
// if not here we are going to error out
if err := copyFile(sb.config.originResolvConfPath, sb.config.resolvConfPath); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("could not copy source resolv.conf file %s to %s: %v", sb.config.originResolvConfPath, sb.config.resolvConfPath, err)
}
log.G(context.TODO()).Infof("%s does not exist, we create an empty resolv.conf for container", sb.config.originResolvConfPath)
if err := createFile(sb.config.resolvConfPath); err != nil {
return err
}
}
return nil
}
originResolvConfPath := sb.config.originResolvConfPath
if originResolvConfPath == "" {
// fallback if not specified
originResolvConfPath = resolvconf.Path()
}
currRC, err := os.ReadFile(originResolvConfPath)
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
if err != nil {
if !os.IsNotExist(err) {
return err
}
// No /etc/resolv.conf found: we'll use the default resolvers (Google's Public DNS).
log.G(context.TODO()).WithField("path", originResolvConfPath).Infof("no resolv.conf found, falling back to defaults")
return err
}
var newRC *resolvconf.File
if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(sb.config.dnsOptionsList) > 0 {
var (
dnsList = sb.config.dnsList
dnsSearchList = sb.config.dnsSearchList
dnsOptionsList = sb.config.dnsOptionsList
)
if len(sb.config.dnsList) == 0 {
dnsList = resolvconf.GetNameservers(currRC, resolvconf.IP)
}
if len(sb.config.dnsSearchList) == 0 {
dnsSearchList = resolvconf.GetSearchDomains(currRC)
}
if len(sb.config.dnsOptionsList) == 0 {
dnsOptionsList = resolvconf.GetOptions(currRC)
}
newRC, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
if err != nil {
return err
}
// After building the resolv.conf from the user config save the
// external resolvers in the sandbox. Note that --dns 127.0.0.x
// config refers to the loopback in the container namespace
sb.setExternalResolvers(newRC.Content, resolvconf.IPv4, len(sb.config.dnsList) == 0)
} else {
// If the host resolv.conf file has 127.0.0.x container should
// use the host resolver for queries. This is supported by the
// docker embedded DNS server. Hence save the external resolvers
// before filtering it out.
sb.setExternalResolvers(currRC, resolvconf.IPv4, true)
// Replace any localhost/127.* (at this point we have no info about ipv6, pass it as true)
newRC, err = resolvconf.FilterResolvDNS(currRC, true)
if err != nil {
return err
}
// No contention on container resolv.conf file at sandbox creation
err = os.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm)
if err != nil {
return types.InternalErrorf("failed to write unhaltered resolv.conf file content when setting up dns for sandbox %s: %v", sb.ID(), err)
}
}
// Write hash
err = os.WriteFile(sb.config.resolvConfHashFile, newRC.Hash, filePerm)
if err != nil {
return types.InternalErrorf("failed to write resolv.conf hash file when setting up dns for sandbox %s: %v", sb.ID(), err)
}
return nil
return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
}
// Called when an endpoint has joined the sandbox.
func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
// This is for the host mode networking
if sb.config.useDefaultSandBox {
return nil
}
if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(sb.config.dnsOptionsList) > 0 {
return nil
}
var currHash []byte
currRC, err := resolvconf.GetSpecific(sb.config.resolvConfPath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
currHash, err = os.ReadFile(sb.config.resolvConfHashFile)
if err != nil && !os.IsNotExist(err) {
return err
}
}
if len(currHash) > 0 && !bytes.Equal(currHash, currRC.Hash) {
// Seems the user has changed the container resolv.conf since the last time
// we checked so return without doing anything.
// log.G(ctx).Infof("Skipping update of resolv.conf file with ipv6Enabled: %t because file was touched by user", ipv6Enabled)
return nil
}
// replace any localhost/127.* and remove IPv6 nameservers if IPv6 disabled.
newRC, err := resolvconf.FilterResolvDNS(currRC.Content, ipv6Enabled)
if err != nil {
return err
}
err = os.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm)
if err != nil {
if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
return err
}
// write the new hash in a temp file and rename it to make the update atomic
dir := path.Dir(sb.config.resolvConfPath)
tmpHashFile, err := os.CreateTemp(dir, "hash")
// Load the host's resolv.conf as a starting point.
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
if err != nil {
return err
}
if err = tmpHashFile.Chmod(filePerm); err != nil {
tmpHashFile.Close()
return err
// For host-networking, no further change is needed.
if !sb.config.useDefaultSandBox {
// The legacy bridge network has no internal nameserver. So, strip localhost
// nameservers from the host's config, then add default nameservers if there
// are none remaining.
rc.TransformForLegacyNw(ipv6Enabled)
}
_, err = tmpHashFile.Write(newRC.Hash)
if err1 := tmpHashFile.Close(); err == nil {
err = err1
}
if err != nil {
return err
}
return os.Rename(tmpHashFile.Name(), sb.config.resolvConfHashFile)
return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
}
// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's
// resolv.conf by doing the following
// - Add only the embedded server's IP to container's resolv.conf
// - If the embedded server needs any resolv.conf options add it to the current list
// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf.
func (sb *Sandbox) rebuildDNS() error {
currRC, err := os.ReadFile(sb.config.resolvConfPath)
// Don't touch the file if the user has modified it.
if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
return err
}
// Load the host's resolv.conf as a starting point.
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
if err != nil {
return err
}
// If the user config and embedded DNS server both have ndots option set,
// remember the user's config so that unqualified names not in the docker
// domain can be dropped.
resOptions := sb.resolver.ResolverOptions()
dnsOptionsList := resolvconf.GetOptions(currRC)
dnsOpt:
for _, resOpt := range resOptions {
if strings.Contains(resOpt, "ndots") {
for _, option := range dnsOptionsList {
if strings.Contains(option, "ndots") {
parts := strings.Split(option, ":")
if len(parts) != 2 {
return fmt.Errorf("invalid ndots option %v", option)
}
if num, err := strconv.Atoi(parts[1]); err != nil {
return fmt.Errorf("invalid number for ndots option: %v", parts[1])
} else if num >= 0 {
// if the user sets ndots, use the user setting
sb.ndotsSet = true
break dnsOpt
} else {
return fmt.Errorf("invalid number for ndots option: %v", num)
}
}
}
// Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers
// will be left in the container's 'resolv.conf'.
// TODO(robmry) - preserving old behaviour, but ...
// IPv6 nameservers should be treated like IPv4 ones, and used as upstream
// servers for the internal resolver (if it has IPv6 connectivity). This
// doesn't need to depend on whether there are currently any IPv6 endpoints.
// Removing IPv6 nameservers from the container's resolv.conf will avoid the
// problem that musl-libc's resolver tries all nameservers in parallel, so an
// external IPv6 resolver can return NXDOMAIN before the internal resolver
// returns the address of a container.
ipv6 := false
for _, ep := range sb.endpoints {
if ep.network.enableIPv6 {
ipv6 = true
break
}
}
if !sb.ndotsSet {
// if the user did not set the ndots, set it to 0 to prioritize the service name resolution
// Ref: https://linux.die.net/man/5/resolv.conf
dnsOptionsList = append(dnsOptionsList, resOptions...)
}
if len(sb.extDNS) == 0 {
sb.setExternalResolvers(currRC, resolvconf.IPv4, false)
intNS, err := netip.ParseAddr(sb.resolver.NameServer())
if err != nil {
return err
}
var (
// external v6 DNS servers have to be listed in resolv.conf
dnsList = append([]string{sb.resolver.NameServer()}, resolvconf.GetNameservers(currRC, resolvconf.IPv6)...)
dnsSearchList = resolvconf.GetSearchDomains(currRC)
)
// Work out whether ndots has been set from host config or overrides.
_, sb.ndotsSet = rc.Option("ndots")
// Swap nameservers for the internal one, and make sure the required options are set.
var extNameServers []resolvconf.ExtDNSEntry
extNameServers, err = rc.TransformForIntNS(ipv6, intNS, sb.resolver.ResolverOptions())
if err != nil {
return err
}
// Extract the list of nameservers that just got swapped out, and store them as
// upstream nameservers.
sb.setExternalResolvers(extNameServers)
_, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
return err
// Write the file for the container - preserving old behaviour, not updating the
// hash file (so, no further updates will be made).
// TODO(robmry) - I think that's probably accidental, I can't find a reason for it,
// and the old resolvconf.Build() function wrote the file but not the hash, which
// is surprising. But, before fixing it, a guard/flag needs to be added to
// sb.updateDNS() to make sure that when an endpoint joins a sandbox that already
// has an internal resolver, the container's resolv.conf is still (re)configured
// for an internal resolver.
return rc.WriteFile(sb.config.resolvConfPath, "", filePerm)
}
func createBasePath(dir string) error {
return os.MkdirAll(dir, dirPerm)
}
func createFile(path string) error {
var f *os.File
dir, _ := filepath.Split(path)
err := createBasePath(dir)
if err != nil {
return err
}
f, err = os.Create(path)
if err == nil {
f.Close()
}
return err
}
func copyFile(src, dst string) error {
sBytes, err := os.ReadFile(src)
if err != nil {

View file

@ -12,7 +12,9 @@ func (sb *Sandbox) setupResolutionFiles() error {
return nil
}
func (sb *Sandbox) restorePath() {}
func (sb *Sandbox) restoreHostsPath() {}
func (sb *Sandbox) restoreResolvConfPath() {}
func (sb *Sandbox) updateHostsFile(ifaceIP []string) error {
return nil

View file

@ -206,7 +206,8 @@ func (c *Controller) sandboxCleanup(activeSandboxes map[string]interface{}) erro
isRestore = true
opts := val.([]SandboxOption)
sb.processOptions(opts...)
sb.restorePath()
sb.restoreHostsPath()
sb.restoreResolvConfPath()
create = !sb.config.useDefaultSandBox
}
sb.osSbox, err = osl.NewSandbox(sb.Key(), create, isRestore)