package networking import ( "context" "fmt" "testing" "time" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/integration/internal/network" "github.com/docker/docker/testutil" "github.com/docker/docker/testutil/daemon" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" ) // TestBridgeICC tries to ping container ctr1 from container ctr2 using its hostname. Thus, this test checks: // 1. DNS resolution ; 2. ARP/NDP ; 3. whether containers can communicate with each other ; 4. kernel-assigned SLAAC // addresses. func TestBridgeICC(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) d := daemon.New(t) d.StartWithBusybox(ctx, t, "-D", "--experimental", "--ip6tables") defer d.Stop(t) c := d.NewClientT(t) defer c.Close() testcases := []struct { name string bridgeOpts []func(*types.NetworkCreate) ctr1MacAddress string linkLocal bool pingHost string }{ { name: "IPv4 non-internal network", bridgeOpts: []func(*types.NetworkCreate){}, }, { name: "IPv4 internal network", bridgeOpts: []func(*types.NetworkCreate){ network.WithInternal(), }, }, { name: "IPv6 ULA on non-internal network", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fdf1:a844:380c:b200::/64", "fdf1:a844:380c:b200::1"), }, }, { name: "IPv6 ULA on internal network", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithInternal(), network.WithIPAM("fdf1:a844:380c:b247::/64", "fdf1:a844:380c:b247::1"), }, }, { name: "IPv6 link-local address on non-internal network", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), // There's no real way to specify an IPv6 network is only used with SLAAC link-local IPv6 addresses. // What we can do instead, is to tell the IPAM driver to assign addresses from the link-local prefix. // Each container will have two link-local addresses: 1. a SLAAC address assigned by the kernel ; // 2. the one dynamically assigned by the IPAM driver. network.WithIPAM("fe80::/64", "fe80::1"), }, linkLocal: true, }, { name: "IPv6 link-local address on internal network", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithInternal(), // See the note above about link-local addresses. network.WithIPAM("fe80::/64", "fe80::1"), }, linkLocal: true, }, { // As for 'LL non-internal', but ping the container by name instead of by address // - the busybox test containers only have one interface with a link local // address, so the zone index is not required: // RFC-4007, section 6: "[...] for nodes with only a single non-loopback // interface (e.g., a single Ethernet interface), the common case, link-local // addresses need not be qualified with a zone index." // So, for this common case, LL addresses should be included in DNS config. name: "IPv6 link-local address on non-internal network ping by name", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fe80::/64", "fe80::1"), }, }, { name: "IPv6 nonstandard link-local subnet on non-internal network ping by name", // No interfaces apart from the one on the bridge network with this non-default // subnet will be on this link local subnet (it's not currently possible to // configure two networks with the same LL subnet, although perhaps it should // be). So, again, no zone index is required and the LL address should be // included in DNS config. bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fe80:1234::/64", "fe80:1234::1"), }, }, { name: "IPv6 non-internal network with SLAAC LL address", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fdf1:a844:380c:b247::/64", "fdf1:a844:380c:b247::1"), }, // Link-local address is derived from the MAC address, so we need to // specify one here to hardcode the SLAAC LL address below. ctr1MacAddress: "02:42:ac:11:00:02", pingHost: "fe80::42:acff:fe11:2%eth0", }, { name: "IPv6 internal network with SLAAC LL address", bridgeOpts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fdf1:a844:380c:b247::/64", "fdf1:a844:380c:b247::1"), }, // Link-local address is derived from the MAC address, so we need to // specify one here to hardcode the SLAAC LL address below. ctr1MacAddress: "02:42:ac:11:00:02", pingHost: "fe80::42:acff:fe11:2%eth0", }, } for tcID, tc := range testcases { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) bridgeName := fmt.Sprintf("testnet-icc-%d", tcID) network.CreateNoError(ctx, t, c, bridgeName, append(tc.bridgeOpts, network.WithDriver("bridge"), network.WithOption("com.docker.network.bridge.name", bridgeName))...) defer network.RemoveNoError(ctx, t, c, bridgeName) ctr1Name := fmt.Sprintf("ctr-icc-%d-1", tcID) var ctr1Opts []func(config *container.TestContainerConfig) if tc.ctr1MacAddress != "" { ctr1Opts = append(ctr1Opts, container.WithMacAddress(bridgeName, tc.ctr1MacAddress)) } id1 := container.Run(ctx, t, c, append(ctr1Opts, container.WithName(ctr1Name), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(bridgeName))...) defer c.ContainerRemove(ctx, id1, containertypes.RemoveOptions{ Force: true, }) pingHost := tc.pingHost if pingHost == "" { if tc.linkLocal { inspect := container.Inspect(ctx, t, c, id1) pingHost = inspect.NetworkSettings.Networks[bridgeName].GlobalIPv6Address + "%eth0" } else { pingHost = ctr1Name } } pingCmd := []string{"ping", "-c1", "-W3", pingHost} ctr2Name := fmt.Sprintf("ctr-icc-%d-2", tcID) attachCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res := container.RunAttach(attachCtx, t, c, container.WithName(ctr2Name), container.WithImage("busybox:latest"), container.WithCmd(pingCmd...), container.WithNetworkMode(bridgeName)) defer c.ContainerRemove(ctx, res.ContainerID, containertypes.RemoveOptions{ Force: true, }) assert.Check(t, is.Equal(res.ExitCode, 0)) assert.Check(t, is.Equal(res.Stderr.Len(), 0)) assert.Check(t, is.Contains(res.Stdout.String(), "1 packets transmitted, 1 packets received")) }) } } // TestBridgeINC makes sure two containers on two different bridge networks can't communicate with each other. func TestBridgeINC(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) d := daemon.New(t) d.StartWithBusybox(ctx, t, "-D", "--experimental", "--ip6tables") defer d.Stop(t) c := d.NewClientT(t) defer c.Close() type bridgesOpts struct { bridge1Opts []func(*types.NetworkCreate) bridge2Opts []func(*types.NetworkCreate) } testcases := []struct { name string bridges bridgesOpts ipv6 bool stdout string stderr string }{ { name: "IPv4 non-internal network", bridges: bridgesOpts{ bridge1Opts: []func(*types.NetworkCreate){}, bridge2Opts: []func(*types.NetworkCreate){}, }, stdout: "1 packets transmitted, 0 packets received", }, { name: "IPv4 internal network", bridges: bridgesOpts{ bridge1Opts: []func(*types.NetworkCreate){network.WithInternal()}, bridge2Opts: []func(*types.NetworkCreate){network.WithInternal()}, }, stderr: "sendto: Network is unreachable", }, { name: "IPv6 ULA on non-internal network", bridges: bridgesOpts{ bridge1Opts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fdf1:a844:380c:b200::/64", "fdf1:a844:380c:b200::1"), }, bridge2Opts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithIPAM("fdf1:a844:380c:b247::/64", "fdf1:a844:380c:b247::1"), }, }, ipv6: true, stdout: "1 packets transmitted, 0 packets received", }, { name: "IPv6 ULA on internal network", bridges: bridgesOpts{ bridge1Opts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithInternal(), network.WithIPAM("fdf1:a844:390c:b200::/64", "fdf1:a844:390c:b200::1"), }, bridge2Opts: []func(*types.NetworkCreate){ network.WithIPv6(), network.WithInternal(), network.WithIPAM("fdf1:a844:390c:b247::/64", "fdf1:a844:390c:b247::1"), }, }, ipv6: true, stderr: "sendto: Network is unreachable", }, } for tcID, tc := range testcases { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) bridge1 := fmt.Sprintf("testnet-inc-%d-1", tcID) bridge2 := fmt.Sprintf("testnet-inc-%d-2", tcID) network.CreateNoError(ctx, t, c, bridge1, append(tc.bridges.bridge1Opts, network.WithDriver("bridge"), network.WithOption("com.docker.network.bridge.name", bridge1))...) defer network.RemoveNoError(ctx, t, c, bridge1) network.CreateNoError(ctx, t, c, bridge2, append(tc.bridges.bridge2Opts, network.WithDriver("bridge"), network.WithOption("com.docker.network.bridge.name", bridge2))...) defer network.RemoveNoError(ctx, t, c, bridge2) ctr1Name := sanitizeCtrName(t.Name() + "-ctr1") id1 := container.Run(ctx, t, c, container.WithName(ctr1Name), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(bridge1)) defer c.ContainerRemove(ctx, id1, containertypes.RemoveOptions{ Force: true, }) ctr1Info := container.Inspect(ctx, t, c, id1) targetAddr := ctr1Info.NetworkSettings.Networks[bridge1].IPAddress if tc.ipv6 { targetAddr = ctr1Info.NetworkSettings.Networks[bridge1].GlobalIPv6Address } pingCmd := []string{"ping", "-c1", "-W3", targetAddr} ctr2Name := sanitizeCtrName(t.Name() + "-ctr2") attachCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res := container.RunAttach(attachCtx, t, c, container.WithName(ctr2Name), container.WithImage("busybox:latest"), container.WithCmd(pingCmd...), container.WithNetworkMode(bridge2)) defer c.ContainerRemove(ctx, res.ContainerID, containertypes.RemoveOptions{ Force: true, }) assert.Check(t, res.ExitCode != 0, "ping unexpectedly succeeded") assert.Check(t, is.Contains(res.Stdout.String(), tc.stdout)) assert.Check(t, is.Contains(res.Stderr.String(), tc.stderr)) }) } } func TestDefaultBridgeIPv6(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) testcases := []struct { name string fixed_cidr_v6 string }{ { name: "IPv6 ULA", fixed_cidr_v6: "fd00:1234::/64", }, { name: "IPv6 LLA only", fixed_cidr_v6: "fe80::/64", }, { name: "IPv6 nonstandard LLA only", fixed_cidr_v6: "fe80:1234::/64", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) d := daemon.New(t) d.StartWithBusybox(ctx, t, "--experimental", "--ip6tables", "--ipv6", "--fixed-cidr-v6", tc.fixed_cidr_v6, ) defer d.Stop(t) c := d.NewClientT(t) defer c.Close() cID := container.Run(ctx, t, c, container.WithImage("busybox:latest"), container.WithCmd("top"), ) defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{ Force: true, }) networkName := "bridge" inspect := container.Inspect(ctx, t, c, cID) pingHost := inspect.NetworkSettings.Networks[networkName].GlobalIPv6Address attachCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() res := container.RunAttach(attachCtx, t, c, container.WithImage("busybox:latest"), container.WithCmd("ping", "-c1", "-W3", pingHost), ) defer c.ContainerRemove(ctx, res.ContainerID, containertypes.RemoveOptions{ Force: true, }) assert.Check(t, is.Equal(res.ExitCode, 0)) assert.Check(t, is.Equal(res.Stderr.String(), "")) assert.Check(t, is.Contains(res.Stdout.String(), "1 packets transmitted, 1 packets received")) }) } } // Check that it's possible to change 'fixed-cidr-v6' and restart the daemon. func TestDefaultBridgeAddresses(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) d := daemon.New(t) type testStep struct { stepName string fixedCIDRV6 string expAddrs []string } testcases := []struct { name string steps []testStep }{ { name: "Unique-Local Subnet Changes", steps: []testStep{ { stepName: "Set up initial UL prefix", fixedCIDRV6: "fd1c:f1a0:5d8d:aaaa::/64", expAddrs: []string{"fd1c:f1a0:5d8d:aaaa::1/64", "fe80::1/64"}, }, { // Modify that prefix, the default bridge's address must be deleted and re-added. stepName: "Modify UL prefix - address change", fixedCIDRV6: "fd1c:f1a0:5d8d:bbbb::/64", expAddrs: []string{"fd1c:f1a0:5d8d:bbbb::1/64", "fe80::1/64"}, }, { // Modify the prefix length, the default bridge's address should not change. stepName: "Modify UL prefix - no address change", fixedCIDRV6: "fd1c:f1a0:5d8d:bbbb::/80", // The prefix length displayed by 'ip a' is not updated - it's informational, and // can't be changed without unnecessarily deleting and re-adding the address. expAddrs: []string{"fd1c:f1a0:5d8d:bbbb::1/64", "fe80::1/64"}, }, }, }, { name: "Link-Local Subnet Changes", steps: []testStep{ { stepName: "Standard LL subnet prefix", fixedCIDRV6: "fe80::/64", expAddrs: []string{"fe80::1/64"}, }, { // Modify that prefix, the default bridge's address must be deleted and re-added. // The bridge must still have an address in the required (standard) LL subnet. stepName: "Nonstandard LL prefix - address change", fixedCIDRV6: "fe80:1234::/32", expAddrs: []string{"fe80:1234::1/32", "fe80::1/64"}, }, { // Modify the prefix length, the addresses should not change. stepName: "Modify LL prefix - no address change", fixedCIDRV6: "fe80:1234::/64", // The prefix length displayed by 'ip a' is not updated - it's informational, and // can't be changed without unnecessarily deleting and re-adding the address. expAddrs: []string{"fe80:1234::1/", "fe80::1/64"}, }, }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { for _, step := range tc.steps { // Check that the daemon starts - regression test for: // https://github.com/moby/moby/issues/46829 d.Start(t, "--experimental", "--ipv6", "--ip6tables", "--fixed-cidr-v6="+step.fixedCIDRV6) d.Stop(t) // Check that the expected addresses have been applied to the bridge. (Skip in // rootless mode, because the bridge is in a different network namespace.) if !testEnv.IsRootless() { res := testutil.RunCommand(ctx, "ip", "-6", "addr", "show", "docker0") assert.Equal(t, res.ExitCode, 0, step.stepName) stdout := res.Stdout() for _, expAddr := range step.expAddrs { assert.Check(t, is.Contains(stdout, expAddr)) } } } }) } } // Test that a container on an 'internal' network has IP connectivity with // the host (on its own subnet, because the n/w bridge has an address on that // subnet, and it's in the host's namespace). // Regression test for https://github.com/moby/moby/issues/47329 func TestInternalNwConnectivity(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) d := daemon.New(t) d.StartWithBusybox(ctx, t, "-D", "--experimental", "--ip6tables") defer d.Stop(t) c := d.NewClientT(t) defer c.Close() const bridgeName = "intnw" const gw4 = "172.30.0.1" const gw6 = "fda9:4130:4715::1234" network.CreateNoError(ctx, t, c, bridgeName, network.WithInternal(), network.WithIPv6(), network.WithIPAM("172.30.0.0/24", gw4), network.WithIPAM("fda9:4130:4715::/64", gw6), network.WithDriver("bridge"), network.WithOption("com.docker.network.bridge.name", bridgeName), ) defer network.RemoveNoError(ctx, t, c, bridgeName) const ctrName = "intctr" id := container.Run(ctx, t, c, container.WithName(ctrName), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(bridgeName), ) defer c.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true}) execCtx, cancel := context.WithTimeout(ctx, 20*time.Second) defer cancel() res := container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", gw4}) assert.Check(t, is.Equal(res.ExitCode, 0)) assert.Check(t, is.Equal(res.Stderr(), "")) assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received")) res = container.ExecT(execCtx, t, c, id, []string{"ping6", "-c1", "-W3", gw6}) assert.Check(t, is.Equal(res.ExitCode, 0)) assert.Check(t, is.Equal(res.Stderr(), "")) assert.Check(t, is.Contains(res.Stdout(), "1 packets transmitted, 1 packets received")) // Addresses outside the internal subnet must not be accessible. res = container.ExecT(execCtx, t, c, id, []string{"ping", "-c1", "-W3", "8.8.8.8"}) assert.Check(t, is.Equal(res.ExitCode, 1)) assert.Check(t, is.Contains(res.Stderr(), "Network is unreachable")) }