moby/integration/networking/bridge_test.go
Rob Murray 27f3abd893 Allow overlapping change in bridge's IPv6 network.
Calculate the IPv6 addreesses needed on a bridge, then reconcile them
with the addresses on an existing bridge by deleting then adding as
required.

(Previously, required addresses were added one-by-one, then unwanted
addresses were removed. This meant the daemon failed to start if, for
example, an existing bridge had address '2000:db8::/64' and the config
was changed to '2000:db8::/80'.)

IPv6 addresses are now calculated and applied in one go, so there's no
need for setupVerifyAndReconcile() to check the set of IPv6 addresses on
the bridge. And, it was guarded by !config.InhibitIPv4, which can't have
been right. So, removed its IPv6 parts, and added IPv4 to its name.

Link local addresses, the example given in the original ticket, are now
released when containers are stopped. Not releasing them meant that
when using an LL subnet on the default bridge, no container could be
started after a container was stopped (because the calculated address
could not be re-allocated). In non-default bridge networks using an
LL subnet, addresses leaked.

Linux always uses the standard 'fe80::/64' LL network. So, if a bridge
is configured with an LL subnet prefix that overlaps with it, a config
error is reported. Non-overlapping LL subnet prefixes are allowed.

Signed-off-by: Rob Murray <rob.murray@docker.com>
2023-12-18 16:10:41 +00:00

479 lines
15 KiB
Go

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