Replace regex matching/replacement and re-reading of generated files
with a simple parser, and struct to remember and manipulate the file
content.
Annotate the generated file with a header comment saying the file is
generated, but can be modified, and a trailing comment describing how
the file was generated and listing external nameservers.
Always start with the host's resolv.conf file, whether generating config
for host networking, or with/without an internal resolver - rather than
editing a file previously generated for a different use-case.
Resolves an issue where rewrites of the generated file resulted in
default IPv6 nameservers being unnecessarily added to the config.
Signed-off-by: Rob Murray <rob.murray@docker.com>
| ... | ... |
@@ -419,6 +419,7 @@ func serviceDiscoveryOnDefaultNetwork() bool {
|
| 419 | 419 |
|
| 420 | 420 |
func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Config, sboxOptions *[]libnetwork.SandboxOption) error {
|
| 421 | 421 |
var err error |
| 422 |
+ var originResolvConfPath string |
|
| 422 | 423 |
|
| 423 | 424 |
// Set the correct paths for /etc/hosts and /etc/resolv.conf, based on the |
| 424 | 425 |
// networking-mode of the container. Note that containers with "container" |
| ... | ... |
@@ -432,8 +433,8 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con |
| 432 | 432 |
*sboxOptions = append( |
| 433 | 433 |
*sboxOptions, |
| 434 | 434 |
libnetwork.OptionOriginHostsPath("/etc/hosts"),
|
| 435 |
- libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
|
|
| 436 | 435 |
) |
| 436 |
+ originResolvConfPath = "/etc/resolv.conf" |
|
| 437 | 437 |
case container.HostConfig.NetworkMode.IsUserDefined(): |
| 438 | 438 |
// The container uses a user-defined network. We use the embedded DNS |
| 439 | 439 |
// server for container name resolution and to act as a DNS forwarder |
| ... | ... |
@@ -446,10 +447,7 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con |
| 446 | 446 |
// If systemd-resolvd is used, the "upstream" DNS servers can be found in |
| 447 | 447 |
// /run/systemd/resolve/resolv.conf. We do not query those DNS servers |
| 448 | 448 |
// directly, as they can be dynamically reconfigured. |
| 449 |
- *sboxOptions = append( |
|
| 450 |
- *sboxOptions, |
|
| 451 |
- libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"),
|
|
| 452 |
- ) |
|
| 449 |
+ originResolvConfPath = "/etc/resolv.conf" |
|
| 453 | 450 |
default: |
| 454 | 451 |
// For other situations, such as the default bridge network, container |
| 455 | 452 |
// discovery / name resolution is handled through /etc/hosts, and no |
| ... | ... |
@@ -462,11 +460,15 @@ func setupPathsAndSandboxOptions(container *container.Container, cfg *config.Con |
| 462 | 462 |
// DNS servers on the host can be dynamically updated. |
| 463 | 463 |
// |
| 464 | 464 |
// Copy the host's resolv.conf for the container (/run/systemd/resolve/resolv.conf or /etc/resolv.conf) |
| 465 |
- *sboxOptions = append( |
|
| 466 |
- *sboxOptions, |
|
| 467 |
- libnetwork.OptionOriginResolvConfPath(cfg.GetResolvConf()), |
|
| 468 |
- ) |
|
| 465 |
+ originResolvConfPath = cfg.GetResolvConf() |
|
| 466 |
+ } |
|
| 467 |
+ |
|
| 468 |
+ // Allow tests to point at their own resolv.conf file. |
|
| 469 |
+ if envPath := os.Getenv("DOCKER_TEST_RESOLV_CONF_PATH"); envPath != "" {
|
|
| 470 |
+ log.G(context.TODO()).Infof("Using OriginResolvConfPath from env: %s", envPath)
|
|
| 471 |
+ originResolvConfPath = envPath |
|
| 469 | 472 |
} |
| 473 |
+ *sboxOptions = append(*sboxOptions, libnetwork.OptionOriginResolvConfPath(originResolvConfPath)) |
|
| 470 | 474 |
|
| 471 | 475 |
container.HostsPath, err = container.GetRootResourcePath("hosts")
|
| 472 | 476 |
if err != nil {
|
| ... | ... |
@@ -1275,10 +1275,11 @@ func (s *DockerCLIRunSuite) TestRunDNSDefaultOptions(c *testing.T) {
|
| 1275 | 1275 |
} |
| 1276 | 1276 |
|
| 1277 | 1277 |
actual := cli.DockerCmd(c, "run", "busybox", "cat", "/etc/resolv.conf").Combined() |
| 1278 |
- // check that the actual defaults are appended to the commented out |
|
| 1279 |
- // localhost resolver (which should be preserved) |
|
| 1278 |
+ actual = regexp.MustCompile("(?m)^#.*$").ReplaceAllString(actual, "")
|
|
| 1279 |
+ actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ") |
|
| 1280 | 1280 |
// NOTE: if we ever change the defaults from google dns, this will break |
| 1281 |
- expected := "#nameserver 127.0.2.1\n\nnameserver 8.8.8.8\nnameserver 8.8.4.4\n" |
|
| 1281 |
+ expected := "nameserver 8.8.8.8 nameserver 8.8.4.4" |
|
| 1282 |
+ |
|
| 1282 | 1283 |
if actual != expected {
|
| 1283 | 1284 |
c.Fatalf("expected resolv.conf be: %q, but was: %q", expected, actual)
|
| 1284 | 1285 |
} |
| ... | ... |
@@ -1295,14 +1296,16 @@ func (s *DockerCLIRunSuite) TestRunDNSOptions(c *testing.T) {
|
| 1295 | 1295 |
c.Fatalf("Expected warning on stderr about localhost resolver, but got %q", result.Stderr())
|
| 1296 | 1296 |
} |
| 1297 | 1297 |
|
| 1298 |
- actual := strings.ReplaceAll(strings.Trim(result.Stdout(), "\r\n"), "\n", " ") |
|
| 1299 |
- if actual != "search mydomain nameserver 127.0.0.1 options ndots:9" {
|
|
| 1300 |
- c.Fatalf("expected 'search mydomain nameserver 127.0.0.1 options ndots:9', but says: %q", actual)
|
|
| 1298 |
+ actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(result.Stdout(), "")
|
|
| 1299 |
+ actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ") |
|
| 1300 |
+ if actual != "nameserver 127.0.0.1 search mydomain options ndots:9" {
|
|
| 1301 |
+ c.Fatalf("nameserver 127.0.0.1 expected 'search mydomain options ndots:9', but says: %q", actual)
|
|
| 1301 | 1302 |
} |
| 1302 | 1303 |
|
| 1303 | 1304 |
out := cli.DockerCmd(c, "run", "--dns=1.1.1.1", "--dns-search=.", "--dns-opt=ndots:3", "busybox", "cat", "/etc/resolv.conf").Combined() |
| 1304 | 1305 |
|
| 1305 |
- actual = strings.ReplaceAll(strings.Trim(strings.Trim(out, "\r\n"), " "), "\n", " ") |
|
| 1306 |
+ actual = regexp.MustCompile("(?m)^#.*$").ReplaceAllString(out, "")
|
|
| 1307 |
+ actual = strings.ReplaceAll(strings.Trim(strings.Trim(actual, "\r\n"), " "), "\n", " ") |
|
| 1306 | 1308 |
if actual != "nameserver 1.1.1.1 options ndots:3" {
|
| 1307 | 1309 |
c.Fatalf("expected 'nameserver 1.1.1.1 options ndots:3', but says: %q", actual)
|
| 1308 | 1310 |
} |
| ... | ... |
@@ -1312,9 +1315,10 @@ func (s *DockerCLIRunSuite) TestRunDNSRepeatOptions(c *testing.T) {
|
| 1312 | 1312 |
testRequires(c, DaemonIsLinux) |
| 1313 | 1313 |
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() |
| 1314 | 1314 |
|
| 1315 |
- actual := strings.ReplaceAll(strings.Trim(out, "\r\n"), "\n", " ") |
|
| 1316 |
- if actual != "search mydomain mydomain2 nameserver 1.1.1.1 nameserver 2.2.2.2 options ndots:9 timeout:3" {
|
|
| 1317 |
- 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)
|
|
| 1315 |
+ actual := regexp.MustCompile("(?m)^#.*$").ReplaceAllString(out, "")
|
|
| 1316 |
+ actual = strings.ReplaceAll(strings.Trim(actual, "\r\n"), "\n", " ") |
|
| 1317 |
+ if actual != "nameserver 1.1.1.1 nameserver 2.2.2.2 search mydomain mydomain2 options ndots:9 timeout:3" {
|
|
| 1318 |
+ 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)
|
|
| 1318 | 1319 |
} |
| 1319 | 1320 |
} |
| 1320 | 1321 |
|
| 1321 | 1322 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,72 @@ |
| 0 |
+package networking |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "os" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ |
|
| 7 |
+ containertypes "github.com/docker/docker/api/types/container" |
|
| 8 |
+ "github.com/docker/docker/integration/internal/container" |
|
| 9 |
+ "github.com/docker/docker/integration/internal/network" |
|
| 10 |
+ "github.com/docker/docker/testutil/daemon" |
|
| 11 |
+ "gotest.tools/v3/assert" |
|
| 12 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 13 |
+ "gotest.tools/v3/skip" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// Regression test for https://github.com/moby/moby/issues/46968 |
|
| 17 |
+func TestResolvConfLocalhostIPv6(t *testing.T) {
|
|
| 18 |
+ // No "/etc/resolv.conf" on Windows. |
|
| 19 |
+ skip.If(t, testEnv.DaemonInfo.OSType == "windows") |
|
| 20 |
+ |
|
| 21 |
+ ctx := setupTest(t) |
|
| 22 |
+ |
|
| 23 |
+ // Write a resolv.conf that only contains a loopback address. |
|
| 24 |
+ // Not using t.TempDir() here because in rootless mode, while the temporary |
|
| 25 |
+ // directory gets mode 0777, it's a subdir of an 0700 directory owned by root. |
|
| 26 |
+ // So, it's not accessible by the daemon. |
|
| 27 |
+ f, err := os.CreateTemp("", "resolv.conf")
|
|
| 28 |
+ assert.NilError(t, err) |
|
| 29 |
+ defer os.Remove(f.Name()) |
|
| 30 |
+ err = f.Chmod(0644) |
|
| 31 |
+ assert.NilError(t, err) |
|
| 32 |
+ f.Write([]byte("nameserver 127.0.0.53\n"))
|
|
| 33 |
+ |
|
| 34 |
+ d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_RESOLV_CONF_PATH="+f.Name()))
|
|
| 35 |
+ d.StartWithBusybox(ctx, t, "--experimental", "--ip6tables") |
|
| 36 |
+ defer d.Stop(t) |
|
| 37 |
+ |
|
| 38 |
+ c := d.NewClientT(t) |
|
| 39 |
+ defer c.Close() |
|
| 40 |
+ |
|
| 41 |
+ netName := "nnn" |
|
| 42 |
+ network.CreateNoError(ctx, t, c, netName, |
|
| 43 |
+ network.WithDriver("bridge"),
|
|
| 44 |
+ network.WithIPv6(), |
|
| 45 |
+ network.WithIPAM("fd49:b5ef:36d9::/64", "fd49:b5ef:36d9::1"),
|
|
| 46 |
+ ) |
|
| 47 |
+ defer network.RemoveNoError(ctx, t, c, netName) |
|
| 48 |
+ |
|
| 49 |
+ result := container.RunAttach(ctx, t, c, |
|
| 50 |
+ container.WithImage("busybox:latest"),
|
|
| 51 |
+ container.WithNetworkMode(netName), |
|
| 52 |
+ container.WithCmd("cat", "/etc/resolv.conf"),
|
|
| 53 |
+ ) |
|
| 54 |
+ defer c.ContainerRemove(ctx, result.ContainerID, containertypes.RemoveOptions{
|
|
| 55 |
+ Force: true, |
|
| 56 |
+ }) |
|
| 57 |
+ |
|
| 58 |
+ output := strings.ReplaceAll(result.Stdout.String(), f.Name(), "RESOLV.CONF") |
|
| 59 |
+ assert.Check(t, is.Equal(output, `# Generated by Docker Engine. |
|
| 60 |
+# This file can be edited; Docker Engine will not make further changes once it |
|
| 61 |
+# has been modified. |
|
| 62 |
+ |
|
| 63 |
+nameserver 127.0.0.11 |
|
| 64 |
+options ndots:0 |
|
| 65 |
+ |
|
| 66 |
+# Based on host file: 'RESOLV.CONF' (internal resolver) |
|
| 67 |
+# ExtServers: [host(127.0.0.53)] |
|
| 68 |
+# Overrides: [] |
|
| 69 |
+# Option ndots from: internal |
|
| 70 |
+`)) |
|
| 71 |
+} |
| 0 | 72 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,510 @@ |
| 0 |
+// Package resolvconf is used to generate a container's /etc/resolv.conf file. |
|
| 1 |
+// |
|
| 2 |
+// Constructor Load and Parse read a resolv.conf file from the filesystem or |
|
| 3 |
+// a reader respectively, and return a ResolvConf object. |
|
| 4 |
+// |
|
| 5 |
+// The ResolvConf object can then be updated with overrides for nameserver, |
|
| 6 |
+// search domains, and DNS options. |
|
| 7 |
+// |
|
| 8 |
+// ResolvConf can then be transformed to make it suitable for legacy networking, |
|
| 9 |
+// a network with an internal nameserver, or used as-is for host networking. |
|
| 10 |
+// |
|
| 11 |
+// This package includes methods to write the file for the container, along with |
|
| 12 |
+// a hash that can be used to detect modifications made by the user to avoid |
|
| 13 |
+// overwriting those updates. |
|
| 14 |
+package resolvconf |
|
| 15 |
+ |
|
| 16 |
+import ( |
|
| 17 |
+ "bufio" |
|
| 18 |
+ "bytes" |
|
| 19 |
+ "context" |
|
| 20 |
+ _ "embed" |
|
| 21 |
+ "fmt" |
|
| 22 |
+ "io" |
|
| 23 |
+ "io/fs" |
|
| 24 |
+ "net/netip" |
|
| 25 |
+ "os" |
|
| 26 |
+ "slices" |
|
| 27 |
+ "strconv" |
|
| 28 |
+ "strings" |
|
| 29 |
+ "text/template" |
|
| 30 |
+ |
|
| 31 |
+ "github.com/containerd/log" |
|
| 32 |
+ "github.com/docker/docker/errdefs" |
|
| 33 |
+ "github.com/docker/docker/pkg/ioutils" |
|
| 34 |
+ "github.com/opencontainers/go-digest" |
|
| 35 |
+ "github.com/pkg/errors" |
|
| 36 |
+) |
|
| 37 |
+ |
|
| 38 |
+// Fallback nameservers, to use if none can be obtained from the host or command |
|
| 39 |
+// line options. |
|
| 40 |
+var ( |
|
| 41 |
+ defaultIPv4NSs = []netip.Addr{
|
|
| 42 |
+ netip.MustParseAddr("8.8.8.8"),
|
|
| 43 |
+ netip.MustParseAddr("8.8.4.4"),
|
|
| 44 |
+ } |
|
| 45 |
+ defaultIPv6NSs = []netip.Addr{
|
|
| 46 |
+ netip.MustParseAddr("2001:4860:4860::8888"),
|
|
| 47 |
+ netip.MustParseAddr("2001:4860:4860::8844"),
|
|
| 48 |
+ } |
|
| 49 |
+) |
|
| 50 |
+ |
|
| 51 |
+// ResolvConf represents a resolv.conf file. It can be constructed by |
|
| 52 |
+// reading a resolv.conf file, using method Parse(). |
|
| 53 |
+type ResolvConf struct {
|
|
| 54 |
+ nameServers []netip.Addr |
|
| 55 |
+ search []string |
|
| 56 |
+ options []string |
|
| 57 |
+ other []string // Unrecognised directives from the host's file, if any. |
|
| 58 |
+ |
|
| 59 |
+ md metadata |
|
| 60 |
+} |
|
| 61 |
+ |
|
| 62 |
+// ExtDNSEntry represents a nameserver address that was removed from the |
|
| 63 |
+// container's resolv.conf when it was transformed by TransformForIntNS(). These |
|
| 64 |
+// are addresses read from the host's file, or applied via an override ('--dns').
|
|
| 65 |
+type ExtDNSEntry struct {
|
|
| 66 |
+ Addr netip.Addr |
|
| 67 |
+ HostLoopback bool // The address is loopback, in the host's namespace. |
|
| 68 |
+} |
|
| 69 |
+ |
|
| 70 |
+func (ed ExtDNSEntry) String() string {
|
|
| 71 |
+ if ed.HostLoopback {
|
|
| 72 |
+ return fmt.Sprintf("host(%s)", ed.Addr)
|
|
| 73 |
+ } |
|
| 74 |
+ return ed.Addr.String() |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 77 |
+// metadata is used to track where components of the generated file have come |
|
| 78 |
+// from, in order to generate comments in the file for debug/info. Struct members |
|
| 79 |
+// are exported for use by 'text/template'. |
|
| 80 |
+type metadata struct {
|
|
| 81 |
+ SourcePath string |
|
| 82 |
+ Header string |
|
| 83 |
+ NSOverride bool |
|
| 84 |
+ SearchOverride bool |
|
| 85 |
+ OptionsOverride bool |
|
| 86 |
+ NDotsFrom string |
|
| 87 |
+ UsedDefaultNS bool |
|
| 88 |
+ Transform string |
|
| 89 |
+ InvalidNSs []string |
|
| 90 |
+ ExtNameServers []ExtDNSEntry |
|
| 91 |
+} |
|
| 92 |
+ |
|
| 93 |
+// Load opens a file at path and parses it as a resolv.conf file. |
|
| 94 |
+// On error, the returned ResolvConf will be zero-valued. |
|
| 95 |
+func Load(path string) (ResolvConf, error) {
|
|
| 96 |
+ f, err := os.Open(path) |
|
| 97 |
+ if err != nil {
|
|
| 98 |
+ return ResolvConf{}, err
|
|
| 99 |
+ } |
|
| 100 |
+ defer f.Close() |
|
| 101 |
+ return Parse(f, path) |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+// Parse parses a resolv.conf file from reader. |
|
| 105 |
+// path is optional if reader is an *os.File. |
|
| 106 |
+// On error, the returned ResolvConf will be zero-valued. |
|
| 107 |
+func Parse(reader io.Reader, path string) (ResolvConf, error) {
|
|
| 108 |
+ var rc ResolvConf |
|
| 109 |
+ rc.md.SourcePath = path |
|
| 110 |
+ if path == "" {
|
|
| 111 |
+ if namer, ok := reader.(interface{ Name() string }); ok {
|
|
| 112 |
+ rc.md.SourcePath = namer.Name() |
|
| 113 |
+ } |
|
| 114 |
+ } |
|
| 115 |
+ |
|
| 116 |
+ scanner := bufio.NewScanner(reader) |
|
| 117 |
+ for scanner.Scan() {
|
|
| 118 |
+ rc.processLine(scanner.Text()) |
|
| 119 |
+ } |
|
| 120 |
+ if err := scanner.Err(); err != nil {
|
|
| 121 |
+ return ResolvConf{}, errdefs.System(err)
|
|
| 122 |
+ } |
|
| 123 |
+ if _, ok := rc.Option("ndots"); ok {
|
|
| 124 |
+ rc.md.NDotsFrom = "host" |
|
| 125 |
+ } |
|
| 126 |
+ return rc, nil |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+// SetHeader sets the content to be included verbatim at the top of the |
|
| 130 |
+// generated resolv.conf file. No formatting or checking is done on the |
|
| 131 |
+// string. It must be valid resolv.conf syntax. (Comments must have '#' |
|
| 132 |
+// or ';' in the first column of each line). |
|
| 133 |
+// |
|
| 134 |
+// For example: |
|
| 135 |
+// |
|
| 136 |
+// SetHeader("# My resolv.conf\n# This file was generated.")
|
|
| 137 |
+func (rc *ResolvConf) SetHeader(c string) {
|
|
| 138 |
+ rc.md.Header = c |
|
| 139 |
+} |
|
| 140 |
+ |
|
| 141 |
+// NameServers returns addresses used in nameserver directives. |
|
| 142 |
+func (rc *ResolvConf) NameServers() []netip.Addr {
|
|
| 143 |
+ return slices.Clone(rc.nameServers) |
|
| 144 |
+} |
|
| 145 |
+ |
|
| 146 |
+// OverrideNameServers replaces the current set of nameservers. |
|
| 147 |
+func (rc *ResolvConf) OverrideNameServers(nameServers []netip.Addr) {
|
|
| 148 |
+ rc.nameServers = nameServers |
|
| 149 |
+ rc.md.NSOverride = true |
|
| 150 |
+} |
|
| 151 |
+ |
|
| 152 |
+// Search returns the current DNS search domains. |
|
| 153 |
+func (rc *ResolvConf) Search() []string {
|
|
| 154 |
+ return slices.Clone(rc.search) |
|
| 155 |
+} |
|
| 156 |
+ |
|
| 157 |
+// OverrideSearch replaces the current DNS search domains. |
|
| 158 |
+func (rc *ResolvConf) OverrideSearch(search []string) {
|
|
| 159 |
+ var filtered []string |
|
| 160 |
+ for _, s := range search {
|
|
| 161 |
+ if s != "." {
|
|
| 162 |
+ filtered = append(filtered, s) |
|
| 163 |
+ } |
|
| 164 |
+ } |
|
| 165 |
+ rc.search = filtered |
|
| 166 |
+ rc.md.SearchOverride = true |
|
| 167 |
+} |
|
| 168 |
+ |
|
| 169 |
+// Options returns the current options. |
|
| 170 |
+func (rc *ResolvConf) Options() []string {
|
|
| 171 |
+ return slices.Clone(rc.options) |
|
| 172 |
+} |
|
| 173 |
+ |
|
| 174 |
+// Option finds the last option named search, and returns (value, true) if |
|
| 175 |
+// found, else ("", false). Options are treated as "name:value", where the
|
|
| 176 |
+// ":value" may be omitted. |
|
| 177 |
+// |
|
| 178 |
+// For example, for "ndots:1 edns0": |
|
| 179 |
+// |
|
| 180 |
+// Option("ndots") -> ("1", true)
|
|
| 181 |
+// Option("edns0") -> ("", true)
|
|
| 182 |
+func (rc *ResolvConf) Option(search string) (string, bool) {
|
|
| 183 |
+ for i := len(rc.options) - 1; i >= 0; i -= 1 {
|
|
| 184 |
+ k, v, _ := strings.Cut(rc.options[i], ":") |
|
| 185 |
+ if k == search {
|
|
| 186 |
+ return v, true |
|
| 187 |
+ } |
|
| 188 |
+ } |
|
| 189 |
+ return "", false |
|
| 190 |
+} |
|
| 191 |
+ |
|
| 192 |
+// OverrideOptions replaces the current DNS options. |
|
| 193 |
+func (rc *ResolvConf) OverrideOptions(options []string) {
|
|
| 194 |
+ rc.options = slices.Clone(options) |
|
| 195 |
+ rc.md.NDotsFrom = "" |
|
| 196 |
+ if _, exists := rc.Option("ndots"); exists {
|
|
| 197 |
+ rc.md.NDotsFrom = "override" |
|
| 198 |
+ } |
|
| 199 |
+ rc.md.OptionsOverride = true |
|
| 200 |
+} |
|
| 201 |
+ |
|
| 202 |
+// AddOption adds a single DNS option. |
|
| 203 |
+func (rc *ResolvConf) AddOption(option string) {
|
|
| 204 |
+ if len(option) > 6 && option[:6] == "ndots:" {
|
|
| 205 |
+ rc.md.NDotsFrom = "internal" |
|
| 206 |
+ } |
|
| 207 |
+ rc.options = append(rc.options, option) |
|
| 208 |
+} |
|
| 209 |
+ |
|
| 210 |
+// TransformForLegacyNw makes sure the resolv.conf file will be suitable for |
|
| 211 |
+// use in a legacy network (one that has no internal resolver). |
|
| 212 |
+// - Remove loopback addresses inherited from the host's resolv.conf, because |
|
| 213 |
+// they'll only work in the host's namespace. |
|
| 214 |
+// - Remove IPv6 addresses if !ipv6. |
|
| 215 |
+// - Add default nameservers if there are no addresses left. |
|
| 216 |
+func (rc *ResolvConf) TransformForLegacyNw(ipv6 bool) {
|
|
| 217 |
+ rc.md.Transform = "legacy" |
|
| 218 |
+ if rc.md.NSOverride {
|
|
| 219 |
+ return |
|
| 220 |
+ } |
|
| 221 |
+ var filtered []netip.Addr |
|
| 222 |
+ for _, addr := range rc.nameServers {
|
|
| 223 |
+ if !addr.IsLoopback() && (!addr.Is6() || ipv6) {
|
|
| 224 |
+ filtered = append(filtered, addr) |
|
| 225 |
+ } |
|
| 226 |
+ } |
|
| 227 |
+ rc.nameServers = filtered |
|
| 228 |
+ if len(rc.nameServers) == 0 {
|
|
| 229 |
+ log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
|
|
| 230 |
+ rc.nameServers = defaultNSAddrs(ipv6) |
|
| 231 |
+ rc.md.UsedDefaultNS = true |
|
| 232 |
+ } |
|
| 233 |
+} |
|
| 234 |
+ |
|
| 235 |
+// TransformForIntNS makes sure the resolv.conf file will be suitable for |
|
| 236 |
+// use in a network sandbox that has an internal DNS resolver. |
|
| 237 |
+// - Add internalNS as a nameserver. |
|
| 238 |
+// - Remove other nameservers, stashing them as ExtNameServers for the |
|
| 239 |
+// internal resolver to use. (Apart from IPv6 nameservers, if keepIPv6.) |
|
| 240 |
+// - Mark ExtNameServers that must be used in the host namespace. |
|
| 241 |
+// - If no ExtNameServer addresses are found, use the defaults. |
|
| 242 |
+// - Return an error if an "ndots" option inherited from the host's config, or |
|
| 243 |
+// supplied in an override is not valid. |
|
| 244 |
+// - Ensure there's an 'options' value for each entry in reqdOptions. If the |
|
| 245 |
+// option includes a ':', and an option with a matching prefix exists, it |
|
| 246 |
+// is not modified. |
|
| 247 |
+func (rc *ResolvConf) TransformForIntNS( |
|
| 248 |
+ keepIPv6 bool, |
|
| 249 |
+ internalNS netip.Addr, |
|
| 250 |
+ reqdOptions []string, |
|
| 251 |
+) ([]ExtDNSEntry, error) {
|
|
| 252 |
+ // The transformed config must list the internal nameserver. |
|
| 253 |
+ newNSs := []netip.Addr{internalNS}
|
|
| 254 |
+ // Filter out other nameservers, keeping them for use as upstream nameservers by the |
|
| 255 |
+ // internal nameserver. |
|
| 256 |
+ rc.md.ExtNameServers = nil |
|
| 257 |
+ for _, addr := range rc.nameServers {
|
|
| 258 |
+ // The internal resolver only uses IPv4 addresses so, keep IPv6 nameservers in |
|
| 259 |
+ // the container's file if keepIPv6, else drop them. |
|
| 260 |
+ if addr.Is6() {
|
|
| 261 |
+ if keepIPv6 {
|
|
| 262 |
+ newNSs = append(newNSs, addr) |
|
| 263 |
+ } |
|
| 264 |
+ } else {
|
|
| 265 |
+ // Extract this NS. Mark loopback addresses that did not come from an override as |
|
| 266 |
+ // 'HostLoopback'. Upstream requests for these servers will be made in the host's |
|
| 267 |
+ // network namespace. (So, '--dns 127.0.0.53' means use a nameserver listening on |
|
| 268 |
+ // the container's loopback interface. But, if the host's resolv.conf contains |
|
| 269 |
+ // 'nameserver 127.0.0.53', the host's resolver will be used.) |
|
| 270 |
+ // |
|
| 271 |
+ // TODO(robmry) - why only loopback addresses? |
|
| 272 |
+ // Addresses from the host's resolv.conf must be usable in the host's namespace, |
|
| 273 |
+ // and a lookup from the container's namespace is more expensive? And, for |
|
| 274 |
+ // example, if the host has a nameserver with an IPv6 LL address with a zone-id, |
|
| 275 |
+ // it won't work from the container's namespace (now, while the address is left in |
|
| 276 |
+ // the container's resolv.conf, or in future for the internal resolver). |
|
| 277 |
+ rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{
|
|
| 278 |
+ Addr: addr, |
|
| 279 |
+ HostLoopback: addr.IsLoopback() && !rc.md.NSOverride, |
|
| 280 |
+ }) |
|
| 281 |
+ } |
|
| 282 |
+ } |
|
| 283 |
+ rc.nameServers = newNSs |
|
| 284 |
+ |
|
| 285 |
+ // If there are no external nameservers, and the only nameserver left is the |
|
| 286 |
+ // internal resolver, use the defaults as ext nameservers. |
|
| 287 |
+ if len(rc.md.ExtNameServers) == 0 && len(rc.nameServers) == 1 {
|
|
| 288 |
+ log.G(context.TODO()).Info("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers")
|
|
| 289 |
+ for _, addr := range defaultNSAddrs(keepIPv6) {
|
|
| 290 |
+ rc.md.ExtNameServers = append(rc.md.ExtNameServers, ExtDNSEntry{Addr: addr})
|
|
| 291 |
+ } |
|
| 292 |
+ rc.md.UsedDefaultNS = true |
|
| 293 |
+ } |
|
| 294 |
+ |
|
| 295 |
+ // Validate the ndots option from host config or overrides, if present. |
|
| 296 |
+ // TODO(robmry) - pre-existing behaviour, but ... |
|
| 297 |
+ // Validating ndots from an override is good, but not-liking something in the |
|
| 298 |
+ // host's resolv.conf isn't a reason to fail - just remove? (And it'll be |
|
| 299 |
+ // replaced by the value in reqdOptions, if given.) |
|
| 300 |
+ if ndots, exists := rc.Option("ndots"); exists {
|
|
| 301 |
+ if n, err := strconv.Atoi(ndots); err != nil || n < 0 {
|
|
| 302 |
+ return nil, errdefs.InvalidParameter( |
|
| 303 |
+ fmt.Errorf("invalid number for ndots option: %v", ndots))
|
|
| 304 |
+ } |
|
| 305 |
+ } |
|
| 306 |
+ // For each option required by the nameserver, add it if not already |
|
| 307 |
+ // present (if the option already has a value don't change it). |
|
| 308 |
+ for _, opt := range reqdOptions {
|
|
| 309 |
+ optName, _, _ := strings.Cut(opt, ":") |
|
| 310 |
+ if _, exists := rc.Option(optName); !exists {
|
|
| 311 |
+ rc.AddOption(opt) |
|
| 312 |
+ } |
|
| 313 |
+ } |
|
| 314 |
+ |
|
| 315 |
+ rc.md.Transform = "internal resolver" |
|
| 316 |
+ return slices.Clone(rc.md.ExtNameServers), nil |
|
| 317 |
+} |
|
| 318 |
+ |
|
| 319 |
+// Generate returns content suitable for writing to a resolv.conf file. If comments |
|
| 320 |
+// is true, the file will include header information if supplied, and a trailing |
|
| 321 |
+// comment that describes how the file was constructed and lists external resolvers. |
|
| 322 |
+func (rc *ResolvConf) Generate(comments bool) ([]byte, error) {
|
|
| 323 |
+ s := struct {
|
|
| 324 |
+ Md *metadata |
|
| 325 |
+ NameServers []netip.Addr |
|
| 326 |
+ Search []string |
|
| 327 |
+ Options []string |
|
| 328 |
+ Other []string |
|
| 329 |
+ Overrides []string |
|
| 330 |
+ Comments bool |
|
| 331 |
+ }{
|
|
| 332 |
+ Md: &rc.md, |
|
| 333 |
+ NameServers: rc.nameServers, |
|
| 334 |
+ Search: rc.search, |
|
| 335 |
+ Options: rc.options, |
|
| 336 |
+ Other: rc.other, |
|
| 337 |
+ Comments: comments, |
|
| 338 |
+ } |
|
| 339 |
+ if rc.md.NSOverride {
|
|
| 340 |
+ s.Overrides = append(s.Overrides, "nameservers") |
|
| 341 |
+ } |
|
| 342 |
+ if rc.md.SearchOverride {
|
|
| 343 |
+ s.Overrides = append(s.Overrides, "search") |
|
| 344 |
+ } |
|
| 345 |
+ if rc.md.OptionsOverride {
|
|
| 346 |
+ s.Overrides = append(s.Overrides, "options") |
|
| 347 |
+ } |
|
| 348 |
+ |
|
| 349 |
+ const templateText = `{{if .Comments}}{{with .Md.Header}}{{.}}
|
|
| 350 |
+ |
|
| 351 |
+{{end}}{{end}}{{range .NameServers -}}
|
|
| 352 |
+nameserver {{.}}
|
|
| 353 |
+{{end}}{{with .Search -}}
|
|
| 354 |
+search {{join . " "}}
|
|
| 355 |
+{{end}}{{with .Options -}}
|
|
| 356 |
+options {{join . " "}}
|
|
| 357 |
+{{end}}{{with .Other -}}
|
|
| 358 |
+{{join . "\n"}}
|
|
| 359 |
+{{end}}{{if .Comments}}
|
|
| 360 |
+# Based on host file: '{{.Md.SourcePath}}'{{with .Md.Transform}} ({{.}}){{end}}
|
|
| 361 |
+{{if .Md.UsedDefaultNS -}}
|
|
| 362 |
+# Used default nameservers. |
|
| 363 |
+{{end -}}
|
|
| 364 |
+{{with .Md.ExtNameServers -}}
|
|
| 365 |
+# ExtServers: {{.}}
|
|
| 366 |
+{{end -}}
|
|
| 367 |
+{{with .Md.InvalidNSs -}}
|
|
| 368 |
+# Invalid nameservers: {{.}}
|
|
| 369 |
+{{end -}}
|
|
| 370 |
+# Overrides: {{.Overrides}}
|
|
| 371 |
+{{with .Md.NDotsFrom -}}
|
|
| 372 |
+# Option ndots from: {{.}}
|
|
| 373 |
+{{end -}}
|
|
| 374 |
+{{end -}}
|
|
| 375 |
+` |
|
| 376 |
+ |
|
| 377 |
+ funcs := template.FuncMap{"join": strings.Join}
|
|
| 378 |
+ var buf bytes.Buffer |
|
| 379 |
+ templ, err := template.New("summary").Funcs(funcs).Parse(templateText)
|
|
| 380 |
+ if err != nil {
|
|
| 381 |
+ return nil, errdefs.System(err) |
|
| 382 |
+ } |
|
| 383 |
+ if err := templ.Execute(&buf, s); err != nil {
|
|
| 384 |
+ return nil, errdefs.System(err) |
|
| 385 |
+ } |
|
| 386 |
+ return buf.Bytes(), nil |
|
| 387 |
+} |
|
| 388 |
+ |
|
| 389 |
+// WriteFile generates content and writes it to path. If hashPath is non-zero, it |
|
| 390 |
+// also writes a file containing a hash of the content, to enable UserModified() |
|
| 391 |
+// to determine whether the file has been modified. |
|
| 392 |
+func (rc *ResolvConf) WriteFile(path, hashPath string, perm os.FileMode) error {
|
|
| 393 |
+ content, err := rc.Generate(true) |
|
| 394 |
+ if err != nil {
|
|
| 395 |
+ return err |
|
| 396 |
+ } |
|
| 397 |
+ |
|
| 398 |
+ // Write the resolv.conf file - it's bind-mounted into the container, so can't |
|
| 399 |
+ // move a temp file into place, just have to truncate and write it. |
|
| 400 |
+ if err := os.WriteFile(path, content, perm); err != nil {
|
|
| 401 |
+ return errdefs.System(err) |
|
| 402 |
+ } |
|
| 403 |
+ |
|
| 404 |
+ // Write the hash file. |
|
| 405 |
+ if hashPath != "" {
|
|
| 406 |
+ hashFile, err := ioutils.NewAtomicFileWriter(hashPath, perm) |
|
| 407 |
+ if err != nil {
|
|
| 408 |
+ return errdefs.System(err) |
|
| 409 |
+ } |
|
| 410 |
+ defer hashFile.Close() |
|
| 411 |
+ |
|
| 412 |
+ digest := digest.FromBytes(content) |
|
| 413 |
+ if _, err = hashFile.Write([]byte(digest)); err != nil {
|
|
| 414 |
+ return err |
|
| 415 |
+ } |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ return nil |
|
| 419 |
+} |
|
| 420 |
+ |
|
| 421 |
+// UserModified can be used to determine whether the resolv.conf file has been |
|
| 422 |
+// modified since it was generated. It returns false with no error if the file |
|
| 423 |
+// matches the hash, true with no error if the file no longer matches the hash, |
|
| 424 |
+// and false with an error if the result cannot be determined. |
|
| 425 |
+func UserModified(rcPath, rcHashPath string) (bool, error) {
|
|
| 426 |
+ currRCHash, err := os.ReadFile(rcHashPath) |
|
| 427 |
+ if err != nil {
|
|
| 428 |
+ // If the hash file doesn't exist, can only assume it hasn't been written |
|
| 429 |
+ // yet (so, the user hasn't modified the file it hashes). |
|
| 430 |
+ if errors.Is(err, fs.ErrNotExist) {
|
|
| 431 |
+ return false, nil |
|
| 432 |
+ } |
|
| 433 |
+ return false, errors.Wrapf(err, "failed to read hash file %s", rcHashPath) |
|
| 434 |
+ } |
|
| 435 |
+ expected, err := digest.Parse(string(currRCHash)) |
|
| 436 |
+ if err != nil {
|
|
| 437 |
+ return false, errors.Wrapf(err, "failed to parse hash file %s", rcHashPath) |
|
| 438 |
+ } |
|
| 439 |
+ v := expected.Verifier() |
|
| 440 |
+ currRC, err := os.Open(rcPath) |
|
| 441 |
+ if err != nil {
|
|
| 442 |
+ return false, errors.Wrapf(err, "failed to open %s to check for modifications", rcPath) |
|
| 443 |
+ } |
|
| 444 |
+ defer currRC.Close() |
|
| 445 |
+ if _, err := io.Copy(v, currRC); err != nil {
|
|
| 446 |
+ return false, errors.Wrapf(err, "failed to hash %s to check for modifications", rcPath) |
|
| 447 |
+ } |
|
| 448 |
+ return !v.Verified(), nil |
|
| 449 |
+} |
|
| 450 |
+ |
|
| 451 |
+func (rc *ResolvConf) processLine(line string) {
|
|
| 452 |
+ fields := strings.Fields(line) |
|
| 453 |
+ |
|
| 454 |
+ // Strip comments. |
|
| 455 |
+ // TODO(robmry) - ignore comment chars except in column 0. |
|
| 456 |
+ // This preserves old behaviour, but it's wrong. For example, resolvers |
|
| 457 |
+ // will honour the option in line "options # ndots:0" (and ignore the |
|
| 458 |
+ // "#" as an unknown option). |
|
| 459 |
+ for i, s := range fields {
|
|
| 460 |
+ if s[0] == '#' || s[0] == ';' {
|
|
| 461 |
+ fields = fields[:i] |
|
| 462 |
+ break |
|
| 463 |
+ } |
|
| 464 |
+ } |
|
| 465 |
+ if len(fields) == 0 {
|
|
| 466 |
+ return |
|
| 467 |
+ } |
|
| 468 |
+ |
|
| 469 |
+ switch fields[0] {
|
|
| 470 |
+ case "nameserver": |
|
| 471 |
+ if len(fields) < 2 {
|
|
| 472 |
+ return |
|
| 473 |
+ } |
|
| 474 |
+ if addr, err := netip.ParseAddr(fields[1]); err != nil {
|
|
| 475 |
+ rc.md.InvalidNSs = append(rc.md.InvalidNSs, fields[1]) |
|
| 476 |
+ } else {
|
|
| 477 |
+ rc.nameServers = append(rc.nameServers, addr) |
|
| 478 |
+ } |
|
| 479 |
+ case "domain": |
|
| 480 |
+ // 'domain' is an obsolete name for 'search'. |
|
| 481 |
+ fallthrough |
|
| 482 |
+ case "search": |
|
| 483 |
+ if len(fields) < 2 {
|
|
| 484 |
+ return |
|
| 485 |
+ } |
|
| 486 |
+ // Only the last 'search' directive is used. |
|
| 487 |
+ rc.search = fields[1:] |
|
| 488 |
+ case "options": |
|
| 489 |
+ if len(fields) < 2 {
|
|
| 490 |
+ return |
|
| 491 |
+ } |
|
| 492 |
+ // Replace options from earlier directives. |
|
| 493 |
+ // TODO(robmry) - preserving incorrect behaviour, options should accumulate. |
|
| 494 |
+ // rc.options = append(rc.options, fields[1:]...) |
|
| 495 |
+ rc.options = fields[1:] |
|
| 496 |
+ default: |
|
| 497 |
+ // Copy anything that's not a recognised directive. |
|
| 498 |
+ rc.other = append(rc.other, line) |
|
| 499 |
+ } |
|
| 500 |
+} |
|
| 501 |
+ |
|
| 502 |
+func defaultNSAddrs(ipv6 bool) []netip.Addr {
|
|
| 503 |
+ var addrs []netip.Addr |
|
| 504 |
+ addrs = append(addrs, defaultIPv4NSs...) |
|
| 505 |
+ if ipv6 {
|
|
| 506 |
+ addrs = append(addrs, defaultIPv6NSs...) |
|
| 507 |
+ } |
|
| 508 |
+ return addrs |
|
| 509 |
+} |
| 0 | 510 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,56 @@ |
| 0 |
+package resolvconf |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "net/netip" |
|
| 5 |
+ "sync" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/containerd/log" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+const ( |
|
| 11 |
+ // defaultPath is the default path to the resolv.conf that contains information to resolve DNS. See Path(). |
|
| 12 |
+ defaultPath = "/etc/resolv.conf" |
|
| 13 |
+ // alternatePath is a path different from defaultPath, that may be used to resolve DNS. See Path(). |
|
| 14 |
+ alternatePath = "/run/systemd/resolve/resolv.conf" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+// For Path to detect systemd (only needed for legacy networking). |
|
| 18 |
+var ( |
|
| 19 |
+ detectSystemdResolvConfOnce sync.Once |
|
| 20 |
+ pathAfterSystemdDetection = defaultPath |
|
| 21 |
+) |
|
| 22 |
+ |
|
| 23 |
+// Path returns the path to the resolv.conf file that libnetwork should use. |
|
| 24 |
+// |
|
| 25 |
+// When /etc/resolv.conf contains 127.0.0.53 as the only nameserver, then |
|
| 26 |
+// it is assumed systemd-resolved manages DNS. Because inside the container 127.0.0.53 |
|
| 27 |
+// is not a valid DNS server, Path() returns /run/systemd/resolve/resolv.conf |
|
| 28 |
+// which is the resolv.conf that systemd-resolved generates and manages. |
|
| 29 |
+// Otherwise Path() returns /etc/resolv.conf. |
|
| 30 |
+// |
|
| 31 |
+// Errors are silenced as they will inevitably resurface at future open/read calls. |
|
| 32 |
+// |
|
| 33 |
+// More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf |
|
| 34 |
+// |
|
| 35 |
+// TODO(robmry) - alternatePath is only needed for legacy networking ... |
|
| 36 |
+// |
|
| 37 |
+// Host networking can use the host's resolv.conf as-is, and with an internal |
|
| 38 |
+// resolver it's also possible to use nameservers on the host's loopback |
|
| 39 |
+// interface. Once legacy networking is removed, this can always return |
|
| 40 |
+// defaultPath. |
|
| 41 |
+func Path() string {
|
|
| 42 |
+ detectSystemdResolvConfOnce.Do(func() {
|
|
| 43 |
+ rc, err := Load(defaultPath) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ // silencing error as it will resurface at next calls trying to read defaultPath |
|
| 46 |
+ return |
|
| 47 |
+ } |
|
| 48 |
+ ns := rc.nameServers |
|
| 49 |
+ if len(ns) == 1 && ns[0] == netip.MustParseAddr("127.0.0.53") {
|
|
| 50 |
+ pathAfterSystemdDetection = alternatePath |
|
| 51 |
+ log.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath)
|
|
| 52 |
+ } |
|
| 53 |
+ }) |
|
| 54 |
+ return pathAfterSystemdDetection |
|
| 55 |
+} |
| 0 | 56 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,577 @@ |
| 0 |
+package resolvconf |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "io/fs" |
|
| 5 |
+ "net/netip" |
|
| 6 |
+ "os" |
|
| 7 |
+ "path/filepath" |
|
| 8 |
+ "runtime" |
|
| 9 |
+ "strings" |
|
| 10 |
+ "testing" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/docker/docker/internal/sliceutil" |
|
| 13 |
+ "github.com/google/go-cmp/cmp/cmpopts" |
|
| 14 |
+ "gotest.tools/v3/assert" |
|
| 15 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 16 |
+ "gotest.tools/v3/golden" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+func TestRCOption(t *testing.T) {
|
|
| 20 |
+ testcases := []struct {
|
|
| 21 |
+ name string |
|
| 22 |
+ options string |
|
| 23 |
+ search string |
|
| 24 |
+ expFound bool |
|
| 25 |
+ expValue string |
|
| 26 |
+ }{
|
|
| 27 |
+ {
|
|
| 28 |
+ name: "Empty options", |
|
| 29 |
+ options: "", |
|
| 30 |
+ search: "ndots", |
|
| 31 |
+ }, |
|
| 32 |
+ {
|
|
| 33 |
+ name: "Not found", |
|
| 34 |
+ options: "ndots:0 edns0", |
|
| 35 |
+ search: "trust-ad", |
|
| 36 |
+ }, |
|
| 37 |
+ {
|
|
| 38 |
+ name: "Found with value", |
|
| 39 |
+ options: "ndots:0 edns0", |
|
| 40 |
+ search: "ndots", |
|
| 41 |
+ expFound: true, |
|
| 42 |
+ expValue: "0", |
|
| 43 |
+ }, |
|
| 44 |
+ {
|
|
| 45 |
+ name: "Found without value", |
|
| 46 |
+ options: "ndots:0 edns0", |
|
| 47 |
+ search: "edns0", |
|
| 48 |
+ expFound: true, |
|
| 49 |
+ expValue: "", |
|
| 50 |
+ }, |
|
| 51 |
+ {
|
|
| 52 |
+ name: "Found last value", |
|
| 53 |
+ options: "ndots:0 edns0 ndots:1", |
|
| 54 |
+ search: "ndots", |
|
| 55 |
+ expFound: true, |
|
| 56 |
+ expValue: "1", |
|
| 57 |
+ }, |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ for _, tc := range testcases {
|
|
| 61 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 62 |
+ rc, err := Parse(bytes.NewBuffer([]byte("options "+tc.options)), "")
|
|
| 63 |
+ assert.NilError(t, err) |
|
| 64 |
+ value, found := rc.Option(tc.search) |
|
| 65 |
+ assert.Check(t, is.Equal(found, tc.expFound)) |
|
| 66 |
+ assert.Check(t, is.Equal(value, tc.expValue)) |
|
| 67 |
+ }) |
|
| 68 |
+ } |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+func TestRCWrite(t *testing.T) {
|
|
| 72 |
+ testcases := []struct {
|
|
| 73 |
+ name string |
|
| 74 |
+ fileName string |
|
| 75 |
+ perm os.FileMode |
|
| 76 |
+ hashFileName string |
|
| 77 |
+ modify bool |
|
| 78 |
+ expUserModified bool |
|
| 79 |
+ }{
|
|
| 80 |
+ {
|
|
| 81 |
+ name: "Write with hash", |
|
| 82 |
+ fileName: "testfile", |
|
| 83 |
+ hashFileName: "testfile.hash", |
|
| 84 |
+ }, |
|
| 85 |
+ {
|
|
| 86 |
+ name: "Write with hash and modify", |
|
| 87 |
+ fileName: "testfile", |
|
| 88 |
+ hashFileName: "testfile.hash", |
|
| 89 |
+ modify: true, |
|
| 90 |
+ expUserModified: true, |
|
| 91 |
+ }, |
|
| 92 |
+ {
|
|
| 93 |
+ name: "Write without hash and modify", |
|
| 94 |
+ fileName: "testfile", |
|
| 95 |
+ modify: true, |
|
| 96 |
+ expUserModified: false, |
|
| 97 |
+ }, |
|
| 98 |
+ {
|
|
| 99 |
+ name: "Write perm", |
|
| 100 |
+ fileName: "testfile", |
|
| 101 |
+ perm: 0640, |
|
| 102 |
+ }, |
|
| 103 |
+ } |
|
| 104 |
+ |
|
| 105 |
+ rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4")), "")
|
|
| 106 |
+ assert.NilError(t, err) |
|
| 107 |
+ |
|
| 108 |
+ for _, tc := range testcases {
|
|
| 109 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 110 |
+ tc := tc |
|
| 111 |
+ d := t.TempDir() |
|
| 112 |
+ path := filepath.Join(d, tc.fileName) |
|
| 113 |
+ var hashPath string |
|
| 114 |
+ if tc.hashFileName != "" {
|
|
| 115 |
+ hashPath = filepath.Join(d, tc.hashFileName) |
|
| 116 |
+ } |
|
| 117 |
+ if tc.perm == 0 {
|
|
| 118 |
+ tc.perm = 0644 |
|
| 119 |
+ } |
|
| 120 |
+ err := rc.WriteFile(path, hashPath, tc.perm) |
|
| 121 |
+ assert.NilError(t, err) |
|
| 122 |
+ |
|
| 123 |
+ fi, err := os.Stat(path) |
|
| 124 |
+ assert.NilError(t, err) |
|
| 125 |
+ // Windows files won't have the expected perms. |
|
| 126 |
+ if runtime.GOOS != "windows" {
|
|
| 127 |
+ assert.Check(t, is.Equal(fi.Mode(), tc.perm)) |
|
| 128 |
+ } |
|
| 129 |
+ |
|
| 130 |
+ if tc.modify {
|
|
| 131 |
+ err := os.WriteFile(path, []byte("modified"), 0644)
|
|
| 132 |
+ assert.NilError(t, err) |
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ um, err := UserModified(path, hashPath) |
|
| 136 |
+ assert.NilError(t, err) |
|
| 137 |
+ assert.Check(t, is.Equal(um, tc.expUserModified)) |
|
| 138 |
+ }) |
|
| 139 |
+ } |
|
| 140 |
+} |
|
| 141 |
+ |
|
| 142 |
+var a2s = sliceutil.Mapper(netip.Addr.String) |
|
| 143 |
+var s2a = sliceutil.Mapper(netip.MustParseAddr) |
|
| 144 |
+ |
|
| 145 |
+// Test that a resolv.conf file can be modified using OverrideXXX() methods |
|
| 146 |
+// to modify nameservers/search/options directives, and tha options can be |
|
| 147 |
+// added via AddOption(). |
|
| 148 |
+func TestRCModify(t *testing.T) {
|
|
| 149 |
+ testcases := []struct {
|
|
| 150 |
+ name string |
|
| 151 |
+ inputNS []string |
|
| 152 |
+ inputSearch []string |
|
| 153 |
+ inputOptions []string |
|
| 154 |
+ noOverrides bool // Whether to apply overrides (empty lists are valid overrides). |
|
| 155 |
+ overrideNS []string |
|
| 156 |
+ overrideSearch []string |
|
| 157 |
+ overrideOptions []string |
|
| 158 |
+ addOption string |
|
| 159 |
+ }{
|
|
| 160 |
+ {
|
|
| 161 |
+ name: "No content no overrides", |
|
| 162 |
+ inputNS: []string{},
|
|
| 163 |
+ }, |
|
| 164 |
+ {
|
|
| 165 |
+ name: "No overrides", |
|
| 166 |
+ noOverrides: true, |
|
| 167 |
+ inputNS: []string{"1.2.3.4"},
|
|
| 168 |
+ inputSearch: []string{"invalid"},
|
|
| 169 |
+ inputOptions: []string{"ndots:0"},
|
|
| 170 |
+ }, |
|
| 171 |
+ {
|
|
| 172 |
+ name: "Empty overrides", |
|
| 173 |
+ inputNS: []string{"1.2.3.4"},
|
|
| 174 |
+ inputSearch: []string{"invalid"},
|
|
| 175 |
+ inputOptions: []string{"ndots:0"},
|
|
| 176 |
+ }, |
|
| 177 |
+ {
|
|
| 178 |
+ name: "Overrides", |
|
| 179 |
+ inputNS: []string{"1.2.3.4"},
|
|
| 180 |
+ inputSearch: []string{"invalid"},
|
|
| 181 |
+ inputOptions: []string{"ndots:0"},
|
|
| 182 |
+ overrideNS: []string{"2.3.4.5", "fdba:acdd:587c::53"},
|
|
| 183 |
+ overrideSearch: []string{"com", "invalid", "example"},
|
|
| 184 |
+ overrideOptions: []string{"ndots:1", "edns0", "trust-ad"},
|
|
| 185 |
+ }, |
|
| 186 |
+ {
|
|
| 187 |
+ name: "Add option no overrides", |
|
| 188 |
+ noOverrides: true, |
|
| 189 |
+ inputNS: []string{"1.2.3.4"},
|
|
| 190 |
+ inputSearch: []string{"invalid"},
|
|
| 191 |
+ inputOptions: []string{"ndots:0"},
|
|
| 192 |
+ addOption: "attempts:3", |
|
| 193 |
+ }, |
|
| 194 |
+ } |
|
| 195 |
+ |
|
| 196 |
+ for _, tc := range testcases {
|
|
| 197 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 198 |
+ tc := tc |
|
| 199 |
+ var input string |
|
| 200 |
+ if len(tc.inputNS) != 0 {
|
|
| 201 |
+ for _, ns := range tc.inputNS {
|
|
| 202 |
+ input += "nameserver " + ns + "\n" |
|
| 203 |
+ } |
|
| 204 |
+ } |
|
| 205 |
+ if len(tc.inputSearch) != 0 {
|
|
| 206 |
+ input += "search " + strings.Join(tc.inputSearch, " ") + "\n" |
|
| 207 |
+ } |
|
| 208 |
+ if len(tc.inputOptions) != 0 {
|
|
| 209 |
+ input += "options " + strings.Join(tc.inputOptions, " ") + "\n" |
|
| 210 |
+ } |
|
| 211 |
+ rc, err := Parse(bytes.NewBuffer([]byte(input)), "") |
|
| 212 |
+ assert.NilError(t, err) |
|
| 213 |
+ assert.Check(t, is.DeepEqual(a2s(rc.NameServers()), tc.inputNS)) |
|
| 214 |
+ assert.Check(t, is.DeepEqual(rc.Search(), tc.inputSearch)) |
|
| 215 |
+ assert.Check(t, is.DeepEqual(rc.Options(), tc.inputOptions)) |
|
| 216 |
+ |
|
| 217 |
+ if !tc.noOverrides {
|
|
| 218 |
+ overrideNS := s2a(tc.overrideNS) |
|
| 219 |
+ rc.OverrideNameServers(overrideNS) |
|
| 220 |
+ rc.OverrideSearch(tc.overrideSearch) |
|
| 221 |
+ rc.OverrideOptions(tc.overrideOptions) |
|
| 222 |
+ |
|
| 223 |
+ assert.Check(t, is.DeepEqual(rc.NameServers(), overrideNS, cmpopts.EquateComparable(netip.Addr{})))
|
|
| 224 |
+ assert.Check(t, is.DeepEqual(rc.Search(), tc.overrideSearch)) |
|
| 225 |
+ assert.Check(t, is.DeepEqual(rc.Options(), tc.overrideOptions)) |
|
| 226 |
+ } |
|
| 227 |
+ |
|
| 228 |
+ if tc.addOption != "" {
|
|
| 229 |
+ options := rc.Options() |
|
| 230 |
+ rc.AddOption(tc.addOption) |
|
| 231 |
+ assert.Check(t, is.DeepEqual(rc.Options(), append(options, tc.addOption))) |
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ d := t.TempDir() |
|
| 235 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 236 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 237 |
+ assert.NilError(t, err) |
|
| 238 |
+ |
|
| 239 |
+ content, err := os.ReadFile(path) |
|
| 240 |
+ assert.NilError(t, err) |
|
| 241 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 242 |
+ }) |
|
| 243 |
+ } |
|
| 244 |
+} |
|
| 245 |
+ |
|
| 246 |
+func TestRCTransformForLegacyNw(t *testing.T) {
|
|
| 247 |
+ testcases := []struct {
|
|
| 248 |
+ name string |
|
| 249 |
+ input string |
|
| 250 |
+ ipv6 bool |
|
| 251 |
+ overrideNS []string |
|
| 252 |
+ }{
|
|
| 253 |
+ {
|
|
| 254 |
+ name: "Routable IPv4 only", |
|
| 255 |
+ input: "nameserver 10.0.0.1", |
|
| 256 |
+ }, |
|
| 257 |
+ {
|
|
| 258 |
+ name: "Routable IPv4 and IPv6, ipv6 enabled", |
|
| 259 |
+ input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", |
|
| 260 |
+ ipv6: true, |
|
| 261 |
+ }, |
|
| 262 |
+ {
|
|
| 263 |
+ name: "Routable IPv4 and IPv6, ipv6 disabled", |
|
| 264 |
+ input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", |
|
| 265 |
+ ipv6: false, |
|
| 266 |
+ }, |
|
| 267 |
+ {
|
|
| 268 |
+ name: "IPv4 localhost, ipv6 disabled", |
|
| 269 |
+ input: "nameserver 127.0.0.53", |
|
| 270 |
+ ipv6: false, |
|
| 271 |
+ }, |
|
| 272 |
+ {
|
|
| 273 |
+ name: "IPv4 localhost, ipv6 enabled", |
|
| 274 |
+ input: "nameserver 127.0.0.53", |
|
| 275 |
+ ipv6: true, |
|
| 276 |
+ }, |
|
| 277 |
+ {
|
|
| 278 |
+ name: "IPv4 and IPv6 localhost, ipv6 disabled", |
|
| 279 |
+ input: "nameserver 127.0.0.53\nnameserver ::1", |
|
| 280 |
+ ipv6: false, |
|
| 281 |
+ }, |
|
| 282 |
+ {
|
|
| 283 |
+ name: "IPv4 and IPv6 localhost, ipv6 enabled", |
|
| 284 |
+ input: "nameserver 127.0.0.53\nnameserver ::1", |
|
| 285 |
+ ipv6: true, |
|
| 286 |
+ }, |
|
| 287 |
+ {
|
|
| 288 |
+ name: "IPv4 localhost, IPv6 routeable, ipv6 enabled", |
|
| 289 |
+ input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", |
|
| 290 |
+ ipv6: true, |
|
| 291 |
+ }, |
|
| 292 |
+ {
|
|
| 293 |
+ name: "IPv4 localhost, IPv6 routeable, ipv6 disabled", |
|
| 294 |
+ input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", |
|
| 295 |
+ ipv6: false, |
|
| 296 |
+ }, |
|
| 297 |
+ {
|
|
| 298 |
+ name: "Override nameservers", |
|
| 299 |
+ input: "nameserver 127.0.0.53", |
|
| 300 |
+ overrideNS: []string{"127.0.0.1", "::1"},
|
|
| 301 |
+ ipv6: false, |
|
| 302 |
+ }, |
|
| 303 |
+ } |
|
| 304 |
+ |
|
| 305 |
+ for _, tc := range testcases {
|
|
| 306 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 307 |
+ tc := tc |
|
| 308 |
+ rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf") |
|
| 309 |
+ assert.NilError(t, err) |
|
| 310 |
+ if tc.overrideNS != nil {
|
|
| 311 |
+ rc.OverrideNameServers(s2a(tc.overrideNS)) |
|
| 312 |
+ } |
|
| 313 |
+ |
|
| 314 |
+ rc.TransformForLegacyNw(tc.ipv6) |
|
| 315 |
+ |
|
| 316 |
+ d := t.TempDir() |
|
| 317 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 318 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 319 |
+ assert.NilError(t, err) |
|
| 320 |
+ |
|
| 321 |
+ content, err := os.ReadFile(path) |
|
| 322 |
+ assert.NilError(t, err) |
|
| 323 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 324 |
+ }) |
|
| 325 |
+ } |
|
| 326 |
+} |
|
| 327 |
+ |
|
| 328 |
+func TestRCTransformForIntNS(t *testing.T) {
|
|
| 329 |
+ mke := func(addr string, hostLoopback bool) ExtDNSEntry {
|
|
| 330 |
+ return ExtDNSEntry{
|
|
| 331 |
+ Addr: netip.MustParseAddr(addr), |
|
| 332 |
+ HostLoopback: hostLoopback, |
|
| 333 |
+ } |
|
| 334 |
+ } |
|
| 335 |
+ |
|
| 336 |
+ testcases := []struct {
|
|
| 337 |
+ name string |
|
| 338 |
+ input string |
|
| 339 |
+ intNameServer string |
|
| 340 |
+ ipv6 bool |
|
| 341 |
+ overrideNS []string |
|
| 342 |
+ overrideOptions []string |
|
| 343 |
+ reqdOptions []string |
|
| 344 |
+ expExtServers []ExtDNSEntry |
|
| 345 |
+ expErr string |
|
| 346 |
+ }{
|
|
| 347 |
+ {
|
|
| 348 |
+ name: "IPv4 only", |
|
| 349 |
+ input: "nameserver 10.0.0.1", |
|
| 350 |
+ expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
|
|
| 351 |
+ }, |
|
| 352 |
+ {
|
|
| 353 |
+ name: "IPv4 and IPv6, ipv6 enabled", |
|
| 354 |
+ input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", |
|
| 355 |
+ ipv6: true, |
|
| 356 |
+ expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
|
|
| 357 |
+ }, |
|
| 358 |
+ {
|
|
| 359 |
+ name: "IPv4 and IPv6, ipv6 disabled", |
|
| 360 |
+ input: "nameserver 10.0.0.1\nnameserver fdb6:b8fe:b528::1", |
|
| 361 |
+ ipv6: false, |
|
| 362 |
+ expExtServers: []ExtDNSEntry{mke("10.0.0.1", false)},
|
|
| 363 |
+ }, |
|
| 364 |
+ {
|
|
| 365 |
+ name: "IPv4 localhost", |
|
| 366 |
+ input: "nameserver 127.0.0.53", |
|
| 367 |
+ ipv6: false, |
|
| 368 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 369 |
+ }, |
|
| 370 |
+ {
|
|
| 371 |
+ // Overriding the nameserver with a localhost address means use the container's |
|
| 372 |
+ // loopback interface, not the host's. |
|
| 373 |
+ name: "IPv4 localhost override", |
|
| 374 |
+ input: "nameserver 10.0.0.1", |
|
| 375 |
+ ipv6: false, |
|
| 376 |
+ overrideNS: []string{"127.0.0.53"},
|
|
| 377 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", false)},
|
|
| 378 |
+ }, |
|
| 379 |
+ {
|
|
| 380 |
+ name: "IPv4 localhost, ipv6 enabled", |
|
| 381 |
+ input: "nameserver 127.0.0.53", |
|
| 382 |
+ ipv6: true, |
|
| 383 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 384 |
+ }, |
|
| 385 |
+ {
|
|
| 386 |
+ name: "IPv6 addr, IPv6 enabled", |
|
| 387 |
+ input: "nameserver fd14:6e0e:f855::1", |
|
| 388 |
+ ipv6: true, |
|
| 389 |
+ // Note that there are no ext servers in this case, the internal resolver |
|
| 390 |
+ // will only look up container names. The default nameservers aren't added |
|
| 391 |
+ // because the host's IPv6 nameserver remains in the container's resolv.conf, |
|
| 392 |
+ // (because only IPv4 ext servers are currently allowed). |
|
| 393 |
+ }, |
|
| 394 |
+ {
|
|
| 395 |
+ name: "IPv4 and IPv6 localhost, IPv6 disabled", |
|
| 396 |
+ input: "nameserver 127.0.0.53\nnameserver ::1", |
|
| 397 |
+ ipv6: false, |
|
| 398 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 399 |
+ }, |
|
| 400 |
+ {
|
|
| 401 |
+ name: "IPv4 and IPv6 localhost, ipv6 enabled", |
|
| 402 |
+ input: "nameserver 127.0.0.53\nnameserver ::1", |
|
| 403 |
+ ipv6: true, |
|
| 404 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 405 |
+ }, |
|
| 406 |
+ {
|
|
| 407 |
+ name: "IPv4 localhost, IPv6 private, IPv6 enabled", |
|
| 408 |
+ input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", |
|
| 409 |
+ ipv6: true, |
|
| 410 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 411 |
+ }, |
|
| 412 |
+ {
|
|
| 413 |
+ name: "IPv4 localhost, IPv6 private, IPv6 disabled", |
|
| 414 |
+ input: "nameserver 127.0.0.53\nnameserver fd3e:2d1a:1f5a::1", |
|
| 415 |
+ ipv6: false, |
|
| 416 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 417 |
+ }, |
|
| 418 |
+ {
|
|
| 419 |
+ name: "No host nameserver, no iv6", |
|
| 420 |
+ input: "", |
|
| 421 |
+ ipv6: false, |
|
| 422 |
+ expExtServers: []ExtDNSEntry{
|
|
| 423 |
+ mke("8.8.8.8", false),
|
|
| 424 |
+ mke("8.8.4.4", false),
|
|
| 425 |
+ }, |
|
| 426 |
+ }, |
|
| 427 |
+ {
|
|
| 428 |
+ name: "No host nameserver, iv6", |
|
| 429 |
+ input: "", |
|
| 430 |
+ ipv6: true, |
|
| 431 |
+ expExtServers: []ExtDNSEntry{
|
|
| 432 |
+ mke("8.8.8.8", false),
|
|
| 433 |
+ mke("8.8.4.4", false),
|
|
| 434 |
+ mke("2001:4860:4860::8888", false),
|
|
| 435 |
+ mke("2001:4860:4860::8844", false),
|
|
| 436 |
+ }, |
|
| 437 |
+ }, |
|
| 438 |
+ {
|
|
| 439 |
+ name: "ndots present and required", |
|
| 440 |
+ input: "nameserver 127.0.0.53\noptions ndots:1", |
|
| 441 |
+ reqdOptions: []string{"ndots:0"},
|
|
| 442 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 443 |
+ }, |
|
| 444 |
+ {
|
|
| 445 |
+ name: "ndots missing but required", |
|
| 446 |
+ input: "nameserver 127.0.0.53", |
|
| 447 |
+ reqdOptions: []string{"ndots:0"},
|
|
| 448 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 449 |
+ }, |
|
| 450 |
+ {
|
|
| 451 |
+ name: "ndots host, override and required", |
|
| 452 |
+ input: "nameserver 127.0.0.53", |
|
| 453 |
+ reqdOptions: []string{"ndots:0"},
|
|
| 454 |
+ overrideOptions: []string{"ndots:2"},
|
|
| 455 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 456 |
+ }, |
|
| 457 |
+ {
|
|
| 458 |
+ name: "Extra required options", |
|
| 459 |
+ input: "nameserver 127.0.0.53\noptions trust-ad", |
|
| 460 |
+ reqdOptions: []string{"ndots:0", "attempts:3", "edns0", "trust-ad"},
|
|
| 461 |
+ expExtServers: []ExtDNSEntry{mke("127.0.0.53", true)},
|
|
| 462 |
+ }, |
|
| 463 |
+ } |
|
| 464 |
+ |
|
| 465 |
+ for _, tc := range testcases {
|
|
| 466 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 467 |
+ tc := tc |
|
| 468 |
+ rc, err := Parse(bytes.NewBuffer([]byte(tc.input)), "/etc/resolv.conf") |
|
| 469 |
+ assert.NilError(t, err) |
|
| 470 |
+ |
|
| 471 |
+ if tc.intNameServer == "" {
|
|
| 472 |
+ tc.intNameServer = "127.0.0.11" |
|
| 473 |
+ } |
|
| 474 |
+ if len(tc.overrideNS) > 0 {
|
|
| 475 |
+ rc.OverrideNameServers(s2a(tc.overrideNS)) |
|
| 476 |
+ } |
|
| 477 |
+ if len(tc.overrideOptions) > 0 {
|
|
| 478 |
+ rc.OverrideOptions(tc.overrideOptions) |
|
| 479 |
+ } |
|
| 480 |
+ intNS := netip.MustParseAddr(tc.intNameServer) |
|
| 481 |
+ extNameServers, err := rc.TransformForIntNS(tc.ipv6, intNS, tc.reqdOptions) |
|
| 482 |
+ if tc.expErr != "" {
|
|
| 483 |
+ assert.Check(t, is.ErrorContains(err, tc.expErr)) |
|
| 484 |
+ return |
|
| 485 |
+ } |
|
| 486 |
+ assert.NilError(t, err) |
|
| 487 |
+ |
|
| 488 |
+ d := t.TempDir() |
|
| 489 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 490 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 491 |
+ assert.NilError(t, err) |
|
| 492 |
+ |
|
| 493 |
+ content, err := os.ReadFile(path) |
|
| 494 |
+ assert.NilError(t, err) |
|
| 495 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 496 |
+ assert.Check(t, is.DeepEqual(extNameServers, tc.expExtServers, |
|
| 497 |
+ cmpopts.EquateComparable(netip.Addr{})))
|
|
| 498 |
+ }) |
|
| 499 |
+ } |
|
| 500 |
+} |
|
| 501 |
+ |
|
| 502 |
+func TestRCRead(t *testing.T) {
|
|
| 503 |
+ d := t.TempDir() |
|
| 504 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 505 |
+ |
|
| 506 |
+ // Try to read a nonexistent file, equivalent to an empty file. |
|
| 507 |
+ _, err := Load(path) |
|
| 508 |
+ assert.Check(t, is.ErrorIs(err, fs.ErrNotExist)) |
|
| 509 |
+ |
|
| 510 |
+ err = os.WriteFile(path, []byte("options edns0"), 0644)
|
|
| 511 |
+ assert.NilError(t, err) |
|
| 512 |
+ |
|
| 513 |
+ // Read that file in the constructor. |
|
| 514 |
+ rc, err := Load(path) |
|
| 515 |
+ assert.NilError(t, err) |
|
| 516 |
+ assert.Check(t, is.DeepEqual(rc.Options(), []string{"edns0"}))
|
|
| 517 |
+ |
|
| 518 |
+ // Pass in an os.File, check the path is extracted. |
|
| 519 |
+ file, err := os.Open(path) |
|
| 520 |
+ assert.NilError(t, err) |
|
| 521 |
+ defer file.Close() |
|
| 522 |
+ rc, err = Parse(file, "") |
|
| 523 |
+ assert.NilError(t, err) |
|
| 524 |
+ assert.Check(t, is.Equal(rc.md.SourcePath, path)) |
|
| 525 |
+} |
|
| 526 |
+ |
|
| 527 |
+func TestRCInvalidNS(t *testing.T) {
|
|
| 528 |
+ d := t.TempDir() |
|
| 529 |
+ |
|
| 530 |
+ // A resolv.conf with an invalid nameserver address. |
|
| 531 |
+ rc, err := Parse(bytes.NewBuffer([]byte("nameserver 1.2.3.4.5")), "")
|
|
| 532 |
+ assert.NilError(t, err) |
|
| 533 |
+ |
|
| 534 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 535 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 536 |
+ assert.NilError(t, err) |
|
| 537 |
+ |
|
| 538 |
+ content, err := os.ReadFile(path) |
|
| 539 |
+ assert.NilError(t, err) |
|
| 540 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 541 |
+} |
|
| 542 |
+ |
|
| 543 |
+func TestRCSetHeader(t *testing.T) {
|
|
| 544 |
+ rc, err := Parse(bytes.NewBuffer([]byte("nameserver 127.0.0.53")), "/etc/resolv.conf")
|
|
| 545 |
+ assert.NilError(t, err) |
|
| 546 |
+ |
|
| 547 |
+ rc.SetHeader("# This is a comment.")
|
|
| 548 |
+ d := t.TempDir() |
|
| 549 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 550 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 551 |
+ assert.NilError(t, err) |
|
| 552 |
+ |
|
| 553 |
+ content, err := os.ReadFile(path) |
|
| 554 |
+ assert.NilError(t, err) |
|
| 555 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 556 |
+} |
|
| 557 |
+ |
|
| 558 |
+func TestRCUnknownDirectives(t *testing.T) {
|
|
| 559 |
+ const input = ` |
|
| 560 |
+something unexpected |
|
| 561 |
+nameserver 127.0.0.53 |
|
| 562 |
+options ndots:1 |
|
| 563 |
+unrecognised thing |
|
| 564 |
+` |
|
| 565 |
+ rc, err := Parse(bytes.NewBuffer([]byte(input)), "/etc/resolv.conf") |
|
| 566 |
+ assert.NilError(t, err) |
|
| 567 |
+ |
|
| 568 |
+ d := t.TempDir() |
|
| 569 |
+ path := filepath.Join(d, "resolv.conf") |
|
| 570 |
+ err = rc.WriteFile(path, "", 0644) |
|
| 571 |
+ assert.NilError(t, err) |
|
| 572 |
+ |
|
| 573 |
+ content, err := os.ReadFile(path) |
|
| 574 |
+ assert.NilError(t, err) |
|
| 575 |
+ assert.Check(t, golden.String(string(content), t.Name()+".golden")) |
|
| 576 |
+} |
| ... | ... |
@@ -11,6 +11,7 @@ import ( |
| 11 | 11 |
"os" |
| 12 | 12 |
"os/exec" |
| 13 | 13 |
"path/filepath" |
| 14 |
+ "regexp" |
|
| 14 | 15 |
"strings" |
| 15 | 16 |
"sync" |
| 16 | 17 |
"testing" |
| ... | ... |
@@ -32,6 +33,8 @@ import ( |
| 32 | 32 |
"github.com/vishvananda/netlink" |
| 33 | 33 |
"github.com/vishvananda/netns" |
| 34 | 34 |
"golang.org/x/sync/errgroup" |
| 35 |
+ "gotest.tools/v3/assert" |
|
| 36 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 35 | 37 |
) |
| 36 | 38 |
|
| 37 | 39 |
const ( |
| ... | ... |
@@ -1278,6 +1281,28 @@ func makeTesthostNetwork(t *testing.T, c *libnetwork.Controller) *libnetwork.Net |
| 1278 | 1278 |
return n |
| 1279 | 1279 |
} |
| 1280 | 1280 |
|
| 1281 |
+func makeTestIPv6Network(t *testing.T, c *libnetwork.Controller) *libnetwork.Network {
|
|
| 1282 |
+ t.Helper() |
|
| 1283 |
+ netOptions := options.Generic{
|
|
| 1284 |
+ netlabel.EnableIPv6: true, |
|
| 1285 |
+ netlabel.GenericData: options.Generic{
|
|
| 1286 |
+ "BridgeName": "testnetwork", |
|
| 1287 |
+ }, |
|
| 1288 |
+ } |
|
| 1289 |
+ ipamV6ConfList := []*libnetwork.IpamConf{
|
|
| 1290 |
+ {PreferredPool: "fd81:fb6e:38ba:abcd::/64", Gateway: "fd81:fb6e:38ba:abcd::9"},
|
|
| 1291 |
+ } |
|
| 1292 |
+ n, err := createTestNetwork(c, |
|
| 1293 |
+ "bridge", |
|
| 1294 |
+ "testnetwork", |
|
| 1295 |
+ netOptions, |
|
| 1296 |
+ nil, |
|
| 1297 |
+ ipamV6ConfList, |
|
| 1298 |
+ ) |
|
| 1299 |
+ assert.NilError(t, err) |
|
| 1300 |
+ return n |
|
| 1301 |
+} |
|
| 1302 |
+ |
|
| 1281 | 1303 |
func TestHost(t *testing.T) {
|
| 1282 | 1304 |
defer netnsutils.SetupTestOSContext(t)() |
| 1283 | 1305 |
controller := newController(t) |
| ... | ... |
@@ -1790,295 +1815,92 @@ func reexecSetKey(key string, containerID string, controllerID string) error {
|
| 1790 | 1790 |
return cmd.Run() |
| 1791 | 1791 |
} |
| 1792 | 1792 |
|
| 1793 |
-func TestEnableIPv6(t *testing.T) {
|
|
| 1794 |
- defer netnsutils.SetupTestOSContext(t)() |
|
| 1795 |
- controller := newController(t) |
|
| 1796 |
- |
|
| 1797 |
- tmpResolvConf := []byte("search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n")
|
|
| 1798 |
- expectedResolvConf := []byte("search pommesfrites.fr\nnameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\noptions ndots:0\n")
|
|
| 1799 |
- // take a copy of resolv.conf for restoring after test completes |
|
| 1800 |
- resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
|
|
| 1801 |
- if err != nil {
|
|
| 1802 |
- t.Fatal(err) |
|
| 1803 |
- } |
|
| 1804 |
- // cleanup |
|
| 1805 |
- defer func() {
|
|
| 1806 |
- if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
|
|
| 1807 |
- t.Fatal(err) |
|
| 1808 |
- } |
|
| 1809 |
- }() |
|
| 1810 |
- |
|
| 1811 |
- netOption := options.Generic{
|
|
| 1812 |
- netlabel.EnableIPv6: true, |
|
| 1813 |
- netlabel.GenericData: options.Generic{
|
|
| 1814 |
- "BridgeName": "testnetwork", |
|
| 1815 |
- }, |
|
| 1816 |
- } |
|
| 1817 |
- ipamV6ConfList := []*libnetwork.IpamConf{{PreferredPool: "fe99::/64", Gateway: "fe99::9"}}
|
|
| 1818 |
- |
|
| 1819 |
- n, err := createTestNetwork(controller, "bridge", "testnetwork", netOption, nil, ipamV6ConfList) |
|
| 1820 |
- if err != nil {
|
|
| 1821 |
- t.Fatal(err) |
|
| 1822 |
- } |
|
| 1823 |
- defer func() {
|
|
| 1824 |
- if err := n.Delete(); err != nil {
|
|
| 1825 |
- t.Fatal(err) |
|
| 1826 |
- } |
|
| 1827 |
- }() |
|
| 1828 |
- |
|
| 1829 |
- ep1, err := n.CreateEndpoint("ep1")
|
|
| 1830 |
- if err != nil {
|
|
| 1831 |
- t.Fatal(err) |
|
| 1832 |
- } |
|
| 1833 |
- |
|
| 1834 |
- if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf, 0o644); err != nil {
|
|
| 1835 |
- t.Fatal(err) |
|
| 1836 |
- } |
|
| 1837 |
- |
|
| 1838 |
- resolvConfPath := "/tmp/libnetwork_test/resolv.conf" |
|
| 1839 |
- defer os.Remove(resolvConfPath) |
|
| 1840 |
- |
|
| 1841 |
- sb, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath)) |
|
| 1842 |
- if err != nil {
|
|
| 1843 |
- t.Fatal(err) |
|
| 1844 |
- } |
|
| 1845 |
- defer func() {
|
|
| 1846 |
- if err := sb.Delete(); err != nil {
|
|
| 1847 |
- t.Fatal(err) |
|
| 1848 |
- } |
|
| 1849 |
- }() |
|
| 1850 |
- |
|
| 1851 |
- err = ep1.Join(sb) |
|
| 1852 |
- if err != nil {
|
|
| 1853 |
- t.Fatal(err) |
|
| 1854 |
- } |
|
| 1855 |
- |
|
| 1856 |
- content, err := os.ReadFile(resolvConfPath) |
|
| 1857 |
- if err != nil {
|
|
| 1858 |
- t.Fatal(err) |
|
| 1859 |
- } |
|
| 1860 |
- |
|
| 1861 |
- if !bytes.Equal(content, expectedResolvConf) {
|
|
| 1862 |
- t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf), string(content))
|
|
| 1863 |
- } |
|
| 1864 |
- |
|
| 1865 |
- if err != nil {
|
|
| 1866 |
- t.Fatal(err) |
|
| 1867 |
- } |
|
| 1868 |
-} |
|
| 1869 |
- |
|
| 1870 |
-func TestResolvConfHost(t *testing.T) {
|
|
| 1871 |
- defer netnsutils.SetupTestOSContext(t)() |
|
| 1872 |
- controller := newController(t) |
|
| 1873 |
- |
|
| 1874 |
- tmpResolvConf := []byte("search localhost.net\nnameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\n")
|
|
| 1875 |
- |
|
| 1876 |
- // take a copy of resolv.conf for restoring after test completes |
|
| 1877 |
- resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
|
|
| 1878 |
- if err != nil {
|
|
| 1879 |
- t.Fatal(err) |
|
| 1880 |
- } |
|
| 1881 |
- // cleanup |
|
| 1882 |
- defer func() {
|
|
| 1883 |
- if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
|
|
| 1884 |
- t.Fatal(err) |
|
| 1885 |
- } |
|
| 1886 |
- }() |
|
| 1887 |
- |
|
| 1888 |
- n := makeTesthostNetwork(t, controller) |
|
| 1889 |
- ep1, err := n.CreateEndpoint("ep1", libnetwork.CreateOptionDisableResolution())
|
|
| 1890 |
- if err != nil {
|
|
| 1891 |
- t.Fatal(err) |
|
| 1892 |
- } |
|
| 1893 |
- |
|
| 1894 |
- if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf, 0o644); err != nil {
|
|
| 1895 |
- t.Fatal(err) |
|
| 1896 |
- } |
|
| 1897 |
- |
|
| 1898 |
- resolvConfPath := "/tmp/libnetwork_test/resolv.conf" |
|
| 1899 |
- defer os.Remove(resolvConfPath) |
|
| 1900 |
- |
|
| 1901 |
- sb, err := controller.NewSandbox(containerID, |
|
| 1902 |
- libnetwork.OptionUseDefaultSandbox(), |
|
| 1903 |
- libnetwork.OptionResolvConfPath(resolvConfPath), |
|
| 1904 |
- libnetwork.OptionOriginResolvConfPath("/etc/resolv.conf"))
|
|
| 1905 |
- if err != nil {
|
|
| 1906 |
- t.Fatal(err) |
|
| 1907 |
- } |
|
| 1908 |
- defer func() {
|
|
| 1909 |
- if err := sb.Delete(); err != nil {
|
|
| 1910 |
- t.Fatal(err) |
|
| 1911 |
- } |
|
| 1912 |
- }() |
|
| 1913 |
- |
|
| 1914 |
- err = ep1.Join(sb) |
|
| 1915 |
- if err != nil {
|
|
| 1916 |
- t.Fatal(err) |
|
| 1917 |
- } |
|
| 1918 |
- defer func() {
|
|
| 1919 |
- err = ep1.Leave(sb) |
|
| 1920 |
- if err != nil {
|
|
| 1921 |
- t.Fatal(err) |
|
| 1922 |
- } |
|
| 1923 |
- }() |
|
| 1924 |
- |
|
| 1925 |
- finfo, err := os.Stat(resolvConfPath) |
|
| 1926 |
- if err != nil {
|
|
| 1927 |
- t.Fatal(err) |
|
| 1928 |
- } |
|
| 1929 |
- |
|
| 1930 |
- fmode := (os.FileMode)(0o644) |
|
| 1931 |
- if finfo.Mode() != fmode {
|
|
| 1932 |
- t.Fatalf("Expected file mode %s, got %s", fmode.String(), finfo.Mode().String())
|
|
| 1933 |
- } |
|
| 1934 |
- |
|
| 1935 |
- content, err := os.ReadFile(resolvConfPath) |
|
| 1936 |
- if err != nil {
|
|
| 1937 |
- t.Fatal(err) |
|
| 1938 |
- } |
|
| 1939 |
- |
|
| 1940 |
- if !bytes.Equal(content, tmpResolvConf) {
|
|
| 1941 |
- t.Fatalf("Expected:\n%s\nGot:\n%s", string(tmpResolvConf), string(content))
|
|
| 1942 |
- } |
|
| 1943 |
-} |
|
| 1944 |
- |
|
| 1945 | 1793 |
func TestResolvConf(t *testing.T) {
|
| 1946 |
- defer netnsutils.SetupTestOSContext(t)() |
|
| 1947 |
- controller := newController(t) |
|
| 1948 |
- |
|
| 1949 |
- tmpResolvConf1 := []byte("search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n")
|
|
| 1950 |
- tmpResolvConf2 := []byte("search pommesfrites.fr\nnameserver 112.34.56.78\nnameserver 2001:4860:4860::8888\n")
|
|
| 1951 |
- expectedResolvConf1 := []byte("search pommesfrites.fr\nnameserver 127.0.0.11\noptions ndots:0\n")
|
|
| 1952 |
- tmpResolvConf3 := []byte("search pommesfrites.fr\nnameserver 113.34.56.78\n")
|
|
| 1953 |
- |
|
| 1954 |
- // take a copy of resolv.conf for restoring after test completes |
|
| 1955 |
- resolvConfSystem, err := os.ReadFile("/etc/resolv.conf")
|
|
| 1956 |
- if err != nil {
|
|
| 1957 |
- t.Fatal(err) |
|
| 1958 |
- } |
|
| 1959 |
- // cleanup |
|
| 1960 |
- defer func() {
|
|
| 1961 |
- if err := os.WriteFile("/etc/resolv.conf", resolvConfSystem, 0o644); err != nil {
|
|
| 1962 |
- t.Fatal(err) |
|
| 1963 |
- } |
|
| 1964 |
- }() |
|
| 1965 |
- |
|
| 1966 |
- netOption := options.Generic{
|
|
| 1967 |
- netlabel.GenericData: options.Generic{
|
|
| 1968 |
- "BridgeName": "testnetwork", |
|
| 1794 |
+ tmpDir := t.TempDir() |
|
| 1795 |
+ originResolvConfPath := filepath.Join(tmpDir, "origin_resolv.conf") |
|
| 1796 |
+ resolvConfPath := filepath.Join(tmpDir, "resolv.conf") |
|
| 1797 |
+ |
|
| 1798 |
+ // Strip comments that end in a newline (a comment with no newline at the end |
|
| 1799 |
+ // of the file will not be stripped). |
|
| 1800 |
+ stripCommentsRE := regexp.MustCompile(`(?m)^#.*\n`) |
|
| 1801 |
+ |
|
| 1802 |
+ testcases := []struct {
|
|
| 1803 |
+ name string |
|
| 1804 |
+ makeNet func(t *testing.T, c *libnetwork.Controller) *libnetwork.Network |
|
| 1805 |
+ delNet bool |
|
| 1806 |
+ epOpts []libnetwork.EndpointOption |
|
| 1807 |
+ sbOpts []libnetwork.SandboxOption |
|
| 1808 |
+ originResolvConf string |
|
| 1809 |
+ expResolvConf string |
|
| 1810 |
+ }{
|
|
| 1811 |
+ {
|
|
| 1812 |
+ name: "IPv6 network", |
|
| 1813 |
+ makeNet: makeTestIPv6Network, |
|
| 1814 |
+ delNet: true, |
|
| 1815 |
+ originResolvConf: "search pommesfrites.fr\nnameserver 12.34.56.78\nnameserver 2001:4860:4860::8888\n", |
|
| 1816 |
+ expResolvConf: "nameserver 127.0.0.11\nnameserver 2001:4860:4860::8888\nsearch pommesfrites.fr\noptions ndots:0", |
|
| 1817 |
+ }, |
|
| 1818 |
+ {
|
|
| 1819 |
+ name: "host network", |
|
| 1820 |
+ makeNet: makeTesthostNetwork, |
|
| 1821 |
+ epOpts: []libnetwork.EndpointOption{libnetwork.CreateOptionDisableResolution()},
|
|
| 1822 |
+ sbOpts: []libnetwork.SandboxOption{libnetwork.OptionUseDefaultSandbox()},
|
|
| 1823 |
+ originResolvConf: "search localhost.net\nnameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\n", |
|
| 1824 |
+ expResolvConf: "nameserver 127.0.0.1\nnameserver 2001:4860:4860::8888\nsearch localhost.net", |
|
| 1969 | 1825 |
}, |
| 1970 |
- } |
|
| 1971 |
- n, err := createTestNetwork(controller, "bridge", "testnetwork", netOption, nil, nil) |
|
| 1972 |
- if err != nil {
|
|
| 1973 |
- t.Fatal(err) |
|
| 1974 |
- } |
|
| 1975 |
- defer func() {
|
|
| 1976 |
- if err := n.Delete(); err != nil {
|
|
| 1977 |
- t.Fatal(err) |
|
| 1978 |
- } |
|
| 1979 |
- }() |
|
| 1980 |
- |
|
| 1981 |
- ep, err := n.CreateEndpoint("ep")
|
|
| 1982 |
- if err != nil {
|
|
| 1983 |
- t.Fatal(err) |
|
| 1984 |
- } |
|
| 1985 |
- |
|
| 1986 |
- if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf1, 0o644); err != nil {
|
|
| 1987 |
- t.Fatal(err) |
|
| 1988 |
- } |
|
| 1989 |
- |
|
| 1990 |
- resolvConfPath := "/tmp/libnetwork_test/resolv.conf" |
|
| 1991 |
- defer os.Remove(resolvConfPath) |
|
| 1992 |
- |
|
| 1993 |
- sb1, err := controller.NewSandbox(containerID, libnetwork.OptionResolvConfPath(resolvConfPath)) |
|
| 1994 |
- if err != nil {
|
|
| 1995 |
- t.Fatal(err) |
|
| 1996 |
- } |
|
| 1997 |
- defer func() {
|
|
| 1998 |
- if err := sb1.Delete(); err != nil {
|
|
| 1999 |
- t.Fatal(err) |
|
| 2000 |
- } |
|
| 2001 |
- }() |
|
| 2002 |
- |
|
| 2003 |
- err = ep.Join(sb1) |
|
| 2004 |
- if err != nil {
|
|
| 2005 |
- t.Fatal(err) |
|
| 2006 |
- } |
|
| 2007 |
- |
|
| 2008 |
- finfo, err := os.Stat(resolvConfPath) |
|
| 2009 |
- if err != nil {
|
|
| 2010 |
- t.Fatal(err) |
|
| 2011 |
- } |
|
| 2012 |
- |
|
| 2013 |
- fmode := (os.FileMode)(0o644) |
|
| 2014 |
- if finfo.Mode() != fmode {
|
|
| 2015 |
- t.Fatalf("Expected file mode %s, got %s", fmode.String(), finfo.Mode().String())
|
|
| 2016 |
- } |
|
| 2017 |
- |
|
| 2018 |
- content, err := os.ReadFile(resolvConfPath) |
|
| 2019 |
- if err != nil {
|
|
| 2020 |
- t.Fatal(err) |
|
| 2021 |
- } |
|
| 2022 |
- |
|
| 2023 |
- if !bytes.Equal(content, expectedResolvConf1) {
|
|
| 2024 |
- fmt.Printf("\n%v\n%v\n", expectedResolvConf1, content)
|
|
| 2025 |
- t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf1), string(content))
|
|
| 2026 |
- } |
|
| 2027 |
- |
|
| 2028 |
- err = ep.Leave(sb1) |
|
| 2029 |
- if err != nil {
|
|
| 2030 |
- t.Fatal(err) |
|
| 2031 |
- } |
|
| 2032 |
- |
|
| 2033 |
- if err := os.WriteFile("/etc/resolv.conf", tmpResolvConf2, 0o644); err != nil {
|
|
| 2034 |
- t.Fatal(err) |
|
| 2035 |
- } |
|
| 2036 |
- |
|
| 2037 |
- sb2, err := controller.NewSandbox(containerID+"_2", libnetwork.OptionResolvConfPath(resolvConfPath)) |
|
| 2038 |
- if err != nil {
|
|
| 2039 |
- t.Fatal(err) |
|
| 2040 |
- } |
|
| 2041 |
- defer func() {
|
|
| 2042 |
- if err := sb2.Delete(); err != nil {
|
|
| 2043 |
- t.Fatal(err) |
|
| 2044 |
- } |
|
| 2045 |
- }() |
|
| 2046 |
- |
|
| 2047 |
- err = ep.Join(sb2) |
|
| 2048 |
- if err != nil {
|
|
| 2049 |
- t.Fatal(err) |
|
| 2050 |
- } |
|
| 2051 |
- |
|
| 2052 |
- content, err = os.ReadFile(resolvConfPath) |
|
| 2053 |
- if err != nil {
|
|
| 2054 |
- t.Fatal(err) |
|
| 2055 |
- } |
|
| 2056 |
- |
|
| 2057 |
- if !bytes.Equal(content, expectedResolvConf1) {
|
|
| 2058 |
- t.Fatalf("Expected:\n%s\nGot:\n%s", string(expectedResolvConf1), string(content))
|
|
| 2059 |
- } |
|
| 2060 |
- |
|
| 2061 |
- if err := os.WriteFile(resolvConfPath, tmpResolvConf3, 0o644); err != nil {
|
|
| 2062 |
- t.Fatal(err) |
|
| 2063 | 1826 |
} |
| 2064 | 1827 |
|
| 2065 |
- err = ep.Leave(sb2) |
|
| 2066 |
- if err != nil {
|
|
| 2067 |
- t.Fatal(err) |
|
| 2068 |
- } |
|
| 1828 |
+ for _, tc := range testcases {
|
|
| 1829 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 1830 |
+ defer netnsutils.SetupTestOSContext(t)() |
|
| 1831 |
+ c := newController(t) |
|
| 2069 | 1832 |
|
| 2070 |
- err = ep.Join(sb2) |
|
| 2071 |
- if err != nil {
|
|
| 2072 |
- t.Fatal(err) |
|
| 2073 |
- } |
|
| 1833 |
+ err := os.WriteFile(originResolvConfPath, []byte(tc.originResolvConf), 0o644) |
|
| 1834 |
+ assert.NilError(t, err) |
|
| 2074 | 1835 |
|
| 2075 |
- content, err = os.ReadFile(resolvConfPath) |
|
| 2076 |
- if err != nil {
|
|
| 2077 |
- t.Fatal(err) |
|
| 2078 |
- } |
|
| 1836 |
+ n := tc.makeNet(t, c) |
|
| 1837 |
+ if tc.delNet {
|
|
| 1838 |
+ defer func() {
|
|
| 1839 |
+ err := n.Delete() |
|
| 1840 |
+ assert.Check(t, err) |
|
| 1841 |
+ }() |
|
| 1842 |
+ } |
|
| 2079 | 1843 |
|
| 2080 |
- if !bytes.Equal(content, tmpResolvConf3) {
|
|
| 2081 |
- t.Fatalf("Expected:\n%s\nGot:\n%s", string(tmpResolvConf3), string(content))
|
|
| 1844 |
+ sbOpts := append(tc.sbOpts, |
|
| 1845 |
+ libnetwork.OptionResolvConfPath(resolvConfPath), |
|
| 1846 |
+ libnetwork.OptionOriginResolvConfPath(originResolvConfPath), |
|
| 1847 |
+ ) |
|
| 1848 |
+ sb, err := c.NewSandbox(containerID, sbOpts...) |
|
| 1849 |
+ assert.NilError(t, err) |
|
| 1850 |
+ defer func() {
|
|
| 1851 |
+ err := sb.Delete() |
|
| 1852 |
+ assert.Check(t, err) |
|
| 1853 |
+ }() |
|
| 1854 |
+ |
|
| 1855 |
+ ep, err := n.CreateEndpoint("ep", tc.epOpts...)
|
|
| 1856 |
+ assert.NilError(t, err) |
|
| 1857 |
+ defer func() {
|
|
| 1858 |
+ err := ep.Delete(false) |
|
| 1859 |
+ assert.Check(t, err) |
|
| 1860 |
+ }() |
|
| 1861 |
+ |
|
| 1862 |
+ err = ep.Join(sb) |
|
| 1863 |
+ assert.NilError(t, err) |
|
| 1864 |
+ defer func() {
|
|
| 1865 |
+ err := ep.Leave(sb) |
|
| 1866 |
+ assert.Check(t, err) |
|
| 1867 |
+ }() |
|
| 1868 |
+ |
|
| 1869 |
+ finfo, err := os.Stat(resolvConfPath) |
|
| 1870 |
+ assert.NilError(t, err) |
|
| 1871 |
+ expFMode := (os.FileMode)(0o644) |
|
| 1872 |
+ assert.Check(t, is.Equal(finfo.Mode().String(), expFMode.String())) |
|
| 1873 |
+ content, err := os.ReadFile(resolvConfPath) |
|
| 1874 |
+ assert.NilError(t, err) |
|
| 1875 |
+ actual := stripCommentsRE.ReplaceAllString(string(content), "") |
|
| 1876 |
+ actual = strings.TrimSpace(actual) |
|
| 1877 |
+ assert.Check(t, is.Equal(actual, tc.expResolvConf)) |
|
| 1878 |
+ }) |
|
| 2082 | 1879 |
} |
| 2083 | 1880 |
} |
| 2084 | 1881 |
|
| ... | ... |
@@ -3,20 +3,12 @@ package resolvconf |
| 3 | 3 |
|
| 4 | 4 |
import ( |
| 5 | 5 |
"bytes" |
| 6 |
- "context" |
|
| 6 |
+ "fmt" |
|
| 7 | 7 |
"os" |
| 8 |
- "regexp" |
|
| 9 | 8 |
"strings" |
| 10 |
- "sync" |
|
| 11 | 9 |
|
| 12 |
- "github.com/containerd/log" |
|
| 13 |
-) |
|
| 14 |
- |
|
| 15 |
-const ( |
|
| 16 |
- // defaultPath is the default path to the resolv.conf that contains information to resolve DNS. See Path(). |
|
| 17 |
- defaultPath = "/etc/resolv.conf" |
|
| 18 |
- // alternatePath is a path different from defaultPath, that may be used to resolve DNS. See Path(). |
|
| 19 |
- alternatePath = "/run/systemd/resolve/resolv.conf" |
|
| 10 |
+ "github.com/docker/docker/libnetwork/internal/resolvconf" |
|
| 11 |
+ "github.com/opencontainers/go-digest" |
|
| 20 | 12 |
) |
| 21 | 13 |
|
| 22 | 14 |
// constants for the IP address type |
| ... | ... |
@@ -26,72 +18,16 @@ const ( |
| 26 | 26 |
IPv6 |
| 27 | 27 |
) |
| 28 | 28 |
|
| 29 |
-var ( |
|
| 30 |
- detectSystemdResolvConfOnce sync.Once |
|
| 31 |
- pathAfterSystemdDetection = defaultPath |
|
| 32 |
-) |
|
| 33 |
- |
|
| 34 |
-// Path returns the path to the resolv.conf file that libnetwork should use. |
|
| 35 |
-// |
|
| 36 |
-// When /etc/resolv.conf contains 127.0.0.53 as the only nameserver, then |
|
| 37 |
-// it is assumed systemd-resolved manages DNS. Because inside the container 127.0.0.53 |
|
| 38 |
-// is not a valid DNS server, Path() returns /run/systemd/resolve/resolv.conf |
|
| 39 |
-// which is the resolv.conf that systemd-resolved generates and manages. |
|
| 40 |
-// Otherwise Path() returns /etc/resolv.conf. |
|
| 41 |
-// |
|
| 42 |
-// Errors are silenced as they will inevitably resurface at future open/read calls. |
|
| 43 |
-// |
|
| 44 |
-// More information at https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html#/etc/resolv.conf |
|
| 45 |
-func Path() string {
|
|
| 46 |
- detectSystemdResolvConfOnce.Do(func() {
|
|
| 47 |
- candidateResolvConf, err := os.ReadFile(defaultPath) |
|
| 48 |
- if err != nil {
|
|
| 49 |
- // silencing error as it will resurface at next calls trying to read defaultPath |
|
| 50 |
- return |
|
| 51 |
- } |
|
| 52 |
- ns := GetNameservers(candidateResolvConf, IP) |
|
| 53 |
- if len(ns) == 1 && ns[0] == "127.0.0.53" {
|
|
| 54 |
- pathAfterSystemdDetection = alternatePath |
|
| 55 |
- log.G(context.TODO()).Infof("detected 127.0.0.53 nameserver, assuming systemd-resolved, so using resolv.conf: %s", alternatePath)
|
|
| 56 |
- } |
|
| 57 |
- }) |
|
| 58 |
- return pathAfterSystemdDetection |
|
| 59 |
-} |
|
| 60 |
- |
|
| 61 |
-const ( |
|
| 62 |
- // ipLocalhost is a regex pattern for IPv4 or IPv6 loopback range. |
|
| 63 |
- ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
|
| 64 |
- ipv4NumBlock = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)` |
|
| 65 |
- ipv4Address = `(` + ipv4NumBlock + `\.){3}` + ipv4NumBlock
|
|
| 66 |
- |
|
| 67 |
- // This is not an IPv6 address verifier as it will accept a super-set of IPv6, and also |
|
| 68 |
- // will *not match* IPv4-Embedded IPv6 Addresses (RFC6052), but that and other variants |
|
| 69 |
- // -- e.g. other link-local types -- either won't work in containers or are unnecessary. |
|
| 70 |
- // For readability and sufficiency for Docker purposes this seemed more reasonable than a |
|
| 71 |
- // 1000+ character regexp with exact and complete IPv6 validation |
|
| 72 |
- ipv6Address = `([0-9A-Fa-f]{0,4}:){2,7}([0-9A-Fa-f]{0,4})(%\w+)?`
|
|
| 73 |
-) |
|
| 74 |
- |
|
| 75 |
-var ( |
|
| 76 |
- // Note: the default IPv4 & IPv6 resolvers are set to Google's Public DNS |
|
| 77 |
- defaultIPv4Dns = []string{"nameserver 8.8.8.8", "nameserver 8.8.4.4"}
|
|
| 78 |
- defaultIPv6Dns = []string{"nameserver 2001:4860:4860::8888", "nameserver 2001:4860:4860::8844"}
|
|
| 79 |
- |
|
| 80 |
- localhostNSRegexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipLocalhost + `\s*\n*`) |
|
| 81 |
- nsIPv6Regexp = regexp.MustCompile(`(?m)^nameserver\s+` + ipv6Address + `\s*\n*`) |
|
| 82 |
- nsRegexp = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `)|(` + ipv6Address + `))\s*$`) |
|
| 83 |
- nsIPv6Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv6Address + `))\s*$`) |
|
| 84 |
- nsIPv4Regexpmatch = regexp.MustCompile(`^\s*nameserver\s*((` + ipv4Address + `))\s*$`) |
|
| 85 |
- searchRegexp = regexp.MustCompile(`^\s*search\s*(([^\s]+\s*)*)$`) |
|
| 86 |
- optionsRegexp = regexp.MustCompile(`^\s*options\s*(([^\s]+\s*)*)$`) |
|
| 87 |
-) |
|
| 88 |
- |
|
| 89 | 29 |
// File contains the resolv.conf content and its hash |
| 90 | 30 |
type File struct {
|
| 91 | 31 |
Content []byte |
| 92 | 32 |
Hash []byte |
| 93 | 33 |
} |
| 94 | 34 |
|
| 35 |
+func Path() string {
|
|
| 36 |
+ return resolvconf.Path() |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 95 | 39 |
// Get returns the contents of /etc/resolv.conf and its hash |
| 96 | 40 |
func Get() (*File, error) {
|
| 97 | 41 |
return GetSpecific(Path()) |
| ... | ... |
@@ -103,7 +39,8 @@ func GetSpecific(path string) (*File, error) {
|
| 103 | 103 |
if err != nil {
|
| 104 | 104 |
return nil, err |
| 105 | 105 |
} |
| 106 |
- return &File{Content: resolv, Hash: hashData(resolv)}, nil
|
|
| 106 |
+ hash := digest.FromBytes(resolv) |
|
| 107 |
+ return &File{Content: resolv, Hash: []byte(hash)}, nil
|
|
| 107 | 108 |
} |
| 108 | 109 |
|
| 109 | 110 |
// FilterResolvDNS cleans up the config in resolvConf. It has two main jobs: |
| ... | ... |
@@ -113,54 +50,34 @@ func GetSpecific(path string) (*File, error) {
|
| 113 | 113 |
// 2. Given the caller provides the enable/disable state of IPv6, the filter |
| 114 | 114 |
// code will remove all IPv6 nameservers if it is not enabled for containers |
| 115 | 115 |
func FilterResolvDNS(resolvConf []byte, ipv6Enabled bool) (*File, error) {
|
| 116 |
- cleanedResolvConf := localhostNSRegexp.ReplaceAll(resolvConf, []byte{})
|
|
| 117 |
- // if IPv6 is not enabled, also clean out any IPv6 address nameserver |
|
| 118 |
- if !ipv6Enabled {
|
|
| 119 |
- cleanedResolvConf = nsIPv6Regexp.ReplaceAll(cleanedResolvConf, []byte{})
|
|
| 120 |
- } |
|
| 121 |
- // if the resulting resolvConf has no more nameservers defined, add appropriate |
|
| 122 |
- // default DNS servers for IPv4 and (optionally) IPv6 |
|
| 123 |
- if len(GetNameservers(cleanedResolvConf, IP)) == 0 {
|
|
| 124 |
- log.G(context.TODO()).Infof("No non-localhost DNS nameservers are left in resolv.conf. Using default external servers: %v", defaultIPv4Dns)
|
|
| 125 |
- dns := defaultIPv4Dns |
|
| 126 |
- if ipv6Enabled {
|
|
| 127 |
- log.G(context.TODO()).Infof("IPv6 enabled; Adding default IPv6 external servers: %v", defaultIPv6Dns)
|
|
| 128 |
- dns = append(dns, defaultIPv6Dns...) |
|
| 129 |
- } |
|
| 130 |
- cleanedResolvConf = append(cleanedResolvConf, []byte("\n"+strings.Join(dns, "\n"))...)
|
|
| 116 |
+ rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") |
|
| 117 |
+ if err != nil {
|
|
| 118 |
+ return nil, err |
|
| 131 | 119 |
} |
| 132 |
- return &File{Content: cleanedResolvConf, Hash: hashData(cleanedResolvConf)}, nil
|
|
| 133 |
-} |
|
| 134 |
- |
|
| 135 |
-// getLines parses input into lines and strips away comments. |
|
| 136 |
-func getLines(input []byte, commentMarker []byte) [][]byte {
|
|
| 137 |
- lines := bytes.Split(input, []byte("\n"))
|
|
| 138 |
- var output [][]byte |
|
| 139 |
- for _, currentLine := range lines {
|
|
| 140 |
- commentIndex := bytes.Index(currentLine, commentMarker) |
|
| 141 |
- if commentIndex == -1 {
|
|
| 142 |
- output = append(output, currentLine) |
|
| 143 |
- } else {
|
|
| 144 |
- output = append(output, currentLine[:commentIndex]) |
|
| 145 |
- } |
|
| 120 |
+ rc.TransformForLegacyNw(ipv6Enabled) |
|
| 121 |
+ content, err := rc.Generate(false) |
|
| 122 |
+ if err != nil {
|
|
| 123 |
+ return nil, err |
|
| 146 | 124 |
} |
| 147 |
- return output |
|
| 125 |
+ hash := digest.FromBytes(content) |
|
| 126 |
+ return &File{Content: content, Hash: []byte(hash)}, nil
|
|
| 148 | 127 |
} |
| 149 | 128 |
|
| 150 | 129 |
// GetNameservers returns nameservers (if any) listed in /etc/resolv.conf |
| 151 | 130 |
func GetNameservers(resolvConf []byte, kind int) []string {
|
| 131 |
+ rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") |
|
| 132 |
+ if err != nil {
|
|
| 133 |
+ return nil |
|
| 134 |
+ } |
|
| 135 |
+ nsAddrs := rc.NameServers() |
|
| 152 | 136 |
var nameservers []string |
| 153 |
- for _, line := range getLines(resolvConf, []byte("#")) {
|
|
| 154 |
- var ns [][]byte |
|
| 137 |
+ for _, addr := range nsAddrs {
|
|
| 155 | 138 |
if kind == IP {
|
| 156 |
- ns = nsRegexp.FindSubmatch(line) |
|
| 157 |
- } else if kind == IPv4 {
|
|
| 158 |
- ns = nsIPv4Regexpmatch.FindSubmatch(line) |
|
| 159 |
- } else if kind == IPv6 {
|
|
| 160 |
- ns = nsIPv6Regexpmatch.FindSubmatch(line) |
|
| 161 |
- } |
|
| 162 |
- if len(ns) > 0 {
|
|
| 163 |
- nameservers = append(nameservers, string(ns[1])) |
|
| 139 |
+ nameservers = append(nameservers, addr.String()) |
|
| 140 |
+ } else if kind == IPv4 && addr.Is4() {
|
|
| 141 |
+ nameservers = append(nameservers, addr.String()) |
|
| 142 |
+ } else if kind == IPv6 && addr.Is6() {
|
|
| 143 |
+ nameservers = append(nameservers, addr.String()) |
|
| 164 | 144 |
} |
| 165 | 145 |
} |
| 166 | 146 |
return nameservers |
| ... | ... |
@@ -170,16 +87,15 @@ func GetNameservers(resolvConf []byte, kind int) []string {
|
| 170 | 170 |
// /etc/resolv.conf as CIDR blocks (e.g., "1.2.3.4/32") |
| 171 | 171 |
// This function's output is intended for net.ParseCIDR |
| 172 | 172 |
func GetNameserversAsCIDR(resolvConf []byte) []string {
|
| 173 |
- var nameservers []string |
|
| 174 |
- for _, nameserver := range GetNameservers(resolvConf, IP) {
|
|
| 175 |
- var address string |
|
| 176 |
- // If IPv6, strip zone if present |
|
| 177 |
- if strings.Contains(nameserver, ":") {
|
|
| 178 |
- address = strings.Split(nameserver, "%")[0] + "/128" |
|
| 179 |
- } else {
|
|
| 180 |
- address = nameserver + "/32" |
|
| 181 |
- } |
|
| 182 |
- nameservers = append(nameservers, address) |
|
| 173 |
+ rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") |
|
| 174 |
+ if err != nil {
|
|
| 175 |
+ return nil |
|
| 176 |
+ } |
|
| 177 |
+ nsAddrs := rc.NameServers() |
|
| 178 |
+ nameservers := make([]string, 0, len(nsAddrs)) |
|
| 179 |
+ for _, addr := range nsAddrs {
|
|
| 180 |
+ str := fmt.Sprintf("%s/%d", addr.WithZone("").String(), addr.BitLen())
|
|
| 181 |
+ nameservers = append(nameservers, str) |
|
| 183 | 182 |
} |
| 184 | 183 |
return nameservers |
| 185 | 184 |
} |
| ... | ... |
@@ -188,36 +104,30 @@ func GetNameserversAsCIDR(resolvConf []byte) []string {
|
| 188 | 188 |
// If more than one search line is encountered, only the contents of the last |
| 189 | 189 |
// one is returned. |
| 190 | 190 |
func GetSearchDomains(resolvConf []byte) []string {
|
| 191 |
- var domains []string |
|
| 192 |
- for _, line := range getLines(resolvConf, []byte("#")) {
|
|
| 193 |
- match := searchRegexp.FindSubmatch(line) |
|
| 194 |
- if match == nil {
|
|
| 195 |
- continue |
|
| 196 |
- } |
|
| 197 |
- domains = strings.Fields(string(match[1])) |
|
| 191 |
+ rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") |
|
| 192 |
+ if err != nil {
|
|
| 193 |
+ return nil |
|
| 198 | 194 |
} |
| 199 |
- return domains |
|
| 195 |
+ return rc.Search() |
|
| 200 | 196 |
} |
| 201 | 197 |
|
| 202 | 198 |
// GetOptions returns options (if any) listed in /etc/resolv.conf |
| 203 | 199 |
// If more than one options line is encountered, only the contents of the last |
| 204 | 200 |
// one is returned. |
| 205 | 201 |
func GetOptions(resolvConf []byte) []string {
|
| 206 |
- var options []string |
|
| 207 |
- for _, line := range getLines(resolvConf, []byte("#")) {
|
|
| 208 |
- match := optionsRegexp.FindSubmatch(line) |
|
| 209 |
- if match == nil {
|
|
| 210 |
- continue |
|
| 211 |
- } |
|
| 212 |
- options = strings.Fields(string(match[1])) |
|
| 202 |
+ rc, err := resolvconf.Parse(bytes.NewBuffer(resolvConf), "") |
|
| 203 |
+ if err != nil {
|
|
| 204 |
+ return nil |
|
| 213 | 205 |
} |
| 214 |
- return options |
|
| 206 |
+ return rc.Options() |
|
| 215 | 207 |
} |
| 216 | 208 |
|
| 217 | 209 |
// Build generates and writes a configuration file to path containing a nameserver |
| 218 | 210 |
// entry for every element in nameservers, a "search" entry for every element in |
| 219 | 211 |
// dnsSearch, and an "options" entry for every element in dnsOptions. It returns |
| 220 | 212 |
// a File containing the generated content and its (sha256) hash. |
| 213 |
+// |
|
| 214 |
+// Note that the resolv.conf file is written, but the hash file is not. |
|
| 221 | 215 |
func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, error) {
|
| 222 | 216 |
content := bytes.NewBuffer(nil) |
| 223 | 217 |
if len(dnsSearch) > 0 {
|
| ... | ... |
@@ -244,5 +154,6 @@ func Build(path string, nameservers, dnsSearch, dnsOptions []string) (*File, err |
| 244 | 244 |
return nil, err |
| 245 | 245 |
} |
| 246 | 246 |
|
| 247 |
- return &File{Content: content.Bytes(), Hash: hashData(content.Bytes())}, nil
|
|
| 247 |
+ hash := digest.FromBytes(content.Bytes()) |
|
| 248 |
+ return &File{Content: content.Bytes(), Hash: []byte(hash)}, nil
|
|
| 248 | 249 |
} |
| ... | ... |
@@ -5,7 +5,12 @@ package resolvconf |
| 5 | 5 |
import ( |
| 6 | 6 |
"bytes" |
| 7 | 7 |
"os" |
| 8 |
+ "strings" |
|
| 8 | 9 |
"testing" |
| 10 |
+ |
|
| 11 |
+ "github.com/opencontainers/go-digest" |
|
| 12 |
+ "gotest.tools/v3/assert" |
|
| 13 |
+ is "gotest.tools/v3/assert/cmp" |
|
| 9 | 14 |
) |
| 10 | 15 |
|
| 11 | 16 |
func TestGet(t *testing.T) {
|
| ... | ... |
@@ -20,7 +25,8 @@ func TestGet(t *testing.T) {
|
| 20 | 20 |
if !bytes.Equal(actual.Content, expected) {
|
| 21 | 21 |
t.Errorf("%s and GetResolvConf have different content.", Path())
|
| 22 | 22 |
} |
| 23 |
- if !bytes.Equal(actual.Hash, hashData(expected)) {
|
|
| 23 |
+ hash := digest.FromBytes(expected) |
|
| 24 |
+ if !bytes.Equal(actual.Hash, []byte(hash)) {
|
|
| 24 | 25 |
t.Errorf("%s and GetResolvConf have different hashes.", Path())
|
| 25 | 26 |
} |
| 26 | 27 |
} |
| ... | ... |
@@ -111,6 +117,14 @@ nameserver 1.2.3.4 |
| 111 | 111 |
nameserver 1.2.3.4 # not 4.3.2.1`, |
| 112 | 112 |
result: []string{"1.2.3.4/32"},
|
| 113 | 113 |
}, |
| 114 |
+ {
|
|
| 115 |
+ input: `nameserver fd6f:c490:ec68::1`, |
|
| 116 |
+ result: []string{"fd6f:c490:ec68::1/128"},
|
|
| 117 |
+ }, |
|
| 118 |
+ {
|
|
| 119 |
+ input: `nameserver fe80::1234%eth0`, |
|
| 120 |
+ result: []string{"fe80::1234/128"},
|
|
| 121 |
+ }, |
|
| 114 | 122 |
} {
|
| 115 | 123 |
test := GetNameserversAsCIDR([]byte(tc.input)) |
| 116 | 124 |
if !strSlicesEqual(test, tc.result) {
|
| ... | ... |
@@ -175,6 +189,10 @@ search foo.example.com example.com |
| 175 | 175 |
nameserver 4.30.20.100`, |
| 176 | 176 |
result: []string{"foo.example.com", "example.com"},
|
| 177 | 177 |
}, |
| 178 |
+ {
|
|
| 179 |
+ input: `domain an.example`, |
|
| 180 |
+ result: []string{"an.example"},
|
|
| 181 |
+ }, |
|
| 178 | 182 |
} {
|
| 179 | 183 |
test := GetSearchDomains([]byte(tc.input)) |
| 180 | 184 |
if !strSlicesEqual(test, tc.result) {
|
| ... | ... |
@@ -338,89 +356,79 @@ func TestBuildWithNoOptions(t *testing.T) {
|
| 338 | 338 |
} |
| 339 | 339 |
|
| 340 | 340 |
func TestFilterResolvDNS(t *testing.T) {
|
| 341 |
- ns0 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 342 |
- |
|
| 343 |
- if result, _ := FilterResolvDNS([]byte(ns0), false); result != nil {
|
|
| 344 |
- if ns0 != string(result.Content) {
|
|
| 345 |
- t.Errorf("Failed No Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 346 |
- } |
|
| 347 |
- } |
|
| 348 |
- |
|
| 349 |
- ns1 := "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n" |
|
| 350 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 351 |
- if ns0 != string(result.Content) {
|
|
| 352 |
- t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 353 |
- } |
|
| 354 |
- } |
|
| 355 |
- |
|
| 356 |
- ns1 = "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n" |
|
| 357 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 358 |
- if ns0 != string(result.Content) {
|
|
| 359 |
- t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 360 |
- } |
|
| 361 |
- } |
|
| 362 |
- |
|
| 363 |
- ns1 = "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n" |
|
| 364 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 365 |
- if ns0 != string(result.Content) {
|
|
| 366 |
- t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 367 |
- } |
|
| 368 |
- } |
|
| 369 |
- |
|
| 370 |
- ns1 = "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n" |
|
| 371 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 372 |
- if ns0 != string(result.Content) {
|
|
| 373 |
- t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 374 |
- } |
|
| 375 |
- } |
|
| 376 |
- |
|
| 377 |
- ns1 = "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1" |
|
| 378 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 379 |
- if ns0 != string(result.Content) {
|
|
| 380 |
- t.Errorf("Failed Localhost: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 381 |
- } |
|
| 382 |
- } |
|
| 383 |
- |
|
| 384 |
- // with IPv6 disabled (false param), the IPv6 nameserver should be removed |
|
| 385 |
- ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1" |
|
| 386 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 387 |
- if ns0 != string(result.Content) {
|
|
| 388 |
- t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 389 |
- } |
|
| 390 |
- } |
|
| 391 |
- |
|
| 392 |
- // with IPv6 disabled (false param), the IPv6 link-local nameserver with zone ID should be removed |
|
| 393 |
- ns1 = "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21\n" |
|
| 394 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 395 |
- if ns0 != string(result.Content) {
|
|
| 396 |
- t.Errorf("Failed Localhost+IPv6 off: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 397 |
- } |
|
| 398 |
- } |
|
| 399 |
- |
|
| 400 |
- // with IPv6 enabled, the IPv6 nameserver should be preserved |
|
| 401 |
- ns0 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\n" |
|
| 402 |
- ns1 = "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1" |
|
| 403 |
- if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
|
|
| 404 |
- if ns0 != string(result.Content) {
|
|
| 405 |
- t.Errorf("Failed Localhost+IPv6 on: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 406 |
- } |
|
| 407 |
- } |
|
| 408 |
- |
|
| 409 |
- // with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added |
|
| 410 |
- ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844" |
|
| 411 |
- ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1" |
|
| 412 |
- if result, _ := FilterResolvDNS([]byte(ns1), true); result != nil {
|
|
| 413 |
- if ns0 != string(result.Content) {
|
|
| 414 |
- t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 415 |
- } |
|
| 341 |
+ testcases := []struct {
|
|
| 342 |
+ name string |
|
| 343 |
+ input string |
|
| 344 |
+ ipv6Enabled bool |
|
| 345 |
+ expOut string |
|
| 346 |
+ }{
|
|
| 347 |
+ {
|
|
| 348 |
+ name: "No localhost", |
|
| 349 |
+ input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\n", |
|
| 350 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 351 |
+ }, |
|
| 352 |
+ {
|
|
| 353 |
+ name: "Localhost last", |
|
| 354 |
+ input: "nameserver 10.16.60.14\nnameserver 10.16.60.21\nnameserver 127.0.0.1\n", |
|
| 355 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 356 |
+ }, |
|
| 357 |
+ {
|
|
| 358 |
+ name: "Localhost middle", |
|
| 359 |
+ input: "nameserver 10.16.60.14\nnameserver 127.0.0.1\nnameserver 10.16.60.21\n", |
|
| 360 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 361 |
+ }, |
|
| 362 |
+ {
|
|
| 363 |
+ name: "Localhost first", |
|
| 364 |
+ input: "nameserver 127.0.1.1\nnameserver 10.16.60.14\nnameserver 10.16.60.21\n", |
|
| 365 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 366 |
+ }, |
|
| 367 |
+ {
|
|
| 368 |
+ name: "IPv6 Localhost", |
|
| 369 |
+ input: "nameserver ::1\nnameserver 10.16.60.14\nnameserver 127.0.2.1\nnameserver 10.16.60.21\n", |
|
| 370 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 371 |
+ }, |
|
| 372 |
+ {
|
|
| 373 |
+ name: "Two IPv6 Localhosts", |
|
| 374 |
+ input: "nameserver 10.16.60.14\nnameserver ::1\nnameserver 10.16.60.21\nnameserver ::1", |
|
| 375 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 376 |
+ }, |
|
| 377 |
+ {
|
|
| 378 |
+ name: "IPv6 disabled", |
|
| 379 |
+ input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1", |
|
| 380 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 381 |
+ }, |
|
| 382 |
+ {
|
|
| 383 |
+ name: "IPv6 link-local disabled", |
|
| 384 |
+ input: "nameserver 10.16.60.14\nnameserver FE80::BB1%1\nnameserver FE80::BB1%eth0\nnameserver 10.16.60.21", |
|
| 385 |
+ expOut: "nameserver 10.16.60.14\nnameserver 10.16.60.21", |
|
| 386 |
+ }, |
|
| 387 |
+ {
|
|
| 388 |
+ name: "IPv6 enabled", |
|
| 389 |
+ input: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21\nnameserver ::1\n", |
|
| 390 |
+ ipv6Enabled: true, |
|
| 391 |
+ expOut: "nameserver 10.16.60.14\nnameserver 2002:dead:beef::1\nnameserver 10.16.60.21", |
|
| 392 |
+ }, |
|
| 393 |
+ {
|
|
| 394 |
+ // with IPv6 enabled, and no non-localhost servers, Google defaults (both IPv4+IPv6) should be added |
|
| 395 |
+ name: "localhost only IPv6", |
|
| 396 |
+ input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1", |
|
| 397 |
+ ipv6Enabled: true, |
|
| 398 |
+ expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4\nnameserver 2001:4860:4860::8888\nnameserver 2001:4860:4860::8844", |
|
| 399 |
+ }, |
|
| 400 |
+ {
|
|
| 401 |
+ // with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added |
|
| 402 |
+ name: "localhost only no IPv6", |
|
| 403 |
+ input: "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1", |
|
| 404 |
+ expOut: "nameserver 8.8.8.8\nnameserver 8.8.4.4", |
|
| 405 |
+ }, |
|
| 416 | 406 |
} |
| 417 | 407 |
|
| 418 |
- // with IPv6 disabled, and no non-localhost servers, Google defaults (only IPv4) should be added |
|
| 419 |
- ns0 = "\nnameserver 8.8.8.8\nnameserver 8.8.4.4" |
|
| 420 |
- ns1 = "nameserver 127.0.0.1\nnameserver ::1\nnameserver 127.0.2.1" |
|
| 421 |
- if result, _ := FilterResolvDNS([]byte(ns1), false); result != nil {
|
|
| 422 |
- if ns0 != string(result.Content) {
|
|
| 423 |
- t.Errorf("Failed no Localhost+IPv6 enabled: expected \n<%s> got \n<%s>", ns0, string(result.Content))
|
|
| 424 |
- } |
|
| 408 |
+ for _, tc := range testcases {
|
|
| 409 |
+ t.Run(tc.name, func(t *testing.T) {
|
|
| 410 |
+ f, err := FilterResolvDNS([]byte(tc.input), tc.ipv6Enabled) |
|
| 411 |
+ assert.Check(t, is.Nil(err)) |
|
| 412 |
+ out := strings.TrimSpace(string(f.Content)) |
|
| 413 |
+ assert.Check(t, is.Equal(out, tc.expOut)) |
|
| 414 |
+ }) |
|
| 425 | 415 |
} |
| 426 | 416 |
} |
| 427 | 417 |
deleted file mode 100644 |
| ... | ... |
@@ -1,14 +0,0 @@ |
| 1 |
-package resolvconf |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "crypto/sha256" |
|
| 5 |
- "encoding/hex" |
|
| 6 |
-) |
|
| 7 |
- |
|
| 8 |
-// hashData returns the sha256 sum of data. |
|
| 9 |
-func hashData(data []byte) []byte {
|
|
| 10 |
- f := sha256.Sum256(data) |
|
| 11 |
- out := make([]byte, 2*sha256.Size) |
|
| 12 |
- hex.Encode(out, f[:]) |
|
| 13 |
- return append([]byte("sha256:"), out...)
|
|
| 14 |
-} |
| 15 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,21 +0,0 @@ |
| 1 |
-package resolvconf |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "bytes" |
|
| 5 |
- "testing" |
|
| 6 |
-) |
|
| 7 |
- |
|
| 8 |
-func TestHashData(t *testing.T) {
|
|
| 9 |
- const expected = "sha256:4d11186aed035cc624d553e10db358492c84a7cd6b9670d92123c144930450aa" |
|
| 10 |
- if actual := hashData([]byte("hash-me")); !bytes.Equal(actual, []byte(expected)) {
|
|
| 11 |
- t.Fatalf("Expecting %s, got %s", expected, string(actual))
|
|
| 12 |
- } |
|
| 13 |
-} |
|
| 14 |
- |
|
| 15 |
-func BenchmarkHashData(b *testing.B) {
|
|
| 16 |
- b.ReportAllocs() |
|
| 17 |
- data := []byte("hash-me")
|
|
| 18 |
- for i := 0; i < b.N; i++ {
|
|
| 19 |
- _ = hashData(data) |
|
| 20 |
- } |
|
| 21 |
-} |
| ... | ... |
@@ -3,22 +3,19 @@ |
| 3 | 3 |
package libnetwork |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
- "bytes" |
|
| 7 | 6 |
"context" |
| 8 |
- "fmt" |
|
| 9 |
- "net" |
|
| 7 |
+ "io/fs" |
|
| 10 | 8 |
"net/netip" |
| 11 | 9 |
"os" |
| 12 |
- "path" |
|
| 13 | 10 |
"path/filepath" |
| 14 |
- "strconv" |
|
| 15 | 11 |
"strings" |
| 16 | 12 |
|
| 17 | 13 |
"github.com/containerd/log" |
| 18 | 14 |
"github.com/docker/docker/errdefs" |
| 19 | 15 |
"github.com/docker/docker/libnetwork/etchosts" |
| 20 |
- "github.com/docker/docker/libnetwork/resolvconf" |
|
| 16 |
+ "github.com/docker/docker/libnetwork/internal/resolvconf" |
|
| 21 | 17 |
"github.com/docker/docker/libnetwork/types" |
| 18 |
+ "github.com/pkg/errors" |
|
| 22 | 19 |
) |
| 23 | 20 |
|
| 24 | 21 |
const ( |
| ... | ... |
@@ -100,6 +97,13 @@ func (sb *Sandbox) setupResolutionFiles() error {
|
| 100 | 100 |
} |
| 101 | 101 |
|
| 102 | 102 |
func (sb *Sandbox) buildHostsFile() error {
|
| 103 |
+ sb.restoreHostsPath() |
|
| 104 |
+ |
|
| 105 |
+ dir, _ := filepath.Split(sb.config.hostsPath) |
|
| 106 |
+ if err := createBasePath(dir); err != nil {
|
|
| 107 |
+ return err |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 103 | 110 |
// This is for the host mode networking |
| 104 | 111 |
if sb.config.useDefaultSandBox && len(sb.config.extraHosts) == 0 {
|
| 105 | 112 |
// 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 {
|
| 208 | 208 |
return nil |
| 209 | 209 |
} |
| 210 | 210 |
|
| 211 |
-func (sb *Sandbox) restorePath() {
|
|
| 211 |
+func (sb *Sandbox) restoreResolvConfPath() {
|
|
| 212 | 212 |
if sb.config.resolvConfPath == "" {
|
| 213 | 213 |
sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf" |
| 214 | 214 |
} |
| 215 | 215 |
sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash" |
| 216 |
+} |
|
| 217 |
+ |
|
| 218 |
+func (sb *Sandbox) restoreHostsPath() {
|
|
| 216 | 219 |
if sb.config.hostsPath == "" {
|
| 217 | 220 |
sb.config.hostsPath = defaultPrefix + "/" + sb.id + "/hosts" |
| 218 | 221 |
} |
| 219 | 222 |
} |
| 220 | 223 |
|
| 221 |
-func (sb *Sandbox) setExternalResolvers(content []byte, addrType int, checkLoopback bool) {
|
|
| 222 |
- servers := resolvconf.GetNameservers(content, addrType) |
|
| 223 |
- for _, ip := range servers {
|
|
| 224 |
- hostLoopback := false |
|
| 225 |
- if checkLoopback && isIPv4Loopback(ip) {
|
|
| 226 |
- hostLoopback = true |
|
| 227 |
- } |
|
| 224 |
+func (sb *Sandbox) setExternalResolvers(entries []resolvconf.ExtDNSEntry) {
|
|
| 225 |
+ sb.extDNS = make([]extDNSEntry, 0, len(entries)) |
|
| 226 |
+ for _, entry := range entries {
|
|
| 228 | 227 |
sb.extDNS = append(sb.extDNS, extDNSEntry{
|
| 229 |
- IPStr: ip, |
|
| 230 |
- HostLoopback: hostLoopback, |
|
| 228 |
+ IPStr: entry.Addr.String(), |
|
| 229 |
+ HostLoopback: entry.HostLoopback, |
|
| 231 | 230 |
}) |
| 232 | 231 |
} |
| 233 | 232 |
} |
| 234 | 233 |
|
| 235 |
-// isIPv4Loopback checks if the given IP address is an IPv4 loopback address. |
|
| 236 |
-// It's based on the logic in Go's net.IP.IsLoopback(), but only the IPv4 part: |
|
| 237 |
-// https://github.com/golang/go/blob/go1.16.6/src/net/ip.go#L120-L126 |
|
| 238 |
-func isIPv4Loopback(ipAddress string) bool {
|
|
| 239 |
- if ip := net.ParseIP(ipAddress); ip != nil {
|
|
| 240 |
- if ip4 := ip.To4(); ip4 != nil {
|
|
| 241 |
- return ip4[0] == 127 |
|
| 242 |
- } |
|
| 234 |
+func (c *containerConfig) getOriginResolvConfPath() string {
|
|
| 235 |
+ if c.originResolvConfPath != "" {
|
|
| 236 |
+ return c.originResolvConfPath |
|
| 243 | 237 |
} |
| 244 |
- return false |
|
| 238 |
+ // Fallback if not specified. |
|
| 239 |
+ return resolvconf.Path() |
|
| 245 | 240 |
} |
| 246 | 241 |
|
| 247 |
-func (sb *Sandbox) setupDNS() error {
|
|
| 248 |
- if sb.config.resolvConfPath == "" {
|
|
| 249 |
- sb.config.resolvConfPath = defaultPrefix + "/" + sb.id + "/resolv.conf" |
|
| 242 |
+// loadResolvConf reads the resolv.conf file at path, and merges in overrides for |
|
| 243 |
+// nameservers, options, and search domains. |
|
| 244 |
+func (sb *Sandbox) loadResolvConf(path string) (*resolvconf.ResolvConf, error) {
|
|
| 245 |
+ rc, err := resolvconf.Load(path) |
|
| 246 |
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
| 247 |
+ return nil, err |
|
| 248 |
+ } |
|
| 249 |
+ // Proceed with rc, which might be zero-valued if path does not exist. |
|
| 250 |
+ |
|
| 251 |
+ rc.SetHeader(`# Generated by Docker Engine. |
|
| 252 |
+# This file can be edited; Docker Engine will not make further changes once it |
|
| 253 |
+# has been modified.`) |
|
| 254 |
+ if len(sb.config.dnsList) > 0 {
|
|
| 255 |
+ var dnsAddrs []netip.Addr |
|
| 256 |
+ for _, ns := range sb.config.dnsList {
|
|
| 257 |
+ addr, err := netip.ParseAddr(ns) |
|
| 258 |
+ if err != nil {
|
|
| 259 |
+ return nil, errors.Wrapf(err, "bad nameserver address %s", ns) |
|
| 260 |
+ } |
|
| 261 |
+ dnsAddrs = append(dnsAddrs, addr) |
|
| 262 |
+ } |
|
| 263 |
+ rc.OverrideNameServers(dnsAddrs) |
|
| 250 | 264 |
} |
| 265 |
+ if len(sb.config.dnsSearchList) > 0 {
|
|
| 266 |
+ rc.OverrideSearch(sb.config.dnsSearchList) |
|
| 267 |
+ } |
|
| 268 |
+ if len(sb.config.dnsOptionsList) > 0 {
|
|
| 269 |
+ rc.OverrideOptions(sb.config.dnsOptionsList) |
|
| 270 |
+ } |
|
| 271 |
+ return &rc, nil |
|
| 272 |
+} |
|
| 251 | 273 |
|
| 252 |
- sb.config.resolvConfHashFile = sb.config.resolvConfPath + ".hash" |
|
| 253 |
- |
|
| 274 |
+// For a new sandbox, write an initial version of the container's resolv.conf. It'll |
|
| 275 |
+// be a copy of the host's file, with overrides for nameservers, options and search |
|
| 276 |
+// domains applied. |
|
| 277 |
+func (sb *Sandbox) setupDNS() error {
|
|
| 278 |
+ // Make sure the directory exists. |
|
| 279 |
+ sb.restoreResolvConfPath() |
|
| 254 | 280 |
dir, _ := filepath.Split(sb.config.resolvConfPath) |
| 255 | 281 |
if err := createBasePath(dir); err != nil {
|
| 256 | 282 |
return err |
| 257 | 283 |
} |
| 258 | 284 |
|
| 259 |
- // When the user specify a conainter in the host namespace and do no have any dns option specified |
|
| 260 |
- // we just copy the host resolv.conf from the host itself |
|
| 261 |
- if sb.config.useDefaultSandBox && len(sb.config.dnsList) == 0 && len(sb.config.dnsSearchList) == 0 && len(sb.config.dnsOptionsList) == 0 {
|
|
| 262 |
- // We are working under the assumption that the origin file option had been properly expressed by the upper layer |
|
| 263 |
- // if not here we are going to error out |
|
| 264 |
- if err := copyFile(sb.config.originResolvConfPath, sb.config.resolvConfPath); err != nil {
|
|
| 265 |
- if !os.IsNotExist(err) {
|
|
| 266 |
- return fmt.Errorf("could not copy source resolv.conf file %s to %s: %v", sb.config.originResolvConfPath, sb.config.resolvConfPath, err)
|
|
| 267 |
- } |
|
| 268 |
- log.G(context.TODO()).Infof("%s does not exist, we create an empty resolv.conf for container", sb.config.originResolvConfPath)
|
|
| 269 |
- if err := createFile(sb.config.resolvConfPath); err != nil {
|
|
| 270 |
- return err |
|
| 271 |
- } |
|
| 272 |
- } |
|
| 273 |
- return nil |
|
| 274 |
- } |
|
| 275 |
- |
|
| 276 |
- originResolvConfPath := sb.config.originResolvConfPath |
|
| 277 |
- if originResolvConfPath == "" {
|
|
| 278 |
- // fallback if not specified |
|
| 279 |
- originResolvConfPath = resolvconf.Path() |
|
| 280 |
- } |
|
| 281 |
- currRC, err := os.ReadFile(originResolvConfPath) |
|
| 285 |
+ rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) |
|
| 282 | 286 |
if err != nil {
|
| 283 |
- if !os.IsNotExist(err) {
|
|
| 284 |
- return err |
|
| 285 |
- } |
|
| 286 |
- // No /etc/resolv.conf found: we'll use the default resolvers (Google's Public DNS). |
|
| 287 |
- log.G(context.TODO()).WithField("path", originResolvConfPath).Infof("no resolv.conf found, falling back to defaults")
|
|
| 288 |
- } |
|
| 289 |
- |
|
| 290 |
- var newRC *resolvconf.File |
|
| 291 |
- if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(sb.config.dnsOptionsList) > 0 {
|
|
| 292 |
- var ( |
|
| 293 |
- dnsList = sb.config.dnsList |
|
| 294 |
- dnsSearchList = sb.config.dnsSearchList |
|
| 295 |
- dnsOptionsList = sb.config.dnsOptionsList |
|
| 296 |
- ) |
|
| 297 |
- if len(sb.config.dnsList) == 0 {
|
|
| 298 |
- dnsList = resolvconf.GetNameservers(currRC, resolvconf.IP) |
|
| 299 |
- } |
|
| 300 |
- if len(sb.config.dnsSearchList) == 0 {
|
|
| 301 |
- dnsSearchList = resolvconf.GetSearchDomains(currRC) |
|
| 302 |
- } |
|
| 303 |
- if len(sb.config.dnsOptionsList) == 0 {
|
|
| 304 |
- dnsOptionsList = resolvconf.GetOptions(currRC) |
|
| 305 |
- } |
|
| 306 |
- newRC, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList) |
|
| 307 |
- if err != nil {
|
|
| 308 |
- return err |
|
| 309 |
- } |
|
| 310 |
- // After building the resolv.conf from the user config save the |
|
| 311 |
- // external resolvers in the sandbox. Note that --dns 127.0.0.x |
|
| 312 |
- // config refers to the loopback in the container namespace |
|
| 313 |
- sb.setExternalResolvers(newRC.Content, resolvconf.IPv4, len(sb.config.dnsList) == 0) |
|
| 314 |
- } else {
|
|
| 315 |
- // If the host resolv.conf file has 127.0.0.x container should |
|
| 316 |
- // use the host resolver for queries. This is supported by the |
|
| 317 |
- // docker embedded DNS server. Hence save the external resolvers |
|
| 318 |
- // before filtering it out. |
|
| 319 |
- sb.setExternalResolvers(currRC, resolvconf.IPv4, true) |
|
| 320 |
- |
|
| 321 |
- // Replace any localhost/127.* (at this point we have no info about ipv6, pass it as true) |
|
| 322 |
- newRC, err = resolvconf.FilterResolvDNS(currRC, true) |
|
| 323 |
- if err != nil {
|
|
| 324 |
- return err |
|
| 325 |
- } |
|
| 326 |
- // No contention on container resolv.conf file at sandbox creation |
|
| 327 |
- err = os.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm) |
|
| 328 |
- if err != nil {
|
|
| 329 |
- return types.InternalErrorf("failed to write unhaltered resolv.conf file content when setting up dns for sandbox %s: %v", sb.ID(), err)
|
|
| 330 |
- } |
|
| 331 |
- } |
|
| 332 |
- |
|
| 333 |
- // Write hash |
|
| 334 |
- err = os.WriteFile(sb.config.resolvConfHashFile, newRC.Hash, filePerm) |
|
| 335 |
- if err != nil {
|
|
| 336 |
- return types.InternalErrorf("failed to write resolv.conf hash file when setting up dns for sandbox %s: %v", sb.ID(), err)
|
|
| 287 |
+ return err |
|
| 337 | 288 |
} |
| 338 |
- |
|
| 339 |
- return nil |
|
| 289 |
+ return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm) |
|
| 340 | 290 |
} |
| 341 | 291 |
|
| 292 |
+// Called when an endpoint has joined the sandbox. |
|
| 342 | 293 |
func (sb *Sandbox) updateDNS(ipv6Enabled bool) error {
|
| 343 |
- // This is for the host mode networking |
|
| 344 |
- if sb.config.useDefaultSandBox {
|
|
| 345 |
- return nil |
|
| 346 |
- } |
|
| 347 |
- |
|
| 348 |
- if len(sb.config.dnsList) > 0 || len(sb.config.dnsSearchList) > 0 || len(sb.config.dnsOptionsList) > 0 {
|
|
| 349 |
- return nil |
|
| 294 |
+ if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
|
|
| 295 |
+ return err |
|
| 350 | 296 |
} |
| 351 | 297 |
|
| 352 |
- var currHash []byte |
|
| 353 |
- currRC, err := resolvconf.GetSpecific(sb.config.resolvConfPath) |
|
| 298 |
+ // Load the host's resolv.conf as a starting point. |
|
| 299 |
+ rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) |
|
| 354 | 300 |
if err != nil {
|
| 355 |
- if !os.IsNotExist(err) {
|
|
| 356 |
- return err |
|
| 357 |
- } |
|
| 358 |
- } else {
|
|
| 359 |
- currHash, err = os.ReadFile(sb.config.resolvConfHashFile) |
|
| 360 |
- if err != nil && !os.IsNotExist(err) {
|
|
| 361 |
- return err |
|
| 362 |
- } |
|
| 301 |
+ return err |
|
| 363 | 302 |
} |
| 364 |
- |
|
| 365 |
- if len(currHash) > 0 && !bytes.Equal(currHash, currRC.Hash) {
|
|
| 366 |
- // Seems the user has changed the container resolv.conf since the last time |
|
| 367 |
- // we checked so return without doing anything. |
|
| 368 |
- // log.G(ctx).Infof("Skipping update of resolv.conf file with ipv6Enabled: %t because file was touched by user", ipv6Enabled)
|
|
| 369 |
- return nil |
|
| 303 |
+ // For host-networking, no further change is needed. |
|
| 304 |
+ if !sb.config.useDefaultSandBox {
|
|
| 305 |
+ // The legacy bridge network has no internal nameserver. So, strip localhost |
|
| 306 |
+ // nameservers from the host's config, then add default nameservers if there |
|
| 307 |
+ // are none remaining. |
|
| 308 |
+ rc.TransformForLegacyNw(ipv6Enabled) |
|
| 370 | 309 |
} |
| 310 |
+ return rc.WriteFile(sb.config.resolvConfPath, sb.config.resolvConfHashFile, filePerm) |
|
| 311 |
+} |
|
| 371 | 312 |
|
| 372 |
- // replace any localhost/127.* and remove IPv6 nameservers if IPv6 disabled. |
|
| 373 |
- newRC, err := resolvconf.FilterResolvDNS(currRC.Content, ipv6Enabled) |
|
| 374 |
- if err != nil {
|
|
| 375 |
- return err |
|
| 376 |
- } |
|
| 377 |
- err = os.WriteFile(sb.config.resolvConfPath, newRC.Content, filePerm) |
|
| 378 |
- if err != nil {
|
|
| 313 |
+// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's resolv.conf. |
|
| 314 |
+func (sb *Sandbox) rebuildDNS() error {
|
|
| 315 |
+ // Don't touch the file if the user has modified it. |
|
| 316 |
+ if mod, err := resolvconf.UserModified(sb.config.resolvConfPath, sb.config.resolvConfHashFile); err != nil || mod {
|
|
| 379 | 317 |
return err |
| 380 | 318 |
} |
| 381 | 319 |
|
| 382 |
- // write the new hash in a temp file and rename it to make the update atomic |
|
| 383 |
- dir := path.Dir(sb.config.resolvConfPath) |
|
| 384 |
- tmpHashFile, err := os.CreateTemp(dir, "hash") |
|
| 320 |
+ // Load the host's resolv.conf as a starting point. |
|
| 321 |
+ rc, err := sb.loadResolvConf(sb.config.getOriginResolvConfPath()) |
|
| 385 | 322 |
if err != nil {
|
| 386 | 323 |
return err |
| 387 | 324 |
} |
| 388 |
- if err = tmpHashFile.Chmod(filePerm); err != nil {
|
|
| 389 |
- tmpHashFile.Close() |
|
| 390 |
- return err |
|
| 391 |
- } |
|
| 392 |
- _, err = tmpHashFile.Write(newRC.Hash) |
|
| 393 |
- if err1 := tmpHashFile.Close(); err == nil {
|
|
| 394 |
- err = err1 |
|
| 325 |
+ |
|
| 326 |
+ // Check for IPv6 endpoints in this sandbox. If there are any, IPv6 nameservers |
|
| 327 |
+ // will be left in the container's 'resolv.conf'. |
|
| 328 |
+ // TODO(robmry) - preserving old behaviour, but ... |
|
| 329 |
+ // IPv6 nameservers should be treated like IPv4 ones, and used as upstream |
|
| 330 |
+ // servers for the internal resolver (if it has IPv6 connectivity). This |
|
| 331 |
+ // doesn't need to depend on whether there are currently any IPv6 endpoints. |
|
| 332 |
+ // Removing IPv6 nameservers from the container's resolv.conf will avoid the |
|
| 333 |
+ // problem that musl-libc's resolver tries all nameservers in parallel, so an |
|
| 334 |
+ // external IPv6 resolver can return NXDOMAIN before the internal resolver |
|
| 335 |
+ // returns the address of a container. |
|
| 336 |
+ ipv6 := false |
|
| 337 |
+ for _, ep := range sb.endpoints {
|
|
| 338 |
+ if ep.network.enableIPv6 {
|
|
| 339 |
+ ipv6 = true |
|
| 340 |
+ break |
|
| 341 |
+ } |
|
| 395 | 342 |
} |
| 343 |
+ |
|
| 344 |
+ intNS, err := netip.ParseAddr(sb.resolver.NameServer()) |
|
| 396 | 345 |
if err != nil {
|
| 397 | 346 |
return err |
| 398 | 347 |
} |
| 399 |
- return os.Rename(tmpHashFile.Name(), sb.config.resolvConfHashFile) |
|
| 400 |
-} |
|
| 401 | 348 |
|
| 402 |
-// Embedded DNS server has to be enabled for this sandbox. Rebuild the container's |
|
| 403 |
-// resolv.conf by doing the following |
|
| 404 |
-// - Add only the embedded server's IP to container's resolv.conf |
|
| 405 |
-// - If the embedded server needs any resolv.conf options add it to the current list |
|
| 406 |
-func (sb *Sandbox) rebuildDNS() error {
|
|
| 407 |
- currRC, err := os.ReadFile(sb.config.resolvConfPath) |
|
| 349 |
+ // Work out whether ndots has been set from host config or overrides. |
|
| 350 |
+ _, sb.ndotsSet = rc.Option("ndots")
|
|
| 351 |
+ // Swap nameservers for the internal one, and make sure the required options are set. |
|
| 352 |
+ var extNameServers []resolvconf.ExtDNSEntry |
|
| 353 |
+ extNameServers, err = rc.TransformForIntNS(ipv6, intNS, sb.resolver.ResolverOptions()) |
|
| 408 | 354 |
if err != nil {
|
| 409 | 355 |
return err |
| 410 | 356 |
} |
| 411 |
- |
|
| 412 |
- // If the user config and embedded DNS server both have ndots option set, |
|
| 413 |
- // remember the user's config so that unqualified names not in the docker |
|
| 414 |
- // domain can be dropped. |
|
| 415 |
- resOptions := sb.resolver.ResolverOptions() |
|
| 416 |
- dnsOptionsList := resolvconf.GetOptions(currRC) |
|
| 417 |
- |
|
| 418 |
-dnsOpt: |
|
| 419 |
- for _, resOpt := range resOptions {
|
|
| 420 |
- if strings.Contains(resOpt, "ndots") {
|
|
| 421 |
- for _, option := range dnsOptionsList {
|
|
| 422 |
- if strings.Contains(option, "ndots") {
|
|
| 423 |
- parts := strings.Split(option, ":") |
|
| 424 |
- if len(parts) != 2 {
|
|
| 425 |
- return fmt.Errorf("invalid ndots option %v", option)
|
|
| 426 |
- } |
|
| 427 |
- if num, err := strconv.Atoi(parts[1]); err != nil {
|
|
| 428 |
- return fmt.Errorf("invalid number for ndots option: %v", parts[1])
|
|
| 429 |
- } else if num >= 0 {
|
|
| 430 |
- // if the user sets ndots, use the user setting |
|
| 431 |
- sb.ndotsSet = true |
|
| 432 |
- break dnsOpt |
|
| 433 |
- } else {
|
|
| 434 |
- return fmt.Errorf("invalid number for ndots option: %v", num)
|
|
| 435 |
- } |
|
| 436 |
- } |
|
| 437 |
- } |
|
| 438 |
- } |
|
| 439 |
- } |
|
| 440 |
- |
|
| 441 |
- if !sb.ndotsSet {
|
|
| 442 |
- // if the user did not set the ndots, set it to 0 to prioritize the service name resolution |
|
| 443 |
- // Ref: https://linux.die.net/man/5/resolv.conf |
|
| 444 |
- dnsOptionsList = append(dnsOptionsList, resOptions...) |
|
| 445 |
- } |
|
| 446 |
- if len(sb.extDNS) == 0 {
|
|
| 447 |
- sb.setExternalResolvers(currRC, resolvconf.IPv4, false) |
|
| 448 |
- } |
|
| 449 |
- |
|
| 450 |
- var ( |
|
| 451 |
- // external v6 DNS servers have to be listed in resolv.conf |
|
| 452 |
- dnsList = append([]string{sb.resolver.NameServer()}, resolvconf.GetNameservers(currRC, resolvconf.IPv6)...)
|
|
| 453 |
- dnsSearchList = resolvconf.GetSearchDomains(currRC) |
|
| 454 |
- ) |
|
| 455 |
- |
|
| 456 |
- _, err = resolvconf.Build(sb.config.resolvConfPath, dnsList, dnsSearchList, dnsOptionsList) |
|
| 457 |
- return err |
|
| 357 |
+ // Extract the list of nameservers that just got swapped out, and store them as |
|
| 358 |
+ // upstream nameservers. |
|
| 359 |
+ sb.setExternalResolvers(extNameServers) |
|
| 360 |
+ |
|
| 361 |
+ // Write the file for the container - preserving old behaviour, not updating the |
|
| 362 |
+ // hash file (so, no further updates will be made). |
|
| 363 |
+ // TODO(robmry) - I think that's probably accidental, I can't find a reason for it, |
|
| 364 |
+ // and the old resolvconf.Build() function wrote the file but not the hash, which |
|
| 365 |
+ // is surprising. But, before fixing it, a guard/flag needs to be added to |
|
| 366 |
+ // sb.updateDNS() to make sure that when an endpoint joins a sandbox that already |
|
| 367 |
+ // has an internal resolver, the container's resolv.conf is still (re)configured |
|
| 368 |
+ // for an internal resolver. |
|
| 369 |
+ return rc.WriteFile(sb.config.resolvConfPath, "", filePerm) |
|
| 458 | 370 |
} |
| 459 | 371 |
|
| 460 | 372 |
func createBasePath(dir string) error {
|
| 461 | 373 |
return os.MkdirAll(dir, dirPerm) |
| 462 | 374 |
} |
| 463 | 375 |
|
| 464 |
-func createFile(path string) error {
|
|
| 465 |
- var f *os.File |
|
| 466 |
- |
|
| 467 |
- dir, _ := filepath.Split(path) |
|
| 468 |
- err := createBasePath(dir) |
|
| 469 |
- if err != nil {
|
|
| 470 |
- return err |
|
| 471 |
- } |
|
| 472 |
- |
|
| 473 |
- f, err = os.Create(path) |
|
| 474 |
- if err == nil {
|
|
| 475 |
- f.Close() |
|
| 476 |
- } |
|
| 477 |
- |
|
| 478 |
- return err |
|
| 479 |
-} |
|
| 480 |
- |
|
| 481 | 376 |
func copyFile(src, dst string) error {
|
| 482 | 377 |
sBytes, err := os.ReadFile(src) |
| 483 | 378 |
if err != nil {
|
| ... | ... |
@@ -12,7 +12,9 @@ func (sb *Sandbox) setupResolutionFiles() error {
|
| 12 | 12 |
return nil |
| 13 | 13 |
} |
| 14 | 14 |
|
| 15 |
-func (sb *Sandbox) restorePath() {}
|
|
| 15 |
+func (sb *Sandbox) restoreHostsPath() {}
|
|
| 16 |
+ |
|
| 17 |
+func (sb *Sandbox) restoreResolvConfPath() {}
|
|
| 16 | 18 |
|
| 17 | 19 |
func (sb *Sandbox) updateHostsFile(ifaceIP []string) error {
|
| 18 | 20 |
return nil |
| ... | ... |
@@ -206,7 +206,8 @@ func (c *Controller) sandboxCleanup(activeSandboxes map[string]interface{}) erro
|
| 206 | 206 |
isRestore = true |
| 207 | 207 |
opts := val.([]SandboxOption) |
| 208 | 208 |
sb.processOptions(opts...) |
| 209 |
- sb.restorePath() |
|
| 209 |
+ sb.restoreHostsPath() |
|
| 210 |
+ sb.restoreResolvConfPath() |
|
| 210 | 211 |
create = !sb.config.useDefaultSandBox |
| 211 | 212 |
} |
| 212 | 213 |
sb.osSbox, err = osl.NewSandbox(sb.Key(), create, isRestore) |