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:
parent
f472dda2e9
commit
beb97f7fdf
50 changed files with 1805 additions and 775 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
72
integration/networking/resolvconf_test.go
Normal file
72
integration/networking/resolvconf_test.go
Normal 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
|
||||
`))
|
||||
}
|
510
libnetwork/internal/resolvconf/resolvconf.go
Normal file
510
libnetwork/internal/resolvconf/resolvconf.go
Normal 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
|
||||
}
|
56
libnetwork/internal/resolvconf/resolvconf_path.go
Normal file
56
libnetwork/internal/resolvconf/resolvconf_path.go
Normal 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
|
||||
}
|
577
libnetwork/internal/resolvconf/resolvconf_test.go
Normal file
577
libnetwork/internal/resolvconf/resolvconf_test.go
Normal 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"))
|
||||
}
|
1
libnetwork/internal/resolvconf/testdata/.gitattributes
vendored
Normal file
1
libnetwork/internal/resolvconf/testdata/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
4
libnetwork/internal/resolvconf/testdata/TestRCInvalidNS.golden
vendored
Normal file
4
libnetwork/internal/resolvconf/testdata/TestRCInvalidNS.golden
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
# Based on host file: ''
|
||||
# Invalid nameservers: [1.2.3.4.5]
|
||||
# Overrides: []
|
7
libnetwork/internal/resolvconf/testdata/TestRCModify/Add_option_no_overrides.golden
vendored
Normal file
7
libnetwork/internal/resolvconf/testdata/TestRCModify/Add_option_no_overrides.golden
vendored
Normal 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
|
3
libnetwork/internal/resolvconf/testdata/TestRCModify/Empty_overrides.golden
vendored
Normal file
3
libnetwork/internal/resolvconf/testdata/TestRCModify/Empty_overrides.golden
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Based on host file: ''
|
||||
# Overrides: [nameservers search options]
|
3
libnetwork/internal/resolvconf/testdata/TestRCModify/No_content_no_overrides.golden
vendored
Normal file
3
libnetwork/internal/resolvconf/testdata/TestRCModify/No_content_no_overrides.golden
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Based on host file: ''
|
||||
# Overrides: [nameservers search options]
|
7
libnetwork/internal/resolvconf/testdata/TestRCModify/No_overrides.golden
vendored
Normal file
7
libnetwork/internal/resolvconf/testdata/TestRCModify/No_overrides.golden
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
nameserver 1.2.3.4
|
||||
search invalid
|
||||
options ndots:0
|
||||
|
||||
# Based on host file: ''
|
||||
# Overrides: []
|
||||
# Option ndots from: host
|
8
libnetwork/internal/resolvconf/testdata/TestRCModify/Overrides.golden
vendored
Normal file
8
libnetwork/internal/resolvconf/testdata/TestRCModify/Overrides.golden
vendored
Normal 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
|
6
libnetwork/internal/resolvconf/testdata/TestRCSetHeader.golden
vendored
Normal file
6
libnetwork/internal/resolvconf/testdata/TestRCSetHeader.golden
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
# This is a comment.
|
||||
|
||||
nameserver 127.0.0.53
|
||||
|
||||
# Based on host file: '/etc/resolv.conf'
|
||||
# Overrides: []
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
nameserver 127.0.0.11
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (internal resolver)
|
||||
# ExtServers: [10.0.0.1]
|
||||
# Overrides: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
5
libnetwork/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost.golden
vendored
Normal file
5
libnetwork/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_localhost.golden
vendored
Normal 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: []
|
|
@ -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]
|
5
libnetwork/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_only.golden
vendored
Normal file
5
libnetwork/internal/resolvconf/testdata/TestRCTransformForIntNS/IPv4_only.golden
vendored
Normal 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: []
|
|
@ -0,0 +1,5 @@
|
|||
nameserver 127.0.0.11
|
||||
nameserver fd14:6e0e:f855::1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (internal resolver)
|
||||
# Overrides: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -0,0 +1,4 @@
|
|||
nameserver fd3e:2d1a:1f5a::1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (legacy)
|
||||
# Overrides: []
|
|
@ -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: []
|
|
@ -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: []
|
|
@ -0,0 +1,5 @@
|
|||
nameserver 127.0.0.1
|
||||
nameserver ::1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (legacy)
|
||||
# Overrides: [nameservers]
|
|
@ -0,0 +1,4 @@
|
|||
nameserver 10.0.0.1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (legacy)
|
||||
# Overrides: []
|
|
@ -0,0 +1,5 @@
|
|||
nameserver 10.0.0.1
|
||||
nameserver fdb6:b8fe:b528::1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (legacy)
|
||||
# Overrides: []
|
4
libnetwork/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_only.golden
vendored
Normal file
4
libnetwork/internal/resolvconf/testdata/TestRCTransformForLegacyNw/Routable_IPv4_only.golden
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
nameserver 10.0.0.1
|
||||
|
||||
# Based on host file: '/etc/resolv.conf' (legacy)
|
||||
# Overrides: []
|
8
libnetwork/internal/resolvconf/testdata/TestRCUnknownDirectives.golden
vendored
Normal file
8
libnetwork/internal/resolvconf/testdata/TestRCUnknownDirectives.golden
vendored
Normal 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
|
|
@ -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)
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer netnsutils.SetupTestOSContext(t)()
|
||||
c := newController(t)
|
||||
|
||||
err := os.WriteFile(originResolvConfPath, []byte(tc.originResolvConf), 0o644)
|
||||
assert.NilError(t, err)
|
||||
|
||||
n := tc.makeNet(t, c)
|
||||
if tc.delNet {
|
||||
defer func() {
|
||||
if err := n.Delete(); err != nil {
|
||||
t.Fatal(err)
|
||||
err := n.Delete()
|
||||
assert.Check(t, 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)
|
||||
}()
|
||||
|
||||
ep, err := n.CreateEndpoint("ep")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf1, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolvConfPath := "/tmp/libnetwork_test/resolv.conf"
|
||||
defer os.Remove(resolvConfPath)
|
||||
|
||||
sb1, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ep, err := n.CreateEndpoint("ep", tc.epOpts...)
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
if err := sb1.Delete(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := ep.Delete(false)
|
||||
assert.Check(t, err)
|
||||
}()
|
||||
|
||||
err = ep.Join(sb1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ep.Join(sb)
|
||||
assert.NilError(t, err)
|
||||
defer func() {
|
||||
err := ep.Leave(sb)
|
||||
assert.Check(t, 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())
|
||||
}
|
||||
|
||||
assert.NilError(t, err)
|
||||
expFMode := (os.FileMode)(0o644)
|
||||
assert.Check(t, is.Equal(finfo.Mode().String(), expFMode.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))
|
||||
assert.NilError(t, err)
|
||||
actual := stripCommentsRE.ReplaceAllString(string(content), "")
|
||||
actual = strings.TrimSpace(actual)
|
||||
assert.Check(t, is.Equal(actual, tc.expResolvConf))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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...)
|
||||
rc.TransformForLegacyNw(ipv6Enabled)
|
||||
content, err := rc.Generate(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
|
||||
}
|
||||
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 {
|
||||
var nameservers []string
|
||||
for _, line := range getLines(resolvConf, []byte("#")) {
|
||||
var ns [][]byte
|
||||
if kind == IP {
|
||||
ns = nsRegexp.FindSubmatch(line)
|
||||
} else if kind == IPv4 {
|
||||
ns = nsIPv4Regexpmatch.FindSubmatch(line)
|
||||
} else if kind == IPv6 {
|
||||
ns = nsIPv6Regexpmatch.FindSubmatch(line)
|
||||
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(ns) > 0 {
|
||||
nameservers = append(nameservers, string(ns[1]))
|
||||
nsAddrs := rc.NameServers()
|
||||
var nameservers []string
|
||||
for _, addr := range nsAddrs {
|
||||
if kind == IP {
|
||||
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"
|
||||
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
nameservers = append(nameservers, address)
|
||||
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
|
||||
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
domains = strings.Fields(string(match[1]))
|
||||
}
|
||||
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
|
||||
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
options = strings.Fields(string(match[1]))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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))
|
||||
}
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
_, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
|
||||
intNS, err := netip.ParseAddr(sb.resolver.NameServer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue