|
@@ -0,0 +1,142 @@
|
|
|
+package networking
|
|
|
+
|
|
|
+import (
|
|
|
+ "net"
|
|
|
+ "os"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/docker/docker/api/types"
|
|
|
+ "github.com/docker/docker/integration/internal/container"
|
|
|
+ "github.com/docker/docker/integration/internal/network"
|
|
|
+ "github.com/docker/docker/testutil/daemon"
|
|
|
+ "github.com/miekg/dns"
|
|
|
+ "gotest.tools/v3/assert"
|
|
|
+ is "gotest.tools/v3/assert/cmp"
|
|
|
+ "gotest.tools/v3/skip"
|
|
|
+)
|
|
|
+
|
|
|
+// writeTempResolvConf writes a resolv.conf that only contains a single
|
|
|
+// nameserver line, with address addr.
|
|
|
+// It returns the name of the temp file.
|
|
|
+func writeTempResolvConf(t *testing.T, addr string) string {
|
|
|
+ t.Helper()
|
|
|
+ // Not using t.TempDir() here because in rootless mode, while the temporary
|
|
|
+ // directory gets mode 0777, it's a subdir of an 0700 directory owned by root.
|
|
|
+ // So, it's not accessible by the daemon.
|
|
|
+ f, err := os.CreateTemp("", "resolv.conf")
|
|
|
+ assert.NilError(t, err)
|
|
|
+ t.Cleanup(func() { os.Remove(f.Name()) })
|
|
|
+ err = f.Chmod(0644)
|
|
|
+ assert.NilError(t, err)
|
|
|
+ f.Write([]byte("nameserver " + addr + "\n"))
|
|
|
+ return f.Name()
|
|
|
+}
|
|
|
+
|
|
|
+const dnsRespAddr = "10.11.12.13"
|
|
|
+
|
|
|
+// startDaftDNS starts and returns a really, really daft DNS server that only
|
|
|
+// responds to type-A requests, and always with address dnsRespAddr.
|
|
|
+func startDaftDNS(t *testing.T, addr string) *dns.Server {
|
|
|
+ serveDNS := func(w dns.ResponseWriter, query *dns.Msg) {
|
|
|
+ if query.Question[0].Qtype == dns.TypeA {
|
|
|
+ resp := &dns.Msg{}
|
|
|
+ resp.SetReply(query)
|
|
|
+ answer := &dns.A{
|
|
|
+ Hdr: dns.RR_Header{
|
|
|
+ Name: query.Question[0].Name,
|
|
|
+ Rrtype: dns.TypeA,
|
|
|
+ Class: dns.ClassINET,
|
|
|
+ Ttl: 600,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ answer.A = net.ParseIP(dnsRespAddr)
|
|
|
+ resp.Answer = append(resp.Answer, answer)
|
|
|
+ _ = w.WriteMsg(resp)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ conn, err := net.ListenUDP("udp", &net.UDPAddr{
|
|
|
+ IP: net.ParseIP(addr),
|
|
|
+ Port: 53,
|
|
|
+ })
|
|
|
+ assert.NilError(t, err)
|
|
|
+
|
|
|
+ server := &dns.Server{Handler: dns.HandlerFunc(serveDNS), PacketConn: conn}
|
|
|
+ go func() {
|
|
|
+ _ = server.ActivateAndServe()
|
|
|
+ }()
|
|
|
+
|
|
|
+ return server
|
|
|
+}
|
|
|
+
|
|
|
+// Check that when a container is connected to an internal network, DNS
|
|
|
+// requests sent to daemon's internal DNS resolver are not forwarded to
|
|
|
+// an upstream resolver listening on a localhost address.
|
|
|
+// (Assumes the host does not already have a DNS server on 127.0.0.1.)
|
|
|
+func TestInternalNetworkDNS(t *testing.T) {
|
|
|
+ skip.If(t, testEnv.DaemonInfo.OSType == "windows", "No resolv.conf on Windows")
|
|
|
+ skip.If(t, testEnv.IsRootless, "Can't use resolver on host in rootless mode")
|
|
|
+ ctx := setupTest(t)
|
|
|
+
|
|
|
+ // Start a DNS server on the loopback interface.
|
|
|
+ server := startDaftDNS(t, "127.0.0.1")
|
|
|
+ defer server.Shutdown()
|
|
|
+
|
|
|
+ // Set up a temp resolv.conf pointing at that DNS server, and a daemon using it.
|
|
|
+ tmpFileName := writeTempResolvConf(t, "127.0.0.1")
|
|
|
+ d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_RESOLV_CONF_PATH="+tmpFileName))
|
|
|
+ d.StartWithBusybox(t, "--experimental", "--ip6tables")
|
|
|
+ defer d.Stop(t)
|
|
|
+
|
|
|
+ c := d.NewClientT(t)
|
|
|
+ defer c.Close()
|
|
|
+
|
|
|
+ intNetName := "intnet"
|
|
|
+ network.CreateNoError(ctx, t, c, intNetName,
|
|
|
+ network.WithDriver("bridge"),
|
|
|
+ network.WithInternal(),
|
|
|
+ )
|
|
|
+ defer network.RemoveNoError(ctx, t, c, intNetName)
|
|
|
+
|
|
|
+ extNetName := "extnet"
|
|
|
+ network.CreateNoError(ctx, t, c, extNetName,
|
|
|
+ network.WithDriver("bridge"),
|
|
|
+ )
|
|
|
+ defer network.RemoveNoError(ctx, t, c, extNetName)
|
|
|
+
|
|
|
+ // Create a container, initially with external connectivity.
|
|
|
+ // Expect the external DNS server to respond to a request from the container.
|
|
|
+ ctrId := container.Run(ctx, t, c, container.WithNetworkMode(extNetName))
|
|
|
+ defer c.ContainerRemove(ctx, ctrId, types.ContainerRemoveOptions{Force: true})
|
|
|
+ res, err := container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"})
|
|
|
+ assert.NilError(t, err)
|
|
|
+ assert.Check(t, is.Equal(res.ExitCode, 0))
|
|
|
+ assert.Check(t, is.Contains(res.Stdout(), dnsRespAddr))
|
|
|
+
|
|
|
+ // Connect the container to the internal network as well.
|
|
|
+ // External DNS should still be used.
|
|
|
+ err = c.NetworkConnect(ctx, intNetName, ctrId, nil)
|
|
|
+ assert.NilError(t, err)
|
|
|
+ res, err = container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"})
|
|
|
+ assert.NilError(t, err)
|
|
|
+ assert.Check(t, is.Equal(res.ExitCode, 0))
|
|
|
+ assert.Check(t, is.Contains(res.Stdout(), dnsRespAddr))
|
|
|
+
|
|
|
+ // Disconnect from the external network.
|
|
|
+ // Expect no access to the external DNS.
|
|
|
+ err = c.NetworkDisconnect(ctx, extNetName, ctrId, true)
|
|
|
+ assert.NilError(t, err)
|
|
|
+ res, err = container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"})
|
|
|
+ assert.NilError(t, err)
|
|
|
+ assert.Check(t, is.Equal(res.ExitCode, 1))
|
|
|
+ assert.Check(t, is.Contains(res.Stdout(), "SERVFAIL"))
|
|
|
+
|
|
|
+ // Reconnect the external network.
|
|
|
+ // Check that the external DNS server is used again.
|
|
|
+ err = c.NetworkConnect(ctx, extNetName, ctrId, nil)
|
|
|
+ assert.NilError(t, err)
|
|
|
+ res, err = container.Exec(ctx, c, ctrId, []string{"nslookup", "test.example"})
|
|
|
+ assert.NilError(t, err)
|
|
|
+ assert.Check(t, is.Equal(res.ExitCode, 0))
|
|
|
+ assert.Check(t, is.Contains(res.Stdout(), dnsRespAddr))
|
|
|
+}
|