Merge pull request #47041 from robmry/46968_refactor_resolvconf
Refactor 'resolv.conf' generation.
This commit is contained in:
commit
6c3b3523c9
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 {
|
func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Config, sboxOptions *[]libnetwork.SandboxOption) error {
|
||||||
var err error
|
var err error
|
||||||
|
var originResolvConfPath string
|
||||||
|
|
||||||
// Set the correct paths for /etc/hosts and /etc/resolv.conf, based on the
|
// Set the correct paths for /etc/hosts and /etc/resolv.conf, based on the
|
||||||
// networking-mode of the container. Note that containers with "container"
|
// 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 = append(
|
||||||
*sboxOptions,
|
*sboxOptions,
|
||||||
libnetwork.OptionOriginHostsPath("/etc/hosts"),
|
libnetwork.OptionOriginHostsPath("/etc/hosts"),
|
||||||
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
|
|
||||||
)
|
)
|
||||||
|
originResolvConfPath = "/etc/resolv.conf"
|
||||||
case container.HostConfig.NetworkMode.IsUserDefined():
|
case container.HostConfig.NetworkMode.IsUserDefined():
|
||||||
// The container uses a user-defined network. We use the embedded DNS
|
// The container uses a user-defined network. We use the embedded DNS
|
||||||
// server for container name resolution and to act as a DNS forwarder
|
// 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
|
// 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
|
// /run/systemd/resolve/resolv.conf. We do not query those DNS servers
|
||||||
// directly, as they can be dynamically reconfigured.
|
// directly, as they can be dynamically reconfigured.
|
||||||
*sboxOptions = append(
|
originResolvConfPath = "/etc/resolv.conf"
|
||||||
*sboxOptions,
|
|
||||||
libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
// For other situations, such as the default bridge network, container
|
// For other situations, such as the default bridge network, container
|
||||||
// discovery / name resolution is handled through /etc/hosts, and no
|
// 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.
|
// 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)
|
// Copy the host's resolv.conf for the container (/run/systemd/resolve/resolv.conf or /etc/resolv.conf)
|
||||||
*sboxOptions = append(
|
originResolvConfPath = cfg.GetResolvConf()
|
||||||
*sboxOptions,
|
|
||||||
libnetwork.OptionOriginResolvConfPath(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")
|
container.HostsPath, err = container.GetRootResourcePath("hosts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1275,10 +1275,11 @@ func (s *DockerCLIRunSuite) TestRunDNSDefaultOptions(c *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := cli.DockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf").Combined()
|
actual := cli.DockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf").Combined()
|
||||||
// check that the actual defaults are appended to the commented out
|
actual = regexp.MustCompile("(?m)^#.*$").ReplaceAllString(actual, "")
|
||||||
// localhost resolver (which should be preserved)
|
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
|
||||||
// NOTE: if we ever change the defaults from google dns, this will break
|
// 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 {
|
if actual != expected {
|
||||||
c.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual)
|
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())
|
c.Fatalf("Expected warning on stderr about localhost resolver, but got %q", result.Stderr())
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.ReplaceAll(strings.Trim(result.Stdout(), "\r\n"), "\n", " ")
|
actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(result.Stdout(), "")
|
||||||
if actual != "search mydomain nameserver 127.0.0.1 options ndots:9" {
|
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
|
||||||
c.Fatalf("expected 'search mydomain nameserver 127.0.0.1 options ndots:9', but says: %q", actual)
|
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()
|
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" {
|
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)
|
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)
|
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()
|
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", " ")
|
actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(out, "")
|
||||||
if actual != "search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3" {
|
actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ")
|
||||||
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)
|
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"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -32,6 +33,8 @@ import (
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"github.com/vishvananda/netns"
|
"github.com/vishvananda/netns"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1278,6 +1281,28 @@ func makeTesthostNetwork(t *testing.T, c *libnetwork.Controller) *libnetwork.Net
|
||||||
return n
|
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) {
|
func TestHost(t *testing.T) {
|
||||||
defer netnsutils.SetupTestOSContext(t)()
|
defer netnsutils.SetupTestOSContext(t)()
|
||||||
controller := newController(t)
|
controller := newController(t)
|
||||||
|
@ -1790,295 +1815,92 @@ func reexecSetKey(key string, containerID string, controllerID string) error {
|
||||||
return cmd.Run()
|
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) {
|
func TestResolvConf(t *testing.T) {
|
||||||
defer netnsutils.SetupTestOSContext(t)()
|
tmpDir := t.TempDir()
|
||||||
controller := newController(t)
|
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")
|
// Strip comments that end in a newline (a comment with no newline at the end
|
||||||
tmpResolvConf2 := []byte("search pommesfrites.fr\nnameserver 112.34.56.78\nnameserver 2001:4860:4860::8888\n")
|
// of the file will not be stripped).
|
||||||
expectedResolvConf1 := []byte("search pommesfrites.fr\nnameserver 127.0.0.11\noptions ndots:0\n")
|
stripCommentsRE := regexp.MustCompile(`(?m)^#.*\n`)
|
||||||
tmpResolvConf3 := []byte("search pommesfrites.fr\nnameserver 113.34.56.78\n")
|
|
||||||
|
|
||||||
// take a copy of resolv.conf for restoring after test completes
|
testcases := []struct {
|
||||||
resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
|
name string
|
||||||
if err != nil {
|
makeNet func(t *testing.T, c *libnetwork.Controller) *libnetwork.Network
|
||||||
t.Fatal(err)
|
delNet bool
|
||||||
}
|
epOpts []libnetwork.EndpointOption
|
||||||
// cleanup
|
sbOpts []libnetwork.SandboxOption
|
||||||
defer func() {
|
originResolvConf string
|
||||||
if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
|
expResolvConf string
|
||||||
t.Fatal(err)
|
}{
|
||||||
}
|
{
|
||||||
}()
|
name: "IPv6 network",
|
||||||
|
makeNet: makeTestIPv6Network,
|
||||||
netOption := options.Generic{
|
delNet: true,
|
||||||
netlabel.GenericData: options.Generic{
|
originResolvConf: "search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n",
|
||||||
"BridgeName": "testnetwork",
|
expResolvConf: "nameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\nsearch pommesfrites.fr\noptions ndots:0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "host network",
|
||||||
|
makeNet: makeTesthostNetwork,
|
||||||
|
epOpts: []libnetwork.EndpointOption{libnetwork.CreateOptionDisableResolution()},
|
||||||
|
sbOpts: []libnetwork.SandboxOption{libnetwork.OptionUseDefaultSandbox()},
|
||||||
|
originResolvConf: "search localhost.net\nnameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\n",
|
||||||
|
expResolvConf: "nameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\nsearch localhost.net",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
n, err := createTestNetwork(controller, "bridge", "testnetwork", netOption, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := n.Delete(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ep, err := n.CreateEndpoint("ep")
|
for _, tc := range testcases {
|
||||||
if err != nil {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Fatal(err)
|
defer netnsutils.SetupTestOSContext(t)()
|
||||||
}
|
c := newController(t)
|
||||||
|
|
||||||
if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf1, 0o644); err != nil {
|
err := os.WriteFile(originResolvConfPath, []byte(tc.originResolvConf), 0o644)
|
||||||
t.Fatal(err)
|
assert.NilError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
resolvConfPath := "/tmp/libnetwork_test/resolv.conf"
|
n := tc.makeNet(t, c)
|
||||||
defer os.Remove(resolvConfPath)
|
if tc.delNet {
|
||||||
|
defer func() {
|
||||||
|
err := n.Delete()
|
||||||
|
assert.Check(t, err)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
sb1, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath))
|
sbOpts := append(tc.sbOpts,
|
||||||
if err != nil {
|
libnetwork.OptionResolvConfPath(resolvConfPath),
|
||||||
t.Fatal(err)
|
libnetwork.OptionOriginResolvConfPath(originResolvConfPath),
|
||||||
}
|
)
|
||||||
defer func() {
|
sb, err := c.NewSandbox(containerID, sbOpts...)
|
||||||
if err := sb1.Delete(); err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
defer func() {
|
||||||
}
|
err := sb.Delete()
|
||||||
}()
|
assert.Check(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
err = ep.Join(sb1)
|
ep, err := n.CreateEndpoint("ep", tc.epOpts...)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
defer func() {
|
||||||
}
|
err := ep.Delete(false)
|
||||||
|
assert.Check(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
finfo, err := os.Stat(resolvConfPath)
|
err = ep.Join(sb)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
defer func() {
|
||||||
}
|
err := ep.Leave(sb)
|
||||||
|
assert.Check(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
fmode := (os.FileMode)(0o644)
|
finfo, err := os.Stat(resolvConfPath)
|
||||||
if finfo.Mode() != fmode {
|
assert.NilError(t, err)
|
||||||
t.Fatalf("Expected file mode %s, got %s", fmode.String(), finfo.Mode().String())
|
expFMode := (os.FileMode)(0o644)
|
||||||
}
|
assert.Check(t, is.Equal(finfo.Mode().String(), expFMode.String()))
|
||||||
|
content, err := os.ReadFile(resolvConfPath)
|
||||||
content, err := os.ReadFile(resolvConfPath)
|
assert.NilError(t, err)
|
||||||
if err != nil {
|
actual := stripCommentsRE.ReplaceAllString(string(content), "")
|
||||||
t.Fatal(err)
|
actual = strings.TrimSpace(actual)
|
||||||
}
|
assert.Check(t, is.Equal(actual, tc.expResolvConf))
|
||||||
|
})
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,20 +3,12 @@ package resolvconf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/containerd/log"
|
"github.com/docker/docker/libnetwork/internal/resolvconf"
|
||||||
)
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants for the IP address type
|
// constants for the IP address type
|
||||||
|
@ -26,72 +18,16 @@ const (
|
||||||
IPv6
|
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
|
// File contains the resolv.conf content and its hash
|
||||||
type File struct {
|
type File struct {
|
||||||
Content []byte
|
Content []byte
|
||||||
Hash []byte
|
Hash []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Path() string {
|
||||||
|
return resolvconf.Path()
|
||||||
|
}
|
||||||
|
|
||||||
// Get returns the contents of /etc/resolv.conf and its hash
|
// Get returns the contents of /etc/resolv.conf and its hash
|
||||||
func Get() (*File, error) {
|
func Get() (*File, error) {
|
||||||
return GetSpecific(Path())
|
return GetSpecific(Path())
|
||||||
|
@ -103,7 +39,8 @@ func GetSpecific(path string) (*File, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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:
|
// 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
|
// 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
|
// code will remove all IPv6 nameservers if it is not enabled for containers
|
||||||
func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) {
|
func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) {
|
||||||
cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
|
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||||
// if IPv6 is not enabled, also clean out any IPv6 address nameserver
|
if err != nil {
|
||||||
if !ipv6Enabled {
|
return nil, err
|
||||||
cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
|
|
||||||
}
|
}
|
||||||
// if the resulting resolvConf has no more nameservers defined, add appropriate
|
rc.TransformForLegacyNw(ipv6Enabled)
|
||||||
// default DNS servers for IPv4 and (optionally) IPv6
|
content, err := rc.Generate(false)
|
||||||
if len(GetNameservers(cleanedResolvConf, IP)) == 0 {
|
if err != nil {
|
||||||
log.G(context.TODO()).Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns)
|
return nil, err
|
||||||
dns := defaultIPv4Dns
|
|
||||||
if ipv6Enabled {
|
|
||||||
log.G(context.TODO()).Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns)
|
|
||||||
dns = append(dns, defaultIPv6Dns...)
|
|
||||||
}
|
|
||||||
cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
|
|
||||||
}
|
}
|
||||||
return &File{Content: cleanedResolvConf, Hash: hashData(cleanedResolvConf)}, nil
|
hash := digest.FromBytes(content)
|
||||||
}
|
return &File{Content: content, Hash: []byte(hash)}, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
|
// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf
|
||||||
func GetNameservers(resolvConf []byte, kind int) []string {
|
func GetNameservers(resolvConf []byte, kind int) []string {
|
||||||
|
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nsAddrs := rc.NameServers()
|
||||||
var nameservers []string
|
var nameservers []string
|
||||||
for _, line := range getLines(resolvConf, []byte("#")) {
|
for _, addr := range nsAddrs {
|
||||||
var ns [][]byte
|
|
||||||
if kind == IP {
|
if kind == IP {
|
||||||
ns = nsRegexp.FindSubmatch(line)
|
nameservers = append(nameservers, addr.String())
|
||||||
} else if kind == IPv4 {
|
} else if kind == IPv4 && addr.Is4() {
|
||||||
ns = nsIPv4Regexpmatch.FindSubmatch(line)
|
nameservers = append(nameservers, addr.String())
|
||||||
} else if kind == IPv6 {
|
} else if kind == IPv6 && addr.Is6() {
|
||||||
ns = nsIPv6Regexpmatch.FindSubmatch(line)
|
nameservers = append(nameservers, addr.String())
|
||||||
}
|
|
||||||
if len(ns) > 0 {
|
|
||||||
nameservers = append(nameservers, string(ns[1]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nameservers
|
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")
|
// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32")
|
||||||
// This function's output is intended for net.ParseCIDR
|
// This function's output is intended for net.ParseCIDR
|
||||||
func GetNameserversAsCIDR(resolvConf []byte) []string {
|
func GetNameserversAsCIDR(resolvConf []byte) []string {
|
||||||
var nameservers []string
|
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||||
for _, nameserver := range GetNameservers(resolvConf, IP) {
|
if err != nil {
|
||||||
var address string
|
return nil
|
||||||
// If IPv6, strip zone if present
|
}
|
||||||
if strings.Contains(nameserver, ":") {
|
nsAddrs := rc.NameServers()
|
||||||
address = strings.Split(nameserver, "%")[0] + "/128"
|
nameservers := make([]string, 0, len(nsAddrs))
|
||||||
} else {
|
for _, addr := range nsAddrs {
|
||||||
address = nameserver + "/32"
|
str := fmt.Sprintf("%s/%d", addr.WithZone("").String(), addr.BitLen())
|
||||||
}
|
nameservers = append(nameservers, str)
|
||||||
nameservers = append(nameservers, address)
|
|
||||||
}
|
}
|
||||||
return nameservers
|
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
|
// If more than one search line is encountered, only the contents of the last
|
||||||
// one is returned.
|
// one is returned.
|
||||||
func GetSearchDomains(resolvConf []byte) []string {
|
func GetSearchDomains(resolvConf []byte) []string {
|
||||||
var domains []string
|
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||||
for _, line := range getLines(resolvConf, []byte("#")) {
|
if err != nil {
|
||||||
match := searchRegexp.FindSubmatch(line)
|
return nil
|
||||||
if match == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
domains = strings.Fields(string(match[1]))
|
|
||||||
}
|
}
|
||||||
return domains
|
return rc.Search()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOptions returns options (if any) listed in /etc/resolv.conf
|
// GetOptions returns options (if any) listed in /etc/resolv.conf
|
||||||
// If more than one options line is encountered, only the contents of the last
|
// If more than one options line is encountered, only the contents of the last
|
||||||
// one is returned.
|
// one is returned.
|
||||||
func GetOptions(resolvConf []byte) []string {
|
func GetOptions(resolvConf []byte) []string {
|
||||||
var options []string
|
rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "")
|
||||||
for _, line := range getLines(resolvConf, []byte("#")) {
|
if err != nil {
|
||||||
match := optionsRegexp.FindSubmatch(line)
|
return nil
|
||||||
if match == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
options = strings.Fields(string(match[1]))
|
|
||||||
}
|
}
|
||||||
return options
|
return rc.Options()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build generates and writes a configuration file to path containing a nameserver
|
// 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
|
// 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
|
// dnsSearch, and an "options" entry for every element in dnsOptions. It returns
|
||||||
// a File containing the generated content and its (sha256) hash.
|
// 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) {
|
func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, error) {
|
||||||
content := bytes.NewBuffer(nil)
|
content := bytes.NewBuffer(nil)
|
||||||
if len(dnsSearch) > 0 {
|
if len(dnsSearch) > 0 {
|
||||||
|
@ -244,5 +154,6 @@ func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, err
|
||||||
return nil, 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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
func TestGet(t *testing.T) {
|
||||||
|
@ -20,7 +25,8 @@ func TestGet(t *testing.T) {
|
||||||
if !bytes.Equal(actual.Content, expected) {
|
if !bytes.Equal(actual.Content, expected) {
|
||||||
t.Errorf("%s and GetResolvConf have different content.", Path())
|
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())
|
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`,
|
nameserver 1.2.3.4 # not 4.3.2.1`,
|
||||||
result: []string{"1.2.3.4/32"},
|
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))
|
test := GetNameserversAsCIDR([]byte(tc.input))
|
||||||
if !strSlicesEqual(test, tc.result) {
|
if !strSlicesEqual(test, tc.result) {
|
||||||
|
@ -175,6 +189,10 @@ search foo.example.com example.com
|
||||||
nameserver 4.30.20.100`,
|
nameserver 4.30.20.100`,
|
||||||
result: []string{"foo.example.com", "example.com"},
|
result: []string{"foo.example.com", "example.com"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `domain an.example`,
|
||||||
|
result: []string{"an.example"},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
test := GetSearchDomains([]byte(tc.input))
|
test := GetSearchDomains([]byte(tc.input))
|
||||||
if !strSlicesEqual(test, tc.result) {
|
if !strSlicesEqual(test, tc.result) {
|
||||||
|
@ -338,89 +356,79 @@ func TestBuildWithNoOptions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterResolvDNS(t *testing.T) {
|
func TestFilterResolvDNS(t *testing.T) {
|
||||||
ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n"
|
testcases := []struct {
|
||||||
|
name string
|
||||||
if result, _ := FilterResolvDNS([]byte(ns0), false); result != nil {
|
input string
|
||||||
if ns0 != string(result.Content) {
|
ipv6Enabled bool
|
||||||
t.Errorf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
expOut string
|
||||||
}
|
}{
|
||||||
|
{
|
||||||
|
name: "No localhost",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\n",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost last",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost middle",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost first",
|
||||||
|
input: "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 Localhost",
|
||||||
|
input: "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Two IPv6 Localhosts",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 disabled",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 link-local disabled",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21",
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 enabled",
|
||||||
|
input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1\n",
|
||||||
|
ipv6Enabled: true,
|
||||||
|
expOut: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
|
||||||
|
name: "localhost only IPv6",
|
||||||
|
input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1",
|
||||||
|
ipv6Enabled: true,
|
||||||
|
expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
|
||||||
|
name: "localhost only no IPv6",
|
||||||
|
input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1",
|
||||||
|
expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n"
|
for _, tc := range testcases {
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if ns0 != string(result.Content) {
|
f, err := FilterResolvDNS([]byte(tc.input), tc.ipv6Enabled)
|
||||||
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
assert.Check(t, is.Nil(err))
|
||||||
}
|
out := strings.TrimSpace(string(f.Content))
|
||||||
}
|
assert.Check(t, is.Equal(out, tc.expOut))
|
||||||
|
})
|
||||||
ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with IPv6 disabled (false param), the IPv6 nameserver should be removed
|
|
||||||
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with IPv6 disabled (false param), the IPv6 link-local nameserver with zone ID should be removed
|
|
||||||
ns1 = "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21\n"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with IPv6 enabled, the IPv6 nameserver should be preserved
|
|
||||||
ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n"
|
|
||||||
ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added
|
|
||||||
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844"
|
|
||||||
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added
|
|
||||||
ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4"
|
|
||||||
ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1"
|
|
||||||
if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
||||||
if ns0 != string(result.Content) {
|
|
||||||
t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package libnetwork
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"io/fs"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containerd/log"
|
"github.com/containerd/log"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/docker/libnetwork/etchosts"
|
"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/docker/docker/libnetwork/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -100,6 +97,13 @@ func (sb *Sandbox) setupResolutionFiles() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *Sandbox) buildHostsFile() 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
|
// This is for the host mode networking
|
||||||
if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
|
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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *Sandbox) restorePath() {
|
func (sb *Sandbox) restoreResolvConfPath() {
|
||||||
if sb.config.resolvConfPath == "" {
|
if sb.config.resolvConfPath == "" {
|
||||||
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
|
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
|
||||||
}
|
}
|
||||||
sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
|
sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sb *Sandbox) restoreHostsPath() {
|
||||||
if sb.config.hostsPath == "" {
|
if sb.config.hostsPath == "" {
|
||||||
sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
|
sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *Sandbox) setExternalResolvers(content []byte, addrType int, checkLoopback bool) {
|
func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) {
|
||||||
servers := resolvconf.GetNameservers(content, addrType)
|
sb.extDNS = make([]extDNSEntry, 0, len(entries))
|
||||||
for _, ip := range servers {
|
for _, entry := range entries {
|
||||||
hostLoopback := false
|
|
||||||
if checkLoopback && isIPv4Loopback(ip) {
|
|
||||||
hostLoopback = true
|
|
||||||
}
|
|
||||||
sb.extDNS = append(sb.extDNS, extDNSEntry{
|
sb.extDNS = append(sb.extDNS, extDNSEntry{
|
||||||
IPStr: ip,
|
IPStr: entry.Addr.String(),
|
||||||
HostLoopback: hostLoopback,
|
HostLoopback: entry.HostLoopback,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isIPv4Loopback checks if the given IP address is an IPv4 loopback address.
|
func (c *containerConfig) getOriginResolvConfPath() string {
|
||||||
// It's based on the logic in Go's net.IP.IsLoopback(), but only the IPv4 part:
|
if c.originResolvConfPath != "" {
|
||||||
// https://github.com/golang/go/blob/go1.16.6/src/net/ip.go#L120-L126
|
return c.originResolvConfPath
|
||||||
func isIPv4Loopback(ipAddress string) bool {
|
|
||||||
if ip := net.ParseIP(ipAddress); ip != nil {
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
return ip4[0] == 127
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
// Fallback if not specified.
|
||||||
|
return resolvconf.Path()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *Sandbox) setupDNS() error {
|
// loadResolvConf reads the resolv.conf file at path, and merges in overrides for
|
||||||
if sb.config.resolvConfPath == "" {
|
// nameservers, options, and search domains.
|
||||||
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf"
|
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)
|
dir, _ := filepath.Split(sb.config.resolvConfPath)
|
||||||
if err := createBasePath(dir); err != nil {
|
if err := createBasePath(dir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the user specify a conainter in the host namespace and do no have any dns option specified
|
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
|
||||||
// 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 err != nil {
|
||||||
if !os.IsNotExist(err) {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
// No /etc/resolv.conf found: we'll use the default resolvers (Google's Public DNS).
|
|
||||||
log.G(context.TODO()).WithField("path", originResolvConfPath).Infof("no resolv.conf found, falling back to defaults")
|
|
||||||
}
|
}
|
||||||
|
return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
|
||||||
var newRC *resolvconf.File
|
|
||||||
if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(sb.config.dnsOptionsList) > 0 {
|
|
||||||
var (
|
|
||||||
dnsList = sb.config.dnsList
|
|
||||||
dnsSearchList = sb.config.dnsSearchList
|
|
||||||
dnsOptionsList = sb.config.dnsOptionsList
|
|
||||||
)
|
|
||||||
if len(sb.config.dnsList) == 0 {
|
|
||||||
dnsList = resolvconf.GetNameservers(currRC, resolvconf.IP)
|
|
||||||
}
|
|
||||||
if len(sb.config.dnsSearchList) == 0 {
|
|
||||||
dnsSearchList = resolvconf.GetSearchDomains(currRC)
|
|
||||||
}
|
|
||||||
if len(sb.config.dnsOptionsList) == 0 {
|
|
||||||
dnsOptionsList = resolvconf.GetOptions(currRC)
|
|
||||||
}
|
|
||||||
newRC, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// After building the resolv.conf from the user config save the
|
|
||||||
// external resolvers in the sandbox. Note that --dns 127.0.0.x
|
|
||||||
// config refers to the loopback in the container namespace
|
|
||||||
sb.setExternalResolvers(newRC.Content, resolvconf.IPv4, len(sb.config.dnsList) == 0)
|
|
||||||
} else {
|
|
||||||
// If the host resolv.conf file has 127.0.0.x container should
|
|
||||||
// use the host resolver for queries. This is supported by the
|
|
||||||
// docker embedded DNS server. Hence save the external resolvers
|
|
||||||
// before filtering it out.
|
|
||||||
sb.setExternalResolvers(currRC, resolvconf.IPv4, true)
|
|
||||||
|
|
||||||
// Replace any localhost/127.* (at this point we have no info about ipv6, pass it as true)
|
|
||||||
newRC, err = resolvconf.FilterResolvDNS(currRC, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// No contention on container resolv.conf file at sandbox creation
|
|
||||||
err = os.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm)
|
|
||||||
if err != nil {
|
|
||||||
return types.InternalErrorf("failed to write unhaltered resolv.conf file content when setting up dns for sandbox %s: %v", sb.ID(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write hash
|
|
||||||
err = os.WriteFile(sb.config.resolvConfHashFile, newRC.Hash, filePerm)
|
|
||||||
if err != nil {
|
|
||||||
return types.InternalErrorf("failed to write resolv.conf hash file when setting up dns for sandbox %s: %v", sb.ID(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when an endpoint has joined the sandbox.
|
||||||
func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
|
func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
|
||||||
// This is for the host mode networking
|
if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// write the new hash in a temp file and rename it to make the update atomic
|
// Load the host's resolv.conf as a starting point.
|
||||||
dir := path.Dir(sb.config.resolvConfPath)
|
rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath())
|
||||||
tmpHashFile, err := os.CreateTemp(dir, "hash")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = tmpHashFile.Chmod(filePerm); err != nil {
|
// For host-networking, no further change is needed.
|
||||||
tmpHashFile.Close()
|
if !sb.config.useDefaultSandBox {
|
||||||
return err
|
// 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)
|
return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm)
|
||||||
if err1 := tmpHashFile.Close(); err == nil {
|
|
||||||
err = err1
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Rename(tmpHashFile.Name(), sb.config.resolvConfHashFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's
|
// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf.
|
||||||
// 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
|
|
||||||
func (sb *Sandbox) rebuildDNS() error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user config and embedded DNS server both have ndots option set,
|
// Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers
|
||||||
// remember the user's config so that unqualified names not in the docker
|
// will be left in the container's 'resolv.conf'.
|
||||||
// domain can be dropped.
|
// TODO(robmry) - preserving old behaviour, but ...
|
||||||
resOptions := sb.resolver.ResolverOptions()
|
// IPv6 nameservers should be treated like IPv4 ones, and used as upstream
|
||||||
dnsOptionsList := resolvconf.GetOptions(currRC)
|
// servers for the internal resolver (if it has IPv6 connectivity). This
|
||||||
|
// doesn't need to depend on whether there are currently any IPv6 endpoints.
|
||||||
dnsOpt:
|
// Removing IPv6 nameservers from the container's resolv.conf will avoid the
|
||||||
for _, resOpt := range resOptions {
|
// problem that musl-libc's resolver tries all nameservers in parallel, so an
|
||||||
if strings.Contains(resOpt, "ndots") {
|
// external IPv6 resolver can return NXDOMAIN before the internal resolver
|
||||||
for _, option := range dnsOptionsList {
|
// returns the address of a container.
|
||||||
if strings.Contains(option, "ndots") {
|
ipv6 := false
|
||||||
parts := strings.Split(option, ":")
|
for _, ep := range sb.endpoints {
|
||||||
if len(parts) != 2 {
|
if ep.network.enableIPv6 {
|
||||||
return fmt.Errorf("invalid ndots option %v", option)
|
ipv6 = true
|
||||||
}
|
break
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sb.ndotsSet {
|
intNS, err := netip.ParseAddr(sb.resolver.NameServer())
|
||||||
// if the user did not set the ndots, set it to 0 to prioritize the service name resolution
|
if err != nil {
|
||||||
// Ref: https://linux.die.net/man/5/resolv.conf
|
return err
|
||||||
dnsOptionsList = append(dnsOptionsList, resOptions...)
|
|
||||||
}
|
|
||||||
if len(sb.extDNS) == 0 {
|
|
||||||
sb.setExternalResolvers(currRC, resolvconf.IPv4, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// Work out whether ndots has been set from host config or overrides.
|
||||||
// external v6 DNS servers have to be listed in resolv.conf
|
_, sb.ndotsSet = rc.Option("ndots")
|
||||||
dnsList = append([]string{sb.resolver.NameServer()}, resolvconf.GetNameservers(currRC, resolvconf.IPv6)...)
|
// Swap nameservers for the internal one, and make sure the required options are set.
|
||||||
dnsSearchList = resolvconf.GetSearchDomains(currRC)
|
var extNameServers []resolvconf.ExtDNSEntry
|
||||||
)
|
extNameServers, err = rc.TransformForIntNS(ipv6, intNS, sb.resolver.ResolverOptions())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Extract the list of nameservers that just got swapped out, and store them as
|
||||||
|
// upstream nameservers.
|
||||||
|
sb.setExternalResolvers(extNameServers)
|
||||||
|
|
||||||
_, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList)
|
// Write the file for the container - preserving old behaviour, not updating the
|
||||||
return err
|
// 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 {
|
func createBasePath(dir string) error {
|
||||||
return os.MkdirAll(dir, dirPerm)
|
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 {
|
func copyFile(src, dst string) error {
|
||||||
sBytes, err := os.ReadFile(src)
|
sBytes, err := os.ReadFile(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -12,7 +12,9 @@ func (sb *Sandbox) setupResolutionFiles() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *Sandbox) restorePath() {}
|
func (sb *Sandbox) restoreHostsPath() {}
|
||||||
|
|
||||||
|
func (sb *Sandbox) restoreResolvConfPath() {}
|
||||||
|
|
||||||
func (sb *Sandbox) updateHostsFile(ifaceIP []string) error {
|
func (sb *Sandbox) updateHostsFile(ifaceIP []string) error {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -210,7 +210,8 @@ func (c *Controller) sandboxCleanup(activeSandboxes map[string]interface{}) erro
|
||||||
isRestore = true
|
isRestore = true
|
||||||
opts := val.([]SandboxOption)
|
opts := val.([]SandboxOption)
|
||||||
sb.processOptions(opts...)
|
sb.processOptions(opts...)
|
||||||
sb.restorePath()
|
sb.restoreHostsPath()
|
||||||
|
sb.restoreResolvConfPath()
|
||||||
create = !sb.config.useDefaultSandBox
|
create = !sb.config.useDefaultSandBox
|
||||||
}
|
}
|
||||||
sb.osSbox, err = osl.NewSandbox(sb.Key(), create, isRestore)
|
sb.osSbox, err = osl.NewSandbox(sb.Key(), create, isRestore)
|
||||||
|
|
Loading…
Reference in a new issue