moby/integration/networking/bridge_test.go
Rob Murray ef5295cda4 Don't configure IPv6 addr/gw when IPv6 disabled.
When IPv6 is disabled in a container by, for example, using the --sysctl
option - an IPv6 address/gateway is still allocated. Don't attempt to
apply that config because doing so enables IPv6 on the interface.

Signed-off-by: Rob Murray <rob.murray@docker.com>
2024-03-06 18:32:31 +00:00

663 lines
20 KiB
Go

package networking
import (
"context"
"fmt"
"regexp"
"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"
"github.com/google/go-cmp/cmp/cmpopts"
"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"))
})
}
}
// TestBridgeICCWindows tries to ping container ctr1 from container ctr2 using its hostname.
// Checks DNS resolution, and whether containers can communicate with each other.
// Regression test for https://github.com/moby/moby/issues/47370
func TestBridgeICCWindows(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType != "windows")
ctx := setupTest(t)
c := testEnv.APIClient()
testcases := []struct {
name string
netName string
}{
{
name: "Default nat network",
netName: "nat",
},
{
name: "User defined nat network",
netName: "mynat",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
if tc.netName != "nat" {
network.CreateNoError(ctx, t, c, tc.netName,
network.WithDriver("nat"),
)
defer network.RemoveNoError(ctx, t, c, tc.netName)
}
const ctr1Name = "ctr1"
id1 := container.Run(ctx, t, c,
container.WithName(ctr1Name),
container.WithNetworkMode(tc.netName),
)
defer c.ContainerRemove(ctx, id1, containertypes.RemoveOptions{Force: true})
pingCmd := []string{"ping", "-n", "1", "-w", "3000", ctr1Name}
const ctr2Name = "ctr2"
attachCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
res := container.RunAttach(attachCtx, t, c,
container.WithName(ctr2Name),
container.WithCmd(pingCmd...),
container.WithNetworkMode(tc.netName),
)
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(), "Sent = 1, Received = 1, Lost = 0"))
})
}
}
// 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"))
}
// Check that the container's interface has no IPv6 address when IPv6 is
// disabled in a container via sysctl.
func TestDisableIPv6Addrs(t *testing.T) {
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
ctx := setupTest(t)
d := daemon.New(t)
d.StartWithBusybox(ctx, t)
defer d.Stop(t)
c := d.NewClientT(t)
defer c.Close()
testcases := []struct {
name string
sysctls map[string]string
expIPv6 bool
}{
{
name: "IPv6 enabled",
expIPv6: true,
},
{
name: "IPv6 disabled",
sysctls: map[string]string{"net.ipv6.conf.all.disable_ipv6": "1"},
},
}
const netName = "testnet"
network.CreateNoError(ctx, t, c, netName,
network.WithIPv6(),
network.WithIPAM("fda0:ef3d:6430:abcd::/64", "fda0:ef3d:6430:abcd::1"),
)
defer network.RemoveNoError(ctx, t, c, netName)
inet6RE := regexp.MustCompile(`inet6[ \t]+[0-9a-f:]*`)
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
opts := []func(config *container.TestContainerConfig){
container.WithCmd("ip", "a"),
container.WithNetworkMode(netName),
}
if len(tc.sysctls) > 0 {
opts = append(opts, container.WithSysctls(tc.sysctls))
}
runRes := container.RunAttach(ctx, t, c, opts...)
defer c.ContainerRemove(ctx, runRes.ContainerID,
containertypes.RemoveOptions{Force: true},
)
stdout := runRes.Stdout.String()
inet6 := inet6RE.FindAllString(stdout, -1)
if tc.expIPv6 {
assert.Check(t, len(inet6) > 0, "Expected IPv6 addresses but found none.")
} else {
assert.Check(t, is.DeepEqual(inet6, []string{}, cmpopts.EquateEmpty()))
}
})
}
}