package networking import ( "context" "encoding/json" "net/http" "net/netip" "slices" "testing" containertypes "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "github.com/moby/moby/v2/daemon/libnetwork/drivers/bridge" "github.com/moby/moby/v2/integration/internal/container" "github.com/moby/moby/v2/integration/internal/network" "github.com/moby/moby/v2/internal/testutil" "github.com/moby/moby/v2/internal/testutil/daemon" "github.com/moby/moby/v2/internal/testutil/request" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" ) // TestMACAddrOnRestart is a regression test for https://github.com/moby/moby/issues/47146 // - Start a container, let it use a generated MAC address. // - Stop that container. // - Start a second container, it'll also use a generated MAC address. // (It's likely to recycle the first container's MAC address.) // - Restart the first container. // (The bug was that it kept its original MAC address, now already in-use.) // - Check that the two containers have different MAC addresses. func TestMACAddrOnRestart(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() const netName = "testmacaddrs" network.CreateNoError(ctx, t, c, netName, network.WithDriver("bridge"), network.WithOption(bridge.BridgeName, netName)) defer network.RemoveNoError(ctx, t, c, netName) const ctr1Name = "ctr1" id1 := container.Run(ctx, t, c, container.WithName(ctr1Name), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(netName)) defer c.ContainerRemove(ctx, id1, client.ContainerRemoveOptions{ Force: true, }) _, err := c.ContainerStop(ctx, ctr1Name, client.ContainerStopOptions{}) assert.Assert(t, is.Nil(err)) // Start a second container, giving the daemon a chance to recycle the first container's // IP and MAC addresses. const ctr2Name = "ctr2" id2 := container.Run(ctx, t, c, container.WithName(ctr2Name), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(netName)) defer c.ContainerRemove(ctx, id2, client.ContainerRemoveOptions{ Force: true, }) // Restart the first container. _, err = c.ContainerStart(ctx, ctr1Name, client.ContainerStartOptions{}) assert.Assert(t, is.Nil(err)) // Check that the containers ended up with different MAC addresses. ctr1Inspect := container.Inspect(ctx, t, c, ctr1Name) ctr1MAC := ctr1Inspect.NetworkSettings.Networks[netName].MacAddress ctr2Inspect := container.Inspect(ctx, t, c, ctr2Name) ctr2MAC := ctr2Inspect.NetworkSettings.Networks[netName].MacAddress assert.Check(t, !slices.Equal(ctr1MAC, ctr2MAC), "expected containers to have different MAC addresses; got %q for both", ctr1MAC) } // Check that a configured MAC address is restored after a container restart, // and after a daemon restart. func TestCfgdMACAddrOnRestart(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() const netName = "testcfgmacaddr" network.CreateNoError(ctx, t, c, netName, network.WithDriver("bridge"), network.WithOption(bridge.BridgeName, netName)) defer network.RemoveNoError(ctx, t, c, netName) const wantMAC = "02:42:ac:11:00:42" const ctr1Name = "ctr1" id1 := container.Run(ctx, t, c, container.WithName(ctr1Name), container.WithImage("busybox:latest"), container.WithCmd("top"), container.WithNetworkMode(netName), container.WithMacAddress(netName, wantMAC)) defer c.ContainerRemove(ctx, id1, client.ContainerRemoveOptions{ Force: true, }) inspect := container.Inspect(ctx, t, c, ctr1Name) gotMAC := inspect.NetworkSettings.Networks[netName].MacAddress assert.Check(t, is.Equal(wantMAC, gotMAC.String())) startAndCheck := func() { t.Helper() _, err := c.ContainerStart(ctx, ctr1Name, client.ContainerStartOptions{}) assert.Assert(t, is.Nil(err)) inspect = container.Inspect(ctx, t, c, ctr1Name) gotMAC = inspect.NetworkSettings.Networks[netName].MacAddress assert.Check(t, is.Equal(wantMAC, gotMAC.String())) } // Restart the container, check that the MAC address is restored. _, err := c.ContainerStop(ctx, ctr1Name, client.ContainerStopOptions{}) assert.Assert(t, is.Nil(err)) startAndCheck() // Restart the daemon, check that the MAC address is restored. _, err = c.ContainerStop(ctx, ctr1Name, client.ContainerStopOptions{}) assert.Assert(t, is.Nil(err)) d.Restart(t) startAndCheck() } // Regression test for https://github.com/moby/moby/issues/47228 - check that a // generated MAC address is not included in the Config section of 'inspect' // output, but a configured address is. func TestInspectCfgdMAC(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") ctx := setupTest(t) d := daemon.New(t, daemon.WithEnvVars("DOCKER_MIN_API_VERSION=1.43")) d.StartWithBusybox(ctx, t) defer d.Stop(t) testcases := []struct { name string desiredMAC string netName string ctrWide bool }{ { name: "generated address default bridge", netName: "bridge", }, { name: "configured address default bridge", desiredMAC: "02:42:ac:11:00:42", netName: "bridge", }, { name: "generated address custom bridge", netName: "testnet", }, { name: "configured address custom bridge", desiredMAC: "02:42:ac:11:00:42", netName: "testnet", }, { name: "ctr-wide address default bridge", desiredMAC: "02:42:ac:11:00:42", netName: "bridge", ctrWide: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { ctx := testutil.StartSpan(ctx, t) var copts []client.Opt if tc.ctrWide { copts = append(copts, client.WithAPIVersion("1.43")) } else { copts = append(copts, client.WithAPIVersion("1.51")) } c := d.NewClientT(t, copts...) defer c.Close() if tc.netName != "bridge" { const netName = "inspectcfgmac" network.CreateNoError(ctx, t, c, netName, network.WithDriver("bridge"), network.WithOption(bridge.BridgeName, netName)) defer network.RemoveNoError(ctx, t, c, netName) } const ctrName = "ctr" opts := []func(*container.TestContainerConfig){ container.WithName(ctrName), container.WithCmd("top"), container.WithImage("busybox:latest"), } // Don't specify the network name for the bridge network, because that // exercises a different code path (the network name isn't set until the // container starts, until then it's "default"). if tc.netName != "bridge" { opts = append(opts, container.WithNetworkMode(tc.netName)) } var id string if tc.desiredMAC != "" { if tc.ctrWide { id = createLegacyContainer(ctx, t, c, tc.desiredMAC, opts...) } else { opts = append(opts, container.WithMacAddress(tc.netName, tc.desiredMAC)) id = container.Create(ctx, t, c, opts...) } } else { id = container.Create(ctx, t, c, opts...) } defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{ Force: true, }) inspect, err := c.ContainerInspect(ctx, id, client.ContainerInspectOptions{}) assert.NilError(t, err) var resp struct { Config struct { // Mac Address of the container. // // MacAddress field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead. MacAddress string `json:",omitempty"` } } err = json.Unmarshal(inspect.Raw, &resp) assert.NilError(t, err, string(inspect.Raw)) configMAC := resp.Config.MacAddress assert.Check(t, is.DeepEqual(configMAC, tc.desiredMAC), string(inspect.Raw)) }) } } // Regression test for https://github.com/moby/moby/issues/47441 // Migration of a container-wide MAC address to the new per-endpoint setting, // where NetworkMode uses network id, and the key in endpoint settings is the // network name. func TestWatchtowerCreate(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows", "no macvlan") ctx := setupTest(t) d := daemon.New(t, daemon.WithEnvVars("DOCKER_MIN_API_VERSION=1.25")) d.StartWithBusybox(ctx, t) defer d.Stop(t) c := d.NewClientT(t, client.WithAPIVersion("1.25")) defer c.Close() // Create a "/29" network, with a single address in iprange for IPAM to // allocate, but no gateway address. So, the gateway will get the single // free address. It'll only be possible to start a container by explicitly // assigning an address. const netName = "wtmvl" netId := network.CreateNoError(ctx, t, c, netName, network.WithIPAMRange("172.30.0.0/29", "172.30.0.1/32", ""), network.WithDriver("macvlan"), ) defer network.RemoveNoError(ctx, t, c, netName) // Start a container, using the network's id in NetworkMode but its name // in EndpointsConfig. (The container-wide MAC address must be merged with // the endpoint config containing the preferred IP address, but the names // don't match.) const ctrName = "ctr1" const ctrIP = "172.30.0.2" const ctrMAC = "02:42:ac:11:00:42" opts := []func(*container.TestContainerConfig){ container.WithName(ctrName), container.WithNetworkMode(netId), container.WithIPv4(netName, ctrIP), } id := createLegacyContainer(ctx, t, c, ctrMAC, opts...) defer c.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true}) _, err := c.ContainerStart(ctx, id, client.ContainerStartOptions{}) assert.NilError(t, err) // Check that the container got the expected addresses. inspect := container.Inspect(ctx, t, c, ctrName) netSettings := inspect.NetworkSettings.Networks[netName] assert.Check(t, is.Equal(netSettings.IPAddress, netip.MustParseAddr(ctrIP))) assert.Check(t, is.Equal(netSettings.MacAddress.String(), ctrMAC)) } type legacyCreateRequest struct { containertypes.CreateRequest // Mac Address of the container. // // MacAddress field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead. MacAddress string `json:",omitempty"` } func createLegacyContainer(ctx context.Context, t *testing.T, apiClient client.APIClient, desiredMAC string, ops ...func(*container.TestContainerConfig)) string { t.Helper() config := container.NewTestConfig(ops...) ep := "/v" + apiClient.ClientVersion() + "/containers/create" if config.Name != "" { ep += "?name=" + config.Name } res, _, err := request.Post(ctx, ep, request.Host(apiClient.DaemonHost()), request.JSONBody(&legacyCreateRequest{ CreateRequest: containertypes.CreateRequest{ Config: config.Config, HostConfig: config.HostConfig, NetworkingConfig: config.NetworkingConfig, }, MacAddress: desiredMAC, })) assert.NilError(t, err) buf, err := request.ReadBody(res.Body) assert.NilError(t, err) assert.Equal(t, res.StatusCode, http.StatusCreated, string(buf)) var resp containertypes.CreateResponse err = json.Unmarshal(buf, &resp) assert.NilError(t, err) return resp.ID }