Add tests for IPAM Config of default bridge

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2024-08-11 16:53:39 +01:00
parent 4a2bd1085e
commit facb2323a0
2 changed files with 314 additions and 6 deletions

View File

@@ -927,10 +927,7 @@ func driverOptions(config *config.Config) nwconfig.Option {
}
func initBridgeDriver(controller *libnetwork.Controller, cfg config.BridgeConfig) error {
bridgeName := bridge.DefaultBridgeName
if cfg.Iface != "" {
bridgeName = cfg.Iface
}
bridgeName, userManagedBridge := getDefaultBridgeName(cfg)
netOption := map[string]string{
bridge.BridgeName: bridgeName,
bridge.DefaultBridge: strconv.FormatBool(true),
@@ -938,7 +935,6 @@ func initBridgeDriver(controller *libnetwork.Controller, cfg config.BridgeConfig
bridge.EnableIPMasquerade: strconv.FormatBool(cfg.EnableIPMasq),
bridge.EnableICC: strconv.FormatBool(cfg.InterContainerCommunication),
}
// --ip processing
if cfg.DefaultIP != nil {
netOption[bridge.DefaultBindingIP] = cfg.DefaultIP.String()
@@ -986,7 +982,7 @@ func initBridgeDriver(controller *libnetwork.Controller, cfg config.BridgeConfig
}
ipamV4Conf.PreferredPool = ipNet.String()
ipamV4Conf.Gateway = ip.String()
} else if bridgeName == bridge.DefaultBridgeName && ipamV4Conf.PreferredPool != "" {
} else if !userManagedBridge && ipamV4Conf.PreferredPool != "" {
log.G(context.TODO()).Infof("Default bridge (%s) is assigned with an IP address %s. Daemon option --bip can be used to set a preferred IP address", bridgeName, ipamV4Conf.PreferredPool)
}
@@ -1069,6 +1065,28 @@ func initBridgeDriver(controller *libnetwork.Controller, cfg config.BridgeConfig
return nil
}
func getDefaultBridgeName(cfg config.BridgeConfig) (bridgeName string, userManagedBridge bool) {
// cfg.Iface is --bridge, the option to supply a user-managed bridge.
if cfg.Iface != "" {
// The default network will use a user-managed bridge, the daemon will not
// create it, and it is not possible to supply an address using --bip.
return cfg.Iface, true
}
// Without a --bridge, the bridge is "docker0", created and managed by the
// daemon. A --bip (cidr) can be supplied to define the bridge's IP address
// and the network's subnet.
//
// Env var DOCKER_TEST_CREATE_DEFAULT_BRIDGE env var modifies the default
// bridge name. Unlike '--bridge', the bridge does not need to be created
// outside the daemon, and it's still possible to use '--bip'. It is
// intended only for use in moby tests; it may be removed, its behaviour
// may be modified, and it may not do what you want anyway!
if bn := os.Getenv("DOCKER_TEST_CREATE_DEFAULT_BRIDGE"); bn != "" {
return bn, false
}
return bridge.DefaultBridgeName, false
}
// Remove default bridge interface if present (--bridge=none use case)
func removeDefaultBridgeInterface() {
if lnk, err := nlwrap.LinkByName(bridge.DefaultBridgeName); err == nil {

View File

@@ -1,11 +1,18 @@
package daemon // import "github.com/docker/docker/integration/daemon"
import (
"context"
"net"
"testing"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/testutil"
"github.com/docker/docker/testutil/daemon"
"github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
"gotest.tools/v3/skip"
)
func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
@@ -29,6 +36,289 @@ func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
d.StartWithBusybox(ctx, t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24")
}
// Test fixed-cidr and bip options, with various addresses on the bridge
// before the daemon starts.
func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
skip.If(t, testEnv.IsRootless, "can't create test bridge in rootless namespace")
ctx := testutil.StartSpan(baseContext, t)
testcases := []defaultBridgeIPAMTestCase{
{
name: "fixed-cidr only",
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
IPRange: "192.168.176.0/24",
},
},
},
{
name: "bip only",
daemonArgs: []string{"--bip", "192.168.176.88/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
Gateway: "192.168.176.88",
},
},
},
{
name: "existing bridge address only",
initialBridgeAddrs: []string{"192.168.176.88/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
Gateway: "192.168.176.88",
},
},
},
{
name: "fixed-cidr within old bridge subnet",
initialBridgeAddrs: []string{"192.168.176.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
// There's no --bip to dictate the subnet, so it's derived from an
// existing bridge address. If fixed-cidr's subnet is made smaller
// following a daemon restart, a user might reasonably expect the
// default bridge network's subnet to shrink to match. However,
// that has not been the behaviour - instead, only the allocatable
// range is reduced (as would happen with a user-managed bridge).
// In this case, if the user wants a smaller subnet, their options
// are to delete docker0, or supply a --bip. A change in this subtle
// behaviour might be best. But, it's probably not causing problems,
// and it'd be a breaking change for anyone relying on the existing
// behaviour.
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/20",
IPRange: "192.168.176.0/24",
Gateway: "192.168.176.88",
},
},
},
{
name: "fixed-cidr within old bridge subnet with new bip",
initialBridgeAddrs: []string{"192.168.176.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24", "--bip", "192.168.176.99/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
IPRange: "192.168.176.0/24",
Gateway: "192.168.176.99",
},
},
},
{
name: "old bridge subnet within fixed-cidr",
initialBridgeAddrs: []string{"192.168.176.88/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/20"},
expIPAMConfig: []network.IPAMConfig{
{
// FIXME(robmry) - subnet didn't change, allocatable range
// is bigger than the subnet.
Subnet: "192.168.176.0/24",
IPRange: "192.168.176.0/20",
Gateway: "192.168.176.88",
},
},
},
{
name: "old bridge subnet outside fixed-cidr",
initialBridgeAddrs: []string{"192.168.176.88/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.177.0/24"},
expIPAMConfig: []network.IPAMConfig{
{
// FIXME(robmry) - subnet and bridge address haven't changed,
// and the allocatable range is outside the subnet.
Subnet: "192.168.176.0/24",
IPRange: "192.168.177.0/24",
Gateway: "192.168.176.88",
},
},
},
{
name: "old bridge subnet outside fixed-cidr with bip",
initialBridgeAddrs: []string{"192.168.176.88/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.177.0/24", "--bip", "192.168.177.99/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.177.0/24",
IPRange: "192.168.177.0/24",
Gateway: "192.168.177.99",
},
},
},
}
for _, tc := range testcases {
testDefaultBridgeIPAM(ctx, t, tc)
}
}
// Like TestDaemonUserDefaultBridgeIPAMDocker0, but with a user-defined/supplied
// bridge, instead of docker0.
func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
skip.If(t, testEnv.IsRootless, "can't create test bridge in rootless namespace")
ctx := testutil.StartSpan(baseContext, t)
testcases := []defaultBridgeIPAMTestCase{
{
name: "bridge only",
initialBridgeAddrs: []string{"192.168.176.88/20"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/20",
Gateway: "192.168.176.88",
},
},
},
{
name: "fixed-cidr only",
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
IPRange: "192.168.176.0/24",
},
},
},
{
name: "fcidr in bridge subnet and bridge ip in fcidr",
initialBridgeAddrs: []string{"192.168.160.88/20", "192.168.176.88/20", "192.168.192.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
// Selected bip should be the one within fixed-cidr
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/20",
IPRange: "192.168.176.0/24",
Gateway: "192.168.176.88",
},
},
},
{
name: "fcidr in bridge subnet and bridge ip not in fcidr",
initialBridgeAddrs: []string{"192.168.160.88/20", "192.168.176.88/20", "192.168.192.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.177.0/24"},
expIPAMConfig: []network.IPAMConfig{
{
// FIXME(robmry) - selected subnet should be the one that encompasses
// fixed-cidr, allocatable range is outside the subnet.
Subnet: "192.168.160.0/20",
IPRange: "192.168.177.0/24",
Gateway: "192.168.160.88",
},
},
},
{
name: "fixed-cidr bigger than bridge subnet",
initialBridgeAddrs: []string{"192.168.176.88/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/20"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.176.0/24",
// FIXME(robmry) - allocatable range is bigger than the subnet.
IPRange: "192.168.176.0/20",
Gateway: "192.168.176.88",
},
},
},
{
name: "no bridge ip within fixed-cidr",
initialBridgeAddrs: []string{"192.168.160.88/20", "192.168.192.88/20"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/24"},
// Selected bip should be the one within fixed-cidr
expIPAMConfig: []network.IPAMConfig{
{
// FIXME(robmry) - allocatable range outside subnet.
Subnet: "192.168.160.0/20",
IPRange: "192.168.176.0/24",
Gateway: "192.168.160.88",
},
},
},
{
name: "fixed-cidr contains bridge subnet",
initialBridgeAddrs: []string{"192.168.177.1/24"},
daemonArgs: []string{"--fixed-cidr", "192.168.176.0/20"},
expIPAMConfig: []network.IPAMConfig{
{
Subnet: "192.168.177.0/24",
// FIXME(robmry) - allocatable range is bigger than subnet
IPRange: "192.168.176.0/20",
Gateway: "192.168.177.1",
},
},
},
}
for _, tc := range testcases {
tc.userDefinedBridge = true
testDefaultBridgeIPAM(ctx, t, tc)
}
}
type defaultBridgeIPAMTestCase struct {
name string
userDefinedBridge bool
initialBridgeAddrs []string
daemonArgs []string
expIPAMConfig []network.IPAMConfig
}
func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIPAMTestCase) {
t.Run(tc.name, func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
const bridgeName = "br-dbi"
createBridge(t, bridgeName, tc.initialBridgeAddrs)
defer deleteInterface(t, bridgeName)
var dOpts []daemon.Option
dArgs := tc.daemonArgs
if tc.userDefinedBridge {
// If a bridge is supplied by the user, the daemon should use its addresses
// to infer --bip (which cannot be specified).
dArgs = append(dArgs, []string{"--bridge", bridgeName}...)
} else {
// The bridge is created and managed by docker, it's always called "docker0",
// unless this test-only env var is set - to avoid conflict with the docker0
// belonging to the daemon started in CI runs.
dOpts = append(dOpts, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
}
d := daemon.New(t, dOpts...)
defer func() {
d.Stop(t)
d.Cleanup(t)
}()
d.StartWithBusybox(ctx, t, dArgs...)
c := d.NewClientT(t)
defer c.Close()
insp, err := c.NetworkInspect(ctx, network.NetworkBridge, network.InspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(insp.IPAM.Config, tc.expIPAMConfig))
})
}
func createBridge(t *testing.T, ifName string, addrs []string) {
t.Helper()
link := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: ifName,
},
}
err := netlink.LinkAdd(link)
assert.NilError(t, err)
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr)
assert.NilError(t, err)
ipNet.IP = ip
err = netlink.AddrAdd(link, &netlink.Addr{IPNet: ipNet})
assert.NilError(t, err)
}
}
func deleteInterface(t *testing.T, ifName string) {
icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success)
icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success)