| 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)) |
| } |